{"openapi":"3.0.3","info":{"title":"Curacheck Partner API","version":"1.0.0","license":{"name":"Proprietary"},"description":"Partner-facing REST API for provider verification. Push a roster, read verification status back, and approve or reject rows — all with an API key. Responses expose merchant-facing outcomes and reason codes only; the decision engine's internals are never returned.\n\nAuthentication: send your API key in the `x-api-key` header. Each endpoint requires a specific scope on the key (noted per operation). Generate a single Salesforce key that carries all required scopes from the Curacheck Integrations page."},"servers":[{"url":"https://{host}","description":"Curacheck API. Replace {host} with your Curacheck API base host.","variables":{"host":{"default":"api.curacheck.io","description":"Curacheck API host"}}}],"tags":[{"name":"Rosters","description":"Create rosters and read verification status."},{"name":"Review","description":"Approve or reject rows under review."},{"name":"Providers","description":"Look up an eligible verified provider."}],"security":[{"apiKeyAuth":[]}],"paths":{"/v1/batches":{"post":{"operationId":"createRoster","tags":["Rosters"],"summary":"Create a roster","description":"Create a roster of providers to verify. Requires the `batches:write` scope. Each provider may carry an `externalId` (your Salesforce record id) and `customFields`, which are stored alongside the row and echoed back on reads and webhooks — they never enter the verification decision. Verification runs asynchronously; poll listRosterRows or subscribe to webhooks.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRosterRequest"}}}},"responses":{"200":{"description":"Roster created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRosterResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"listRosters","tags":["Rosters"],"summary":"List rosters","description":"List your organization's rosters with per-status counts. Requires the `batches:read` scope.","parameters":[{"$ref":"#/components/parameters/Page"},{"name":"pageSize","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"Rosters per page (max 200)."}],"responses":{"200":{"description":"A page of rosters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RosterSummaryPage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/batches/{id}":{"get":{"operationId":"getRoster","tags":["Rosters"],"summary":"Get a roster summary","description":"Fetch a single roster's summary and per-status counts (no rows). Requires the `batches:read` scope.","parameters":[{"$ref":"#/components/parameters/RosterId"}],"responses":{"200":{"description":"The roster summary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RosterSummary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/batches/{id}/rows":{"get":{"operationId":"listRosterRows","tags":["Rosters"],"summary":"List roster rows","description":"List a roster's rows with verification status, decision, reason codes, and your echoed `externalId`/`customFields`. Requires the `batches:read` scope. Rate limited to 60 requests/minute.","parameters":[{"$ref":"#/components/parameters/RosterId"},{"$ref":"#/components/parameters/Page"},{"name":"pageSize","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"description":"Rows per page (max 500)."},{"name":"status","in":"query","required":false,"schema":{"$ref":"#/components/schemas/RowStatus"},"description":"Filter to a single status."}],"responses":{"200":{"description":"A page of rows.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RosterRowPage"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/batch-rows/{id}":{"patch":{"operationId":"decideRow","tags":["Review"],"summary":"Approve or reject a row","description":"Set the decision of record for a single row — approve (`auto_approve`) or `reject`. Requires the `review:write` scope. This is the same decision path as the Curacheck dashboard: the row leaves the review queue and the change is audited. A `review.decided` webhook fires with `source: \"api\"` so your own change can be ignored by your inbound handler (echo guard).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Row id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecideRequest"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/providers":{"get":{"operationId":"lookupProvider","tags":["Providers"],"summary":"Look up an eligible provider","description":"Look up a single eligible verified provider by `npi` or `email`, or list eligible providers with cursor pagination. Requires the `providers:read` scope. Rate limited to 60 requests/minute.","parameters":[{"name":"npi","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{10}$"},"description":"10-digit NPI."},{"name":"email","in":"query","required":false,"schema":{"type":"string","format":"email"},"description":"Provider email."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"List page size (when neither npi nor email is given)."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque cursor from a prior list response."}],"responses":{"200":{"description":"A lookup result or a page of eligible providers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProviderLookupResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/v1/providers/claim":{"get":{"operationId":"claimSubmission","tags":["Providers"],"summary":"Resolve a returned provider (claim)","description":"Resolve the `?s=<submissionId>` that Curacheck appends to your return URL / approval-email link into an eligibility state, so you can gate and prefill your signup. Requires the `providers:read` scope. **Single-use:** the first `eligible` read consumes the claim (a later read returns `claimed`). Only `eligible` includes the `provider` object — a declined or in-review provider yields a status with no data, so a failed provider never receives verified data. Rate limited to 60 requests/minute.","parameters":[{"name":"submission","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"The submission id from the `?s=` return parameter."}],"responses":{"200":{"description":"The claim state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionClaimState"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}}},"components":{"securitySchemes":{"apiKeyAuth":{"type":"apiKey","in":"header","name":"x-api-key","description":"Curacheck API key (format `curacheck_sk_…`)."}},"parameters":{"RosterId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Roster id."},"Page":{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1},"description":"1-based page number."}},"responses":{"BadRequest":{"description":"Invalid request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"The key is missing the required scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Not found in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Too many requests (60/minute).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InternalError":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"RowStatus":{"type":"string","enum":["pending","auto_approve","review","reject","error"],"description":"Row processing status / decision of record."},"Decision":{"type":"string","enum":["auto_approve","review","reject"],"description":"Verification decision."},"CustomFields":{"type":"object","additionalProperties":{"type":"string"},"description":"Your key→value metadata, echoed back verbatim. Max 20 keys, values max 500 chars. NOTE: Salesforce External Services does not map free-form objects to Apex types — see the admin runbook for the recommended handling (treat as a JSON string in Apex)."},"ProviderInput":{"type":"object","description":"One provider to verify. Provide an `npi` or a name (`lastName`/`name`/`organizationName`).","properties":{"npi":{"type":"string","description":"10-digit NPI."},"firstName":{"type":"string"},"lastName":{"type":"string"},"organizationName":{"type":"string"},"name":{"type":"string","description":"Full name, if first/last are not separated."},"address1":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"},"licenseType":{"type":"string"},"externalId":{"type":"string","maxLength":128,"description":"Your record id (e.g. a Salesforce record id). Echoed back on reads and webhooks."},"customFields":{"$ref":"#/components/schemas/CustomFields"}}},"CreateRosterRequest":{"type":"object","required":["providers"],"properties":{"name":{"type":"string","maxLength":120,"description":"A label for the roster."},"providers":{"type":"array","minItems":1,"maxItems":1000,"items":{"$ref":"#/components/schemas/ProviderInput"}}}},"CreateRosterResponse":{"type":"object","required":["batchId","count"],"properties":{"batchId":{"type":"string","format":"uuid","description":"The created roster id."},"count":{"type":"integer","description":"Number of provider rows created."}}},"BatchCounts":{"type":"object","required":["pending","auto_approve","review","reject","error"],"properties":{"pending":{"type":"integer"},"auto_approve":{"type":"integer"},"review":{"type":"integer"},"reject":{"type":"integer"},"error":{"type":"integer"}}},"RosterSummary":{"type":"object","required":["id","name","status","rowCount","counts","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"status":{"type":"string","description":"Roster processing status: pending | running | done | error | paused | stopped."},"rowCount":{"type":"integer"},"counts":{"$ref":"#/components/schemas/BatchCounts"},"createdAt":{"type":"string","format":"date-time"},"finishedAt":{"type":"string","format":"date-time","nullable":true}}},"RosterSummaryPage":{"type":"object","required":["batches","total","page","pageSize"],"properties":{"batches":{"type":"array","items":{"$ref":"#/components/schemas/RosterSummary"}},"total":{"type":"integer"},"page":{"type":"integer"},"pageSize":{"type":"integer"}}},"RosterRowProvider":{"type":"object","description":"Merchant-safe provider identity. No scores or engine internals.","properties":{"providerId":{"type":"string","nullable":true,"description":"Curacheck provider id, once verified."},"npi":{"type":"string","nullable":true},"firstName":{"type":"string","nullable":true},"lastName":{"type":"string","nullable":true},"organizationName":{"type":"string","nullable":true},"email":{"type":"string","nullable":true}}},"RosterRow":{"type":"object","required":["id","status","reasons","manualOverride","provider","customFields","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"status":{"$ref":"#/components/schemas/RowStatus"},"decision":{"type":"string","enum":["auto_approve","review","reject"],"nullable":true,"description":"Decision of record (null while pending/error)."},"reasons":{"type":"array","items":{"type":"string"},"description":"Merchant-facing reason codes."},"manualOverride":{"type":"boolean","description":"True if a human override changed the engine decision."},"provider":{"$ref":"#/components/schemas/RosterRowProvider"},"externalId":{"type":"string","nullable":true,"description":"Your record id, as supplied on createRoster."},"customFields":{"$ref":"#/components/schemas/CustomFields"},"createdAt":{"type":"string","format":"date-time"}}},"RosterRowPage":{"type":"object","required":["rows","total","page","pageSize"],"properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/RosterRow"}},"total":{"type":"integer"},"page":{"type":"integer"},"pageSize":{"type":"integer"}}},"DecideRequest":{"type":"object","required":["decision"],"properties":{"decision":{"type":"string","enum":["auto_approve","reject"],"description":"Approve (auto_approve) or reject the row."},"note":{"type":"string","maxLength":500,"description":"Optional reviewer note (audited)."}}},"OkResponse":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"}}},"VerifiedProvider":{"type":"object","properties":{"id":{"type":"string"},"npi":{"type":"string","nullable":true},"firstName":{"type":"string","nullable":true},"lastName":{"type":"string","nullable":true},"organizationName":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"eligible":{"type":"boolean","enum":[true],"description":"Always true — only eligible providers are returned."},"verifiedAt":{"type":"string","format":"date-time","nullable":true}}},"ProviderLookupResponse":{"type":"object","description":"When `npi` or `email` is supplied: an eligibility result `{eligible, provider?}`. Otherwise a cursor-paginated list `{providers, nextCursor}`.","properties":{"eligible":{"type":"boolean","description":"Present on a single lookup."},"provider":{"$ref":"#/components/schemas/VerifiedProvider"},"providers":{"type":"array","items":{"$ref":"#/components/schemas/VerifiedProvider"},"description":"Present on a list."},"nextCursor":{"type":"string","nullable":true,"description":"Pass back as `cursor` for the next page."}}},"VerifiedClaim":{"type":"object","description":"The provider's own verified identity + contact data, for prefilling your signup. Merchant-safe allow-list only — no scores, screening, reason codes, or raw identity.","required":["id","eligible","verifiedAt"],"properties":{"id":{"type":"string"},"firstName":{"type":"string","nullable":true},"lastName":{"type":"string","nullable":true},"npi":{"type":"string","nullable":true},"organizationName":{"type":"string","nullable":true},"email":{"type":"string","nullable":true,"description":"The provider's verified email — your match key."},"address1":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"zip":{"type":"string","nullable":true},"licenseType":{"type":"string","nullable":true},"eligible":{"type":"boolean","enum":[true],"description":"Always true — a claim only carries a provider when eligible."},"verifiedAt":{"type":"string","format":"date-time"}}},"SubmissionClaimState":{"type":"object","description":"Triage state for a returned self-submission. Render your signup form only on `eligible`; `provider` is present only then. `declined` / `pending_review` / `processing` / `expired` / `claimed` carry no provider data — a failed provider never receives verified fields.","required":["status"],"properties":{"status":{"type":"string","enum":["processing","pending_review","declined","expired","claimed","eligible"]},"provider":{"$ref":"#/components/schemas/VerifiedClaim"}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}},"EventOrg":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"}}},"EventProvider":{"type":"object","properties":{"id":{"type":"string"},"npi":{"type":"string","nullable":true},"firstName":{"type":"string","nullable":true},"lastName":{"type":"string","nullable":true},"organizationName":{"type":"string","nullable":true},"email":{"type":"string","nullable":true}}},"EntitlementEvent":{"type":"object","description":"The payload posted to a GENERIC webhook endpoint (Integrations → Webhook endpoint) and to the Shopify/WooCommerce apps. `type` carries the action — `provider.eligible` when a provider becomes eligible, `provider.revoked` when eligibility is lost — so act on `type`, not on `decision` (which is always `auto_approve`). `reason` is present only on `provider.revoked`. Signed with `X-Attesta-Signature: t=<unix>,v1=<hex>` where v1 = HMAC-SHA256(secret, `<t>.<rawBody>`).","required":["id","type","occurredAt","org","provider","decision"],"properties":{"id":{"type":"string","description":"Unique event id — dedupe on this."},"type":{"type":"string","enum":["provider.eligible","provider.revoked"]},"occurredAt":{"type":"string","format":"date-time"},"org":{"$ref":"#/components/schemas/EventOrg"},"provider":{"$ref":"#/components/schemas/EventProvider"},"decision":{"type":"string","enum":["auto_approve"],"description":"Always `auto_approve`; the action is in `type`."},"reason":{"type":"string","enum":["exclusion_match","manual_reject"],"description":"Present only on `provider.revoked`."}}},"VerificationCompletedEvent":{"type":"object","description":"SALESFORCE-target payload (Integrations → Salesforce, event `verification.completed`). A generic webhook endpoint receives the `EntitlementEvent` shape instead — see that schema. Sent on every verification decision. `reasons` are merchant-safe codes only (never engine internals). Signed with `X-Attesta-Signature: t=<unix>,v1=<hex>` where v1 = HMAC-SHA256(secret, `<t>.<rawBody>`).","required":["id","type","occurredAt","org","provider","decision","reasons"],"properties":{"id":{"type":"string","description":"Unique event id — dedupe on this."},"type":{"type":"string","enum":["verification.completed"]},"occurredAt":{"type":"string","format":"date-time"},"org":{"$ref":"#/components/schemas/EventOrg"},"provider":{"$ref":"#/components/schemas/EventProvider"},"externalId":{"type":"string","nullable":true},"customFields":{"$ref":"#/components/schemas/CustomFields"},"decision":{"$ref":"#/components/schemas/Decision"},"reasons":{"type":"array","items":{"type":"string"},"description":"Merchant-safe reason codes (e.g. npi_active, excluded, manual_review)."}}},"ReviewDecidedEvent":{"type":"object","description":"SALESFORCE-target payload (Integrations → Salesforce, event `review.decided`), sent when a row is approved or rejected. Honor the `source` echo guard: ignore events with `source: \"api\"` (those originate from your own decideRow call) to avoid update loops.","required":["id","type","occurredAt","org","provider","decision","source"],"properties":{"id":{"type":"string","description":"Unique event id — dedupe on this."},"type":{"type":"string","enum":["review.decided"]},"occurredAt":{"type":"string","format":"date-time"},"org":{"$ref":"#/components/schemas/EventOrg"},"provider":{"$ref":"#/components/schemas/EventProvider"},"externalId":{"type":"string","nullable":true},"customFields":{"$ref":"#/components/schemas/CustomFields"},"decision":{"type":"string","enum":["auto_approve","reject"]},"note":{"type":"string","nullable":true},"source":{"type":"string","enum":["dashboard","api"],"description":"Origin of the decision; ignore \"api\" (your own change)."}}}}}}