This document is identical in every Didit server SDK repository: sdk-python, sdk-node, sdk-go, sdk-java, sdk-php, sdk-ruby and sdk-dotnet.
It is the contract that keeps the seven SDKs consistent.
A change to it is made in all seven repositories, in the same words.
Where this document and an SDK disagree, the SDK is wrong.
Version 1, 2026-10-17.
A backend developer integrates Didit in minutes, in their own language, with an SDK that feels native to that language. The SDK covers the whole public server-side API, with real types for every request and response, and it stays current as the API changes. Quality bar: the official Stripe, OpenAI and Twilio libraries. The SDKs are thin, predictable and boring: no hidden magic, no framework lock-in, minimal dependencies.
| Language | Repository | Package | Import / namespace | Minimum runtime |
|---|---|---|---|---|
| Python | didit-protocol/sdk-python |
PyPI didit-sdk (see 2.1) |
import didit |
Python 3.10 |
| TypeScript / JavaScript | didit-protocol/sdk-node |
npm @didit-protocol/sdk-node |
import Didit from "@didit-protocol/sdk-node" |
Node 18 (also Bun, Deno, edge runtimes with fetch) |
| Go | didit-protocol/sdk-go |
module github.com/didit-protocol/sdk-go |
package didit |
Go 1.22 |
| Java / Kotlin | didit-protocol/sdk-java |
Maven me.didit:didit-java |
me.didit |
Java 11 |
| PHP | didit-protocol/sdk-php |
Packagist didit-protocol/sdk-php |
Didit\ |
PHP 8.1 |
| Ruby | didit-protocol/sdk-ruby |
RubyGems didit-sdk (the name didit is taken) |
require "didit", module Didit |
Ruby 3.1 |
| C# / .NET | didit-protocol/sdk-dotnet |
NuGet Didit |
namespace Didit |
.NET 8 and .NET Standard 2.0 |
The repository names follow the existing client SDKs (sdk-web, sdk-ios, sdk-android, sdk-flutter, sdk-react-native).
The Maven group me.didit is the one sdk-android already publishes under.
PyPI didit belongs to an unrelated project from 2013.
PyPI didit-sdk is a community package by Agustin Saiz (MIT, unofficial, sessions only).
He transferred the didit-sdk project to Didit, and the official SDK published 1.0.0 under that name on 2026-10-29.
As agreed with him, his releases 0.1.0 to 0.4.0 stay published, his PyPI user stays on the project, and the 1.0.0 release notes credit the initial community versions.
Every SDK is generated from Didit's published OpenAPI documents and checked against them in CI.
File in openapi/ |
Upstream | Base URL |
|---|---|---|
verification.json |
https://docs.didit.me/openapi-25.json (mintlify-docs openapi-25.json, branch main) |
https://verification.didit.me |
account.json |
https://docs.didit.me/openapi-auth.json (mintlify-docs openapi-auth.json) |
https://apx.didit.me/auth/v2 |
overlay.json |
this repository | - |
webhook-vectors.json |
generated from service-didit-verification/src/sessions/utils/webhooks.py |
- |
SOURCE.json |
URL, SHA-256 and fetch time of each upstream file | - |
The upstream files are copied byte for byte and never edited by hand.
overlay.json holds the SDK naming (section 4) for operations whose upstream definition does not carry it yet.
When an operation carries x-didit-sdk upstream, the upstream value wins and the overlay entry is deleted.
The Authentication runtime (section 10) has no OpenAPI document yet; mintlify-docs will publish openapi-authentication.json, and until then that module is hand-written from section 10.
Every repository has .github/workflows/spec-sync.yml, run daily and on workflow_dispatch.
It downloads the upstream files, and when any of them changed it regenerates the code, runs the full test suite and opens a pull request titled chore(spec): sync API spec YYYY-MM-DD.
The pull request body lists added, removed and changed operations and schemas, and calls out anything breaking.
A new server operation without SDK naming fails the generator with a message that names the operation; the fix is an entry in overlay.json (or upstream).
The sync never deletes a public SDK method silently: a removed operation is marked deprecated for one minor version first, and the pull request says so.
Every operation has an x-didit-sdk object:
{"resource": "sessions.imports", "method": "create", "auth": "api_key", "server": true, "scope": "application"}resource: dot path of snake_case resource names; each segment becomes a nested resource accessor.method: snake_case method name on that resource.auth:api_key(headerx-api-key),account_token(Bearer fromaccount.login),none,device_tokenorpublic.server:falsefor operations that use an end-user or device token (Session-Token,X-Transaction-Token, case-request tokens, Travel Rule pickup and widget links). Server SDKs do not expose them.scope: "application": the path starts with/v3/organization/{organization_id}/application/{application_id}/. The SDK fills both ids from the client options, so methods never take them as arguments. Calling such a method without both options raises a configuration error before any request is sent.
Convert names to the language's idiom and nothing else:
| Language | Example: sessions.imports.create |
|---|---|
| Python | client.sessions.imports.create(...) and await client.sessions.imports.create(...) on AsyncDidit |
| TypeScript | await didit.sessions.imports.create({...}) |
| Go | client.Sessions.Imports.Create(ctx, params) |
| Java | client.sessions().imports().create(params) plus createAsync returning CompletableFuture |
| PHP | $didit->sessions->imports->create([...]) |
| Ruby | didit.sessions.imports.create(...) |
| C# | await client.Sessions.Imports.CreateAsync(...) |
The full resource tree (version 1, 248 server operations in 47 resources) is in overlay.json.
Highlights: sessions, sessions.imports, sessions.reviews, the standalone APIs (id_verification.verify, face_match.compare, face_search.search, passive_liveness.check, age_estimation.estimate, proof_of_address.verify, document_ai.analyze, aml.screen, database_validation.validate, phone.send_code, email.check_code, ip.assess_risk, kyb.search), id_wallets, fraud_checks, users, businesses, transactions, transactions.rules, transactions.travel_rule, wallet_screening, travel_rule, travel_rule.wallet_addresses, workflows, questionnaires, webhooks.destinations, billing, biometric_templates, customization, cases (with notes, links, checklist, requests, teams, routing_rules, blueprints, regulatory_reports), regulatory_reports, report_templates, networks, and account (account.organizations, account.applications).
Two layers, kept strictly apart.
Generated layer (resources/ and types/ or the language equivalent): one class or module per resource, one method per operation, one type per request and response schema.
Every generated file starts with Code generated by codegen from openapi/*.json. DO NOT EDIT.
CI regenerates and fails when the result differs from what is committed.
The generator lives in the repository (codegen/), is written in the SDK's own language or in Python, and reads only openapi/.
Core layer (hand-written, small, fully tested): configuration, HTTP transport, authentication headers, retries, timeouts, errors, pagination, multipart and binary bodies, webhook verification (section 9) and the Authentication helpers (section 10).
- A
$refcomponent keeps its component name in the language's case (SessionDecision). - An inline request body becomes
<Resource><Method>Params(SessionsImportsCreateParams); an inline response becomes<Resource><Method>Response. - Inline nested objects take their parent's name plus the property name (
SessionsCreateParamsContactDetails). - Enums are open: unknown values from the API must never raise. Python uses
Literal[...] | str; TypeScript a string union plus(string & {}); Go, Java and C# a string-backed type with constants for the known values; PHP string constants; Ruby frozen string constants. - Unknown response fields are preserved and readable (forward compatibility), never dropped and never an error.
- Distinguish absent from
nullwhere the language allows it (PythonNotGiven, TypeScript optional versus| null, GoOpt[T], JavaOptional/JsonNullable, C#Optional<T>). - Date-time strings are exposed as the language's date-time type; money and score fields keep the spec's type (string decimals stay strings).
Didit(api_key, base_url?, organization_id?, application_id?, timeout = 60s, max_retries = 2, default_headers?, http_client?)
api_keyfalls back to theDIDIT_API_KEYenvironment variable;organization_idandapplication_idtoDIDIT_ORGANIZATION_IDandDIDIT_APPLICATION_ID.base_urldefaults tohttps://verification.didit.me;account_base_urltohttps://apx.didit.me/auth/v2.- Every option can be overridden per request (
with_options(...)or a request options argument). - Headers on every request:
x-api-key,User-Agent: didit-<language>/<version> (<runtime> <version>; <os>),X-Didit-Client: <language>/<version>,Accept: application/json. - The API key is never logged, never included in an error message and never sent to any origin other than
base_url(a paginationnextURL on another origin is refused). - Optional debug logging (
DIDIT_LOG=debug) prints method, path, status, duration and request id, never bodies or headers. - Thread-safe and reusable: one client per process is the documented pattern.
| Error | When |
|---|---|
DiditError |
base of everything below |
APIConnectionError / APITimeoutError |
no response |
APIStatusError |
any non-2xx, with status, body (parsed JSON when possible), headers, request_id |
BadRequestError 400, AuthenticationError 401, PermissionDeniedError 403, NotFoundError 404, ConflictError 409, UnprocessableEntityError 422, RateLimitError 429, InternalServerError 5xx |
subclasses of APIStatusError |
WebhookVerificationError |
section 9, with a machine-readable reason |
ConfigurationError |
missing API key, missing organization or application id |
Retries: up to max_retries (default 2), exponential backoff starting at 0.5 s with full jitter, capped at 8 s, honouring Retry-After (seconds or HTTP date, capped at 60 s).
Retry on connection errors, timeouts, 408, 429 and 5xx for GET, HEAD, PUT and DELETE.
Never retry POST or PATCH after the request may have reached the server (a 5xx or a read timeout); retry them only on a connection that failed before sending, or on 429.
Every response exposes the raw HTTP response (status, headers, request id) through the language's usual pattern (with_raw_response, .asResponse(), a Response field).
- Lists use
limitandoffsetand return{count, next, previous, results}. The SDK returns a page object with those fields plus auto-pagination: Pythonfor x in client.sessions.list(...)andasync for, TypeScriptfor await, Go an iterator (iter.Seq2), JavaIterableandautoPager(), PHP and Rubyeach/auto_paging_each, C#IAsyncEnumerable. - Multipart operations (standalone APIs with images, document uploads, face uploads) accept a path, bytes or a stream, with an optional file name and content type.
- Binary responses (PDF reports, network reports, wallet screening PDFs) return bytes or a stream, never parsed JSON.
webhooks.verify(raw_body, headers, secret, tolerance = 300, allow_simple = false) returns the typed event or raises WebhookVerificationError.
secret may be one secret or a list (rotation).
A second entry point, webhooks.verify_parsed(payload, headers, secret), exists for frameworks that only expose the parsed JSON body; it can check X-Signature-V2 only.
Ship the verifier in the core with no framework dependency; framework examples (FastAPI, Django, Flask, Express, Next.js, Gin, Spring Boot, Laravel, Symfony, Rails, ASP.NET Core) live in examples/, not in the package.
From build_webhook_signature_headers in service-didit-verification/src/sessions/utils/webhooks.py:
- The body is
json.dumps(shorten_floats(payload), sort_keys=True, separators=(",", ":"))in UTF-8, with Python's defaultensure_ascii=True(non-ASCII as\uXXXX). X-Signature: lowercase hex HMAC-SHA256 of the exact body bytes.X-Signature-V2: lowercase hex HMAC-SHA256 of the canonical JSON (9.2). Recommended, because it survives middleware that re-encodes the body.X-Signature-Simple(deprecated, envelope only): HMAC-SHA256 of"{timestamp}:{session_id}:{status}:{webhook_type}", missing fields as empty strings.- During a secret rotation the same three signatures made with the previous secret arrive as
X-Signature-Previous,X-Signature-V2-PreviousandX-Signature-Simple-Previous. X-Timestamp: unix seconds, equal to the body'stimestamp.- The HMAC key is the secret's UTF-8 bytes.
Parse the body, then serialise it exactly like Python's json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False) after shorten_floats:
- Objects: keys sorted by Unicode code point (not UTF-16 code unit order), written as
{"k":v,...}with no whitespace. - Strings: escape
"as\",\as\\, and\n,\r,\t,\b,\fwith their short escapes; every other character below U+0020 as\u00XXwith lowercase hex; everything else literal UTF-8 (including/, U+007F, U+2028 and U+2029). - Numbers: a float with an integral value is written as an integer (
87.0becomes87,1e16becomes10000000000000000); integers in plain decimal; other floats in Pythonreprform, which is the shortest round-trip digits with scientific notation when the decimal exponent is below -4 or at least 16, written1e-05,1e-07,1.5e-05(sign always, at least two exponent digits). true,false,nullas is.
Implementations must parse numbers losslessly where the language allows it (Go json.Number, Java BigDecimal, C# JsonElement raw text, Python and Ruby native), so integers above 2^53 survive.
In TypeScript, canonicalise from the raw body with a token-preserving parser, because JSON.parse loses precision on such integers.
- Read the timestamp from the body's
timestamp(an integer); ifX-Timestampis present it must be equal (timestamp_mismatch). - Reject when
|now - timestamp| > tolerance(timestamp_out_of_tolerance). - For each secret, accept when
X-Signaturematches the HMAC of the raw body, orX-Signature-V2matches the HMAC of the canonical JSON, or their-Previousvariants match. X-Signature-Simple(and its-Previous) counts only withallow_simple = true; even then the result says the body is unauthenticated, and the docs tell the receiver to re-fetch the record from the API.- Compare in constant time, and only well-formed 64-character lowercase hex values.
- No signature header at all is
missing_signature; no match isinvalid_signature; an empty secret is aConfigurationError.
openapi/webhook-vectors.json holds vectors produced by running the backend's own signing functions.
Every SDK test suite runs every vector and asserts the expected outcome, and asserts its canonical JSON equals canonical_v2 byte for byte.
Parse the verified body into a discriminated union on webhook_type: status.updated, data.updated, user.status.updated, user.data.updated, business.status.updated, business.data.updated, activity.created, transaction.created, transaction.status.updated, travel_rule.status.updated, workflow.compliance_changed, kyb.registry_search.resolved, plus an UnknownEvent that keeps the raw payload for any other type.
Envelope fields on every event: event_id, webhook_type, timestamp, created_at, application_id, environment (live or sandbox).
Session events add session_id, business_session_id, session_kind, status, workflow_id, workflow_version, vendor_data, metadata, decision, resubmit_info; transaction events add transaction_id, txn_id, status, score, severity, amount, currency, direction, action_required; Travel Rule events add transaction_id, txn_id, travel_rule_status, rail, direction; user and business events add vendor_user_id or vendor_business_id, vendor_data, status.
Where the spec has a schema for the payload (for example the session decision), reuse the generated type.
Didit Authentication gives each application its own OpenID Connect issuer for the application's end users: hosted sign-in pages, passwords, codes, magic links, passkeys, social and enterprise SSO, MFA, organizations, step-up verification and agent access.
Source: service-didit-auth/src/authentication/ (README, urls.py, runtime_urls.py, host_urls.py) on development; docs in mintlify-docs authentication/ and quickstarts/.
It is in early access and reaches production in the 2026-11-01 release.
The server SDKs ship an authentication module for the customer's backend.
It never takes the API key: it talks to the application's issuer (https://<slug>.auth.didit.me or the customer's own domain), whose discovery document is /.well-known/openid-configuration.
Authentication(issuer)orAuthentication.from_publishable_key("pk_live_..." | "pk_test_...")(the key carries the base64url issuer).- Discovery and JWKS fetched lazily and cached, honouring
Cache-Control(5 minutes today); an unknownkidtriggers at most one refetch per minute. verify_access_token(token, audience?): ES256 only (rejectnone, HS*, RS* and any other algorithm), headertypat+jwt,issequal to the issuer,audwhen given,exp,nbfandiatwith 60 s leeway. Returns typed claims:iss,aud,client_id,sub,sid,jti,iat,exp,vendor_data,amr,org(id,role),verified,act.verify_id_token(token, nonce, audience): same checks plusnonce; claims addauth_time,email,email_verified,name,phone_number.create_pkce()(S256 verifier and challenge),create_state()andcreate_nonce().exchange_code(code, code_verifier, redirect_uri)andrefresh(refresh_token)againstPOST token/; document that refreshes for one session must be serialised, because reusing a consumed refresh token revokes the session.get_session(access_token)(GET session/) for instant revocation checks, andrevoke_session(access_token)(DELETE session/).create_step_up(access_token, action, amount?, currency?, return_to?)(POST step-up/) andretrieve_step_up(access_token, id); document that the backend acts only on statusapproved.- Browser-only routes (hosted pages, password, email and phone codes, passkeys, MFA, social, SSO) are out of scope.
- The console management API for Authentication uses console sessions, not API keys, so it is out of scope until it accepts API keys.
- Authentication events do not exist as webhooks yet; do not invent
auth.*webhook types.
Tests use a locally generated ES256 key pair and a stubbed issuer; never call a real issuer in unit tests.
- Unit tests for the core: configuration, headers, errors per status, retry policy (including the no-retry cases for
POST),Retry-After, timeouts, pagination, multipart, binary responses, origin check, redaction. - Generated tests: one test per server operation from the spec's examples, with a mocked HTTP layer, asserting method, path (ids filled, application scope expanded), query, body encoding and response parsing.
- Contract test: every
server: trueoperation inopenapi/has exactly one SDK method, and nothing else exists. - Webhook vectors (9.3) and Authentication token tests (10).
- Optional live smoke tests against a sandbox application, run only when
DIDIT_SANDBOX_API_KEYis set; never in default CI; they create nothing that costs money beyond sandbox sessions. - Strict static checks: Python
mypy --strictandpyright, ruff; TypeScriptstrictwith noanyin public types, eslint; Gogo vet,staticcheck,golangci-lint; Java-Werrorwith errorprone and nullness annotations; PHP PHPStan level max and PHP-CS-Fixer; Ruby RBS signatures checked with Steep, RuboCop; C# nullable enabled with warnings as errors. - CI matrix over every supported runtime version on Linux, plus macOS and Windows where the language is commonly used there.
- Coverage of the core at 90 % or more; the number is not a substitute for behaviour tests.
Minimal and mainstream.
Python: httpx, pydantic>=2, typing-extensions, and cryptography only for the Authentication module (optional extra didit-sdk[auth]).
TypeScript: no runtime dependencies (fetch, crypto.subtle).
Go: standard library only.
Java: java.net.http and Jackson.
PHP: PSR-18 and PSR-17 with discovery, Guzzle as the suggested default, firebase/php-jwt or an equivalent only for Authentication.
Ruby: net/http from the standard library, jwt only for Authentication.
.NET: System.Net.Http, System.Text.Json, Microsoft.IdentityModel.Tokens only for Authentication.
- Semantic versioning; a breaking change to the public SDK surface is a major version.
- Releases are cut by release-please from conventional commits;
CHANGELOG.mdis generated and never edited by hand. - Release workflows publish with trusted publishing where the registry supports it (PyPI, npm with provenance, RubyGems and NuGet), Maven Central through the Central Portal with signed artifacts (token and signing key in the
maven-centralenvironment), Packagist through its GitHub hook, and Go module tags announced to proxy.golang.org. - 1.0.0 shipped on 2026-10-29 with Alex's approval, and the repositories are public.
- Every release after that publishes straight from the repository: merging the release-please pull request is the approval, and the workflow creates the vX.Y.Z tag and publishes.
- A release created with
GITHUB_TOKENdoes not trigger other workflows, so a repository that publishes from a separaterelease.ymlhas release-please start it withworkflow_dispatchfor the new tag. No personal access token is stored.
- README: one-line pitch ("The official Didit library"), install, a quickstart (create a session, read the decision, verify a webhook), configuration, errors, pagination, webhooks, Authentication, links to docs.didit.me, supported runtimes, and versioning policy.
examples/: runnable examples for the main flows (KYC session, standalone ID verification, AML screening, transaction screening, Travel Rule, Authentication token verification) and webhook receivers for the popular frameworks.- Package metadata with description, keywords (identity verification, KYC, KYB, AML, liveness, transaction monitoring, Travel Rule, Didit), homepage
https://didit.me, docshttps://docs.didit.me, repository and license. - docs.didit.me gets a Server SDKs section and per-operation code samples in every language (OpenAPI
x-codeSamples), published together with the first release. - License MIT (
Copyright (c) 2026 Didit), like the client SDKs.SECURITY.mdpoints to GitHub private vulnerability reporting and support@didit.me.
Any change to a public API endpoint, parameter, request or response field, enum value, error, webhook payload or authentication flow in service-didit-verification or service-didit-auth must update the OpenAPI documents in mintlify-docs in the same change set.
The spec sync (3.1) then opens a pull request in every SDK repository; whoever changed the backend makes sure those pull requests merge, adding hand-written core changes (webhooks, Authentication) where the change needs them.
The rule lives in CLAUDE.md and AGENTS.md of the backends, of mintlify-docs, of every SDK repository, and of the Didit workspace.