Skip to main content
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:
ScopeGrants
cases:writeSubmit new cases (POST /cases)
cases:readList the paused nodes on a case (GET /cases/{caseId}/pending)
cases:callbackDeliver 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.
workflowId
string
required
Provisioned for you by Frayme.
workflowVersion
integer
Pin the version your client expects. If omitted, the workflow’s current published version is used.
type
string
required
One of KYC, KYB, or Transaction.
payload
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.
metadata
object
Free-form audit fields; surfaced in the portal and event log.
subject
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.
idempotencyKey
string
If provided and a case with the same key already exists, the existing case is returned with 200 OK instead of 201 Created.
eventTimestamp
string
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):
typeNotes
cpfBrazilian individual taxpayer ID
cnpjBrazilian company registry ID
passportRequires country
national_idRequires country
company_registrationRequires country
external_customer_idYour own stable customer/account reference
email
phone
wallet_addressCrypto 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.

Transactiontransaction

FieldTypeRequiredNotes
amountnumberyesMust be > 0.
currencystringyesISO-4217 or a registered crypto-asset code.
amountUsdnumberconditionalRequired (and > 0) when currency is a crypto asset.
directionstringyesoutbound (sender is your customer) or inbound (receiver is your customer).
typestringnoFree-form transaction type label (e.g. pix, wire).
externalTransactionIdstringnoYour reference for the transaction.
partiesarrayyesExactly 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.

KYCperson

FieldTypeRequiredNotes
dateOfBirthstringnoISO-8601.
identifiersarrayyesMust include at least one document identifier (cpf, passport, or national_id) and an external_customer_id.

KYBbusiness

FieldTypeRequiredNotes
legalNamestringyes
countrystringyesISO 3166-1 alpha-2.
identifiersarrayyesMust include a registration identifier (cnpj or company_registration) and an external_customer_id.
relatedPartiesarraynoEach { "role": "owner" | "representative" | "ubo", "displayName": <optional>, "identifiers": [...] }; each related party needs at least one identifier.

Response

{
  "caseId": "case_01HABCXYZ...",
  "requestId": "req_01HABCXYZ...",
  "status": "received"
}
StatusMeaning
201 CreatedNew case accepted.
200 OKCase already existed for the supplied idempotencyKey.
400 Bad RequestMalformed body, unknown type, missing required field, or payload does not match the workflow’s input schema.
403 ForbiddenAPI key is missing the cases:write scope.
404 Not FoundWorkflow 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.
StatusMeaning
receivedCase accepted; workflow not yet finished. result is absent.
completedWorkflow finished. Inspect result.decision.valueapproved, 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_typeWhenBody shape
case.decidedWorkflow finished with approved or declined.Decision payload
case.pending_reviewWorkflow finished with in_review; case is now in a queue.Decision payload
case.decision_overriddenAn analyst overrode an existing decision.Decision payload (the new result.decision reflects the override)
external_callback.dispatchYour 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.notificationA 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"
}

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

HeaderValue
X-Frayme-SignatureHMAC-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-Typeapplication/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
  1. Compute HMAC over the exact bytes of the request body — do not re-serialize the JSON.
  2. Use a constant-time comparison (for example, hmac.compare_digest).
  3. If X-Frayme-Secret-ID is present, pick the matching secret from your rotation set.
  4. 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.
  5. 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.
StatusMeaning
202 AcceptedCallback accepted.
409 ConflictThe callback was already consumed.
404 Not FoundThe token is invalid or expired.
401 UnauthorizedNo API key was supplied.
403 ForbiddenThe 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" }
}
StatusMeaning
200 OKAction performed; output carries its result.
400 Bad RequestUnknown action for this node, or invalid/missing params.
401 UnauthorizedNo API key was supplied.
403 ForbiddenThe key lacks the cases:callback scope.
404 Not FoundThe handle is unknown, expired, already resolved, or belongs to another tenant.
502 Bad GatewayThe 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

ActionMethod and pathScope
Submit a casePOST /casescases:write
Get a caseGET /cases/{caseId}any valid key
List paused nodes on a caseGET /cases/{caseId}/pendingcases:read
Invoke a pending actionPOST /pending/{handle}/actions/{actionId}cases:callback
Deliver an external callbackPOST /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.