Developers

Integrate Curacheck into anything

A provider portal, pharmacy or clinic system, EHR, membership site, or storefront — the same three primitives wire them all up.

The model

Verify a provider once, then react to the outcome in your own system. The integration is platform- and form-agnostic: a signed webhook plus one REST call is the whole contract, so it works with any stack — a Gravity Forms portal, a custom provider signup, an EHR, a membership site. The Shopify app and WooCommerce plugin are prebuilt drop-ins for those storefronts; every other platform uses the same webhook + API — no Curacheck plugin required.

1

Entry

Send the provider to verify — a hosted link, or an API call.

2

Result

Receive a signed webhook (provider.eligible / provider.revoked), or poll the read API.

3

Act

Reconcile by email or NPI — unlock the benefit or your gated signup for eligible providers, reject the rest.

Two guarantees that shape everything: you receive outcomes only (a decision + reason codes, never engine internals), and verification is continuously re-screened — a provider can be revoked later, so handle both events.

Base URL & auth

API basehttps://api.curacheck.io
Auth headerx-api-key: <your key>
Content typeapplication/json
ReferenceRendered API reference · openapi.json (import into Postman / Salesforce)

Create keys in Integrations → API keys. Grant only the scopes you use:

ScopeLets a key…
verifyrun a verification (POST /v1/verify)
invites:writemint per-provider invite links (POST /v1/self-submissions/invite)
providers:readlook up eligible providers & resolve returns (GET /v1/providers, /v1/providers/claim)
batches:write / batches:readsubmit & read bulk rosters

1 · Verify the webhook signature

Set your HTTPS endpoint + generate a signing secret in Integrations → Webhook endpoint. Every delivery carries an X-Attesta-Signature: t=<unix>,v1=<hex> header, where v1 = HMAC-SHA256(secret, "<t>.<raw body>"). Verify it — against the raw body — before trusting anything.

import crypto from "node:crypto";

function verifyCuracheck(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected, "hex"), b = Buffer.from(parts.v1 ?? "", "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/curacheck/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verifyCuracheck(raw, req.get("X-Attesta-Signature") ?? "", process.env.CURACHECK_SECRET)) {
    return res.sendStatus(401);
  }
  handleEvent(JSON.parse(raw));   // see step 3
  res.sendStatus(200);            // 2xx = delivered; non-2xx retries
});

X-Attesta-Signature is a stable wire contract — it is intentionally not renamed across releases, so you build against it once.

2 · Send providers to verify

A · Hosted wizard — identity-grade (recommended)

Link the provider to the wizard; they complete government-ID identity + NPI + screening. No API call to start it. Add ?returnUrl= (allow-listed in your org settings) to hand them back into your onboarding. They must verify with the same email they use in your system — that's the match key.

Want a single-use, email-pinned link per provider instead of the org-wide one? A key with the invites:write scope can mint them from your backend:

curl -sX POST https://api.curacheck.io/v1/self-submissions/invite \
  -H "x-api-key: $KEY" -H "content-type: application/json" \
  -d '{ "contactEmail": "jordan@clinic.example", "sendEmail": true }'
# → { "token": "…", "inviteUrl": "https://app.curacheck.io/verify/<token>", "emailQueued": true }

B · POST /v1/verify — registry + screening, server-side

For a programmatic check when you already hold the NPI and don't need document-identity proof.

curl -sX POST https://api.curacheck.io/v1/verify \
  -H "x-api-key: $KEY" -H "content-type: application/json" \
  -d '{ "npi":"1234567890", "firstName":"Jordan", "lastName":"Lee", "state":"TX" }'
# → { "decision": "auto_approve" | "review" | "reject", "reasons": ["npi_active", …] }

Assurance difference: the hosted wizard (A) proves identity; /v1/verify (B) checks the registry + exclusions only. Pick A when "is this really that provider?" matters.

C · Gate & prefill your signup — the claim endpoint

Sending approved providers on to your own signup (a form, a portal, a create-account page)? When they return, Curacheck appends ?s=<submissionId>. Resolve it from your backend to gate and prefill — only an eligible provider gets the form; a declined or in-review one gets your rejection or "pending" message, never the form.

curl -s "https://api.curacheck.io/v1/providers/claim?submission=<id>" -H "x-api-key: $KEY"
# eligible → { "status":"eligible", "provider":{ "firstName":"…","npi":"…","email":"…","licenseType":"…" } }
# not yet  → { "status": "declined" | "pending_review" | "processing" | "expired" | "claimed" }

Single-use — the first eligible read consumes it, so resolve once and cache it for the session. Only eligible returns the provider record, which you use to prefill. This is the same generic pattern for any form or platform (Gravity Forms, a bespoke portal, anything) — no form-specific code. Want the copy-paste WordPress + Gravity Forms example? Ask us.

Optional — prefill with zero API calls (signed token). The return link also carries &ct=<jwt>: a short-lived RS256 token of the same merchant-safe fields. Verify it against our public keys at https://api.curacheck.io/.well-known/jwks.json (issuer https://api.curacheck.io, audience = your org slug) and prefill straight from the claims — no round-trip. It's minted only on the first handoff while the claim is open, so it's a convenience, not the gate: still resolve ?s= server-side for the authoritative single-use check. The token prefills; the claim gates.

3 · Receive & apply the outcome

Consume the webhook (push), or poll GET /v1/providers?email=… (pull) for reconciliation. The event body is outcomes-only:

{
  "id": "evt_…",                   // dedupe on this (idempotent handler)
  "type": "provider.eligible",     // or "provider.revoked"
  "occurredAt": "2026-07-05T18:00:00Z",
  "org": { "id": "…", "slug": "your-slug" },
  "provider": {
    "npi": "1234567890",
    "firstName": "Jordan", "lastName": "Lee",
    "organizationName": null,
    "email": "jordan@clinic.example"   // your match key
  },
  "reason": "exclusion_match"       // present only on provider.revoked
}
function handleEvent(event) {
  const p = event.provider;
  const provider = findByEmail(p.email) ?? findByNpi(p.npi);
  if (!provider) return storeForLater(p);   // verified before they existed here → apply at signup
  provider.setVerified(event.type === "provider.eligible", event.reason);
}

4 · Handle revocation

A previously-eligible provider can lose the benefit. Downgrade them on provider.revoked:

reasonMeaning
exclusion_matcha continuous rescreen matched an OIG / SAM exclusion
manual_rejectan org admin rejected them in Review

Security checklist

  • Verify X-Attesta-Signature on every webhook, against the raw body.
  • Reject stale timestamps (replay window, e.g. 5 min) and dedupe on the event id.
  • HTTPS-only endpoint; keep the signing secret and API keys in a secret manager.

Questions? support@curacheck.io.