> ## Documentation Index
> Fetch the complete documentation index at: https://docs.frayme.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Integration guide

> Submit cases, poll for decisions, and receive signed webhooks from the Frayme fraud-analysis platform.

A reference for partners integrating with the Frayme fraud-analysis platform.

<Info>
  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.
</Info>

<Note>
  This guide is the narrative walkthrough. For the field-level reference of each
  endpoint, see the [API reference](/api-reference/introduction); for the events
  Frayme pushes to you, see [Webhooks](/api-reference/webhooks).
</Note>

## 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:

```http theme={null}
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`     | Read a case (`GET /cases/{caseId}`), list its paused nodes (`GET /cases/{caseId}/pending`), and list its durable actions (`GET /cases/{caseId}/actions`) |
| `cases:callback` | Deliver an external callback result, or invoke a pending or durable action                                                                               |
| `tags:read`      | List a case's tags (`GET /cases/{caseId}/tags`)                                                                                                          |
| `tags:write`     | Add or remove a case's tags (`POST` / `DELETE /cases/{caseId}/tags`)                                                                                     |

## Submitting a case

`POST /cases` — requires scope `cases:write`.

### Request body

```json theme={null}
{
  "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"
}
```

<Note>
  Do not send `tenantId` — your tenant is derived from the API key, and any `tenantId` in the body is ignored.
</Note>

<ParamField body="workflowId" type="string" required>
  Provisioned for you by Frayme.
</ParamField>

<ParamField body="workflowVersion" type="integer">
  Pin the version your client expects. If omitted, the workflow's current published version is used.
</ParamField>

<ParamField body="type" type="string" required>
  One of `KYC`, `KYB`, or `Transaction`.
</ParamField>

<ParamField body="payload" type="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`.
</ParamField>

<ParamField body="metadata" type="object">
  Free-form audit fields; surfaced in the portal and event log.
</ParamField>

<ParamField body="subject" type="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](#subject-sub-structs) for the per-type required fields.
</ParamField>

<ParamField body="idempotencyKey" type="string">
  If provided and a case with the same key already exists, the existing case is returned with `200 OK` instead of `201 Created`.
</ParamField>

<ParamField body="eventTimestamp" type="string">
  ISO-8601 timestamp of the originating business event.
</ParamField>

### 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

```json theme={null}
{
  "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 scope `cases:read`.

### Response

```json theme={null}
{
  "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`.

<Tip>
  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.
</Tip>

## Webhooks

Frayme delivers signed HTTPS POSTs to a URL configured for you. The dedicated
[Webhooks](/api-reference/webhooks) page is the canonical reference for every
event, payload shape, and signature-verification detail.

### 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

<Tabs>
  <Tab title="Decision payload">
    Sent for `case.decided`, `case.pending_review`, and `case.decision_overridden`.

    ```json theme={null}
    {
      "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"
    }
    ```
  </Tab>

  <Tab title="External callback dispatch">
    Sent for `external_callback.dispatch`. The receiving system performs its work, then calls back (see [Delivering an external callback](#delivering-an-external-callback)) before `expiresAt`.

    ```json theme={null}
    {
      "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" }
    }
    ```
  </Tab>

  <Tab title="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](#pending-actions-on-a-paused-node).

    ```json theme={null}
    {
      "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.

    The same `node.notification` event carries other `payloadType`s too. A Sumsub node that allows retries emits `sumsub-retry-requested` each time the applicant must resubmit — see [Data sources → Sumsub → Resubmission retries](/data-sources/kyc/sumsub#resubmission-retries).
  </Tab>
</Tabs>

### 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

| 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

```python theme={null}
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 `external_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.

| 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. |

## Tagging a case

Tags are short, non-PII operator labels used to organise and triage cases (for example `high-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`.

```json theme={null}
{ "tag": "high-risk" }
```

Both success responses return the stored tag as `{ "tag", "createdBy", "createdAt" }`. The applier is recorded as your API key (`createdBy: "apikey:<keyId>"`); any `createdBy` in the request body is ignored.

| Status            | Meaning                                                                                                                    |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `201 Created`     | The tag was newly applied.                                                                                                 |
| `200 OK`          | The case already carried the tag (idempotent re-apply); the body reflects the originally stored `createdBy` / `createdAt`. |
| `400 Bad Request` | The tag is empty, exceeds 64 characters, or is not an enabled option in your managed vocabulary.                           |
| `403 Forbidden`   | The key lacks the `tags:write` scope.                                                                                      |
| `502 Bad Gateway` | The tag vocabulary could not be resolved right now; retry.                                                                 |

**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 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](#webhooks)).
* By listing the case's paused nodes (below).

### List paused nodes

`GET /cases/{caseId}/pending` — requires scope `cases:read`.

```json theme={null}
{
  "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 },
        { "id": "prefill_company_info", "label": "Prefill Company Info", "resolvesAwait": false },
        { "id": "reset_level_step", "label": "Reset Verification Steps", "resolvesAwait": false },
        { "id": "reset_applicant", "label": "Reset Applicant", "resolvesAwait": false }
      ]
    }
  ]
}
```

`availableActions` is provider-specific — it lists exactly the `id`s 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`.

