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

# Advanced & troubleshooting

> Scopes and roles, cross-app/org behavior, the V3 response contract, webhook handling, troubleshooting, security notes, and self-hosting the Didit MCP server.

## Scopes & role permissions

The hosted connector requests two coarse scopes — `didit:management` (workspace operations) and `didit:verification` (running checks). Inside those, **your console role is the real boundary**, enforced server-side on every call:

| Role                 | Can do                                                         |
| -------------------- | -------------------------------------------------------------- |
| `OWNER` / `ADMIN`    | Everything the scopes allow (reads, writes, deletes, billing). |
| `COMPLIANCE_OFFICER` | Reads + compliance writes (reviews, cases, lists).             |
| `DEVELOPER`          | Developer resources (workflows, webhooks, API keys, sessions). |
| `READER`             | Reads only — writes return `403`.                              |

The MCP never escalates beyond your role. A `403` on one tool means your role lacks that permission, not that the whole connection failed.

***

## Cross-app & org resolution

The Didit management API is scoped per application (`/organization/{org}/application/{app}/…`). The MCP resolves scope for you:

* **Single org + app** → resolved automatically; you never pass IDs.
* **Multiple apps** → the cross-app tools (`didit_session_search`, `didit_transaction_search`, `didit_case_search`, `didit_vendor_user_search`, `didit_vendor_business_search`, `didit_analytics`) aggregate over **all** of them in one call, each row tagged with its `organization_id` / `application_id`.
* The per-app list tools (`didit_session_list`, `didit_transaction_list`, …) **auto-span every app** when you don't specify one, so a plain "list my sessions" works even with many apps. Pass `organization_id` + `application_id` to narrow to a single app.

Use `didit_context_get` to see all your organizations and applications (and their IDs) in a single call.

***

## Branching workflows

Simple workflows are a flat list of features. For **conditional logic** — decline on a status, route on an extracted field, gate a step on a match, add a Document-AI request — the MCP drives Didit's **node graph** (the same structure as the console's visual builder). A graph is `{ start_node, nodes }`; each node is a `feature`, `branch`, `status` (terminal), `action`, or `webhook`. Branch rules support operators including **`fuzzy_match`** (string fields, with a `score` 0–100 similarity threshold) on extracted fields like `kyc.extra_fields.profession`, and a `DOCUMENT_AI` feature node requests documents (e.g. proof of funds).

### Editing an existing workflow (recommended)

Real workflows carry **large** feature configs — an OCR `documents_allowed` allow-list can be 150 KB+, plus POA lists and phone-country lists. So **don't** read the whole graph and resend it. Instead edit with small **operations**:

<Steps>
  <Step title="Locate & inspect">
    `didit_workflow_search { workflow_id }` finds it across all your apps. `didit_workflow_get_graph` returns the structure with big configs **summarized** (it never overflows); `didit_workflow_get_field_definitions` lists branchable fields + valid operators.
  </Step>

  <Step title="Edit with ops">
    `didit_workflow_edit_graph` takes a few small ops — `set_next`, `set_node`, `set_branches`, `merge_node_config` — and the MCP fetches the **full graph server-side**, applies them, validates, auto-creates a **draft**, and saves. Your allow-lists are preserved byte-for-byte and never pass through the request. A validation failure returns `applied: false` (nothing saved).
  </Step>

  <Step title="Review & publish">
    It stays a reviewable draft until you `publish` (in the console or via `didit_workflow_publish`). The live version is never touched.
  </Step>
</Steps>

For example, to insert a branch after ID verification that **rejoins your existing pipeline**, you only send: rewire the OCR node's `next` to a new branch, and add the branch + a terminal `Declined` node + a `DOCUMENT_AI` step (whose `next` points back at the original next step). The other 10 nodes — and their allow-lists — are untouched.

> *"After ID verification, if it was declined, decline; if profession ≈ 'Software Engineer' (≥80%), request proof of funds; then continue."* → one `branch` node: `kyc.status == Declined` → a `Declined` status node; `kyc.extra_fields.profession` `fuzzy_match` "Software Engineer" score 80 → a `DOCUMENT_AI` proof-of-funds node → and an **else** path back into the pipeline.

<Note>
  **A branch always needs an explicit else.** The catch-all (the path taken when no condition matches) must be a branch with empty `rules` — e.g. `{ "id": "else", "rules": [], "goto": "<liveness>" }` — not the branch node's bare `next`. The MCP (and the backend) auto-convert a branch node's `next` into an else branch, so a branch never renders as an ambiguous edge with no visible else.
</Note>

To build a **new** workflow from scratch, use `didit_workflow_validate_graph` then `didit_workflow_set_graph` (full graph). Even a plain `didit_workflow_create` is saved as this same node graph — the MCP assembles your `features` list into chained feature nodes ending in an auto-decide status, so the features actually run (it publishes by default; pass `status: "draft"` to keep it unpublished). See [Create Workflow → branching & node-based workflows](/management-api/workflows/create) for the full JSON shape.

### Feature configs and allow-lists

A feature node's `config` restricts what it accepts. The canonical OCR allow-list is per-country → per-document → object: `{ "<ISO3>": { "<CODE>": { "enabled": 1 } } }` (e.g. `{ "ESP": { "P": { "enabled": 1 } } }`). The MCP **normalizes friendlier shapes** for you before validating, so you don't have to hand-write that structure:

