Skip to main content
Rather than polling, let Frayme push updates to you. Frayme delivers signed HTTPS POSTs to a URL configured for your tenant whenever a case decision is reached (or later overridden), when a workflow node asks an external system to act, or when a node emits a notification mid-run.
Your webhook URL, signing secret, secret ID, and retry policy are provisioned for you by the Frayme team. These will move to self-service in the portal.

Events

event_typeWhenPayload
case.decidedWorkflow finished with approved or declined.Decision
case.pending_reviewWorkflow finished with in_review; the case is now in a queue.Decision
case.decision_overriddenAn analyst overrode an existing decision.Decision
external_callback.dispatchA workflow node paused for an external system. Frayme is asking that system to do work and post back to a one-time callback URL.Callback dispatch
node.notificationA 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.Node notification
case.rfi_requestedA request for information was raised on a case (by an analyst, a workflow node, or an API call). Your systems send the resulting email — Frayme never does.RFI requested
event_type is always snake_case. Decision events and case.rfi_requested also use snake_case for their top-level identity keys (case_id, tenant_id); the external_callback.dispatch and node.notification events use camelCase (caseId, tenantId). The examples below reflect the exact keys on the wire.

Payload shapes

Decision events

Sent for case.decided, case.pending_review, and case.decision_overridden. The body carries the full decision result so you can act without re-fetching the case.
{
  "webhookId": "8f1d2e6c-3a4b-4c5d-9e8f-0a1b2c3d4e5f",
  "event_type": "case.decided",
  "case_id": "case_01HABCXYZ",
  "external_customer_id": "cust-00481",
  "tenant_id": "tenant_01HABCXYZ",
  "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" }
  },
  "timestamp": "2026-05-19T14:32:04Z"
}
  • external_customer_id is present only when the case carried one — it is the stable key to correlate the decision back to your own user record.
  • result is the same envelope returned by GET /cases/{caseId}: result.decision is 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.
  • result.workflow_result is write-once — analyst overrides never mutate it.
For a case.decision_overridden event, the new result.decision reflects the analyst’s override and a new entry is appended to result.decisionHistory.

External callback dispatch

Sent for external_callback.dispatch. The receiving system performs its work, then posts the result to the one-time callbackUrl (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://core.us.api.frayme.io/cases/case_01HABCXYZ/callbacks/cbk_oneTimeToken",
  "expiresAt": "2026-05-19T15:32:04Z",
  "payload": { "...": "node-specific input" }
}

Node notification

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"
  }
}
payloadType and tag appear only when configured on the node. The output keys depend on the node’s provider and configuration. A Sumsub node that allows retries emits a node.notification with payloadType: sumsub-retry-requested each time the applicant must resubmit — see Data sources → Sumsub → Resubmission retries.

RFI requested

Sent for case.rfi_requested when a request for information is raised on a case — by an analyst in the review console, a workflow node, or your own API call. Frayme never sends the email itself; your systems send it to the customer using this payload. Delivered to your tenant’s default webhook configuration (or the one named by webhook_key).
{
  "webhookId": "b7c1a2d3-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
  "event_type": "case.rfi_requested",
  "tenant_id": "tenant_01HABCXYZ",
  "case_id": "case_01HABCXYZ",
  "rfi_id": "rfi_01HABCXYZ",
  "case_type": "KYC",
  "to": "maria.silva@example.com",
  "subject": "One more document to verify your account",
  "body": "Hi Maria, please reply with a photo of your proof of address.",
  "requested_by": "analyst:user_01HABCXYZ",
  "source": "analyst",
  "timestamp": "2026-05-19T14:40:00Z"
}
  • source is one of analyst, workflow, or api — who raised the request. requested_by is a prefixed identifier: analyst:<userId>, node:<nodeId>, or apikey:<keyId>.
  • template_id (when a template was used), cc, and webhook_key are present only when set.
This event carries PIIto, cc, subject, and body are the customer-facing email content. Handle and store it accordingly, and keep it out of your logs.
Deduplicate this event on rfi_id, not webhookId. Re-raising a still-pending RFI legitimately re-publishes the event with a fresh webhookId, so the webhookId rule below would let a duplicate through.

Headers

Every outbound webhook carries these 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

Compute the HMAC over the exact bytes of the request body — do not re-serialize the JSON — and compare it to X-Frayme-Signature in constant time.
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-SHA256 over the raw body bytes; compare hex digests with a constant-time comparison (for example, hmac.compare_digest).
  2. If X-Frayme-Secret-ID is present, select the matching secret from your rotation set before computing the HMAC.
  3. Respond 2xx within the per-delivery timeout (about 10 seconds).

Deduplication

Every outbound webhook carries a unique webhookId (UUID). 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 short-circuit any event whose webhookId you have seen before. The one exception is case.rfi_requested: deduplicate it on rfi_id, because re-raising a still-pending RFI re-publishes with a fresh webhookId.

Retries

A non-2xx response or a timeout is retried with exponential backoff — each wait is twice the last (1s, 2s, 4s, 8s, …) — up to a configured maximum number of attempts (5 by default). After the final attempt the delivery is recorded as failed and not retried further, so treat webhooks as best-effort and reconcile with GET /cases/{caseId} if you must be certain of a case’s current decision.