```json theme={null}
{
  "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:

```json theme={null}
{
  "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](/data-sources/kyc/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 by `caseId` (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.

<Info>
  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).
</Info>

### 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:

```json theme={null}
{
  "actions": [
    { "brokerId": "sumsub", "id": "regenerate_share_token", "label": "Regenerate Share Token" }
  ]
}
```

### Invoke a durable action

`POST /cases/{caseId}/actions/{brokerId}/{action}` — requires scope `cases:callback`.

```json theme={null}
{
  "params": { "...": "action-specific, see the provider's page" }
}
```

The response wraps the action's output, exactly like a pending action:

```json theme={null}
{
  "output": { "...": "action-specific fields" }
}
```

| Status                     | Meaning                                                               |
| -------------------------- | --------------------------------------------------------------------- |
| `200 OK`                   | Action performed; `output` carries its result.                        |
| `400 Bad Request`          | Invalid body, or a required param is missing.                         |
| `401 Unauthorized`         | No API key was supplied.                                              |
| `403 Forbidden`            | The key lacks the `cases:callback` scope.                             |
| `404 Not Found`            | The case or action is unknown, or belongs to another tenant.          |
| `422 Unprocessable Entity` | The case has no `external_customer_id` to resolve the applicant from. |
| `502 Bad Gateway`          | The upstream provider rejected or failed the action.                  |

<Warning>
  Each invocation is an **independent** vendor call — there is no idempotency, so a retry mints a new artifact. Use the returned value rather than calling again "to be safe".
</Warning>

For Sumsub, see [Data sources → Sumsub → Regenerate a share token](/data-sources/kyc/sumsub#regenerate-a-share-token).

## Quick reference

| Action                       | Method and path                                    | Scope            |
| ---------------------------- | -------------------------------------------------- | ---------------- |
| Submit a case                | `POST /cases`                                      | `cases:write`    |
| Get a case                   | `GET /cases/{caseId}`                              | `cases:read`     |
| List paused nodes on a case  | `GET /cases/{caseId}/pending`                      | `cases:read`     |
| Invoke a pending action      | `POST /pending/{handle}/actions/{actionId}`        | `cases:callback` |
| List durable case actions    | `GET /cases/{caseId}/actions`                      | `cases:read`     |
| Invoke a durable case action | `POST /cases/{caseId}/actions/{brokerId}/{action}` | `cases:callback` |
| Deliver an external callback | `POST /cases/{caseId}/callbacks/{callbackRef}`     | `cases:callback` |
| List a case's tags           | `GET /cases/{caseId}/tags`                         | `tags:read`      |
| Add a tag                    | `POST /cases/{caseId}/tags`                        | `tags:write`     |
| Remove a tag                 | `DELETE /cases/{caseId}/tags?tag=`                 | `tags:write`     |

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.