| You can pass                                                                                                                                   | Normalized to                                                 |
| ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| A doc-type array — `["PASSPORT"]`                                                                                                              | that document for **every** country (passport-only worldwide) |
| Document **names** or **codes** — `PASSPORT`/`P`, `NATIONAL_ID`/`ID`, `DRIVER_LICENSE`/`DL`, `RESIDENCE_PERMIT`/`RP` (also `SSC`, `HIC`, `TC`) | the canonical code                                            |
| An `"ALL"` country key — `{ "ALL": { "P": 1 } }`                                                                                               | applied to every country                                      |
| Scalar flags — `{ "ESP": { "P": 1 } }`                                                                                                         | `{ "ESP": { "P": { "enabled": 1 } } }`                        |
| Omitted entirely                                                                                                                               | all documents, all countries                                  |

`PROOF_OF_ADDRESS`'s `poa_languages_allowed` is the same idea — pass a language array (`["es"]`) or a map (`{ "es": 1 }`). These conveniences apply wherever you give the MCP a feature config: `didit_workflow_create` features, and the node `config`s in `set_graph` / `edit_graph` / `validate_graph`.

***

## Response shape (V3 contract)

Every tool returns the verbatim JSON of the underlying REST endpoint. Two things to know:

* **Synchronous tools** (workflows, billing, standalone `verify_*`, list management) return their final result inline — act on it the same turn.
* **Session creation is asynchronous** — `didit_session_create` returns a `url` the user must complete; the decision arrives by webhook or `didit_session_get_decision`.

Decision keys are **always plural arrays** with a `node_id` per entry (`id_verifications[]`, `liveness_checks[]`, `face_matches[]`, `aml_screenings[]`, `poa_verifications[]`, `phone_verifications[]`, `email_verifications[]`, `database_validations[]`, `reviews[]`, plus KYB `registry_checks[]`, `document_verifications[]`, `key_people_checks[]`). See [Data models](/reference/data-models) and [Verification statuses](/integration/verification-statuses).

***

## Handling webhooks

Let a webhook deliver the final decision to your backend rather than polling from the agent:

1. The agent calls `didit_webhook_create` once with your public HTTPS endpoint, `webhook_version: "v3"`, and `subscribed_events` (typically `["status.updated", "data.updated"]`). The response includes a `secret_shared_key` — store it as `DIDIT_WEBHOOK_SECRET`.
2. Your backend verifies the canonical **X-Signature-V2** header on every delivery: `shortenFloats` → `sortKeys` → `JSON.stringify` (unescaped unicode) → HMAC-SHA256 → `timingSafeEqual`. Reject if `abs(now − X-Timestamp) > 300` seconds.
3. Deduplicate on `event_id`. Return `2xx` within **5 seconds**; move heavy work to a queue.
4. Didit retries on `5xx` / `404` up to **2 times** (\~1 min, then \~4 min), then drops the delivery — replay failed ones from the **Deliveries** tab in the console.

The agent only needs to *wire* the receiver; it doesn't listen itself. Full contract: [Webhooks](/integration/webhooks).

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="The agent calls a tool and gets a 403">
    Re-authenticate: reconnect / re-run *Log in with Didit*. If only *one* tool 403s, your **role** lacks that permission (e.g. a `READER` attempting a write). (Note: the MCP is Bearer/OAuth only — an `x-api-key` is rejected by every tool, so an API key won't fix a 403.)
  </Accordion>

  <Accordion title="A write or delete didn't run">
    Destructive tools require confirmation in your client. Approve the call, or check that the tool's confirmation prompt isn't being auto-denied. `didit_org_reveal_application_api_key` additionally needs `confirm: true`.
  </Accordion>

  <Accordion title="'organization_id is required' or no results">
    You likely have multiple apps. Use a `*_search` tool (which spans everything) or run `didit_context_get` and pass the `organization_id` / `application_id` you want.
  </Accordion>

  <Accordion title="Browser didn't open for sign-in (hosted)">
    Some clients (Windsurf, Zed) connect through the `mcp-remote` bridge — make sure `npx -y mcp-remote@latest https://mcp.didit.me/mcp` is in the config. Re-open the client to retrigger the OAuth handshake.
  </Accordion>

  <Accordion title="Tools don't appear in the client">
    Confirm the server connected (most clients show a status dot) and that you completed *Log in with Didit*. Restart the MCP host after editing its config.
  </Accordion>
</AccordionGroup>

***

## Security

The MCP acts with your real permissions and can mutate live resources. Keep **"review tool calls before running"** enabled in your client — especially for **Write** and **Destructive** tools (deletes, `org_top_up`, `org_reveal_application_api_key`). The MCP itself stores **no secret** in your config (it uses **Log in with Didit**); and if a tool returns an application API key (a REST credential), don't paste it into a shared chat.

***

## Self-hosting

The server is open source (MIT) — [github.com/didit-protocol/mcp](https://github.com/didit-protocol/mcp). Clone it and run with **Docker** or Node; it always authenticates as a Didit **user** (there is no API-key mode), in either transport:

```bash theme={null}
git clone https://github.com/didit-protocol/mcp.git && cd mcp

# Docker (recommended) — serves /mcp + /healthz on port 3000
docker build -t didit-mcp . && docker run -p 3000:3000 --env-file .env didit-mcp

# …or with Node
npm install && npm run build
node dist/http.js                                          # hosted HTTP/OAuth
DIDIT_ACCESS_TOKEN=<user-access-token> node dist/index.js  # stdio (headless)
```

Base URLs and OAuth endpoints are environment variables with public defaults (`verification.didit.me`, `apx.didit.me`, `business.didit.me`) — override them for a private deployment. See the repository's `README` and `ARCHITECTURE` for the full environment-variable reference and the resource-server / authorization-server split.

***

<CardGroup cols={2}>
  <Card title="Tools reference" icon="wrench" href="/integration/mcp/tools">
    Every tool, grouped by area.
  </Card>

  <Card title="Examples" icon="comments" href="/integration/mcp/examples">
    Prompts and end-to-end conversations.
  </Card>
</CardGroup>
