Today, API keys, the workflow IDs you may submit cases to, and webhook configuration (URL, signing secret) are all provisioned for you by the Frayme team. In the future, both API keys and webhook configuration will be self-served from the Frayme portal.
This guide is the narrative walkthrough. For the field-level reference of each
endpoint, see the API reference; for the events
Frayme pushes to you, see Webhooks.
Concepts
You submit cases (KYC, KYB, or Transaction) to Frayme. Each case is routed through a tenant-defined workflow that produces a decision (approved, declined, or in_review). You can:
- Submit cases via the Case API.
- Poll case status via the Case API.
- Receive webhooks when a decision is reached (or when an analyst later overrides it).
Authentication
All Case API calls require an API key scoped to your tenant. The key is sent on every request using theX-API-Key header:
Submitting a case
POST /cases — requires scope cases:write.
Request body
Do not send
tenantId — your tenant is derived from the API key, and any tenantId in the body is ignored.string
required
Provisioned for you by Frayme.
integer
Pin the version your client expects. If omitted, the workflow’s current published version is used.
string
required
One of
KYC, KYB, or Transaction.object
required
Validated against the workflow’s input schema. Fields and types are dictated by the workflow you target; submitting a payload that does not match the schema returns
400.object
Free-form audit fields; surfaced in the portal and event log.
object
required
Typed business attributes used for entity resolution and review.
displayName is required on every subject and is the label shown in review queues. Then set exactly one sub-struct, and it must match type: transaction for Transaction, person for KYC, business for KYB. Setting none, more than one, or the wrong one returns 400. See Subject sub-structs for the per-type required fields.string
If provided and a case with the same key already exists, the existing case is returned with
200 OK instead of 201 Created.string
RFC 3339 timestamp of the originating business event (e.g.
2026-05-19T14:32:00Z). Required for Transaction cases, optional otherwise. A value that doesn’t parse, or one too far in the future, returns 400.Subject sub-structs
displayName is required on every subject. Then set the one sub-struct matching type. Each sub-struct carries one or more identifiers that drive entity resolution.
Identifiers
An identifier is{ "type": <string>, "value": <string>, "country": <string, optional> }. type must be one of the closed enum below (exact lowercase match):
Identifiers are classed strong (resolve identity:
cpf, cnpj, passport, national_id, company_registration, tax_id, drivers_license, external_customer_id, wallet_address, pix_key) or weak (attributes only: email, phone, bank_account). A submission that doesn’t meet the per-type identifier requirements below returns 400.
Bank account formats
Abank_account value starts with a scheme, and the scheme determines the country:
- Brazil accepts either
IBANorBRISP. ForBRISP, the account check digit is required after a hyphen; the branch check digit is optional. - Other countries cannot send a
bank_accountyet. - IBAN check digits (mod-97), the US routing-number checksum and the CLABE check digit are validated. An invalid value returns
400. bank_accountis weak. It never identifies a party on its own, so a transaction party (sender or receiver) whose only identifiers are weak returns400. Send a strong identifier with it, such ascpf,cnpj,pix_keyorexternal_customer_id.
Transaction → transaction
Each party is
{ "role": "sender" | "receiver", "displayName": <string, optional>, "identifiers": [...] }. Both parties need at least one strong identifier; a party carrying only weak identifiers returns 400. The customer-side party (the sender when outbound, the receiver when inbound) must also have a non-empty displayName and an external_customer_id.
KYC → person
KYB → business
Response
Polling case status
GET /cases/{caseId} — requires scope cases:read.
Response
Status values
The casestatus reflects only lifecycle, not the outcome — read result.decision.value for the outcome.
Notes:
resultis absent until the workflow completes.result.decision.valueis one ofapproved,declined, orin_review.result.decisionis always the current decision;result.decisionHistory[0]is the workflow’s initial decision, and later entries are analyst overrides.result.decision.sourceis one ofworkflow,risk_evaluation, oranalyst.risk_evaluationmeans the case was decided by the pre-DAG risk gate before the workflow ran;analystmeans a human overrode the decision.- Each decision entry (current and historical) may also carry optional fields:
declineReason,queueName,riskScore, andnotes. They are present only when set. result.workflow_resultis write-once: analyst overrides never mutate it.result.riskEvaluationis the audit record of the pre-DAG risk gate (when it ran):status(ok/failed_closed),action(deny/review/workflow),highestSeverity(low/medium/high/critical), and a list oftriggeredRules(each withid,name,severity,conditions,ruleVersion). Treat it as read-only audit evidence.result.dataSources(when present) lists the enrichment providers the workflow called, each withnodeId,providerId, andstatus.
Webhooks
Frayme delivers signed HTTPS POSTs to a URL configured for you. The dedicated Webhooks page is the canonical reference for every event, payload shape, and signature-verification detail.Events you may receive
Payload shapes
- Decision payload
- External callback dispatch
- Node notification
Sent for
case.decided, case.pending_review, and case.decision_overridden.Deduplication via webhookId
Every outbound webhook (decision events and dispatch events alike) carries a unique webhookId (UUID). Frayme’s delivery is at-least-once — a transient failure or network blip can produce duplicate deliveries with the same webhookId. Persist the webhookId of every event you successfully process and reject (or short-circuit) any event whose webhookId you’ve seen before.
Headers
Verifying the signature
- Compute HMAC over the exact bytes of the request body — do not re-serialize the JSON.
- Use a constant-time comparison (for example,
hmac.compare_digest). - If
X-Frayme-Secret-IDis present, pick the matching secret from your rotation set. - Respond
2xxwithin ~10 seconds (the per-delivery timeout). Non-2xx responses and timeouts are retried with exponential backoff — 1s, 2s, 4s, 8s, 16s — up to the configured retry limit (5 attempts by default); final failures are recorded but not retried indefinitely. - Treat deliveries as at-least-once; deduplicate by
webhookIdas described above.
Delivering an external callback
Used only if your workflow has a node that pauses awaiting an external system. After receiving anexternal_callback.dispatch webhook, the external system posts the result to the one-time URL from the dispatch payload:
POST /cases/{caseId}/callbacks/{callbackRef} — requires scope cases:callback.
Content-Type: application/json; the body is a JSON object the workflow node consumes.
Answering an information request
An information request asks a case’s customer for more information. One is raised by an analyst in the Frayme console or by a workflow node — raising one is not yet part of the published Case API — and it publishes acase.rfi_requested webhook to your endpoint; your systems send the actual email, Frayme never does.
That webhook carries the rfi_id you need below. A case with a request outstanding also reports openRfiCount: 1 on GET /cases/{caseId}, and only one request may be open per case at a time. When the case reaches a terminal decision, any pending request is cancelled silently — you receive the case.decided webhook, never a cancel event.
When your customer replies, your backend finishes the request in two steps: upload each document into a presigned slot, then submit the answer. Both routes require scope cases:write, and the request must still be pending.
1
Mint an upload slot, once per file
POST /cases/{caseId}/rfi/{rfiId}/documents201 Created returns { "uploadId", "uploadUrl", "expiresAt" }. Accepted types are application/pdf, image/jpeg, and image/png, at 1 byte – 20 MB.Every slot you mint counts against the budget for good, even one you never upload to or that expires: 30 per request, 100 per case (a response attaches at most 10 documents). Exceeding it is 422 too_many_documents. A request that can no longer take documents is 409 — already_answered, not_pending, case_terminal, or dry_run.2
PUT the bytes
PUT the file to uploadUrl before expiresAt, with exactly the Content-Type and Content-Length you declared and the header If-None-Match: * — all three are pinned into the signature and the upload is rejected otherwise.PUT returns 412 Precondition Failed, so an upload cannot be replaced once it has landed. expiresAt also bounds step 3 — a slot not attached by then is refused, and you mint a fresh one.3
Submit the answer, once
POST /cases/{caseId}/rfi/{rfiId}/response200 OK returns { "rfiId", "status": "answered", "answeredAt", "fieldCount", "documentCount" }.fields is an ordered array of scalar answers (string, number, boolean, null). Each entry’s key is an identifier (^[A-Za-z_][A-Za-z0-9_]*$) and must be unique across the array — a repeat is 422 invalid_field_key, never a silent last-one-wins. Limits: ≤ 100 entries, ≤ 4 KB per value, ≤ 16 KB in total. At least one field or document is required. The console shows the answers in the order you send them.
Each entry may carry an optional label, a human display name for the console (≤ 256 bytes, counted against the same 16 KB total). Labels are display-only — they are never bound into the workflow, so rules keep matching on the key.
Every uploadId must have been minted for this request and its object must already be in the bucket with the declared size and type — a minted-but-never-uploaded slot fails the whole submission, and nothing is recorded partially.
A request is answered once — there is no amend and no re-open; a follow-up is a new request.
answeredBy is apikey:<keyId> when a key answered, so a retry after a lost response recognises its own answer, or analyst when an analyst recorded the reply in the console.case.rfi_answered and, when the request was raised by an awaiting Information request workflow node, resumes the workflow with the answers available to its rules.
Tagging a case
Tags are short, non-PII operator labels used to organise and triage cases (for examplehigh-risk, review-later) — the same labels analysts apply in the Frayme console. Your tenant is taken from the API key, so tag paths are not tenant-scoped in the URL.
Tags applied via the API must come from your tenant’s managed tag vocabulary, configured under Settings → Tags in the portal. An arbitrary label is rejected, so the console’s chips, colours, and filters stay consistent.
Add a tag — POST /cases/{caseId}/tags, requires scope tags:write.
{ "tag", "createdBy", "createdAt" }. The applier is recorded as your API key (createdBy: "apikey:<keyId>"); any createdBy in the request body is ignored.
List a case’s tags —
GET /cases/{caseId}/tags, requires scope tags:read. Returns { "tags": [ { "tag", "createdBy", "createdAt" } ] }, oldest-first (an empty array for an untagged case).
Remove a tag — DELETE /cases/{caseId}/tags?tag=<label>, requires scope tags:write. Idempotent: removing an absent tag still returns 204 No Content.
Pending actions on a paused node
Some workflow nodes pause to await an external review — a Sumsub identity check, for example — and while paused they expose named actions you can invoke. Unlike an external callback (which resolves the node and resumes the workflow), a pending action is a side effect on the paused node: it returns data and leaves the node paused. Typical uses are re-minting an expiring access token or verification link, or pushing applicant data to the provider before the end user starts.Handles
Each paused node is addressed by an opaquepending_handle:
- It is per-paused-node and stable across retries — the same paused node always resolves to the same handle.
- It is tenant-scoped: a handle from another tenant resolves to
404(handle existence is never revealed across tenants). - It expires with the node’s await window (
expiresAt); after that, invoking it returns404.
- From a
node.notificationwebhook — theoutput.pending_handlefield (see Webhooks). - By listing the case’s paused nodes (below).
List paused nodes
GET /cases/{caseId}/pending — requires scope cases:read.
availableActions is provider-specific — it lists exactly the ids you may invoke on that node. resolvesAwait is false for every action available today (all are side effects).
Invoke an action
POST /pending/{handle}/actions/{actionId} — requires scope cases:callback.
{ "params": { ... } }; an empty body is valid for actions that take no parameters. The response wraps the action’s output:
The available actions and their
params depend on the node’s provider. For Sumsub, see Data sources → Sumsub → Real-time actions on a paused node.
Durable case actions
A pending action only exists while a node is paused — once the case is decided, the handle is gone. Some provider operations, though, stay meaningful long after the case has finished. The clearest example is re-issuing a Sumsub share token so a partner client can access the applicant months later. For those, Frayme exposes durable case actions, addressed bycaseId (no handle) and scoped to the broker that owns them — so they work from the moment the node ran, through the decision, and indefinitely afterward.
Durable case actions are addressed by
(brokerId, action) so action ids never collide across brokers. The applicant is re-resolved from the case’s external_customer_id — so the case must have been submitted with one (it is required on every case type).List durable actions
GET /cases/{caseId}/actions — requires scope cases:read.
Lists the durable actions invocable on the case, derived from the brokers it actually ran:
Invoke a durable action
POST /cases/{caseId}/actions/{brokerId}/{action} — requires scope cases:callback.
For Sumsub, see Data sources → Sumsub → Regenerate a share token.
Quick reference
All requests authenticate via the
X-API-Key header. The base URL of the Case API and your webhook configuration (URL, signing secret, secret ID, retry policy) are provided by Frayme — these will move to self-service in the portal.