Skip to main content

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:
RoleCan do
OWNER / ADMINEverything the scopes allow (reads, writes, deletes, billing).
COMPLIANCE_OFFICERReads + compliance writes (reviews, cases, lists).
DEVELOPERDeveloper resources (workflows, webhooks, API keys, sessions).
READERReads 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). 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:
1

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

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

Review & publish

It stays a reviewable draft until you publish (in the console or via didit_workflow_publish). The live version is never touched.
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.
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.
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 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 passNormalized to
A doc-type array — ["PASSPORT"]that document for every country (passport-only worldwide)
Document names or codesPASSPORT/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 entirelyall 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 configs 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 asynchronousdidit_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 and 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: shortenFloatssortKeysJSON.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.

Troubleshooting

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

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) — @didit-protocol/mcp-server / github.com/didit-protocol/mcp. It always authenticates as a Didit user (there is no API-key mode); run it in either transport:
# Hosted HTTP/OAuth — your client signs the user in via the browser
node dist/http.js   # serves /mcp + /healthz on $MCP_PORT

# stdio (headless) — supply a user Bearer access token
DIDIT_ACCESS_TOKEN=<user-access-token> node dist/index.js
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.

Tools reference

Every tool, grouped by area.

Examples

Prompts and end-to-end conversations.