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_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 forcase.decided, case.pending_review, and case.decision_overridden.
The body carries the full decision result so you can act without re-fetching
the case.
external_customer_idis present only when the case carried one — it is the stable key to correlate the decision back to your own user record.resultis the same envelope returned byGET /cases/{caseId}:result.decisionis 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.result.workflow_resultis write-once — analyst overrides never mutate it.
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 forexternal_callback.dispatch. The receiving system performs its work,
then posts the result to the one-time callbackUrl (see
Delivering an external callback)
before expiresAt.
Node notification
Sent fornode.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.
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 forcase.rfi_requested when a request for information is raised on a case
— by an analyst in the review console or by a workflow node.
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).
sourcesays who raised the request:analyst(the review console),workflow(a node in the case’s workflow), orapi.requested_byis a prefixed identifier:analyst:<userId>,node:<nodeId>, orapikey:<keyId>.template_id(when a template was used),cc, andwebhook_keyare present only when set.awaits_responseandresponse_expires_atare present when an awaiting Information request workflow node raised the request: the run is paused until the request is answered, cancelled, or the deadline passes.
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.RFI answered
Sent forcase.rfi_answered once an information request is answered. A request
is answered once, so there is at most one answer per rfi_id — but, as with
every event, delivery is at-least-once. Deduplicate on (event_type, rfi_id),
not webhookId: a redelivery carries a fresh webhookId, so the webhookId
rule would let a duplicate through and you could apply the same answer twice.
The payload carries no answer content — only attribution and counts. Fetch
the answers from the case, or receive them in result.workflow_result when an
awaiting workflow node consumed them.
sourceisrfi_responsewhen your backend answered through the API, oranalystwhen an analyst recorded the reply in the console.answered_byisapikey:<keyId>oranalyst:<userId>— both identifiers you already hold, so you can reconcile who answered.webhook_keyis present only when set.
Headers
Every outbound webhook carries these headers.Verifying the signature
Compute the HMAC over the exact bytes of the request body — do not re-serialize the JSON — and compare it toX-Frayme-Signature in constant time.
- Compute HMAC-SHA256 over the raw body bytes; compare hex digests with a
constant-time comparison (for example,
hmac.compare_digest). - If
X-Frayme-Secret-IDis present, select the matching secret from your rotation set before computing the HMAC. - Respond
2xxwithin the per-delivery timeout (about 10 seconds).
Deduplication
Every outbound webhook carries a uniquewebhookId (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 exceptions are the two RFI events —
case.rfi_requested and
case.rfi_answered — which you deduplicate on
(event_type, rfi_id) instead. Both re-publish with a fresh webhookId:
the first when a still-pending request is re-raised, the second on any
redelivery.
Key on the pair, not on rfi_id alone: the two events carry the same
rfi_id for the same request, so a single rfi_id seen-set is already
populated by the time the answer arrives and would silently swallow it.
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.