Scopes & role permissions
The hosted connector requests two coarse scopes —didit:management (workspace operations) and didit:verification (running checks). Both boundaries are enforced server-side on every call: the token carries the scopes you approved, and within them your console role decides what you can do. Effective access is the intersection, so approving only didit:verification keeps the management surface closed even for an OWNER:
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 itsorganization_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. Passorganization_id+application_idto narrow to a single app.
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 OCRdocuments_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.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.” → onebranchnode:kyc.status == Declined→ aDeclinedstatus node;kyc.extra_fields.professionfuzzy_match“Software Engineer” score 80 → aDOCUMENT_AIproof-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.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.
NFC skip-reason branches with MCP
For a new graph, ask:“Create a draft workflow with ID verification followed by NFC. IfThe agent should callnfc.skip_reasonisDEVICE_WITHOUT_NFCorDOCUMENT_WITHOUT_CHIP, send the session to In Review; otherwise approve it. Keep an explicit else branch and validate the graph before saving.”
didit_workflow_get_field_definitions, didit_workflow_validate_graph, then didit_workflow_set_graph. For an existing workflow, ask:
“Edit workflowThe agent should call26d33c04…: after its NFC node, route any of the six knownnfc.skip_reasonvalues to In Review and preserve the existing successful-NFC path as the explicit else. Save a draft; do not publish it.”
didit_workflow_search, didit_workflow_get_graph, didit_workflow_get_field_definitions, then didit_workflow_edit_graph. The edit tool applies small operations server-side, so it preserves unrelated nodes and large allow-lists. See Create Workflow → Branch on why NFC did not run for the six values, equals/in rules, null behavior, and a complete ordered graph.
Feature configs and allow-lists
A feature node’sconfig 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:
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 asynchronous —
didit_session_createreturns aurlthe user must complete; the decision arrives by webhook ordidit_session_get_decision.
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:- The agent calls
didit_webhook_createonce with your public HTTPS endpoint,webhook_version: "v3", andsubscribed_events(typically["status.updated", "data.updated"]). The response includes asecret_shared_key— store it asDIDIT_WEBHOOK_SECRET. - Your backend verifies the canonical X-Signature-V2 header on every delivery:
shortenFloats→sortKeys→JSON.stringify(unescaped unicode) → HMAC-SHA256 →timingSafeEqual. Reject ifabs(now − X-Timestamp) > 300seconds. - Deduplicate on
event_id. Return2xxwithin 5 seconds; move heavy work to a queue. - Didit retries on
5xx/404up to 2 times (~1 min, then ~4 min), then drops the delivery — replay failed ones from the Deliveries tab in the console.
Troubleshooting
The agent calls a tool and gets a 403
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.)A write or delete didn't run
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, didit_session_delete, and didit_session_batch_delete additionally need confirm: true in the tool arguments - without it the call is rejected before anything runs.'organization_id is required' or no results
'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.Browser didn't open for sign-in (hosted)
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.Tools don't appear in the client
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.
VS Code (or another client) can't discover OAuth / never opens a browser
VS Code (or another client) can't discover OAuth / never opens a browser
Make sure you’re pointed at
https://mcp.didit.me/mcp, not the bare https://mcp.didit.me origin — the resource metadata some clients probe for is served at /.well-known/oauth-protected-resource/mcp, scoped to that path. If you’re self-hosting, this usually means MCP_RESOURCE_URI is set to the origin instead of the full transport URL; see Self-hosting.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. Clone it and run with Docker or Node; it always authenticates as a Didit user (there is no API-key mode), in either transport: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.
MCP_RESOURCE_URI must match your deployment’s actual Streamable HTTP transport endpoint, including the path — for example https://your-host.example.com/mcp, not just the bare origin. The server derives its RFC 9728 protected-resource metadata URL from this value, so a mismatch breaks OAuth discovery for clients (such as VS Code) that request the path-specific metadata document.
Tools reference
Every tool, grouped by area.
Examples
Prompts and end-to-end conversations.