A reference for partners integrating with the Frayme fraud-analysis platform.
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.
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 the X-API-Key header:
X-API-Key: <your-api-key>
No other authentication mechanism is supported on the partner endpoints.
Each key carries one or more scopes:
| Scope | Grants |
|---|
cases:write | Submit new cases (POST /cases) |
cases:read | List the paused nodes on a case (GET /cases/{caseId}/pending) |
cases:callback | Deliver an external callback result, or invoke a pending action, on a paused workflow node |
Submitting a case
POST /cases — requires scope cases:write.
Request body
{
"workflowId": "wf_transactions_v2",
"workflowVersion": 3,
"type": "Transaction",
"payload": {
"documentNumber": "DOC-001-BR",
"documentType": "cpf",
"countryCode": "BR"
},
"metadata": {
"source": "checkout-service"
},
"subject": {
"displayName": "Maria Silva",
"transaction": {
"amount": 1250.00,
"currency": "BRL",
"direction": "outbound",
"type": "pix",
"externalTransactionId": "txn-9f8e7d6c",
"parties": [
{
"role": "sender",
"displayName": "Maria Silva",
"identifiers": [
{ "type": "cpf", "value": "52998224725", "country": "BR" },
{ "type": "external_customer_id", "value": "cust-00481" }
]
},
{
"role": "receiver",
"displayName": "Acme Pagamentos Ltda",
"identifiers": [
{ "type": "pix_key", "value": "a1b2-evp-key" }
]
}
]
}
},
"idempotencyKey": "order-9f8e7d6c",
"eventTimestamp": "2026-05-19T14:32:00Z"
}
Do not send tenantId — your tenant is derived from the API key, and any tenantId in the body is ignored.
Provisioned for you by Frayme.
Pin the version your client expects. If omitted, the workflow’s current published version is used.
One of KYC, KYB, or Transaction.
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.
Free-form audit fields; surfaced in the portal and event log.
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.
If provided and a case with the same key already exists, the existing case is returned with 200 OK instead of 201 Created.
ISO-8601 timestamp of the originating business event.
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):
type | Notes |
|---|
cpf | Brazilian individual taxpayer ID |
cnpj | Brazilian company registry ID |
passport | Requires country |
national_id | Requires country |
company_registration | Requires country |
external_customer_id | Your own stable customer/account reference |
email | |
phone | |
wallet_address | Crypto wallet |
pix_key | |
Identifiers are classed strong (resolve identity: cpf, cnpj, passport, national_id, company_registration, external_customer_id, and wallet_address on a wallet party) or weak (attributes only: email, phone, pix_key). A submission that doesn’t meet the per-type identifier requirements below returns 400.
Transaction → transaction
| Field | Type | Required | Notes |
|---|
amount | number | yes | Must be > 0. |
currency | string | yes | ISO-4217 or a registered crypto-asset code. |
amountUsd | number | conditional | Required (and > 0) when currency is a crypto asset. |
direction | string | yes | outbound (sender is your customer) or inbound (receiver is your customer). |
type | string | no | Free-form transaction type label (e.g. pix, wire). |
externalTransactionId | string | no | Your reference for the transaction. |
parties | array | yes | Exactly one party with role: "sender" and at least one with role: "receiver". |
Each party is { "role": "sender" | "receiver", "displayName": <string, optional>, "identifiers": [...] }. The customer-side party (the sender when outbound, the first receiver when inbound) must have a non-empty displayName and at least one strong identifier.
KYC → person
| Field | Type | Required | Notes |
|---|
dateOfBirth | string | no | ISO-8601. |
identifiers | array | yes | Must include at least one document identifier (cpf, passport, or national_id) and an external_customer_id. |
KYB → business
| Field | Type | Required | Notes |
|---|
legalName | string | yes | |
country | string | yes | ISO 3166-1 alpha-2. |
identifiers | array | yes | Must include a registration identifier (cnpj or company_registration) and an external_customer_id. |
relatedParties | array | no | Each { "role": "owner" | "representative" | "ubo", "displayName": <optional>, "identifiers": [...] }; each related party needs at least one identifier. |
Response
{
"caseId": "case_01HABCXYZ...",
"requestId": "req_01HABCXYZ...",
"status": "received"
}
| Status | Meaning |
|---|
201 Created | New case accepted. |
200 OK | Case already existed for the supplied idempotencyKey. |
400 Bad Request | Malformed body, unknown type, missing required field, or payload does not match the workflow’s input schema. |
403 Forbidden | API key is missing the cases:write scope. |
404 Not Found | Workflow not found. |
Polling case status
GET /cases/{caseId} — requires any valid API key.
Response
{
"caseId": "case_01HABCXYZ...",
"requestId": "req_01HABCXYZ...",
"workflowId": "wf_transactions_v2",
"workflowVersion": 3,
"type": "Transaction",
"status": "completed",
"createdAt": "2026-05-19T14:32:01Z",
"completedAt": "2026-05-19T14:32:04Z",
"payload": { "...": "..." },
"metadata": { "...": "..." },
"subject": { "...": "..." },
"result": {
"decision": {
"value": "approved",
"source": "workflow",
"actor": "wf_transactions_v2",
"decidedAt": "2026-05-19T14:32:04Z"
},
"decisionHistory": [
{
"value": "approved",
"source": "workflow",
"actor": "wf_transactions_v2",
"decidedAt": "2026-05-19T14:32:04Z"
}
],
"workflow_result": { "...": "raw vars emitted by the output node" },
"riskEvaluation": {
"evaluatedAt": "2026-05-19T14:32:02Z",
"status": "ok",
"action": "workflow",
"highestSeverity": "low",
"triggeredRules": [
{
"id": "rule_high_amount",
"name": "High amount",
"severity": "low",
"conditions": [ "amount > 1000" ],
"ruleVersion": "v3"
}
]
},
"dataSources": [
{
"nodeId": "node_sumsub",
"providerId": "sumsub",
"status": "ok"
}
]
}
}
Status values
The case status reflects only lifecycle, not the outcome — read result.decision.value for the outcome.
| Status | Meaning |
|---|
received | Case accepted; workflow not yet finished. result is absent. |
completed | Workflow finished. Inspect result.decision.value — approved, declined, or in_review (awaiting analyst decision). |
Notes:
result is absent until the workflow completes.
result.decision.value is one of approved, declined, or in_review.
result.decision is always the current decision; result.decisionHistory[0] is the workflow’s initial decision, and later entries are analyst overrides.
result.decision.source is one of workflow, risk_evaluation, or analyst. risk_evaluation means the case was decided by the pre-DAG risk gate before the workflow ran; analyst means a human overrode the decision.
- Each decision entry (current and historical) may also carry optional fields:
declineReason, queueName, riskScore, and notes. They are present only when set.
result.workflow_result is write-once: analyst overrides never mutate it.
result.riskEvaluation is 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 of triggeredRules (each with id, name, severity, conditions, ruleVersion). Treat it as read-only audit evidence.
result.dataSources (when present) lists the enrichment providers the workflow called, each with nodeId, providerId, and status.
Poll on a backoff (for example, 1s, 2s, 5s, then every 10s) until status is completed, then read result.decision.value. Prefer webhooks over long polling whenever possible.
Webhooks
Frayme delivers signed HTTPS POSTs to a URL configured for you.
Events you may receive
event_type | When | Body shape |
|---|
case.decided | Workflow finished with approved or declined. | Decision payload |
case.pending_review | Workflow finished with in_review; case is now in a queue. | Decision payload |
case.decision_overridden | An analyst overrode an existing decision. | Decision payload (the new result.decision reflects the override) |
external_callback.dispatch | Your tenant uses a workflow node that pauses for an external system. Frayme is asking that system to perform work and post back to a one-time callback URL. | External callback dispatch payload |
node.notification | A workflow node configured to notify emitted output mid-run — for example, a Sumsub node publishing a hosted verification link and a pending-action handle before it pauses for review. | Node notification payload |
Payload shapes
Sent for case.decided, case.pending_review, and case.decision_overridden.{
"webhookId": "8f1d2e6c-3a4b-4c5d-9e8f-0a1b2c3d4e5f",
"event_type": "case.decided",
"case_id": "case_01HABCXYZ...",
"tenant_id": "tenant_01HABCXYZ...",
"result": {
"decision": {
"value": "approved",
"source": "workflow",
"actor": "wf_transactions_v2",
"decidedAt": "2026-05-19T14:32:04Z"
},
"decisionHistory": [ "..." ],
"workflow_result": { "...": "..." }
},
"timestamp": "2026-05-19T14:32:04Z"
}
Sent for externalCallback.dispatch. The receiving system performs its work, then calls back (see Delivering an external callback) before expiresAt.{
"webhookId": "1c2b3a4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"event_type": "external_callback.dispatch",
"tenantId": "tenant_01HABCXYZ...",
"caseId": "case_01HABCXYZ...",
"nodeId": "node_partner_review",
"webhookKey": "partner-review",
"callbackUrl": "https://api.frayme.com/cases/case_01HABCXYZ/callbacks/cbk_oneTimeToken",
"expiresAt": "2026-05-19T15:32:04Z",
"payload": { "...": "node-specific input" }
}
Sent for node.notification when a node configured to notify emits output. payloadType identifies the output schema (for example, sumsub-application-start); output carries the node’s emitted fields, including a pending_handle when the node also exposes pending actions.{
"webhookId": "3f2a1b0c-9d8e-7f6a-5b4c-3d2e1f0a9b8c",
"event_type": "node.notification",
"payloadType": "sumsub-application-start",
"tag": "kyc-onboarding",
"tenantId": "tenant_01HABCXYZ...",
"caseId": "case_01HABCXYZ...",
"nodeId": "node_sumsub",
"webhookKey": "kyc-links",
"output": {
"applicant_id": "6a3d4ca539de5a064a06f245",
"external_user_id": "tenant_01HABCXYZ:cust-00481:node_sumsub",
"verification_url": "https://in.sumsub.com/websdk/p/abc123",
"sdk_token": "_act-sbx-jwt-...",
"sdk_token_expires_at": "2026-05-19T15:00:00Z",
"pending_handle": "84147546-1b38-46d9-95ed-537d0dd4945e"
}
}
event_type is the only snake_case key; identity keys are camelCase. payloadType and tag appear only when configured on the node. The output keys depend on the node’s provider and configuration.
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.
| Header | Value |
|---|
X-Frayme-Signature | HMAC-SHA256 of the raw request body, keyed by your signing secret, hex-encoded. |
X-Frayme-Secret-ID | (When set) identifier of the active signing secret — used during secret rotation. |
Content-Type | application/json |
Verifying the signature
import hmac, hashlib
expected = hmac.new(SIGNING_SECRET.encode(), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers["X-Frayme-Signature"]):
return 401
- 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-ID is present, pick the matching secret from your rotation set.
- Respond
2xx within ~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
webhookId as described above.
Delivering an external callback
Used only if your workflow has a node that pauses awaiting an external system. After receiving an externalCallback.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.
| Status | Meaning |
|---|
202 Accepted | Callback accepted. |
409 Conflict | The callback was already consumed. |
404 Not Found | The token is invalid or expired. |
401 Unauthorized | No API key was supplied. |
403 Forbidden | The key lacks the cases:callback scope. |
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 opaque pending_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 returns 404.
You obtain a handle in one of two ways:
- From a
node.notification webhook — the output.pending_handle field (see Webhooks).
- By listing the case’s paused nodes (below).
List paused nodes
GET /cases/{caseId}/pending — requires scope cases:read.
{
"pending": [
{
"nodeId": "node_sumsub",
"providerId": "sumsub",
"pendingHandle": "84147546-1b38-46d9-95ed-537d0dd4945e",
"expiresAt": "2026-05-19T15:32:04Z",
"availableActions": [
{ "id": "refresh_sdk_token", "label": "Refresh SDK Token", "resolvesAwait": false },
{ "id": "regenerate_link", "label": "Regenerate Verification Link", "resolvesAwait": false },
{ "id": "prefill_applicant_info", "label": "Prefill Applicant Info", "resolvesAwait": false }
]
}
]
}
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": { "...": "action-specific, see the provider's page" }
}
The body is { "params": { ... } }; an empty body is valid for actions that take no parameters. The response wraps the action’s output:
{
"output": { "...": "action-specific fields" }
}
| Status | Meaning |
|---|
200 OK | Action performed; output carries its result. |
400 Bad Request | Unknown action for this node, or invalid/missing params. |
401 Unauthorized | No API key was supplied. |
403 Forbidden | The key lacks the cases:callback scope. |
404 Not Found | The handle is unknown, expired, already resolved, or belongs to another tenant. |
502 Bad Gateway | The upstream provider rejected or failed the action. |
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.
Quick reference
| Action | Method and path | Scope |
|---|
| Submit a case | POST /cases | cases:write |
| Get a case | GET /cases/{caseId} | any valid key |
| List paused nodes on a case | GET /cases/{caseId}/pending | cases:read |
| Invoke a pending action | POST /pending/{handle}/actions/{actionId} | cases:callback |
| Deliver an external callback | POST /cases/{caseId}/callbacks/{callbackRef} | cases:callback |
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.