diff --git a/api.md b/api.md index 2f44a0a..0f3d022 100644 --- a/api.md +++ b/api.md @@ -2,7 +2,7 @@ # API reference -Every method of `@didit-protocol/sdk-node`: 359 methods, generated from `openapi/`. Path parameters are positional, the other fields go in one `params` object, and every method takes request options as its last argument. Every type named here is exported from the package. _Application scoped_ methods need the `organizationId` and `applicationId` client options; a _page_ can be iterated with `for await`. Webhook verification and Authentication are described in the README. +Every method of `@didit-protocol/sdk-node`: 368 methods, generated from `openapi/`. Path parameters are positional, the other fields go in one `params` object, and every method takes request options as its last argument. Every type named here is exported from the package. _Application scoped_ methods need the `organizationId` and `applicationId` client options; a _page_ can be iterated with `for await`. Webhook verification and Authentication are described in the README. ## `didit.account` @@ -28,6 +28,12 @@ Every method of `@didit-protocol/sdk-node`: 359 methods, generated from `openapi | --------------------------------------------------------------------------- | ------------------------ | ---------------------- | -------------------- | | `didit.account.organizations.list(params?: AccountOrganizationsListParams)` | `GET /organizations/me/` | page of `Organization` | needs `accountToken` | +## `didit.addressValidation` + +| Method | HTTP | Returns | Notes | +| --------------------------------------------------------------------------- | ------------------------------ | --------------------------- | ----- | +| `didit.addressValidation.validate(params: AddressValidationValidateParams)` | `POST /v3/address-validation/` | `AddressValidationResponse` | | + ## `didit.ageEstimation` | Method | HTTP | Returns | Notes | @@ -440,7 +446,7 @@ Every method of `@didit-protocol/sdk-node`: 359 methods, generated from `openapi | Method | HTTP | Returns | Notes | | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------ | | `didit.regulatoryReports.create(params: RegulatoryReportsCreateParams)` | `POST /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/` | `RegulatoryReportDetail` | application scoped | -| `didit.regulatoryReports.delete(reportUuid)` | `DELETE /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/` | nothing | application scoped | +| `didit.regulatoryReports.delete(reportUuid, params?: RegulatoryReportsDeleteParams)` | `DELETE /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/` | nothing | application scoped | | `didit.regulatoryReports.finalize(reportUuid)` | `POST /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/finalize/` | `RegulatoryReportDetail` | application scoped | | `didit.regulatoryReports.list(params?: RegulatoryReportsListParams)` | `GET /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/` | page of `RegulatoryReportDetail` | application scoped | | `didit.regulatoryReports.retrieve(reportUuid)` | `GET /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/` | `RegulatoryReportDetail` | application scoped | @@ -537,16 +543,17 @@ Every method of `@didit-protocol/sdk-node`: 359 methods, generated from `openapi ## `didit.transactions` -| Method | HTTP | Returns | Notes | -| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------- | ------------------ | -| `didit.transactions.create(params: TransactionsCreateParams)` | `POST /v3/transactions/` | `TransactionDetail` | | -| `didit.transactions.createSdkToken(params: TransactionsCreateSdkTokenParams)` | `POST /v3/transactions/sdk-token/` | `TransactionsCreateSdkTokenResponse` | | -| `didit.transactions.generatePdf(transactionUuid)` | `GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/pdf/` | `Blob` | application scoped | -| `didit.transactions.list(params?: TransactionsListParams)` | `GET /v3/transactions/` | page of `TransactionsListResponseResults` | | -| `didit.transactions.listNetworks(transactionUuid)` | `GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/networks/` | `EntityNetworkMembershipList` | application scoped | -| `didit.transactions.listPartyNetworks(transactionUuid, transactionPartyUuid)` | `GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/parties/{transaction_party_uuid}/networks/` | `EntityNetworkMembershipList` | application scoped | -| `didit.transactions.listRelated(transactionId, params?: TransactionsListRelatedParams)` | `GET /v3/transactions/{transaction_id}/related/` | page of `TransactionSummary` | | -| `didit.transactions.retrieve(transactionId)` | `GET /v3/transactions/{transaction_id}/` | `TransactionDetail` | | +| Method | HTTP | Returns | Notes | +| ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------- | ------------------ | +| `didit.transactions.batchDelete(params: TransactionsBatchDeleteParamsSelectedTransactions \| TransactionsBatchDeleteParamsAllTransactions)` | `DELETE /v3/organization/{organization_id}/application/{application_id}/transactions/delete/` | nothing | application scoped | +| `didit.transactions.create(params: TransactionsCreateParams)` | `POST /v3/transactions/` | `TransactionDetail` | | +| `didit.transactions.createSdkToken(params: TransactionsCreateSdkTokenParams)` | `POST /v3/transactions/sdk-token/` | `TransactionsCreateSdkTokenResponse` | | +| `didit.transactions.generatePdf(transactionUuid)` | `GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/pdf/` | `Blob` | application scoped | +| `didit.transactions.list(params?: TransactionsListParams)` | `GET /v3/transactions/` | page of `TransactionsListResponseResults` | | +| `didit.transactions.listNetworks(transactionUuid)` | `GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/networks/` | `EntityNetworkMembershipList` | application scoped | +| `didit.transactions.listPartyNetworks(transactionUuid, transactionPartyUuid)` | `GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/parties/{transaction_party_uuid}/networks/` | `EntityNetworkMembershipList` | application scoped | +| `didit.transactions.listRelated(transactionId, params?: TransactionsListRelatedParams)` | `GET /v3/transactions/{transaction_id}/related/` | page of `TransactionSummary` | | +| `didit.transactions.retrieve(transactionId)` | `GET /v3/transactions/{transaction_id}/` | `TransactionDetail` | | ## `didit.transactions.notes` @@ -606,6 +613,16 @@ Every method of `@didit-protocol/sdk-node`: 359 methods, generated from `openapi | `didit.travelRule.counterpartyContacts.delete(contactUuid)` | `DELETE /v3/travel-rule/counterparty-contacts/{contact_uuid}/` | nothing | | | `didit.travelRule.counterpartyContacts.list(params?: TravelRuleCounterpartyContactsListParams)` | `GET /v3/travel-rule/counterparty-contacts/` | page of `TravelRuleCounterpartyContact` | | +## `didit.travelRule.networks` + +| Method | HTTP | Returns | Notes | +| ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -------------------------------- | ----- | +| `didit.travelRule.networks.requestAccess(network)` | `POST /v3/travel-rule/networks/{network}/request/` | `TravelRuleNetworkParticipant` | | +| `didit.travelRule.networks.retrieve()` | `GET /v3/travel-rule/networks/` | `TravelRuleNetworksOverview` | | +| `didit.travelRule.networks.retrieveQuestionnaire()` | `GET /v3/travel-rule/networks/questionnaire/` | `TravelRuleNetworkQuestionnaire` | | +| `didit.travelRule.networks.submitQuestionnaire()` | `POST /v3/travel-rule/networks/questionnaire/submit/` | `TravelRuleNetworkQuestionnaire` | | +| `didit.travelRule.networks.updateQuestionnaire(params?: TravelRuleNetworksUpdateQuestionnaireParams)` | `PUT /v3/travel-rule/networks/questionnaire/` | `TravelRuleNetworkQuestionnaire` | | + ## `didit.travelRule.transfers` | Method | HTTP | Returns | Notes | @@ -622,6 +639,13 @@ Every method of `@didit-protocol/sdk-node`: 359 methods, generated from `openapi | `didit.travelRule.walletAddresses.list(params?: TravelRuleWalletAddressesListParams)` | `GET /v3/travel-rule/wallet-addresses/` | page of `WalletAddressEntry` | | | `didit.travelRule.walletAddresses.update(entryUuid, params?: TravelRuleWalletAddressesUpdateParams)` | `PATCH /v3/travel-rule/wallet-addresses/{entry_uuid}/` | `WalletAddressEntryUpdate` | | +## `didit.travelRule.walletProofs` + +| Method | HTTP | Returns | Notes | +| --------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------- | ----- | +| `didit.travelRule.walletProofs.decide(proofUuid, params: TravelRuleWalletProofsDecideParams)` | `POST /v3/travel-rule/wallet-proofs/{proof_uuid}/decision/` | `TravelRuleWalletProofReview` | | +| `didit.travelRule.walletProofs.list(params?: TravelRuleWalletProofsListParams)` | `GET /v3/travel-rule/wallet-proofs/` | page of `TravelRuleWalletProofReview` | | + ## `didit.travelRule.workspace` | Method | HTTP | Returns | Notes | diff --git a/openapi/SOURCE.json b/openapi/SOURCE.json index cfff748..969f012 100644 --- a/openapi/SOURCE.json +++ b/openapi/SOURCE.json @@ -1,13 +1,13 @@ { "verification.json": { "url": "https://docs.didit.me/openapi-25.json", - "sha256": "b2658eb22e23f868d2df5fe513c8dcdadce3abf2e35d1bbebce0382c911038db", - "fetched_at": "2026-10-27T06:59:31Z" + "sha256": "12b2fdb05572755d61b93791f6964b7d883ba7b20f7ea9b31543903b6edab8a9", + "fetched_at": "2026-10-29T06:47:08Z" }, "account.json": { "url": "https://docs.didit.me/openapi-auth.json", - "sha256": "5fafb88b306ab60c7c105b0bf00fa2a6a49b5d2ca8115eb003b332f073844b39", - "fetched_at": "2026-10-27T06:59:31Z" + "sha256": "ae1b5e07df822fe93cae27a9eaa0790948069352c9d98c87eecf7efd644c92cb", + "fetched_at": "2026-10-29T06:47:08Z" }, "webhook-vectors.json": { "generator": "product-automations scripts/server-sdks/make_vectors.py (seeded 2026-10-17)", diff --git a/openapi/account.json b/openapi/account.json index ce6a041..b832f39 100644 --- a/openapi/account.json +++ b/openapi/account.json @@ -25,7 +25,7 @@ "/programmatic/register/": { "post": { "summary": "Register Account", - "description": "Step 1 of programmatic onboarding. Creates a pending account and emails a 6-char code (10 min TTL). Rate limit: 5/IP/hour.", + "description": "Step 1 of programmatic onboarding. Creates a pending account and emails a 6-char code (10 min TTL). The address is checked before any account is created: one that cannot receive the code returns 400 `email_undeliverable`. Rate limits: 5/IP/hour, plus a shared hourly budget per network (/24 for IPv4, /64 for IPv6).", "operationId": "programmatic_register", "x-didit-sdk": { "resource": "account", @@ -82,11 +82,18 @@ } }, "400": { - "description": "Validation error. The intended envelope is `{\"field\": [\"...\"]}` for field-level errors and `{\"detail\": \"...\"}` for business-rule errors (e.g. duplicate email). Password-strength rules currently respond with a bare array `[\"...\"]`, one rule at a time -- known server-side inconsistency; treat it as `{\"password\": [\"...\"]}` until fixed.", + "description": "Validation error. The intended envelope is `{\"field\": [\"...\"]}` for field-level errors and `{\"detail\": \"...\"}` for business-rule errors (e.g. duplicate email). Password-strength rules currently respond with a bare array `[\"...\"]`, one rule at a time -- known server-side inconsistency; treat it as `{\"password\": [\"...\"]}` until fixed. When the address cannot receive the code (disposable, reserved or test domain, no mail server, a likely typo, or a previously bounced address), the body is `EmailUndeliverableError` with `code: \"email_undeliverable\"`; no account is created or changed.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationError" + "oneOf": [ + { + "$ref": "#/components/schemas/ValidationError" + }, + { + "$ref": "#/components/schemas/EmailUndeliverableError" + } + ] }, "examples": { "MissingFields": { @@ -145,13 +152,30 @@ "value": { "detail": "An account with this email already exists." } + }, + "EmailUndeliverable": { + "summary": "Address cannot receive email (disposable domain)", + "value": { + "detail": "This email can't receive messages. Please use a different email address.", + "code": "email_undeliverable", + "reason": "disposable_domain" + } + }, + "EmailLikelyTypo": { + "summary": "Likely typo in the domain", + "value": { + "detail": "This email can't receive messages. Please use a different email address.", + "code": "email_undeliverable", + "reason": "possible_typo", + "suggestion": "gmail.com" + } } } } } }, "429": { - "description": "Rate limited. More than 5 registration attempts from the same IP in the last hour. Also returned, with a `Retry-After: 5` header, when the service is momentarily checking as many passwords or codes as it can at once; retry after the indicated number of seconds.", + "description": "Rate limited. More than 5 registration attempts from the same IP in the last hour, or the hourly budget for the caller's network (/24 for IPv4, /64 for IPv6, shared by registration and code resends) is used up. Both carry `Retry-After: 3600`. Also returned, with a `Retry-After: 5` header, when the service is momentarily checking as many passwords or codes as it can at once; retry after the indicated number of seconds.", "content": { "application/json": { "schema": { @@ -472,6 +496,27 @@ } } }, + "403": { + "description": "The account was recovered and must set up a new sign-in method before it can log in. A link to do that was emailed to the account's address.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecoverySetupRequiredError" + }, + "examples": { + "RecoverySetupRequired": { + "summary": "Recovered account with no sign-in method yet", + "value": { + "recovery_setup_required": true, + "setup_link_sent": true, + "message": "You recovered your account. Open the link we emailed you to set up a new sign-in method.", + "code": "recovery_setup_required" + } + } + } + } + } + }, "429": { "description": "Account locked (too many recent failures on this email) or IP rate-limited (too many attempts from this address). Also returned, with a `Retry-After: 5` header, when the service is momentarily checking as many passwords or codes as it can at once; retry after the indicated number of seconds.", "content": { @@ -835,11 +880,18 @@ } }, "403": { - "description": "You cannot create applications in this organization. A member of the organization who is not an owner or admin gets `Only organization owners and admins can manage applications.` An owner or admin whose access to the organization is suspended gets `Your access to this organization is suspended.` A request without an `Authorization` header gets `You do not have permission to perform this action.`", + "description": "You cannot create applications in this organization. A member of the organization who is not an owner or admin gets `Only organization owners and admins can manage applications.` An owner or admin whose access to the organization is suspended gets `Your access to this organization is suspended.` A request without an `Authorization` header gets `You do not have permission to perform this action.` For 72 hours after you recovered your account the request is refused with `code` `recovery_hold` and `until`, the time the hold ends.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AuthError" + "oneOf": [ + { + "$ref": "#/components/schemas/AuthError" + }, + { + "$ref": "#/components/schemas/RecoveryHoldError" + } + ] }, "examples": { "NotOwnerOrAdmin": { @@ -859,6 +911,14 @@ "value": { "detail": "You do not have permission to perform this action." } + }, + "RecoveryHold": { + "summary": "Account recovered less than 72 hours ago", + "value": { + "detail": "This action is on hold because you recovered your account.", + "code": "recovery_hold", + "until": "2026-11-04T10:15:00+00:00" + } } } } @@ -1596,7 +1656,7 @@ "tags": [ "Programmatic Auth" ], - "description": "Resend the 6-character verification code for an unverified programmatic account. Requires the account's email and password so only the owner can trigger a new code (no console Basic Auth). A fresh code replaces the previous one and is valid for 10 minutes.\n\nRate limits: a per-email cooldown of 60 seconds between resends (HTTP 429 with `Retry-After`), and the same 5-per-IP-per-hour budget as registration.", + "description": "Resend the 6-character verification code for an unverified programmatic account. Requires the account's email and password so only the owner can trigger a new code (no console Basic Auth). A fresh code replaces the previous one and is valid for 10 minutes.\n\nRate limits: a per-email cooldown of 60 seconds between resends (HTTP 429 with `Retry-After`), and the same 5-per-IP-per-hour and per-network hourly budgets as registration.", "requestBody": { "required": true, "content": { @@ -1678,7 +1738,7 @@ } }, "429": { - "description": "Cooldown active (60s between resends) or IP budget exhausted (5/hour, shared with registration). Also returned, with a `Retry-After: 5` header, when the service is momentarily checking as many passwords or codes as it can at once; retry after the indicated number of seconds.", + "description": "Cooldown active (60s between resends) or IP or network budget exhausted (5/IP/hour and an hourly per-network budget, both shared with registration). Also returned, with a `Retry-After: 5` header, when the service is momentarily checking as many passwords or codes as it can at once; retry after the indicated number of seconds.", "content": { "application/json": { "examples": { @@ -1925,7 +1985,7 @@ }, "api_key": { "type": "string", - "description": "Long-lived secret (also called `client_secret`). Use as the `x-api-key` header for every call to `https://verification.didit.me/v3/...` (sessions, workflows, AML, etc.). Treat as a credential; never expose client-side. New keys start with `sk_live_` (live application) or `sk_test_` (sandbox application), followed by 43 URL-safe characters (letters, digits, `-` and `_`). Keys created before this format have no prefix and keep working unchanged.", + "description": "Long-lived secret (also called `client_secret`). Use as the `x-api-key` header for every call to `https://verification.didit.me/v3/...` (sessions, workflows, AML, etc.). Treat as a credential; never expose client-side. New keys start with `sk_live_` (live application) or `sk_test_` (sandbox application), followed by 43 URL-safe characters (letters, digits, `-` and `_`). Keys created before this format have no prefix and keep working unchanged. For 72 hours after you recovered your account, list, retrieve and update return an empty string here instead of the key.", "example": "sk_live_example_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, "website_url": { @@ -2245,6 +2305,105 @@ "status", "modules" ] + }, + "EmailUndeliverableError": { + "type": "object", + "description": "Returned by `POST /programmatic/register/` when the address cannot receive the verification code. The check runs before any account is created or changed, so nothing is left behind.", + "required": [ + "detail", + "code", + "reason" + ], + "properties": { + "detail": { + "type": "string", + "description": "Human-readable message.", + "example": "This email can't receive messages. Please use a different email address." + }, + "code": { + "type": "string", + "enum": [ + "email_undeliverable" + ], + "description": "Always `email_undeliverable`." + }, + "reason": { + "type": "string", + "enum": [ + "invalid_address", + "reserved_domain", + "no_mail_records", + "disposable_domain", + "possible_typo", + "suppressed" + ], + "description": "Why the address was refused: `invalid_address` (not a valid address), `reserved_domain` (a reserved or test domain such as `example.com`), `no_mail_records` (the domain has no mail server), `disposable_domain` (a throwaway mailbox provider), `possible_typo` (a likely misspelling of a common provider; see `suggestion`), `suppressed` (the address previously bounced or complained)." + }, + "suggestion": { + "type": "string", + "description": "The likely intended domain, present only when the domain looks like a typo (for example `gmail.com` for `gmial.com`). The API does not accept a confirmation: register again with the corrected address.", + "example": "gmail.com" + } + } + }, + "RecoveryHoldError": { + "type": "object", + "description": "Returned for 72 hours after the account was recovered (lost sign-in methods were reset). Actions that create organizations, applications or secrets are refused until `until`; read operations keep working.", + "required": [ + "detail", + "code", + "until" + ], + "properties": { + "detail": { + "type": "string", + "example": "This action is on hold because you recovered your account." + }, + "code": { + "type": "string", + "enum": [ + "recovery_hold" + ], + "example": "recovery_hold" + }, + "until": { + "type": "string", + "format": "date-time", + "description": "When the hold ends (ISO 8601). Retry the request after this time.", + "example": "2026-11-04T10:15:00+00:00" + } + } + }, + "RecoverySetupRequiredError": { + "type": "object", + "description": "The account was recovered and has no sign-in method set up yet. No tokens are issued; a one-time link to set up a new method was emailed to the account's address. Open it, set up the method, then log in again.", + "required": [ + "recovery_setup_required", + "setup_link_sent", + "message", + "code" + ], + "properties": { + "recovery_setup_required": { + "type": "boolean", + "example": true + }, + "setup_link_sent": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "You recovered your account. Open the link we emailed you to set up a new sign-in method." + }, + "code": { + "type": "string", + "enum": [ + "recovery_setup_required" + ], + "example": "recovery_setup_required" + } + } } } } diff --git a/openapi/verification.json b/openapi/verification.json index 1125811..7736883 100644 --- a/openapi/verification.json +++ b/openapi/verification.json @@ -754,6 +754,17 @@ "account_id": "ABC123" } }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The tags on the session, by name.", + "example": [ + "VIP", + "ONBOARDING Q4" + ] + }, "status": { "type": "string", "enum": [ @@ -1078,6 +1089,19 @@ }, "x-readme-id": "0.4" }, + "tags": { + "type": "array", + "items": { + "type": "string", + "maxLength": 50 + }, + "maxItems": 10, + "description": "Tags added to the session when it is created, by name. Up to 10, each up to 50 characters. Names are case-insensitive and stored in upper case. A name that does not exist yet in the application is created as a new tag. The tags show on the session in the console, where you can filter by them.", + "example": [ + "VIP", + "ONBOARDING Q4" + ] + }, "language": { "type": "string", "description": "Language code (ISO 639-1) for the verification process interface. Controls the language displayed to the end user during verification. If not provided, the browser's language will be automatically detected and used. Check all the supported languages [here](/integration/supported-languages).", @@ -1412,6 +1436,32 @@ } } } + }, + "user_state": { + "type": "string", + "maxLength": 16, + "description": "Where the person lives, when you know it: an ISO 3166-2 subdivision code (`US-IL`, `ES-M`) or a US state code or name (`IL`, `Illinois`). A state whose law requires explicit biometric consent (Illinois, Texas, Colorado) makes the verification flow ask for it before capturing a face, with trigger `declared_state`. An unrecognized value returns `400`. See [Biometric consent](/getting-started/biometric-consent).", + "example": "US-IL" + }, + "biometric_consent": { + "type": "object", + "description": "Explicit biometric consent you already collected from the person. It satisfies the requirement, so the verification flow does not ask again, and the decision records it as `status: \"given\"` with trigger `customer_collected`. An answer the person already gave in the flow is never replaced. See [Biometric consent](/getting-started/biometric-consent).", + "required": [ + "given_at", + "text_version" + ], + "properties": { + "given_at": { + "type": "string", + "format": "date-time", + "description": "When the person gave explicit biometric consent (ISO 8601). Cannot be in the future." + }, + "text_version": { + "type": "string", + "maxLength": 64, + "description": "Version id of the consent text the person accepted, in your own records." + } + } } }, "example": { @@ -2822,7 +2872,7 @@ "status": "Success", "reason": null }, - "fee": 0.03 + "fee": 0.02 }, { "type": "VALID_CODE_ENTERED", @@ -3912,7 +3962,36 @@ } ], "created_at": "2024-07-24T08:54:25.443172Z", - "expires_at": "2025-07-24T08:54:25.443172Z" + "expires_at": "2025-07-24T08:54:25.443172Z", + "biometric_consent": { + "required": false, + "trigger": null, + "status": "not_required", + "given_at": null, + "declined_at": null, + "text_version": null, + "text_hash": null, + "locale": null, + "policy_url": null, + "step": null, + "node_id": null, + "ip_address": null, + "ip_country_code": null, + "ip_state": null, + "ip_city": null, + "user_agent": null, + "detected_ip_address": "83.50.226.71", + "detected_country_code": "ES", + "detected_state": "ES-B", + "detected_city": "Barcelona", + "declared_state": null, + "document_state": null, + "jurisdictions": [], + "retention_months": null, + "reason": null, + "decline_reason": null, + "face_data_deleted_at": null + } } }, "BusinessExample": { @@ -4528,7 +4607,7 @@ "/v3/session/{sessionId}/delete/": { "delete": { "summary": "Delete a verification session (KYC or KYB)", - "description": "Delete a single verification session, User Verification (KYC) or Business Verification (KYB), by its `session_id`. The id is resolved against KYC sessions first, then KYB sessions, so both kinds are deleted through this one URL.\n\n**What is deleted:**\n- The session and every verification attempt it holds, including results that a resubmission replaced: the decision, extracted data, all related feature records (ID verification, face, liveness, face match, AML, registry checks, IP analysis, and so on), its reviews and its activity log.\n- All stored files owned by the session: document front/back photos (full, cropped, and privacy-blurred variants), document-capture videos, portrait crops, NFC chip portrait and signature images, face reference images, liveness videos, face-match source/target images, Proof of Address documents, and any extra uploaded files (KYC); uploaded company documents and extra files (KYB). Electronic Signature documents, signed PDFs, evidence PDFs and signature images (KYC and KYB): their download paths stop working. Previously issued media URLs (`https:///...`) stop resolving.\n- The session's face data. Its face embedding is deleted too, unless you choose to keep an image-free template (see **Face biometric data**).\n- Webhook deliveries still queued for the session, and their retries. Webhooks already delivered are not affected.\n- For a Business Verification (KYB) session: the verifications it created for its key people (KYC) and its corporate owners (KYB), recursively, reported as `deleted_child_verifications`. A verification it only links to, such as an existing approved KYC it reused, is not deleted.\n\nThe session disappears from every read endpoint immediately: `GET /v3/session/{sessionId}/decision/` returns `404` and `GET /v3/sessions/` stops listing it. Ongoing AML monitoring and KYB registry monitoring started by the deleted verifications stop.\n\n**What is not affected:** blocklist entries created from this session (face or document) are **not** removed; manage those with the blocklist endpoints. Share tokens issued for the session are not revoked, but there is nothing left for them to import: an import with one returns `400` with `{\"share_token\": [\"Original session does not exist.\"]}`. Credits already consumed are **not** refunded. The parent User or Business entity is **not** deleted; use `POST /v3/users/delete/` or `POST /v3/businesses/delete/` for those. If the User's profile portrait came from this session, the portrait is removed with it.\n\n**Face biometric data:** by default the session's face embedding is deleted with the session (`face_retention_outcome: deleted`). An application that opted in to biometric-template retention (`face_retention_policy: retain_until_user_deleted` on `PATCH /v3/webhook/`), or a request with `retain_face_embeddings: true`, keeps one separately managed, image-free face biometric template anchored to the session's User (`retained_with_user`) so that duplicate detection, Face Search, and biometric authentication keep working. The template never contains the face image, media, identity fields, the decision, or the deleted session's id; it is biometric data deleted at `biometric_template_expires_at`, or earlier on User deletion, privacy erasure, or an explicit purge through `/v3/biometric-templates/`. A session without a linked User cannot retain a template (`ineligible_no_vendor_user`). Send `deletion_instruction: privacy_erasure` for data-subject requests: it purges every retained template for that User and never retains a new one.\n\n**Irreversible:** deletion is permanent and there is no restore endpoint. Export anything you need (for example a decision PDF) before you call it.\n\n**Side effects:** no webhook is emitted for deletions.\n\n**Response:** `200 OK` once the deletion has completed, with a JSON body reporting the `face_retention_outcome`, the retained template's expiry and, for a KYB session, `deleted_child_verifications`. Earlier versions of this endpoint returned `204 No Content`; update clients that assert on `204`. Keep your `instruction_id` in your own erasure log.\n\n**Idempotency:** not idempotent at the HTTP level: the first call returns `200`; repeating it returns `404` because the session no longer resolves. After a `503` the session is still in place; repeat the same request to finish deleting it.\n\n**Authentication:** send your application's API key in the `x-api-key` header; the session must belong to that application. Console user access tokens (`Authorization: Bearer ...`) may also call this endpoint when they carry the `delete:sessions` permission. Authentication and permission failures both return `403`; this API never returns `401`.\n\n**Rate limit:** shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.\n\nTo bulk-delete KYC sessions by their numeric `session_number`, use `POST /v3/sessions/delete/` instead.", + "description": "Delete a single verification session, User Verification (KYC) or Business Verification (KYB), by its `session_id`. The id is resolved against KYC sessions first, then KYB sessions, so both kinds are deleted through this one URL.\n\n**What is deleted:**\n- The session and every verification attempt it holds, including results that a resubmission replaced: the decision, extracted data, all related feature records (ID verification, face, liveness, face match, AML, registry checks, IP analysis, and so on), its reviews and its activity log.\n- All stored files owned by the session: document front/back photos (full, cropped, and privacy-blurred variants), document-capture videos, portrait crops, NFC chip portrait and signature images, face reference images, liveness videos, face-match source/target images, Proof of Address documents, and any extra uploaded files (KYC); uploaded company documents and extra files (KYB). Electronic Signature documents, signed PDFs, evidence PDFs and signature images (KYC and KYB): their download paths stop working. Previously issued media URLs (`https:///...`) stop resolving.\n- The session's face data. Its face embedding is deleted too, unless you choose to keep an image-free template (see **Face biometric data**).\n- Webhook deliveries still queued for the session, and their retries. Webhooks already delivered are not affected.\n- For a Business Verification (KYB) session: the verifications it created for its key people (KYC) and its corporate owners (KYB), recursively, reported as `deleted_child_verifications`. A verification it only links to, such as an existing approved KYC it reused, is not deleted.\n\nThe session disappears from every read endpoint immediately: `GET /v3/session/{sessionId}/decision/` returns `404` and `GET /v3/sessions/` stops listing it. Ongoing AML monitoring and KYB registry monitoring started by the deleted verifications stop.\n\n**What is not affected:** blocklist entries created from this session (face or document) are **not** removed; manage those with the blocklist endpoints. Share tokens issued for the session are not revoked, but there is nothing left for them to import: an import with one returns `400` with `{\"share_token\": [\"Original session does not exist.\"]}`. Credits already consumed are **not** refunded. The parent User or Business entity is **not** deleted; use `POST /v3/users/delete/` or `POST /v3/businesses/delete/` for those. If the User's profile portrait came from this session, the portrait is removed with it.\n\n**Face biometric data:** by default the session's face embedding is deleted with the session (`face_retention_outcome: deleted`). An application that opted in to biometric-template retention (`face_retention_policy: retain_until_user_deleted` on `PATCH /v3/webhook/`), or a request with `retain_face_embeddings: true`, keeps one separately managed, image-free face biometric template anchored to the session's User (`retained_with_user`) so that duplicate detection, Face Search, and biometric authentication keep working. The template never contains the face image, media, identity fields, the decision, or the deleted session's id; it is biometric data deleted at `biometric_template_expires_at`, or earlier on User deletion, privacy erasure, or an explicit purge through `/v3/biometric-templates/`. A session without a linked User cannot retain a template (`ineligible_no_vendor_user`). Send `deletion_instruction: privacy_erasure` for data-subject requests: it purges every retained template for that User and never retains a new one.\n\n**Irreversible:** deletion is permanent and there is no restore endpoint. Export anything you need (for example a decision PDF) before you call it.\n\n**Side effects:** no webhook is emitted for deletions.\n\n**Response:** `200 OK` once the deletion has completed, with a JSON body reporting the `face_retention_outcome`, the retained template's expiry and, for a KYB session, `deleted_child_verifications`. Earlier versions of this endpoint returned `204 No Content`; update clients that assert on `204`. Keep your `instruction_id` in your own erasure log.\n\n**Idempotency:** not idempotent at the HTTP level: the first call returns `200`; repeating it returns `404` because the session no longer resolves. After a `503` the session is still in place; repeat the same request to finish deleting it.\n\n**Authentication:** send your application's API key in the `x-api-key` header; the session must belong to that application. Console user access tokens (`Authorization: Bearer ...`) may also call this endpoint when they carry the `delete:sessions` permission. Authentication and permission failures both return `403`; this API never returns `401`.\n\n**Rate limit:** shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.\n\nTo bulk-delete KYC sessions by their numeric `session_number`, use `POST /v3/sessions/delete/` instead.\n\n**Filed regulatory reports:** if the selected data is in a finalized regulatory report, the whole request is refused with `409` and nothing is deleted. The body uses `code: in_filed_report` and names each report with `uuid`, `report_number`, `report_type` and `case_uuid`. Delete the reports, then repeat the request.", "operationId": "delete_v3_session_by_id", "x-didit-sdk": { "resource": "sessions", @@ -4770,6 +4849,16 @@ } } } + }, + "409": { + "description": "The session is part of a finalized regulatory report. Nothing is deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InFiledReportError" + } + } + } } }, "security": [ @@ -5639,7 +5728,7 @@ "/v3/id-verification/": { "post": { "summary": "ID Verification (document OCR + fraud checks)", - "description": "OCR an identity document and run document fraud checks in one call — returns the extracted holder fields, the parsed MRZ, an `Approved`/`Declined` `status`, and a `warnings` list explaining every issue found. Supports 14,000+ document types from 220+ countries and territories.\n\n**How it works.** Send the document's `front_image` (required) and, for two-sided documents, the `back_image`. Upload uncropped photos with all four corners of the document visible — the service detects, aligns, and crops the document itself, so do not pre-crop. The document is classified automatically (you never declare the country or document type), the visual zone is read with OCR, and the MRZ and any barcodes are decoded. Cross-checks then validate dates, number formats, and MRZ check digits, and compare the visual zone against the MRZ. With `perform_document_liveness=true`, the images are additionally screened for screen replays, printed copies, and portrait manipulation.\n\n**Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Fraud and hard-failure risks always decline: `DOCUMENT_EXPIRED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, `PUBLIC_DOCUMENT_IMAGE_DETECTED`, plus extraction failures (`NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`, `DOCUMENT_NUMBER_NOT_DETECTED`). (`PORTRAIT_IMAGE_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `INVALID_DATE`, and `MRZ_NOT_DETECTED` exist in workflow sessions but are never produced by this standalone endpoint.) Three risk groups are configurable per request via `invalid_mrz_action`, `inconsistent_data_action`, and `expiration_date_not_detected_action` (`DECLINE` or `NO_ACTION`). All other warnings (e.g. `POSSIBLE_DUPLICATED_USER`) are informational and never decline on their own. A document that cannot be processed at all returns `400` with `{\"error\": \"COULD_NOT_RECOGNIZE_DOCUMENT\"}`; a readable but problematic document returns `200` with `status: \"Declined\"` — always inspect `id_verification.status` and `id_verification.warnings`, not just the HTTP code.\n\n**Billing.** Each `200` response consumes one ID Verification API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing.\n\n**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the cropped document/portrait images are stored and returned as short-lived media URLs (`https:///ocr/...`), and a `status.updated` webhook is emitted to your configured webhook endpoints. When `false`, nothing is stored, `request_id` is a one-off correlation UUID, and `portrait_image`/`front_image`/`back_image` are returned inline as base64-encoded JPEG strings.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (a fictional \"Sandbox User\" USA identity card), no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", + "description": "OCR an identity document and run document fraud checks in one call — returns the extracted holder fields, the parsed MRZ, an `Approved`/`Declined`/`In Review` `status`, and a `warnings` list explaining every issue found. Supports 14,000+ document types from 220+ countries and territories.\n\n**How it works.** Send the document's `front_image` (required) and, for two-sided documents, the `back_image`. Upload uncropped photos with all four corners of the document visible — the service detects, aligns, and crops the document itself, so do not pre-crop. The document is classified automatically (you never declare the country or document type), the visual zone is read with OCR, and the MRZ and any barcodes are decoded. Cross-checks then validate dates, number formats, and MRZ check digits, and compare the visual zone against the MRZ. With `perform_document_liveness=true`, the images are additionally screened for screen replays, printed copies, and portrait manipulation.\n\n**Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Fraud and hard-failure risks always decline: `DOCUMENT_EXPIRED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, `PUBLIC_DOCUMENT_IMAGE_DETECTED`, plus extraction failures (`NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`, `DOCUMENT_NUMBER_NOT_DETECTED`). (`PORTRAIT_IMAGE_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `INVALID_DATE`, and `MRZ_NOT_DETECTED` exist in workflow sessions but are never produced by this standalone endpoint.) Three risk groups are configurable per request via `invalid_mrz_action`, `inconsistent_data_action`, and `expiration_date_not_detected_action` (`DECLINE` or `NO_ACTION`). All other warnings (e.g. `POSSIBLE_DUPLICATED_USER`) are informational and never decline on their own. When `perform_document_liveness=true` (the default) and the forgery check returns no usable score — the provider failed, answered non-200, or answered HTTP 200 with `status=\"ERR\"` - the call returns `200` with `status: \"In Review\"` and a `DOCUMENT_LIVENESS_NOT_COMPUTED` warning (`log_type: \"warning\"`) instead of `Approved`: a check that did not run is not evidence that the document is genuine. This escalation is not configurable and never overrides a `Declined`; with `perform_document_liveness=false` the check is skipped and the status follows the remaining warnings as before. A document that cannot be processed at all returns `400` with `{\"error\": \"COULD_NOT_RECOGNIZE_DOCUMENT\"}`; a readable but problematic document returns `200` with `status: \"Declined\"` — always inspect `id_verification.status` and `id_verification.warnings`, not just the HTTP code.\n\n**Billing.** Each `200` response consumes one ID Verification API credit (paid from your plan credit or top-up balance). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing.\n\n**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the cropped document/portrait images are stored and returned as short-lived media URLs (`https:///ocr/...`), and a `status.updated` webhook is emitted to your configured webhook endpoints. When `false`, nothing is stored, `request_id` is a one-off correlation UUID, and `portrait_image`/`front_image`/`back_image` are returned inline as base64-encoded JPEG strings.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (a fictional \"Sandbox User\" USA identity card), no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", "operationId": "post_v3id-verification", "x-didit-sdk": { "resource": "id_verification", @@ -5684,7 +5773,7 @@ "perform_document_liveness": { "type": "boolean", "default": true, - "description": "Screens both images for presentation fraud: screen replays (`SCREEN_CAPTURE_DETECTED`), printed copies (`PRINTED_COPY_DETECTED`), and portrait manipulation (`PORTRAIT_MANIPULATION_DETECTED`). Any of these auto-declines. Enabled by default; set to `false` to skip fraud screening and reduce latency.", + "description": "Screens both images for presentation fraud: screen replays (`SCREEN_CAPTURE_DETECTED`), printed copies (`PRINTED_COPY_DETECTED`), and portrait manipulation (`PORTRAIT_MANIPULATION_DETECTED`). Any of these auto-declines. Enabled by default; set to `false` to skip fraud screening and reduce latency. When enabled and the provider returns no usable score, the response is `In Review` with a `DOCUMENT_LIVENESS_NOT_COMPUTED` warning rather than `Approved`.", "example": true }, "minimum_age": { @@ -5785,7 +5874,7 @@ }, "responses": { "200": { - "description": "Document processed. `id_verification.status` is `Approved` or `Declined`; every detected issue is itemized in `id_verification.warnings`. A problematic document still returns `200` with `status: \"Declined\"` — inspect the body, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id and image fields are short-lived media URLs; with `save_api_request=false` they are inline base64 JPEG strings.", + "description": "Document processed. `id_verification.status` is `Approved`, `Declined`, or `In Review`; every detected issue is itemized in `id_verification.warnings`. `In Review` is returned when a requested document-liveness check produced no usable result (`DOCUMENT_LIVENESS_NOT_COMPUTED`). A problematic document still returns `200` with `status: \"Declined\"` — inspect the body, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id and image fields are short-lived media URLs; with `save_api_request=false` they are inline base64 JPEG strings.", "content": { "application/json": { "examples": { @@ -6031,9 +6120,10 @@ "type": "string", "enum": [ "Approved", - "Declined" + "Declined", + "In Review" ], - "description": "`Approved` when no warning resolves to a decline; `Declined` otherwise — the `warnings` list explains why." + "description": "`Approved` when no warning resolves to a decline and the requested document-liveness check produced a usable result; `In Review` when that requested check produced no usable result (`DOCUMENT_LIVENESS_NOT_COMPUTED`); `Declined` when a warning resolves to a decline — the `warnings` list explains why." }, "document_type": { "type": "string", @@ -6450,13 +6540,13 @@ }, "warnings": { "type": "array", - "description": "Empty on a clean approval. Every entry explains one detected risk; entries with `log_type: \"error\"` set `status` to `Declined`.", + "description": "Empty on a clean approval. Every entry explains one detected risk; entries with `log_type: \"error\"` set `status` to `Declined`, and `DOCUMENT_LIVENESS_NOT_COMPUTED` (`log_type: \"warning\"`) sets it to `In Review`.", "items": { "type": "object", "properties": { "risk": { "type": "string", - "description": "Machine-readable risk code. Always-decline risks: `DOCUMENT_EXPIRED`, `MINIMUM_AGE_NOT_MET`, `PORTRAIT_IMAGE_NOT_DETECTED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, `PUBLIC_DOCUMENT_IMAGE_DETECTED`, `INVALID_DATE`, `COULD_NOT_RECOGNIZE_DOCUMENT` (400-only, never a warning), `DOCUMENT_NUMBER_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`. Governed by request options: `MRZ_NOT_DETECTED` / `MRZ_VALIDATION_FAILED` (`invalid_mrz_action`), `DATA_INCONSISTENT` / `MRZ_AND_DATA_EXTRACTED_FROM_OCR_NOT_SAME` / `DOCUMENT_NAME_DIFFERENT_FROM_OTHER_APPROVED_DOCUMENTS` (`inconsistent_data_action`), `EXPIRATION_DATE_NOT_DETECTED` (`expiration_date_not_detected_action`). Other risks (e.g. `POSSIBLE_DUPLICATED_USER`) are informational.", + "description": "Machine-readable risk code. Always-decline risks: `DOCUMENT_EXPIRED`, `MINIMUM_AGE_NOT_MET`, `PORTRAIT_IMAGE_NOT_DETECTED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, `PUBLIC_DOCUMENT_IMAGE_DETECTED`, `INVALID_DATE`, `COULD_NOT_RECOGNIZE_DOCUMENT` (400-only, never a warning), `DOCUMENT_NUMBER_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`. Governed by request options: `MRZ_NOT_DETECTED` / `MRZ_VALIDATION_FAILED` (`invalid_mrz_action`), `DATA_INCONSISTENT` / `MRZ_AND_DATA_EXTRACTED_FROM_OCR_NOT_SAME` / `DOCUMENT_NAME_DIFFERENT_FROM_OTHER_APPROVED_DOCUMENTS` (`inconsistent_data_action`), `EXPIRATION_DATE_NOT_DETECTED` (`expiration_date_not_detected_action`). Other risks (e.g. `POSSIBLE_DUPLICATED_USER`) are informational. `DOCUMENT_LIVENESS_NOT_COMPUTED` is raised when `perform_document_liveness=true` but the forgery check returned no usable score; it sets `status` to `In Review`.", "example": "DOCUMENT_EXPIRED" }, "feature": { @@ -6479,7 +6569,7 @@ "warning", "information" ], - "description": "Severity. `error` warnings drive `status` to `Declined`; `information` entries are advisory and never decline on their own. (`warning` is not produced by this endpoint — its options only accept `DECLINE` or `NO_ACTION`.)" + "description": "Severity. `error` warnings drive `status` to `Declined`; `warning` (only `DOCUMENT_LIVENESS_NOT_COMPUTED`, when the requested forgery check produced no usable score) drives `status` to `In Review`; `information` entries are advisory and never change the status." }, "short_description": { "type": "string", @@ -6684,7 +6774,7 @@ "/v3/face-match/": { "post": { "summary": "Face Match", - "description": "Compare two face images (1:1) and get back a similarity `score` (0–100) plus an `Approved`/`Declined` `status`.\n\n**How it works.** Both images are decoded and analyzed by a biometric model (set `rotate_image=true` if captures may be sideways or upside down), then a similarity score between the two faces is computed. A score **strictly above** `face_match_score_decline_threshold` (default `30`) returns `status: \"Approved\"`; a score at or below it returns `Declined` with a `LOW_FACE_MATCH_SIMILARITY` warning. If no face pair can be scored at all, the call still returns `200` with `status: \"Declined\"` and a `NO_REFERENCE_IMAGE` warning — `score` is `null` on the default save path, or `0` with `save_api_request=false`. Submit one clear, front-facing face per image for best results.\n\n**Billing.** Each `200` response consumes one Face Match API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing.\n\n**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, both images are stored as the session's face-match source/target images, and a `status.updated` webhook is emitted to your configured webhook endpoints. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later.\n\n**Face search index.** This endpoint never enrolls faces into your face search index and never runs blocklist or duplicate screening — use `POST /v3/passive-liveness/` or `POST /v3/face-search/` for those.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", + "description": "Compare two face images (1:1) and get back a similarity `score` (0–100) plus an `Approved`/`Declined` `status`.\n\n**How it works.** Both images are decoded and analyzed by a biometric model (set `rotate_image=true` if captures may be sideways or upside down), then a similarity score between the two faces is computed. A score **strictly above** `face_match_score_decline_threshold` (default `30`) returns `status: \"Approved\"`; a score at or below it returns `Declined` with a `LOW_FACE_MATCH_SIMILARITY` warning. If no face pair can be scored at all, the call still returns `200` with `status: \"Declined\"` and a `NO_REFERENCE_IMAGE` warning — `score` is `null` on the default save path, or `0` with `save_api_request=false`. Submit one clear, front-facing face per image for best results.\n\n**Billing.** Each `200` response consumes one Face Match API credit (paid from your plan credit or top-up balance). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing.\n\n**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, both images are stored as the session's face-match source/target images, and a `status.updated` webhook is emitted to your configured webhook endpoints. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later.\n\n**Face search index.** This endpoint never enrolls faces into your face search index and never runs blocklist or duplicate screening — use `POST /v3/passive-liveness/` or `POST /v3/face-search/` for those.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", "operationId": "post_v3face-match", "x-didit-sdk": { "resource": "face_match", @@ -7212,7 +7302,7 @@ "/v3/age-estimation/": { "post": { "summary": "Age Estimation (face age + passive liveness)", - "description": "Estimate a person's age from a single face photo and run a passive liveness check in the same call — returns the model-predicted `age_estimation` (in years), a liveness `score` (0–100), and an `Approved`/`Declined` `status`. The age is **model-predicted from the face, not verified against a document** — use it for age gates and low-friction checks, not as legal proof of age.\n\n**How it works.** The image is analyzed for faces (set `rotate_image=true` if captures may be sideways or upside down); each detected face gets an estimated age, and the largest face in the frame drives `age_estimation`. In parallel, a passive liveness model scores whether the photo shows a live person (no user challenge required) and detects presentation attacks (photos of photos, screens, masks). Submit one clear, front-facing, well-lit face per image for best results.\n\n**Decision logic.** Any warning declines: `status` is `Approved` only when the `warnings` array is empty. A liveness `score` at or below `face_liveness_score_decline_threshold` (default `30`) adds `LOW_LIVENESS_SCORE`; a detected attack adds `LIVENESS_FACE_ATTACK`; no face found by the liveness model adds `NO_FACE_DETECTED`; no estimable face age adds `AGE_NOT_DETECTED` (`age_estimation` is `null`); an estimated age below `age_estimation_decline_threshold` (default `18`) adds `AGE_BELOW_MINIMUM`. Borderline ages near your threshold deserve a fallback to document-based verification — the model's error grows for faces close to the limit.\n\n**Billing.** Each `200` response consumes one Age Estimation API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing.\n\n**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the result is exposed there in `liveness_checks[]`, with `features: [\"AGE_ESTIMATION\"]`), the face crop and reference image are stored, the face embedding is added to your application's face index (used by face search and duplicate detection), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (`age_estimation: 30`, no warnings), no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", + "description": "Estimate a person's age from a single face photo and run a passive liveness check in the same call — returns the model-predicted `age_estimation` (in years), a liveness `score` (0–100), and an `Approved`/`Declined` `status`. The age is **model-predicted from the face, not verified against a document** — use it for age gates and low-friction checks, not as legal proof of age.\n\n**How it works.** The image is analyzed for faces (set `rotate_image=true` if captures may be sideways or upside down); each detected face gets an estimated age, and the largest face in the frame drives `age_estimation`. In parallel, a passive liveness model scores whether the photo shows a live person (no user challenge required) and detects presentation attacks (photos of photos, screens, masks). Submit one clear, front-facing, well-lit face per image for best results.\n\n**Decision logic.** Any warning declines: `status` is `Approved` only when the `warnings` array is empty. A liveness `score` at or below `face_liveness_score_decline_threshold` (default `30`) adds `LOW_LIVENESS_SCORE`; a detected attack adds `LIVENESS_FACE_ATTACK`; no face found by the liveness model adds `NO_FACE_DETECTED`; no estimable face age adds `AGE_NOT_DETECTED` (`age_estimation` is `null`); an estimated age below `age_estimation_decline_threshold` (default `18`) adds `AGE_BELOW_MINIMUM`. Borderline ages near your threshold deserve a fallback to document-based verification — the model's error grows for faces close to the limit.\n\n**Billing.** Each `200` response consumes one Age Estimation API credit (paid from your plan credit or top-up balance). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing.\n\n**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the result is exposed there in `liveness_checks[]`, with `features: [\"AGE_ESTIMATION\"]`), the face crop and reference image are stored, the face embedding is added to your application's face index (used by face search and duplicate detection), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (`age_estimation: 30`, no warnings), no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", "operationId": "post_v3age-estimation", "x-didit-sdk": { "resource": "age_estimation", @@ -7701,7 +7791,7 @@ "/v3/poa/": { "post": { "summary": "Proof of Address (document OCR + address checks)", - "description": "Extract and validate a Proof of Address document — utility bills, bank statements, and government-issued documents — in one call. Returns the issuer, issue/expiry dates, the holder name, the raw and parsed/geocoded address, an `Approved`/`Declined` `status`, and a `warnings` list explaining every issue found.\n\n**Latency.** Extraction is LLM-based: typical calls take **5–15 seconds**, multi-page PDFs up to **~30 seconds**. Configure a client timeout of **at least 45 seconds** and do not retry before the call completes.\n\n**How it works.** The document (image or PDF, up to 15 MB; encrypted PDFs require `document_password`) is classified into a document type and subtype (e.g. `UTILITY_BILL` / `WATER_BILL`), and the issuer, dates, holder name, and address are extracted with LLM-based extraction. The address is parsed and geocoded into `poa_parsed_address`. PDF/EXIF forensics screen for tampering — modification after digital signing, known PDF editors, overlay-text manipulation, suspicious re-exports — surfacing `SUSPECTED_DOCUMENT_MANIPULATION` or `DOCUMENT_METADATA_MISMATCH`. Document age is validated against `poa_document_age_months`, and the language against `poa_languages_allowed`. If you pass `expected_address`, `expected_country`, or `expected_first_name`/`expected_last_name`, the extracted values are cross-checked and mismatches are flagged.\n\n**Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Hard failures always decline: `MISSING_ADDRESS_INFORMATION`, `POA_DOCUMENT_EXPIRED`, `INVALID_DOCUMENT_TYPE`, `UNABLE_TO_VALIDATE_DOCUMENT_AGE`, plus `UNABLE_TO_EXTRACT_ISSUE_DATE` and `POA_NAME_NOT_DETECTED` (not configurable on this endpoint). Five risk groups are configurable per request via the `poa_*_action` options (`DECLINE` or `NO_ACTION`, all defaulting to `DECLINE`). `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION` and `UNPARSABLE_OR_INVALID_ADDRESS` are informational on this endpoint and never decline. A document that cannot be processed at all returns `400`; a readable but problematic document returns `200` with `status: \"Declined\"` — always inspect `poa.status` and `poa.warnings`, not just the HTTP code.\n\n**Billing.** Each `200` response consumes one Proof of Address API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any document processing.\n\n**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the uploaded document is stored, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the decision additionally exposes `document_file` and the `document_metadata` forensics, which this response omits), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (a fictional \"Sandbox Power Co.\" electricity bill), no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.\n\nNote: `FUTURE_ISSUE_DATE` and `POOR_DOCUMENT_QUALITY` exist in workflow sessions but are never produced by this standalone endpoint — a future-dated document is not auto-declined here.", + "description": "Extract and validate a Proof of Address document — utility bills, bank statements, and government-issued documents — in one call. Returns the issuer, issue/expiry dates, the holder name, the raw and parsed/geocoded address, an `Approved`/`Declined` `status`, and a `warnings` list explaining every issue found.\n\n**Latency.** Extraction is LLM-based: typical calls take **5–15 seconds**, multi-page PDFs up to **~30 seconds**. Configure a client timeout of **at least 45 seconds** and do not retry before the call completes.\n\n**How it works.** The document (image or PDF, up to 15 MB; encrypted PDFs require `document_password`) is classified into a document type and subtype (e.g. `UTILITY_BILL` / `WATER_BILL`), and the issuer, dates, holder name, and address are extracted with LLM-based extraction. The address is parsed and geocoded into `poa_parsed_address`. PDF/EXIF forensics screen for tampering — modification after digital signing, known PDF editors, overlay-text manipulation, suspicious re-exports — surfacing `SUSPECTED_DOCUMENT_MANIPULATION` or `DOCUMENT_METADATA_MISMATCH`. Document age is validated against `poa_document_age_months`, and the language against `poa_languages_allowed`. If you pass `expected_address`, `expected_country`, or `expected_first_name`/`expected_last_name`, the extracted values are cross-checked and mismatches are flagged.\n\n**Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Hard failures always decline: `MISSING_ADDRESS_INFORMATION`, `POA_DOCUMENT_EXPIRED`, `INVALID_DOCUMENT_TYPE`, `UNABLE_TO_VALIDATE_DOCUMENT_AGE`, plus `UNABLE_TO_EXTRACT_ISSUE_DATE` and `POA_NAME_NOT_DETECTED` (not configurable on this endpoint). Five risk groups are configurable per request via the `poa_*_action` options (`DECLINE` or `NO_ACTION`, all defaulting to `DECLINE`). `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION` and `UNPARSABLE_OR_INVALID_ADDRESS` are informational on this endpoint and never decline. A document that cannot be processed at all returns `400`; a readable but problematic document returns `200` with `status: \"Declined\"` — always inspect `poa.status` and `poa.warnings`, not just the HTTP code.\n\n**Billing.** Each `200` response consumes one Proof of Address API credit (paid from your plan credit or top-up balance). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any document processing.\n\n**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the uploaded document is stored, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the decision additionally exposes `document_file` and the `document_metadata` forensics, which this response omits), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (a fictional \"Sandbox Power Co.\" electricity bill), no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.\n\nNote: `FUTURE_ISSUE_DATE` and `POOR_DOCUMENT_QUALITY` exist in workflow sessions but are never produced by this standalone endpoint — a future-dated document is not auto-declined here.", "operationId": "post_v3poa", "x-didit-sdk": { "resource": "proof_of_address", @@ -8506,7 +8596,7 @@ "/v3/document-ai/": { "post": { "summary": "Document AI (custom field extraction + tamper checks)", - "description": "Extract the fields **you** define from any supporting document — payslips, proof of funds, source-of-wealth letters, tax statements, contracts, invoices — in one call. Send the file plus a `fields` definition (key, name, extraction instruction, type) and get back typed values (`text`, `number`, `date` as `YYYY-MM-DD`), an optional name-match score against the expected person or company, PDF/EXIF tamper signals, an `Approved`/`Declined`/`In Review` `status`, and a `warnings` list explaining every issue found. This is the server-to-server counterpart of the hosted Document AI workflow step (see `/core-technology/document-ai/overview`); it uses Didit's default extraction model and does not expose provider selection.\n\n**Latency.** Extraction is LLM-based: typical calls take **5–15 seconds**, multi-page PDFs up to **~30 seconds**. Configure a client timeout of **at least 45 seconds** and do not retry before the call completes.\n\n**Request format.** `multipart/form-data` only. `fields` is **one JSON-encoded array string** (1–30 items, at most 16 KB); bracket-style form arrays are rejected with `400`. Each item is `{key, name, instruction, type, required, is_full_name}` — `key` is the JSON key you get back (`^[a-z0-9_]{1,64}$`, unique), `instruction` tells the model what to read, `type` is `text` (default) | `number` | `date`, `required` raises `DOCUMENT_AI_MISSING_REQUIRED_FIELDS` when the value cannot be read, and at most one field may set `is_full_name: true` to mark the value compared against the expected name.\n\n**Name matching.** Pass `expected_first_name` + `expected_last_name` (person) **or** `expected_company_name` (+ optional `expected_company_country`) — not both. The value of the `is_full_name` field (or, when none is marked, the first text field whose key looks like a name) is compared fuzzily; `name_match_score` (0–100) and `name_on_document` are always returned when both sides exist, and a score below `document_ai_name_match_score_threshold` (default 80) adds `DOCUMENT_AI_NAME_MISMATCH`, governed by `document_ai_name_mismatch_action`.\n\n**Decision logic.** Each risk maps to a configurable action: `DECLINE` → `status: \"Declined\"`, `REVIEW` → `status: \"In Review\"`, `NO_ACTION` → informational only. Defaults mirror the workflow step: unreadable document `REVIEW`, missing required fields `REVIEW`, suspected manipulation `DECLINE`, name mismatch `NO_ACTION`, unsupported file `REVIEW`. A document that cannot be processed at all returns `400`; a readable but problematic document returns `200` with `Declined`/`In Review` — always inspect `document_ai.status` and `document_ai.warnings`, not just the HTTP code.\n\n**Billing.** Each `200` response consumes one Document AI API credit — **$0.20 per document, no free tier** (standalone APIs never have one). When the organization's balance cannot cover the call, the endpoint returns `403` before any processing. Sandbox API keys return a deterministic mock response and are not charged.\n\n**Persistence.** With `save_api_request=true` (default) the call is stored as an API-type session: the document, the extracted values, the field definitions and the name-match snapshot are kept, the session shows up in the Business Console, and `request_id` works with `GET /v3/session/{sessionId}/decision/`.", + "description": "Extract the fields **you** define from any supporting document — payslips, proof of funds, source-of-wealth letters, tax statements, contracts, invoices — in one call. Send the file plus a `fields` definition (key, name, extraction instruction, type) and get back typed values (`text`, `number`, `date` as `YYYY-MM-DD`), an optional name-match score against the expected person or company, PDF/EXIF tamper signals, an `Approved`/`Declined`/`In Review` `status`, and a `warnings` list explaining every issue found. This is the server-to-server counterpart of the hosted Document AI workflow step (see `/core-technology/document-ai/overview`); it uses Didit's default extraction model and does not expose provider selection.\n\n**Latency.** Extraction is LLM-based: typical calls take **5–15 seconds**, multi-page PDFs up to **~30 seconds**. Configure a client timeout of **at least 45 seconds** and do not retry before the call completes.\n\n**Request format.** `multipart/form-data` only. `fields` is **one JSON-encoded array string** (1–30 items, at most 16 KB); bracket-style form arrays are rejected with `400`. Each item is `{key, name, instruction, type, required, is_full_name}` — `key` is the JSON key you get back (`^[a-z0-9_]{1,64}$`, unique), `instruction` tells the model what to read, `type` is `text` (default) | `number` | `date`, `required` raises `DOCUMENT_AI_MISSING_REQUIRED_FIELDS` when the value cannot be read, and at most one field may set `is_full_name: true` to mark the value compared against the expected name.\n\n**Name matching.** Pass `expected_first_name` + `expected_last_name` (person) **or** `expected_company_name` (+ optional `expected_company_country`) — not both. The value of the `is_full_name` field (or, when none is marked, the first text field whose key looks like a name) is compared fuzzily; `name_match_score` (0–100) and `name_on_document` are always returned when both sides exist, and a score below `document_ai_name_match_score_threshold` (default 80) adds `DOCUMENT_AI_NAME_MISMATCH`, governed by `document_ai_name_mismatch_action`.\n\n**Decision logic.** Each risk maps to a configurable action: `DECLINE` → `status: \"Declined\"`, `REVIEW` → `status: \"In Review\"`, `NO_ACTION` → informational only. Defaults mirror the workflow step: unreadable document `REVIEW`, missing required fields `REVIEW`, suspected manipulation `DECLINE`, name mismatch `NO_ACTION`, unsupported file `REVIEW`. A document that cannot be processed at all returns `400`; a readable but problematic document returns `200` with `Declined`/`In Review` — always inspect `document_ai.status` and `document_ai.warnings`, not just the HTTP code.\n\n**Billing.** Each `200` response consumes one Document AI API credit — **$0.20 per document** at list, paid from your plan credit or top-up balance. When the organization's balance cannot cover the call, the endpoint returns `403` before any processing. Sandbox API keys return a deterministic mock response and are not charged.\n\n**Persistence.** With `save_api_request=true` (default) the call is stored as an API-type session: the document, the extracted values, the field definitions and the name-match snapshot are kept, the session shows up in the Business Console, and `request_id` works with `GET /v3/session/{sessionId}/decision/`.", "operationId": "post_v3document-ai", "x-didit-sdk": { "resource": "document_ai", @@ -10093,7 +10183,7 @@ "/v3/face-search/": { "post": { "summary": "Face Search", - "description": "Search a face image (1:N) against your application's face search index — faces enrolled from verification sessions, saved standalone API calls, direct user-profile face uploads, and your block/allow lists — and get back ranked similarity matches plus blocklist and duplicate warnings.\n\n**How it works.** The largest detected face in `user_image` is embedded and searched against the index, scoped to your application. Up to **5 matches** above the similarity floor are returned in `matches`, each with `similarity_percentage`, the originating session (`session_id`, `session_number`, `status`, `vendor_data`), `user_details` extracted during that session's document verification, `is_blocklisted` / `is_allowlisted` flags, and a `source` discriminator (`session`, `imported`, or `list_entry`).\n\n- `search_type=most_similar` (default) ranks every enrolled face by similarity — use it for deduplication and fraud-ring investigation.\n- `search_type=blocklisted_or_approved` restricts candidates to blocklisted faces, allowlisted faces, faces from approved sessions, and imported user-profile faces, ranking blocklisted entries first — use it when the primary goal is blocklist screening.\n\n`status` is `Declined` only when a `FACE_IN_BLOCKLIST` or `POSSIBLE_FACE_IN_BLOCKLIST` warning fires; `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` (`information`) and `MULTIPLE_FACES_DETECTED` (`warning`) never decline. Unlike Passive Liveness, this endpoint does **not** exclude prior sessions with the same `vendor_data` from matching. A passive-liveness analysis also runs internally and is stored with the persisted session, but its result is not part of this response.\n\n**Billing.** Each `200` response consumes one Face Search API credit (standalone APIs have no free tier). Insufficient balance returns `403` before any image processing.\n\n**Session persistence and face enrollment (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session (Business Console, `GET /v3/session/{sessionId}/decision/` via the returned `request_id`, `status.updated` webhook) and the searched face is enrolled into your face search index. Faces enrolled by Face Search calls are excluded from future Face Search results, so repeated searches never match each other. When `false`, the call is one-shot: nothing is stored, the face is not enrolled, and `match_image_url` values are internal storage paths rather than downloadable URLs.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", + "description": "Search a face image (1:N) against your application's face search index — faces enrolled from verification sessions, saved standalone API calls, direct user-profile face uploads, and your block/allow lists — and get back ranked similarity matches plus blocklist and duplicate warnings.\n\n**How it works.** The largest detected face in `user_image` is embedded and searched against the index, scoped to your application. Up to **5 matches** above the similarity floor are returned in `matches`, each with `similarity_percentage`, the originating session (`session_id`, `session_number`, `status`, `vendor_data`), `user_details` extracted during that session's document verification, `is_blocklisted` / `is_allowlisted` flags, and a `source` discriminator (`session`, `imported`, or `list_entry`).\n\n- `search_type=most_similar` (default) ranks every enrolled face by similarity — use it for deduplication and fraud-ring investigation.\n- `search_type=blocklisted_or_approved` restricts candidates to blocklisted faces, allowlisted faces, faces from approved sessions, and imported user-profile faces, ranking blocklisted entries first — use it when the primary goal is blocklist screening.\n\n`status` is `Declined` only when a `FACE_IN_BLOCKLIST` or `POSSIBLE_FACE_IN_BLOCKLIST` warning fires; `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` (`information`) and `MULTIPLE_FACES_DETECTED` (`warning`) never decline. Unlike Passive Liveness, this endpoint does **not** exclude prior sessions with the same `vendor_data` from matching. A passive-liveness analysis also runs internally and is stored with the persisted session, but its result is not part of this response.\n\n**Billing.** Each `200` response consumes one Face Search API credit (paid from your plan credit or top-up balance). Insufficient balance returns `403` before any image processing.\n\n**Session persistence and face enrollment (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session (Business Console, `GET /v3/session/{sessionId}/decision/` via the returned `request_id`, `status.updated` webhook) and the searched face is enrolled into your face search index. Faces enrolled by Face Search calls are excluded from future Face Search results, so repeated searches never match each other. When `false`, the call is one-shot: nothing is stored, the face is not enrolled, and `match_image_url` values are internal storage paths rather than downloadable URLs.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", "operationId": "post_v3face-search", "x-didit-sdk": { "resource": "face_search", @@ -10771,7 +10861,7 @@ "name": "search", "in": "query", "required": false, - "description": "Smart free-text search. A full UUID matches `session_id`; numeric input matches session-number prefix, document/personal numbers, phone prefix, or `vendor_data`; input containing `@` matches email prefixes; other text matches names (accent-insensitive) and `vendor_data`. Free text containing digits additionally matches document/personal-number search fields and session-number prefixes.", + "description": "A complete displayed reference such as `S-0001` (user) or `B-0001` (business) matches only that session number and kind; leading zeros and letter case do not affect matching. Smart free-text search. A full UUID matches `session_id`; numeric input matches session-number prefix, document/personal numbers, phone prefix, or `vendor_data`; input containing `@` matches email prefixes; other text matches names (accent-insensitive) and `vendor_data`. Free text containing digits additionally matches document/personal-number search fields and session-number prefixes.", "schema": { "type": "string" }, @@ -12605,9 +12695,10 @@ "enum": [ "not_requested", "delivered", + "not_held", "unavailable" ], - "description": "`delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `not_requested` on the Lite tier." + "description": "`delivered` when the ownership data came back, `not_held` when the ownership registry answered and does not list this company (see `registry_data.ownership_error`), `unavailable` when an ownership tier was requested but the registry had none for this company or could not be asked (see `registry_data.ownership_error`), `not_requested` on the Lite tier." }, "ownership": { "$ref": "#/components/schemas/KYBRegistryOwnership" @@ -12867,7 +12958,7 @@ "/v3/passive-liveness/": { "post": { "summary": "Passive Liveness", - "description": "Detect presentation attacks — printed photos, screen replays, masks, deepfakes — from a single face image. No video, motion, or user interaction required.\n\n**How it works.** The image is analyzed by a biometric model; the largest detected face is evaluated and a liveness `score` (0–100) is computed. `status` is `Declined` when any `error`-severity warning fires:\n- `NO_FACE_DETECTED` — the liveness model found no face,\n- `LOW_LIVENESS_SCORE` — score at or below `face_liveness_score_decline_threshold` (default `30`),\n- `LIVENESS_FACE_ATTACK` — the model detected a presentation attack,\n- `FACE_IN_BLOCKLIST` / `POSSIBLE_FACE_IN_BLOCKLIST` — the face matches an entry on your face blocklist.\n\n`MULTIPLE_FACES_DETECTED` is a `warning`-severity signal (does not decline), and `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` are `information`-severity signals that the same face already appeared in another approved session (they never decline). `face_quality` and `face_luminance` (0–100, nullable) describe capture quality.\n\n**Blocklist and duplicate screening.** Runs on **every** call — with or without `save_api_request` — by matching the detected face against your application's face search index. Prior faces with the same `vendor_data` are excluded from the entire index search — so re-running liveness for the same user does not trigger `DUPLICATED_FACE`, and a blocklisted face originating from a session with the caller's own `vendor_data` is also NOT flagged (`FACE_IN_BLOCKLIST` screening is bypassed for same-`vendor_data` faces).\n\n**Billing.** Each `200` response consumes one Passive Liveness API credit (standalone APIs have no free tier). Insufficient balance returns `403` before any image processing.\n\n**Session persistence and face enrollment (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session (Business Console, `GET /v3/session/{sessionId}/decision/` via the returned `request_id`, `status.updated` webhook), the reference image and face crop are stored, and the detected face is **enrolled into your face search index** so later Face Search calls and duplicate checks can match it. When `false`, the call is one-shot: nothing is stored and the face is not enrolled (blocklist/duplicate screening still runs).\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", + "description": "Detect presentation attacks — printed photos, screen replays, masks, deepfakes — from a single face image. No video, motion, or user interaction required.\n\n**How it works.** The image is analyzed by a biometric model; the largest detected face is evaluated and a liveness `score` (0–100) is computed. `status` is `Declined` when any `error`-severity warning fires:\n- `NO_FACE_DETECTED` — the liveness model found no face,\n- `LOW_LIVENESS_SCORE` — score at or below `face_liveness_score_decline_threshold` (default `30`),\n- `LIVENESS_FACE_ATTACK` — the model detected a presentation attack,\n- `FACE_IN_BLOCKLIST` / `POSSIBLE_FACE_IN_BLOCKLIST` — the face matches an entry on your face blocklist.\n\n`MULTIPLE_FACES_DETECTED` is a `warning`-severity signal (does not decline), and `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` are `information`-severity signals that the same face already appeared in another approved session (they never decline). `face_quality` and `face_luminance` (0–100, nullable) describe capture quality.\n\n**Blocklist and duplicate screening.** Runs on **every** call — with or without `save_api_request` — by matching the detected face against your application's face search index. Prior faces with the same `vendor_data` are excluded from the entire index search — so re-running liveness for the same user does not trigger `DUPLICATED_FACE`, and a blocklisted face originating from a session with the caller's own `vendor_data` is also NOT flagged (`FACE_IN_BLOCKLIST` screening is bypassed for same-`vendor_data` faces).\n\n**Billing.** Each `200` response consumes one Passive Liveness API credit (paid from your plan credit or top-up balance). Insufficient balance returns `403` before any image processing.\n\n**Session persistence and face enrollment (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session (Business Console, `GET /v3/session/{sessionId}/decision/` via the returned `request_id`, `status.updated` webhook), the reference image and face crop are stored, and the detected face is **enrolled into your face search index** so later Face Search calls and duplicate checks can match it. When `false`, the call is one-shot: nothing is stored and the face is not enrolled (blocklist/duplicate screening still runs).\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", "operationId": "post_v3passive-liveness", "x-didit-sdk": { "resource": "passive_liveness", @@ -15340,7 +15431,7 @@ "channel": "whatsapp", "actual_channel": "whatsapp" }, - "fee": 0.0709 + "fee": 0.0509 }, { "type": "PHONE_DELIVERY_DELIVERED", @@ -15428,7 +15519,7 @@ "channel": "whatsapp", "actual_channel": "whatsapp" }, - "fee": 0.0709 + "fee": 0.0509 }, { "type": "INVALID_CODE_ENTERED", @@ -15515,7 +15606,7 @@ "channel": "whatsapp", "actual_channel": "sms" }, - "fee": 0.0709 + "fee": 0.0509 }, { "type": "VALID_CODE_ENTERED", @@ -16510,7 +16601,7 @@ "status": "Success", "reason": null }, - "fee": 0.03 + "fee": 0.02 }, { "type": "VALID_CODE_ENTERED", @@ -16582,7 +16673,7 @@ "status": "Success", "reason": null }, - "fee": 0.03 + "fee": 0.02 }, { "type": "VALID_CODE_ENTERED", @@ -16652,7 +16743,7 @@ "status": "Success", "reason": null }, - "fee": 0.03 + "fee": 0.02 }, { "type": "VALID_CODE_ENTERED", @@ -19313,7 +19404,7 @@ "/v3/users/delete/": { "post": { "summary": "Batch delete users", - "description": "**Permanently delete** users: the user records are removed permanently. For everyday blocking prefer [Update User Status](#patch-/v3/users/-vendor_data-/update-status/) with `BLOCKED`. Select users with `vendor_data_list`, with `didit_internal_id_list`, or with both at once; or pass `delete_all: true`. This endpoint never narrows `delete_all` with filters. The Console's People filters are refused with a `400` (see the `400` response), so a narrowed intent does not turn into a full wipe. Two exceptions: `filter_has_approved_sessions` and `filter_has_declined_sessions` are not refused and not applied, so with `delete_all: true` they still delete every user; do not send them.\n\n**Biometric templates:** every image-free biometric template retained for these users after session deletion (see `/v3/biometric-templates/`) is purged before the user is deleted, so the person stops matching in duplicate detection and Face Search. If a template cannot be purged because a biometric store is unavailable, the call returns `503`, no user in the request is deleted, and repeating the request finishes the job.", + "description": "**Permanently delete** users: the user records are removed permanently. For everyday blocking prefer [Update User Status](#patch-/v3/users/-vendor_data-/update-status/) with `BLOCKED`. Select users with `vendor_data_list`, with `didit_internal_id_list`, or with both at once; or pass `delete_all: true`. This endpoint never narrows `delete_all` with filters. The Console's People filters are refused with a `400` (see the `400` response), so a narrowed intent does not turn into a full wipe. Two exceptions: `filter_has_approved_sessions` and `filter_has_declined_sessions` are not refused and not applied, so with `delete_all: true` they still delete every user; do not send them.\n\n**Biometric templates:** every image-free biometric template retained for these users after session deletion (see `/v3/biometric-templates/`) is purged before the user is deleted, so the person stops matching in duplicate detection and Face Search. If a template cannot be purged because a biometric store is unavailable, the call returns `503`, no user in the request is deleted, and repeating the request finishes the job.\n\n**Filed regulatory reports:** if the selected data is in a finalized regulatory report, the whole request is refused with `409` and nothing is deleted. The body uses `code: in_filed_report` and names each report with `uuid`, `report_number`, `report_type` and `case_uuid`. Delete the reports, then repeat the request.", "operationId": "batch_delete_users", "x-didit-sdk": { "resource": "users", @@ -19513,6 +19604,16 @@ } } } + }, + "409": { + "description": "The selection includes a person in a finalized regulatory report. Nothing is deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InFiledReportError" + } + } + } } }, "security": [ @@ -19525,7 +19626,7 @@ "/v3/sessions/delete/": { "post": { "summary": "Bulk delete KYC sessions by session_number", - "description": "Bulk delete User Verification (KYC) sessions by their numeric `session_number`, or every KYC session in your application with `delete_all: true`.\n\n`session_number` is the human-friendly incrementing counter shown in the Console session list and returned as `session_number` by the create-session and decision endpoints; it is **not** the `session_id` UUID. To delete a session by UUID, or to delete a Business Verification (KYB) session, which this endpoint never touches, use `DELETE /v3/session/{sessionId}/delete/` instead.\n\n**Up to 20 sessions are deleted in the request, more run as a job.** The number of matching sessions decides, whether they are selected by `session_numbers` or with `delete_all`:\n- Up to 20 matching sessions: they are deleted before the response, which is `200` with one entry per session in `results[]`.\n- More than 20: the response is `202` with a deletion job, and the sessions are deleted in the background. Poll `GET /v3/sessions/delete/{job_id}/` until `status` is `COMPLETED` or `FAILED`. A job reports counts per outcome, never which sessions it deleted. A `delete_all` job deletes only the sessions created before the request; sessions started while it runs are not deleted.\n\nEach deleted session gets exactly the same treatment as the single-session delete endpoint: the session, every verification attempt it holds, its decision and extracted data, its related feature records (ID verification, face, liveness, face match, and so on) and all of its stored files are deleted, it disappears from every read endpoint, and previously issued media URLs stop resolving. Webhook deliveries still queued for a deleted session, and their retries, are dropped; webhooks already delivered are not affected. Blocklist entries are **not** removed, credits already consumed are **not** refunded, and the parent User entities are **not** deleted (use `POST /v3/users/delete/` for those); no webhook is emitted for the deletion. Deletion is permanent and irreversible.\n\n**Selection semantics:**\n- With `delete_all: true` you can narrow the selection with the filters of `GET /v3/sessions/`, each prefixed with `filter_` (for example `filter_status`, `filter_workflow_id`, `filter_date_from`). Filters that take several values in List Sessions (`status`, `workflow_id`, `country`, `region`, `document_type`, `document_name`, `session_type`, `warning`, `organization_name`) accept a comma-separated string or an array; the others take one value. An empty value is ignored. Filters apply only together with `delete_all`, and a deletion job applies the same filters.\n- An unknown `filter_*` key, or a filter value that cannot be read, rejects the whole request with `400` before anything is deleted, so a mistyped filter never widens the deletion to every session.\n- Entries in `session_numbers` that do not match a live KYC session in your application (unknown numbers, already-deleted sessions, or sessions owned by another application) are **silently skipped**: they do not appear in `results` and are not counted in a job's `total`.\n- Validation failures (a non-numeric entry, an empty list, neither `session_numbers` nor `delete_all` provided, or a template retention the application cannot honour) reject the **whole request** with `400` and delete nothing.\n- Sessions are deleted in groups. If a group cannot be deleted (for example because a storage service is unavailable), every session in that group is reported as `face_retention_outcome: failed_retryable` and stays in place; the other groups are unaffected. Repeat the request for those sessions. A job retries such a group by itself.\n- There is no documented cap on the list length; the shared write rate limit (below) is the practical bound.\n\n**Face biometric data:** the optional `retain_face_embeddings`, `face_retention_days`, `face_retention_deadline`, `deletion_instruction`, and `instruction_id` fields have the same meaning as on `DELETE /v3/session/{sessionId}/delete/` and apply to every matched session. Each result reports its own `face_retention_outcome`.\n\n**Response:** `200 OK` with `results[]` for up to 20 matching sessions, or `202 Accepted` with a deletion job for more. Every result confirms deletion except `face_retention_outcome: failed_retryable`, which reports a session that was NOT deleted and should be retried. Earlier versions of this endpoint returned `204 No Content`; update clients that assert on `204`.\n\n**Idempotency:** repeating the same request deletes nothing more: numbers that no longer match a session are skipped.\n\n**`delete_all: true` (destructive):** deletes **every** remaining KYC session in the application; `session_numbers` is ignored when set. There is no confirmation step and no undo.\n\n**Authentication:** API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`); this API never returns `401`.\n\n**Rate limit:** shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.\n\nThis endpoint accepts API keys only. Business Console user Bearer tokens are rejected with `403` (unlike the single-session delete, which accepts both).", + "description": "Bulk delete User Verification (KYC) sessions by their numeric `session_number`, or every KYC session in your application with `delete_all: true`.\n\n`session_number` is the human-friendly incrementing counter shown in the Console session list and returned as `session_number` by the create-session and decision endpoints; it is **not** the `session_id` UUID. To delete a session by UUID, or to delete a Business Verification (KYB) session, which this endpoint never touches, use `DELETE /v3/session/{sessionId}/delete/` instead.\n\n**Up to 20 sessions are deleted in the request, more run as a job.** The number of matching sessions decides, whether they are selected by `session_numbers` or with `delete_all`:\n- Up to 20 matching sessions: they are deleted before the response, which is `200` with one entry per session in `results[]`.\n- More than 20: the response is `202` with a deletion job, and the sessions are deleted in the background. Poll `GET /v3/sessions/delete/{job_id}/` until `status` is `COMPLETED` or `FAILED`. A job reports counts per outcome, never which sessions it deleted. A `delete_all` job deletes only the sessions created before the request; sessions started while it runs are not deleted.\n\nEach deleted session gets exactly the same treatment as the single-session delete endpoint: the session, every verification attempt it holds, its decision and extracted data, its related feature records (ID verification, face, liveness, face match, and so on) and all of its stored files are deleted, it disappears from every read endpoint, and previously issued media URLs stop resolving. Webhook deliveries still queued for a deleted session, and their retries, are dropped; webhooks already delivered are not affected. Blocklist entries are **not** removed, credits already consumed are **not** refunded, and the parent User entities are **not** deleted (use `POST /v3/users/delete/` for those); no webhook is emitted for the deletion. Deletion is permanent and irreversible.\n\n**Selection semantics:**\n- With `delete_all: true` you can narrow the selection with the filters of `GET /v3/sessions/`, each prefixed with `filter_` (for example `filter_status`, `filter_workflow_id`, `filter_date_from`). Filters that take several values in List Sessions (`status`, `workflow_id`, `country`, `region`, `document_type`, `document_name`, `session_type`, `warning`, `organization_name`) accept a comma-separated string or an array; the others take one value. An empty value is ignored. Filters apply only together with `delete_all`, and a deletion job applies the same filters.\n- An unknown `filter_*` key, or a filter value that cannot be read, rejects the whole request with `400` before anything is deleted, so a mistyped filter never widens the deletion to every session.\n- Entries in `session_numbers` that do not match a live KYC session in your application (unknown numbers, already-deleted sessions, or sessions owned by another application) are **silently skipped**: they do not appear in `results` and are not counted in a job's `total`.\n- Validation failures (a non-numeric entry, an empty list, neither `session_numbers` nor `delete_all` provided, or a template retention the application cannot honour) reject the **whole request** with `400` and delete nothing.\n- Sessions are deleted in groups. If a group cannot be deleted (for example because a storage service is unavailable), every session in that group is reported as `face_retention_outcome: failed_retryable` and stays in place; the other groups are unaffected. Repeat the request for those sessions. A job retries such a group by itself.\n- There is no documented cap on the list length; the shared write rate limit (below) is the practical bound.\n\n**Face biometric data:** the optional `retain_face_embeddings`, `face_retention_days`, `face_retention_deadline`, `deletion_instruction`, and `instruction_id` fields have the same meaning as on `DELETE /v3/session/{sessionId}/delete/` and apply to every matched session. Each result reports its own `face_retention_outcome`.\n\n**Response:** `200 OK` with `results[]` for up to 20 matching sessions, or `202 Accepted` with a deletion job for more. Every result confirms deletion except `face_retention_outcome: failed_retryable`, which reports a session that was NOT deleted and should be retried. Earlier versions of this endpoint returned `204 No Content`; update clients that assert on `204`.\n\n**Idempotency:** repeating the same request deletes nothing more: numbers that no longer match a session are skipped.\n\n**`delete_all: true` (destructive):** deletes **every** remaining KYC session in the application; `session_numbers` is ignored when set. There is no confirmation step and no undo.\n\n**Authentication:** API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`); this API never returns `401`.\n\n**Rate limit:** shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.\n\nThis endpoint accepts API keys only. Business Console user Bearer tokens are rejected with `403` (unlike the single-session delete, which accepts both).\n\n**Filed regulatory reports:** if the selected data is in a finalized regulatory report, the whole request is refused with `409` and nothing is deleted. The body uses `code: in_filed_report` and names each report with `uuid`, `report_number`, `report_type` and `case_uuid`. Delete the reports, then repeat the request.", "operationId": "batch_delete_sessions", "x-didit-sdk": { "resource": "sessions", @@ -20081,6 +20182,16 @@ } } } + }, + "409": { + "description": "A selected session is part of a finalized regulatory report. Nothing is deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InFiledReportError" + } + } + } } }, "x-codeSamples": [ @@ -20863,7 +20974,7 @@ "auto_refill_amount": { "type": "string", "nullable": true, - "description": "USD amount that will be charged on each auto-refill. `null` when auto-refill is disabled or unset." + "description": "USD amount that will be charged and added to the balance on each auto-refill, at face value. Set from \\$50 to \\$10,000 in Console → Billing. `null` when auto-refill is disabled or unset." }, "auto_refill_threshold": { "type": "string", @@ -20932,7 +21043,7 @@ "/v3/billing/top-up/": { "post": { "summary": "Top up credits", - "description": "Add credits via a hosted checkout session (**$50 minimum**); not available on annual plans. Redirect the customer to the returned `checkout_session_url`.", + "description": "Add credits via a hosted Stripe Checkout session (**\\$50 to \\$10,000** per top-up). Top-ups are available on the Free, Growth and Scale plans, added at face value with no discount or bonus, and last 12 months; Enterprise annual contracts cannot top up. Redirect the customer to the returned `checkout_session_url`.", "operationId": "billing_top_up", "x-didit-sdk": { "resource": "billing", @@ -20956,7 +21067,8 @@ "amount_in_dollars": { "type": "number", "minimum": 50, - "description": "Amount to charge, in USD. **Must be at least 50.** Tax is added automatically at checkout based on the customer's billing address." + "maximum": 10000, + "description": "Amount to charge, in USD. **Must be from 50 to 10,000.** The balance receives the same amount, with no discount or bonus. Tax is added automatically at checkout based on the customer's billing address." }, "success_url": { "type": "string", @@ -21039,7 +21151,7 @@ } }, "400": { - "description": "Invalid payload — the amount is missing, not a number, below the $50 minimum, or the organization is on an annual plan. The error body is a JSON array.", + "description": "Invalid payload: the amount is missing, not a number, below the \\$50 minimum or above the \\$10,000 maximum, or the organization is on an Enterprise annual contract. The error body is a JSON array.", "content": { "application/json": { "examples": { @@ -21058,6 +21170,11 @@ "Amount must be at least $50." ] }, + "Above maximum": { + "value": [ + "Amount must be at most $10,000." + ] + }, "Annual plan": { "value": [ "You have an annual plan. Contact your account manager to top up." @@ -26422,7 +26539,7 @@ "/v3/businesses/delete/": { "post": { "summary": "Batch delete businesses", - "description": "**Permanently delete** businesses — this cannot be undone. For blocking prefer [Update Business Status](#patch-/v3/businesses/-vendor_data-/update-status/) with `BLOCKED`.", + "description": "**Permanently delete** businesses — this cannot be undone. For blocking prefer [Update Business Status](#patch-/v3/businesses/-vendor_data-/update-status/) with `BLOCKED`.\n\n**Filed regulatory reports:** if the selected data is in a finalized regulatory report, the whole request is refused with `409` and nothing is deleted. The body uses `code: in_filed_report` and names each report with `uuid`, `report_number`, `report_type` and `case_uuid`. Delete the reports, then repeat the request.", "operationId": "batch_delete_businesses", "x-didit-sdk": { "resource": "businesses", @@ -26579,6 +26696,16 @@ } } } + }, + "409": { + "description": "The selection includes a company in a finalized regulatory report. Nothing is deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InFiledReportError" + } + } + } } }, "security": [ @@ -31276,26 +31403,46 @@ "gtr": { "rail": "GTR", "mode": "unavailable", + "state": "didit_onboarding", + "reason": "didit_membership_not_live", "platform_available": false, - "connected": false + "connected": false, + "participant": null, + "vasps_reachable": 0, + "routed_30d": 0 }, "trust": { "rail": "TRUST", "mode": "unavailable", + "state": "unavailable", + "reason": "own_membership_only", "platform_available": false, - "connected": false + "connected": false, + "participant": null, + "vasps_reachable": 0, + "routed_30d": 0 }, "verifyvasp": { "rail": "VERIFYVASP", "mode": "unavailable", + "state": "didit_onboarding", + "reason": "didit_membership_not_live", "platform_available": false, - "connected": false + "connected": false, + "participant": null, + "vasps_reachable": 0, + "routed_30d": 0 }, "sygna": { "rail": "SYGNA", "mode": "unavailable", + "state": "didit_onboarding", + "reason": "didit_membership_not_live", "platform_available": false, - "connected": false + "connected": false, + "participant": null, + "vasps_reachable": 0, + "routed_30d": 0 } }, "didit_network": { @@ -32882,7 +33029,7 @@ "/v3/transactions/{transaction_id}/travel-rule/": { "patch": { "summary": "Finish, cancel, resend, or update the originator wallet of a Travel Rule transfer", - "description": "Provide `payment_txn_id` to report the on-chain hash and move an outbound COMPLETED transfer to FINISHED (its INTERNAL-rail counterpart also moves to FINISHED), `{\"action\": \"cancel\"}` to cancel a non-terminal transfer, or `{\"action\": \"resend\"}` to route an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA again (improve the counterparty data first, e.g. add a travel_address to the destination wallet entry). resend applies to outbound transfers only. `{\"action\": \"update_compliance_email\", \"compliance_email\": \"desk@example.com\"}` replaces the compliance email of an outbound transfer in one of those three statuses, the fix for `reason: email_undeliverable`: the status does not change, and the next `resend` sends the request to that address. A resend is queued, not run inside the request: the response shows the transfer back in AWAITING_COUNTERPARTY with a `resend queued` timeline entry and a queued `delivery`, and where it went arrives as a `travel_rule.status.updated` webhook. When the transfer went over GTR, VerifyVASP or Sygna Bridge, the network learns the on-chain hash or the cancellation after the response, with retries; `delivery` shows how that message stands. `{\"action\": \"update_originator_wallet\", \"wallet_address\": \"...\"}` records the wallet the funds actually left from on an outbound transfer, for example the address a UTXO withdrawal was broadcast from rather than the main wallet you declared. It changes the record only: nothing is sent to the counterparty again and the status stays. The originator becomes `chain_details.originator` with `source: updated`, the change is added to the `timeline` with the previous and the new wallet, and the INTERNAL-rail counterpart of the transfer is updated the same way.", + "description": "Provide `payment_txn_id` to report the on-chain hash and move an outbound COMPLETED transfer to FINISHED (its INTERNAL-rail counterpart also moves to FINISHED), `{\"action\": \"cancel\"}` to cancel a non-terminal transfer, or `{\"action\": \"resend\"}` to route an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA again (improve the counterparty data first, e.g. add a travel_address to the destination wallet entry). resend applies to outbound transfers only. `{\"action\": \"update_compliance_email\", \"compliance_email\": \"desk@example.com\"}` replaces the compliance email of an outbound transfer in one of those three statuses, the fix for `reason: email_undeliverable`: the status does not change, and the next `resend` sends the request to that address. A resend is queued, not run inside the request: the response shows the transfer back in AWAITING_COUNTERPARTY with a `resend queued` timeline entry and a queued `delivery`, and where it went arrives as a `travel_rule.status.updated` webhook. When the transfer went over GTR, VerifyVASP or Sygna Bridge, the network learns the on-chain hash or the cancellation after the response, with retries; `delivery` shows how that message stands. `{\"action\": \"update_originator_wallet\", \"wallet_address\": \"...\"}` records the wallet the funds actually left from on an outbound transfer, for example the address a UTXO withdrawal was broadcast from rather than the main wallet you declared. It changes the record only: nothing is sent to the counterparty again and the status stays. The originator becomes `chain_details.originator` with `source: updated`, the change is added to the `timeline` with the previous and the new wallet, and the INTERNAL-rail counterpart of the transfer is updated the same way. `{\"action\": \"beneficiary_vasp\", \"beneficiary_vasp\": \"\"}` names the counterparty yourself when discovery could not, on an outbound transfer in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE or NOT_ENOUGH_COUNTERPARTY_DATA: the transfer is routed to that VASP and sent again.", "operationId": "patchTravelRuleTransfer", "x-didit-sdk": { "resource": "transactions.travel_rule", @@ -32944,6 +33091,12 @@ "action": "update_originator_wallet", "wallet_address": "bc1qOriginatorInputAddress01" } + }, + "Name the counterparty": { + "value": { + "action": "beneficiary_vasp", + "beneficiary_vasp": "9f1c2b34-5d6e-4f70-8a91-b2c3d4e5f607" + } } } } @@ -32968,7 +33121,7 @@ } }, "400": { - "description": "Neither payment_txn_id nor a supported action was provided, `compliance_email` is missing or not an email address on `update_compliance_email`, or, for `update_originator_wallet`, no wallet_address, a wallet or memo the transfer's chain cannot carry, or a wallet that is not one of the transfer's UTXO inputs.", + "description": "Neither payment_txn_id nor a supported action was provided, `compliance_email` is missing or not an email address on `update_compliance_email`, or, for `update_originator_wallet`, no wallet_address, a wallet or memo the transfer's chain cannot carry, or a wallet that is not one of the transfer's UTXO inputs, or `beneficiary_vasp` is missing or not a valid directory VASP id.", "content": { "application/json": { "examples": { @@ -32991,10 +33144,10 @@ } }, "404": { - "description": "Unknown transaction, or the transaction has no Travel Rule transfer." + "description": "Unknown transaction, the transaction has no Travel Rule transfer, or (action `beneficiary_vasp`) no directory entry has that id." }, "409": { - "description": "The transfer is not in a state that allows this operation (e.g. not COMPLETED/outbound for finish, already terminal for cancel, or not an outbound transfer in a resendable status for resend). `update_compliance_email` answers `409` for an inbound transfer, and for an outbound one outside COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE and NOT_ENOUGH_COUNTERPARTY_DATA. `update_originator_wallet` answers `409` for an inbound, cancelled or expired transfer.", + "description": "The transfer is not in a state that allows this operation (e.g. not COMPLETED/outbound for finish, already terminal for cancel, or not an outbound transfer in a resendable status for resend). `update_compliance_email` answers `409` for an inbound transfer, and for an outbound one outside COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE and NOT_ENOUGH_COUNTERPARTY_DATA. `update_originator_wallet` answers `409` for an inbound, cancelled or expired transfer. `beneficiary_vasp` answers `409` unless the transfer is outbound and in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE or NOT_ENOUGH_COUNTERPARTY_DATA.", "content": { "application/json": { "examples": { @@ -34188,43 +34341,32 @@ "description": "The session expired." } } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/": { - "patch": { - "summary": "Toggle AML ongoing monitoring for users", - "description": "Enables or disables AML ongoing monitoring at the user level. Exactly one monitored AML screening is kept per user; users without any AML screening (for example imported users) get one created and screened first, billed as a regular AML screening. Set estimate_only to preview counts and costs without writing. Batches of 20 or fewer resolve inline (200, COMPLETED); larger batches return 202 and run asynchronously.", - "operationId": "toggle_vendor_users_ongoing_monitoring", + }, + "post": { + "summary": "Pick the counterparty VASP in the wallet widget", + "description": "Public write endpoint keyed by the widget session token, no API key required: the end user chose the destination VASP that automatic discovery could not name. Only an outbound transfer in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE or NOT_ENOUGH_COUNTERPARTY_DATA accepts it. The transfer is routed to that VASP and sent again; automatic discovery never overrides the pick.", + "operationId": "selectTravelRuleWidgetVasp", "x-didit-sdk": { - "resource": "users", - "method": "set_ongoing_monitoring", - "auth": "api_key", - "server": true, - "scope": "application" + "resource": "travel_rule", + "method": "select_widget_vasp", + "auth": "public", + "server": false }, "tags": [ - "Users" + "Travel Rule" + ], + "security": [ + {} ], "parameters": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", + "name": "token", "in": "path", "required": true, + "description": "Opaque session token returned when the widget session was minted.", "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "type": "string" + } } ], "requestBody": { @@ -34232,126 +34374,107 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OngoingMonitoringToggleRequest" + "type": "object", + "required": [ + "vasp" + ], + "properties": { + "vasp": { + "type": "string", + "format": "uuid", + "description": "The `id` of a VASP from `GET /v3/travel-rule/widget/{token}/vasps/`." + } + } + }, + "example": { + "vasp": "9f1c2b34-5d6e-4f70-8a91-b2c3d4e5f607" } } } }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/widget/{token}/vasps/ \\\n -H 'Content-Type: application/json' \\\n -d '{\"vasp\": \"9f1c2b34-5d6e-4f70-8a91-b2c3d4e5f607\"}'" + } + ], "responses": { "200": { - "description": "Estimate (when estimate_only) or the completed job for small batches processed inline.", + "description": "The pick was recorded and the transfer is being sent again.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OngoingMonitoringToggleResult" + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "transfer_status": { + "type": "string", + "description": "The transfer status after the pick." + }, + "counterparty_vasp": { + "type": "string", + "description": "The name of the VASP the end user picked." + } + } + }, + "example": { + "detail": "Counterparty VASP selected; the transfer is being resent.", + "transfer_status": "AWAITING_COUNTERPARTY", + "counterparty_vasp": "Northwind Exchange" } } } }, - "202": { - "description": "Larger batches are processed asynchronously; poll the job endpoint with the returned job_id.", + "400": { + "description": "`vasp` is missing, or no live directory entry has that id.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OngoingMonitoringJob" + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "vasp": [ + "No directory entry has this id." + ] } } } }, - "403": { - "description": "Missing write:sessions privilege, or insufficient balance to enable (the estimate is included in the error body)." - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - } - }, - "/v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/{job_id}/": { - "get": { - "summary": "Get AML ongoing monitoring job", - "description": "Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING.", - "operationId": "get_vendor_users_ongoing_monitoring_job", - "x-didit-sdk": { - "resource": "users", - "method": "retrieve_ongoing_monitoring_job", - "auth": "api_key", - "server": true, - "scope": "application" - }, - "tags": [ - "Users" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "404": { + "description": "Unknown token." }, - { - "name": "job_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Monitoring job UUID returned by the toggle endpoint." - } - ], - "responses": { - "200": { - "description": "The job.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OngoingMonitoringJob" - } - } - } + "409": { + "description": "The widget session is not linked to a Travel Rule transfer, or the transfer is not an outbound one in a status that accepts a counterparty." }, - "404": { - "description": "Unknown job for this application." - } - }, - "security": [ - { - "ApiKeyAuth": [] + "410": { + "description": "The session expired." } - ] + } } }, - "/v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/": { + "/v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/": { "patch": { - "summary": "Toggle AML ongoing monitoring for businesses", - "description": "Business-level equivalent of the user toggle: one monitored AML screening per business, with businesses lacking any AML screening screened first (company entity). Same estimate_only, inline fast-path and async job contract.", - "operationId": "toggle_vendor_businesses_ongoing_monitoring", + "summary": "Toggle AML ongoing monitoring for users", + "description": "Enables or disables AML ongoing monitoring at the user level. Exactly one monitored AML screening is kept per user; users without any AML screening (for example imported users) get one created and screened first, billed as a regular AML screening. Set estimate_only to preview counts and costs without writing. Batches of 20 or fewer resolve inline (200, COMPLETED); larger batches return 202 and run asynchronously.", + "operationId": "toggle_vendor_users_ongoing_monitoring", "x-didit-sdk": { - "resource": "businesses", + "resource": "users", "method": "set_ongoing_monitoring", "auth": "api_key", "server": true, "scope": "application" }, "tags": [ - "Businesses" + "Users" ], "parameters": [ { @@ -34417,20 +34540,168 @@ ] } }, - "/v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/{job_id}/": { + "/v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/{job_id}/": { "get": { "summary": "Get AML ongoing monitoring job", "description": "Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING.", - "operationId": "get_vendor_businesses_ongoing_monitoring_job", + "operationId": "get_vendor_users_ongoing_monitoring_job", "x-didit-sdk": { - "resource": "businesses", + "resource": "users", "method": "retrieve_ongoing_monitoring_job", "auth": "api_key", "server": true, "scope": "application" }, "tags": [ - "Businesses" + "Users" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Monitoring job UUID returned by the toggle endpoint." + } + ], + "responses": { + "200": { + "description": "The job.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OngoingMonitoringJob" + } + } + } + }, + "404": { + "description": "Unknown job for this application." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/": { + "patch": { + "summary": "Toggle AML ongoing monitoring for businesses", + "description": "Business-level equivalent of the user toggle: one monitored AML screening per business, with businesses lacking any AML screening screened first (company entity). Same estimate_only, inline fast-path and async job contract.", + "operationId": "toggle_vendor_businesses_ongoing_monitoring", + "x-didit-sdk": { + "resource": "businesses", + "method": "set_ongoing_monitoring", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Businesses" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OngoingMonitoringToggleRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Estimate (when estimate_only) or the completed job for small batches processed inline.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OngoingMonitoringToggleResult" + } + } + } + }, + "202": { + "description": "Larger batches are processed asynchronously; poll the job endpoint with the returned job_id.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OngoingMonitoringJob" + } + } + } + }, + "403": { + "description": "Missing write:sessions privilege, or insufficient balance to enable (the estimate is included in the error body)." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/{job_id}/": { + "get": { + "summary": "Get AML ongoing monitoring job", + "description": "Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING.", + "operationId": "get_vendor_businesses_ongoing_monitoring_job", + "x-didit-sdk": { + "resource": "businesses", + "method": "retrieve_ongoing_monitoring_job", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Businesses" ], "parameters": [ { @@ -35380,7 +35651,7 @@ }, "delete": { "summary": "Delete case", - "description": "Delete a case. There is no undelete endpoint.", + "description": "Delete a case permanently. Its draft regulatory reports are deleted with it. Linked verifications, people, companies and transactions are not deleted. There is no undelete endpoint.\n\nIf the case has a finalized regulatory report, the whole request is refused with `409` and nothing is deleted. The body uses `code: in_filed_report` and names each report with `uuid`, `report_number`, `report_type` and `case_uuid`. Delete the reports, then repeat the request.", "operationId": "delete_case", "x-didit-sdk": { "resource": "cases", @@ -35435,107 +35706,43 @@ "responses": { "204": { "description": "Case deleted." - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/assign/": { - "post": { - "summary": "Assign or unassign case", - "description": "Assign the case to a user, or unassign it by sending assignee_user_id: null. assignee_email/assignee_name auto-provision a User row when the assignee does not exist yet.", - "operationId": "assign_case", - "x-didit-sdk": { - "resource": "cases", - "method": "assign", - "auth": "api_key", - "server": true, - "scope": "application" - }, - "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "403": { + "description": "The key cannot delete cases. It requires a full-access key with `write:cases`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "assignee_user_id" - ], - "properties": { - "assignee_user_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "null to unassign." - }, - "assignee_email": { - "type": "string", - "format": "email", - "nullable": true - }, - "assignee_name": { - "type": "string", - "nullable": true + "404": { + "description": "The case does not exist in this application or was already deleted.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/assign/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"assignee_user_id\": \"9a000000-0000-4000-8000-0000000000aa\"}'" - } - ], - "responses": { - "200": { - "description": "Case assigned/unassigned.", + }, + "409": { + "description": "The case has a finalized regulatory report. Nothing is deleted.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CaseDetail" + "$ref": "#/components/schemas/InFiledReportError" } } } @@ -35548,14 +35755,118 @@ ] } }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/resolve/": { + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/assign/": { "post": { - "summary": "Resolve case", - "description": "Resolve the case with one of five resolutions: FALSE_POSITIVE (no concern found), VALID_THREAT (concern confirmed), INCONCLUSIVE, REQUIREMENTS_NOT_MET or REQUIREMENTS_SATISFIED. Blocked with 400 if the blueprint's checklist is not optional and items remain incomplete, or if the blueprint requires a resolution note and none is given. When the case's blueprint has a four-eyes checker step (four_eyes_checker_blueprint), the resolution is staged (case.pending_resolution) and the case moves to the checker blueprint instead of resolving immediately; a different officer must call approve or reject-approval. Submit a four-eyes resolution as a console member: a resolution an API key submits has no recorded maker, so approve and reject-approval return 403 for every caller. On a four-eyes blueprint you can name the member who must approve with `checker`: then only that member can approve or reject the resolution, and the response's `checker` names them. Without `checker`, the resolution waits on the checker blueprint's queue. Sending `checker` for a blueprint without a checker step returns 400.", - "operationId": "resolve_case", + "summary": "Assign or unassign case", + "description": "Assign the case to a user, or unassign it by sending assignee_user_id: null. assignee_email/assignee_name auto-provision a User row when the assignee does not exist yet.", + "operationId": "assign_case", "x-didit-sdk": { "resource": "cases", - "method": "resolve", + "method": "assign", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "assignee_user_id" + ], + "properties": { + "assignee_user_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "null to unassign." + }, + "assignee_email": { + "type": "string", + "format": "email", + "nullable": true + }, + "assignee_name": { + "type": "string", + "nullable": true + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/assign/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"assignee_user_id\": \"9a000000-0000-4000-8000-0000000000aa\"}'" + } + ], + "responses": { + "200": { + "description": "Case assigned/unassigned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseDetail" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/resolve/": { + "post": { + "summary": "Resolve case", + "description": "Resolve the case with one of five resolutions: FALSE_POSITIVE (no concern found), VALID_THREAT (concern confirmed), INCONCLUSIVE, REQUIREMENTS_NOT_MET or REQUIREMENTS_SATISFIED. Blocked with 400 if the blueprint's checklist is not optional and items remain incomplete, or if the blueprint requires a resolution note and none is given. When the case's blueprint has a four-eyes checker step (four_eyes_checker_blueprint), the resolution is staged (case.pending_resolution) and the case moves to the checker blueprint instead of resolving immediately; a different officer must call approve or reject-approval. Submit a four-eyes resolution as a console member: a resolution an API key submits has no recorded maker, so approve and reject-approval return 403 for every caller. On a four-eyes blueprint you can name the member who must approve with `checker`: then only that member can approve or reject the resolution, and the response's `checker` names them. Without `checker`, the resolution waits on the checker blueprint's queue. Sending `checker` for a blueprint without a checker step returns 400.", + "operationId": "resolve_case", + "x-didit-sdk": { + "resource": "cases", + "method": "resolve", "auth": "api_key", "server": true, "scope": "application" @@ -40494,7 +40805,7 @@ }, "delete": { "summary": "Delete report", - "description": "Delete a draft or validated report. Finalized reports cannot be deleted (409).", + "description": "Delete a regulatory report permanently. The report, its copied narrative and reasons, its transaction links, and its generated goAML XML and PDF are erased, including every stored version of those files. The report disappears from lists and download URLs stop working. Other records, including the case, transactions, people and companies, are not deleted. There is no undo.\n\nA draft or validated report can be deleted without a request body. Deleting a finalized report requires `confirm_delete: true` in the request body. If the report is missing, the request returns `404`; if a finalized report is sent without confirmation, it returns `400` and nothing is deleted.", "operationId": "delete_regulatory_report", "x-didit-sdk": { "resource": "regulatory_reports", @@ -40542,16 +40853,44 @@ "x-codeSamples": [ { "lang": "curl", - "label": "curl", + "label": "curl (draft report)", "source": "curl -X DELETE 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "curl", + "label": "curl (finalized report)", + "source": "curl -X DELETE 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"confirm_delete\": true}'" } ], "responses": { "204": { - "description": "Report deleted." + "description": "Report deleted, including its generated files." }, - "409": { - "description": "Report is already finalized.", + "400": { + "description": "A finalized report was sent without `confirm_delete: true`. Nothing is deleted.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "confirm_delete": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "example": { + "confirm_delete": [ + "This field is required to delete a finalized report." + ] + } + } + } + }, + "403": { + "description": "The API key cannot delete reports. The endpoint requires `write:cases`.", "content": { "application/json": { "schema": { @@ -40564,105 +40903,62 @@ } } } - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - } - }, - "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/validate/": { - "post": { - "summary": "Validate report", - "description": "Check the report against the obligatory goAML header fields (reporting entity ID, submission code, local currency, action taken, submission date), plus at least one attached transaction and a 2-letter jurisdiction, without generating any files. Also stores the result on the report: validation_errors and validated_at are updated, and a non-finalized report's status moves to VALIDATED (no errors) or back to DRAFT (errors).", - "operationId": "validate_regulatory_report", - "x-didit-sdk": { - "resource": "regulatory_reports", - "method": "validate", - "auth": "api_key", - "server": true, - "scope": "application" - }, - "tags": [ - "Regulatory Reports" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "404": { + "description": "Report not found in this application.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } }, - { - "name": "report_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "5ec00000-0000-4000-8000-000000000007" - }, - "description": "Report UUID." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/validate/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "200": { - "description": "Validation result.", + "409": { + "description": "The report is already being deleted. Repeat the request to finish erasing its files.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "status": { - "type": "string", - "enum": [ - "DRAFT", - "VALIDATED", - "FINALIZED" - ] + "detail": { + "type": "string" }, - "is_valid": { - "type": "boolean" + "code": { + "type": "string" + } + } + }, + "example": { + "detail": "This report is being deleted. Retry deleting it to finish erasing its files.", + "code": "report_deletion_pending" + } + } + } + }, + "503": { + "description": "The report files could not be erased right now. The report is still in place. Repeat the request.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" }, - "errors": { - "type": "array", - "description": "Empty when valid.", - "items": { - "type": "object", - "properties": { - "field": { - "type": "string" - }, - "message": { - "type": "string" - } - } - } + "code": { + "type": "string" } } + }, + "example": { + "detail": "The report files could not be erased. Please retry deleting the report.", + "code": "report_storage_unavailable" } } } @@ -40672,17 +40968,139 @@ { "ApiKeyAuth": [] } - ] + ], + "requestBody": { + "required": false, + "description": "Required only for a finalized report.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "confirm_delete": { + "type": "boolean", + "description": "Set to true to confirm that a finalized report and its generated files are deleted permanently." + } + }, + "example": { + "confirm_delete": true + } + } + } + } + } } }, - "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/finalize/": { + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/validate/": { "post": { - "summary": "Finalize report", - "description": "Re-validate, then render the goAML XML and PDF and upload both to S3. On SAR/STR/TFR reports, this also moves the case's transaction alerts from Pending SAR to SAR filed.", - "operationId": "finalize_regulatory_report", + "summary": "Validate report", + "description": "Check the report against the obligatory goAML header fields (reporting entity ID, submission code, local currency, action taken, submission date), plus at least one attached transaction and a 2-letter jurisdiction, without generating any files. Also stores the result on the report: validation_errors and validated_at are updated, and a non-finalized report's status moves to VALIDATED (no errors) or back to DRAFT (errors).", + "operationId": "validate_regulatory_report", "x-didit-sdk": { "resource": "regulatory_reports", - "method": "finalize", + "method": "validate", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Regulatory Reports" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "report_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "5ec00000-0000-4000-8000-000000000007" + }, + "description": "Report UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/validate/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Validation result.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "DRAFT", + "VALIDATED", + "FINALIZED" + ] + }, + "is_valid": { + "type": "boolean" + }, + "errors": { + "type": "array", + "description": "Empty when valid.", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/finalize/": { + "post": { + "summary": "Finalize report", + "description": "Re-validate, then render the goAML XML and PDF and upload both to S3. On SAR/STR/TFR reports, this also moves the case's transaction alerts from Pending SAR to SAR filed.", + "operationId": "finalize_regulatory_report", + "x-didit-sdk": { + "resource": "regulatory_reports", + "method": "finalize", "auth": "api_key", "server": true, "scope": "application" @@ -41992,7 +42410,7 @@ }, "cross_org_available": { "type": "boolean", - "description": "Whether cross-organization signals apply to this organization. They need both the Identity Enterprise plan and the `global` sharing mode.", + "description": "Whether cross-organization signals apply to this organization. They need both the Enterprise plan and the `global` sharing mode.", "example": true }, "cross_org_unavailable_reason": { @@ -42002,7 +42420,7 @@ "sharing_mode" ], "nullable": true, - "description": "Why cross-organization signals do not apply, checked in this order. `plan`: the organization's Identity plan does not include the cross-organization fraud network (it needs Identity Enterprise), whatever the sharing mode. `sharing_mode`: the plan includes it, but the sharing mode is `private` or `off`. Null when `cross_org_available` is `true`.", + "description": "Why cross-organization signals do not apply, checked in this order. `plan`: the organization's plan does not include the cross-organization fraud network (it needs the Enterprise plan), whatever the sharing mode. `sharing_mode`: the plan includes it, but the sharing mode is `private` or `off`. Null when `cross_org_available` is `true`.", "example": null } }, @@ -45442,11 +45860,12 @@ "enum": [ "not_requested", "delivered", + "not_held", "unavailable", "pending", null ], - "description": "`delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier." + "description": "`delivered` when the ownership data came back, `not_held` when the ownership registry answered and does not list this company (see `registry_data.ownership_error`), `unavailable` when an ownership tier was requested but the registry had none for this company or could not be asked (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier." }, "ownership": { "$ref": "#/components/schemas/KYBRegistryOwnership" @@ -46315,11 +46734,12 @@ "enum": [ "not_requested", "delivered", + "not_held", "unavailable", "pending", null ], - "description": "`delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier." + "description": "`delivered` when the ownership data came back, `not_held` when the ownership registry answered and does not list this company (see `registry_data.ownership_error`), `unavailable` when an ownership tier was requested but the registry had none for this company or could not be asked (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier." }, "ownership": { "$ref": "#/components/schemas/KYBRegistryOwnership" @@ -58605,6 +59025,7 @@ "data_retention_months": 24, "face_retention_policy": "delete_with_session", "face_retention_days": null, + "biometric_consent_mode": "auto", "available_events": [ { "value": "status.updated", @@ -58873,6 +59294,15 @@ "maximum": 3650, "description": "Retention period, in days, of biometric templates under `retain_until_user_deleted`." }, + "biometric_consent_mode": { + "type": "string", + "enum": [ + "auto", + "always", + "never" + ], + "description": "When the hosted flow and the native SDKs ask the person for explicit biometric consent before a face capture. `auto` (default) asks only where a trigger applies: a declared state, the person's IP, or the identity document's issuer is in Illinois, Texas or Colorado, or the person looks to be in the US without a known state. `always` asks every person whose workflow captures a face. `never` never shows the step, and you collect consent yourself before the selfie. Each change is recorded in the audit log. See [Biometric consent](https://docs.didit.me/getting-started/biometric-consent)." + }, "allowed_return_hosts": { "type": "array", "maxItems": 20, @@ -58888,7 +59318,8 @@ "Enable biometric template retention": { "value": { "face_retention_policy": "retain_until_user_deleted", - "face_retention_days": 365 + "face_retention_days": 365, + "biometric_consent_mode": "auto" } }, "Rotate the legacy secret": { @@ -58948,6 +59379,7 @@ "data_retention_months": 24, "face_retention_policy": "retain_until_user_deleted", "face_retention_days": 365, + "biometric_consent_mode": "auto", "available_events": [ { "value": "status.updated", @@ -59038,7 +59470,8 @@ "value": { "face_retention_days": [ "A finite retention duration is required when face retention is enabled." - ] + ], + "biometric_consent_mode": "auto" } }, "Retention window out of range": { @@ -73627,522 +74060,1696 @@ } ] } - } - }, - "components": { - "schemas": { - "DocumentAIDetectedCode": { - "type": "object", - "required": [ - "type", - "page_number", - "position", - "x", - "y", - "width", - "height", - "page_width", - "page_height", - "raw_payload", - "parsed_payload" + }, + "/v3/organization/{organization_id}/application/{application_id}/transactions/delete/": { + "delete": { + "summary": "Batch delete transactions", + "description": "Delete one or more transactions permanently. Select them with `transaction_uuids`, or delete every transaction in the application with `delete_all: true`.\n\nA deleted transaction goes with the records that are only its own: its parties, tags, notes, payment method links, documents and every stored version of their files, rule runs, alerts, provider results, network snapshot and Travel Rule data. Cases, people and companies stay; their link to the deleted transaction is cleared. Draft regulatory reports stop listing it and are rebuilt without it. Deletion is permanent and irreversible; there is no undo endpoint.\n\nIf any selected transaction is listed in a finalized regulatory report, the whole request is refused with `409` and nothing is deleted. The body uses `code: in_filed_report` and names each transaction and the reports that list it. Delete those reports, then repeat the request.", + "operationId": "batch_delete_transactions", + "x-didit-sdk": { + "resource": "transactions", + "method": "batch_delete", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Transactions" ], - "properties": { - "type": { - "type": "string", - "description": "Detected symbology, for example `QR_CODE`, `CODE_128`, `EAN_13`, `DATA_MATRIX`, or `PDF_417`." - }, - "page_number": { - "type": "integer", - "minimum": 1, - "description": "1-based page number." + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, - "position": { - "type": "array", - "nullable": true, - "description": "Detected polygon in rendered-page pixel coordinates. `null` when no reliable location is available.", - "items": { - "type": "array", - "minItems": 2, - "maxItems": 2, - "items": { - "type": "integer" + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "oneOf": [ + { + "title": "Selected transactions", + "required": [ + "transaction_uuids" + ], + "properties": { + "transaction_uuids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Didit transaction UUIDs to delete. Required unless `delete_all` is true.", + "minItems": 1 + }, + "delete_all": { + "type": "boolean", + "enum": [ + false + ], + "default": false, + "description": "Leave out or send `false` when you select transactions with `transaction_uuids`." + } + } + }, + { + "title": "All transactions", + "required": [ + "delete_all" + ], + "properties": { + "delete_all": { + "type": "boolean", + "enum": [ + true + ], + "description": "Delete every transaction in the application. `transaction_uuids` is ignored." + }, + "transaction_uuids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Ignored when `delete_all` is true." + } + } + } + ] + }, + "examples": { + "Selected transactions": { + "value": { + "transaction_uuids": [ + "abcdef12-3456-7890-abcd-ef1234567890", + "12345678-90ab-cdef-1234-567890abcdef" + ] + } + }, + "All transactions": { + "value": { + "delete_all": true + } + } } } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (selected transactions)", + "source": "curl -X DELETE 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/transactions/delete/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"transaction_uuids\": [\"abcdef12-3456-7890-abcd-ef1234567890\"]}'" + } + ], + "responses": { + "204": { + "description": "Selected transactions deleted." }, - "x": { - "type": "integer", - "nullable": true, - "description": "Left edge of the bounding box in rendered-page pixels." - }, - "y": { - "type": "integer", - "nullable": true, - "description": "Top edge of the bounding box in rendered-page pixels." - }, - "width": { - "type": "integer", - "nullable": true, - "description": "Bounding-box width in rendered-page pixels." - }, - "height": { - "type": "integer", - "nullable": true, - "description": "Bounding-box height in rendered-page pixels." + "400": { + "description": "No transaction selector was supplied.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "transaction_uuids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "example": { + "transaction_uuids": [ + "This field is required unless delete_all is true." + ] + } + } + } }, - "page_width": { - "type": "integer", - "description": "Rendered page width in pixels." + "403": { + "description": "The API key cannot delete transactions. The endpoint requires `write:transactions`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } }, - "page_height": { - "type": "integer", - "description": "Rendered page height in pixels." + "409": { + "description": "At least one selected transaction is listed in a finalized regulatory report. Nothing is deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InFiledReportError" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/travel-rule/networks/": { + "get": { + "summary": "Get Travel Rule networks", + "description": "Read, for this application, every network's true state: whether it sends through your own membership or through Didit's, and why it cannot yet. Also returns the due-diligence questionnaire and your participants. Reading creates nothing.", + "operationId": "getTravelRuleNetworks", + "x-didit-sdk": { + "resource": "travel_rule.networks", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET https://verification.didit.me/v3/travel-rule/networks/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Network states, questionnaire and participants.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleNetworksOverview" + }, + "examples": { + "Before the questionnaire": { + "value": { + "platform_networks_enabled": false, + "questionnaire": { + "status": "draft", + "answers": {}, + "submitted_at": null, + "reviewed_at": null, + "review_note": "", + "history": [], + "updated_at": null + }, + "participants": [], + "networks": { + "gtr": { + "rail": "GTR", + "mode": "unavailable", + "state": "didit_onboarding", + "reason": "didit_membership_not_live", + "platform_available": false, + "connected": false, + "participant": null, + "vasps_reachable": 0, + "routed_30d": 0 + }, + "trust": { + "rail": "TRUST", + "mode": "unavailable", + "state": "unavailable", + "reason": "own_membership_only", + "platform_available": false, + "connected": false, + "participant": null, + "vasps_reachable": 0, + "routed_30d": 0 + }, + "verifyvasp": { + "rail": "VERIFYVASP", + "mode": "unavailable", + "state": "didit_onboarding", + "reason": "didit_membership_not_live", + "platform_available": false, + "connected": false, + "participant": null, + "vasps_reachable": 0, + "routed_30d": 0 + }, + "sygna": { + "rail": "SYGNA", + "mode": "unavailable", + "state": "didit_onboarding", + "reason": "didit_membership_not_live", + "platform_available": false, + "connected": false, + "participant": null, + "vasps_reachable": 0, + "routed_30d": 0 + } + } + } + } + } + } + } }, - "raw_payload": { - "type": "string", - "description": "Raw decoder payload as a base64 string when available; otherwise an empty string." + "401": { + "description": "Missing or invalid API key." + } + } + } + }, + "/v3/travel-rule/networks/questionnaire/": { + "get": { + "summary": "Get the network questionnaire", + "description": "Read Didit's network due-diligence questionnaire for this application, its status and Didit's review note. An application that never saved one reads an empty draft.", + "operationId": "getTravelRuleNetworkQuestionnaire", + "x-didit-sdk": { + "resource": "travel_rule.networks", + "method": "retrieve_questionnaire", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET https://verification.didit.me/v3/travel-rule/networks/questionnaire/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "The questionnaire.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleNetworkQuestionnaire" + } + } + } }, - "parsed_payload": { - "nullable": true, - "description": "Decoded string or structured value. `null` when the code was located but could not be decoded." + "401": { + "description": "Missing or invalid API key." } } }, - "SessionImportJob": { - "type": "object", - "required": [ - "uuid", - "status", - "import_type", - "source_format", - "source_filename", - "total_rows", - "processed_rows", - "created_count", - "updated_count", - "skipped_count", - "failed_count" + "put": { + "summary": "Save network questionnaire answers", + "description": "Save answers to the questionnaire while it is draft or changes_requested. Saves are partial: answers you do not send are kept.", + "operationId": "updateTravelRuleNetworkQuestionnaire", + "x-didit-sdk": { + "resource": "travel_rule.networks", + "method": "update_questionnaire", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" ], - "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Import job id. Use this value to poll progress and fetch row errors." - }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED", - "PAUSED", - "CANCELLED", - "READY" - ] + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PUT https://verification.didit.me/v3/travel-rule/networks/questionnaire/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"legal_name\": \"Example VASP Ltd\", \"incorporation_country\": \"ES\", \"aml_ctf_policy_attested\": true}'" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleNetworkQuestionnaireAnswers" + } + } + } + }, + "responses": { + "200": { + "description": "The questionnaire with the merged answers.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleNetworkQuestionnaire" + } + } + } }, - "import_type": { - "type": "string", - "enum": [ - "user_verification", - "business_verification", - "status_rules", - "transactions" - ] + "400": { + "description": "Validation error, keyed by field." }, - "source_format": { - "type": "string", - "enum": [ - "csv", - "ndjson" - ] + "401": { + "description": "Missing or invalid API key." }, - "source_filename": { - "type": "string" + "403": { + "description": "Network access through Didit's membership is not open yet.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "enum": [ + "travel_rule_platform_networks_disabled" + ] + } + } + } + } + } }, - "total_rows": { - "type": "integer", - "format": "int64" - }, - "processed_rows": { - "type": "integer", - "format": "int64" - }, - "checkpoint_row": { - "type": "integer", - "format": "int64" - }, - "created_count": { - "type": "integer", - "format": "int64" - }, - "updated_count": { - "type": "integer", - "format": "int64" - }, - "skipped_count": { - "type": "integer", - "format": "int64" - }, - "failed_count": { - "type": "integer", - "format": "int64" - }, - "total_media_bytes": { - "type": "integer", - "format": "int64" - }, - "last_error": { - "type": "string", - "nullable": true - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { - "type": "string", - "format": "date-time" - }, - "started_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "completed_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "state": { - "type": "string", - "enum": [ - "ready", - "queued", - "running", - "paused", - "completed", - "errors", - "failed", - "cancelled" - ], - "description": "The job's state as the console shows it; `errors` is a completed job with rejected rows." - }, - "source": { - "type": "string", - "enum": [ - "file", - "api", - "provider" - ] - }, - "provider": { - "type": "string", - "description": "Migration source; empty for a file or API import." - }, - "provider_options": { - "type": "object" - }, - "credential_hint": { - "type": "string", - "description": "The first characters of the key the migration holds. The key itself is never returned." - }, - "credentials_expires_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "resumes_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When a job paused by the provider's rate limit resumes by itself." - }, - "status_mapping": { - "type": "object", - "additionalProperties": { - "type": "string" + "409": { + "description": "The questionnaire is with Didit or decided: it can only change while draft or changes_requested.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "enum": [ + "travel_rule_questionnaire_not_editable" + ] + } + } + } + } + } + } + } + } + }, + "/v3/travel-rule/networks/questionnaire/submit/": { + "post": { + "summary": "Submit the network questionnaire", + "description": "Send the saved questionnaire to Didit's review. The saved answers must be complete: every field except lei and data_protection_contact_email, with the three attestations true. Didit approves it, asks for changes (edit and submit again) or rejects it. Networks you requested while it was pending are handed to their onboarding when it is approved.", + "operationId": "submitTravelRuleNetworkQuestionnaire", + "x-didit-sdk": { + "resource": "travel_rule.networks", + "method": "submit_questionnaire", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/networks/questionnaire/submit/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "The submitted questionnaire.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleNetworkQuestionnaire" + } + } } }, - "is_dry_run": { - "type": "boolean" - }, - "dry_run_summary": { - "type": "object", - "description": "What a dry run found: `total`, `by_status`, `already_in_didit`, `same_person`, `not_finished`, `failed`, `unmapped_statuses`." - }, - "face_templates_enabled": { - "type": "boolean" - }, - "lawful_basis_confirmed_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "not_finished_count": { - "type": "integer", - "format": "int64" + "400": { + "description": "The saved answers are incomplete or invalid, keyed by field." }, - "same_person_count": { - "type": "integer", - "format": "int64" + "401": { + "description": "Missing or invalid API key." }, - "face_templates_count": { - "type": "integer", - "format": "int64" + "403": { + "description": "Network access through Didit's membership is not open yet.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "enum": [ + "travel_rule_platform_networks_disabled" + ] + } + } + } + } + } }, - "face_duplicates_count": { - "type": "integer", - "format": "int64" + "409": { + "description": "The questionnaire is with Didit or decided: it can only change while draft or changes_requested.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "enum": [ + "travel_rule_questionnaire_not_editable" + ] + } + } + } + } + } + } + } + } + }, + "/v3/travel-rule/networks/{network}/request/": { + "post": { + "summary": "Request a Travel Rule network", + "description": "Ask for this application's own participant on a network Didit's membership carries (GTR or Sygna). Without an approved questionnaire the participant waits for it (questionnaire_pending); with one it goes to the network's onboarding (submitted_to_network) and becomes active once the network gives it a code. Idempotent: a pending, onboarding or live participant is returned unchanged. If the network refused the registration (last_error is set), requesting again retries. TRUST and VerifyVASP are reachable only through your own membership: TRUST admits members per VASP, and a VerifyVASP Enclave serves one VASP id.", + "operationId": "requestTravelRuleNetworkAccess", + "x-didit-sdk": { + "resource": "travel_rule.networks", + "method": "request_access", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "network", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "gtr", + "sygna" + ] + }, + "description": "The network to join through Didit's membership." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/networks/gtr/request/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Your participant on the network.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleNetworkParticipant" + } + } + } }, - "face_blocklist_hits_count": { - "type": "integer", - "format": "int64" + "400": { + "description": "Unknown network, a network Didit does not carry (connect your own membership), or a sandbox application.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "enum": [ + "travel_rule_network_own_membership_only", + "travel_rule_network_sandbox_application" + ] + } + } + } + } + } }, - "created_by_name": { - "type": "string", - "nullable": true + "401": { + "description": "Missing or invalid API key." }, - "created_by_email": { - "type": "string", - "nullable": true + "403": { + "description": "Network access through Didit's membership is not open yet.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "enum": [ + "travel_rule_platform_networks_disabled" + ] + } + } + } + } + } } } - }, - "SessionImportError": { - "type": "object", - "required": [ - "uuid", - "row_number", - "error", - "raw_row" + } + }, + "/v3/address-validation/": { + "post": { + "summary": "Validate an address", + "description": "Confirm that an address exists and get it back normalised: its delivery lines, components and coordinates. It confirms the address only; it never links the address to a person.\n\n**Request.** Send the address on one line (`address`) or as structured fields (`address_line_1` and, optionally, `address_line_2`, `city`, `region`, `postal_code`), never both, and always its `country` (ISO 3166-1 alpha-2 or alpha-3).\n\n**Result.** `address_validation.status` is `verified` (confirmed to the building), `partially_verified` (confirmed only to the street or above), `not_verified` (only the region or city matched, or nothing did) or `unavailable` (the validation could not run). HTTP is `200` for every completed request, `unavailable` included; retry an `unavailable` result later.\n\n**Billing.** A live call costs $0.10 per validated address. An `unavailable` result is not charged. Nothing is stored apart from the usage record: no session is created and the result is returned only in this response.\n\n**Sandbox.** Sandbox keys validate the request and always return a deterministic `verified` result that echoes the address sent, without consuming credits.\n\n**Authentication.** Send your application's API key in `x-api-key`. Missing or invalid credentials return `403` - this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests per minute per API key across all POST/PATCH/DELETE endpoints.", + "operationId": "post_v3_address_validation", + "x-didit-sdk": { + "resource": "address_validation", + "method": "validate", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" ], - "properties": { - "uuid": { - "type": "string", - "format": "uuid" + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/address-validation/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"address_line_1\": \"1600 Amphitheatre Pkwy\",\n \"city\": \"Mountain View\",\n \"region\": \"CA\",\n \"postal_code\": \"94043\",\n \"country\": \"US\"\n }'" }, - "row_number": { - "type": "integer", - "format": "int64" + { + "lang": "python", + "label": "Python", + "source": "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/address-validation/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"], \"Content-Type\": \"application/json\"},\n json={\n \"address_line_1\": \"1600 Amphitheatre Pkwy\",\n \"city\": \"Mountain View\",\n \"region\": \"CA\",\n \"postal_code\": \"94043\",\n \"country\": \"US\",\n },\n timeout=15,\n)\nresp.raise_for_status()\nresult = resp.json()[\"address_validation\"]\nprint(result[\"status\"], result[\"formatted_address\"])" }, - "external_id": { - "type": "string", - "nullable": true + { + "lang": "javascript", + "label": "JavaScript", + "source": "const res = await fetch('https://verification.didit.me/v3/address-validation/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ address: 'Paseo de la Castellana 259, 28046 Madrid', country: 'ES' }),\n});\nconst { address_validation } = await res.json();\nconsole.log(address_validation.status, address_validation.formatted_address);" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressValidationRequest" + }, + "examples": { + "Structured": { + "summary": "Structured address fields", + "value": { + "address_line_1": "1600 Amphitheatre Pkwy", + "city": "Mountain View", + "region": "CA", + "postal_code": "94043", + "country": "US" + } + }, + "One line": { + "summary": "The whole address on one line", + "value": { + "address": "Paseo de la Castellana 259, 28046 Madrid", + "country": "ES" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The validation completed. Read `address_validation.status`; HTTP is always `200` for a completed request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddressValidationResponse" + }, + "examples": { + "Verified": { + "summary": "A real address, confirmed to the building", + "value": { + "request_id": "6f7d2c1e-1c5a-4a1e-9b8f-2a1f0f4b9c3d", + "address_validation": { + "status": "verified", + "verification_level": "premise", + "address": { + "address_line_1": "1600 Amphitheatre Pkwy", + "address_line_2": null, + "city": "Mountain View", + "region": "CA", + "postal_code": "94043-1351", + "country": "US" + }, + "formatted_address": "1600 Amphitheatre Pkwy, Mountain View CA 94043-1351", + "latitude": 37.422022, + "longitude": -122.08429 + }, + "created_at": "2026-10-27T11:20:00.000000+00:00" + } + }, + "Not verified": { + "summary": "An invented street: only the city matched", + "value": { + "request_id": "0b9e3f4a-6c2d-4e8b-a1f7-5d3c2b1a0e9f", + "address_validation": { + "status": "not_verified", + "verification_level": "locality", + "address": { + "address_line_1": "Calle Inventada Que No Existe 99999 28046", + "address_line_2": null, + "city": "Madrid", + "region": "Madrid", + "postal_code": null, + "country": "ES" + }, + "formatted_address": "Calle Inventada Que No Existe 99999 28046, Madrid, MADRID, SPAIN", + "latitude": 40.42603054, + "longitude": -3.68645331 + }, + "created_at": "2026-10-27T11:21:00.000000+00:00" + } + }, + "Unavailable": { + "summary": "The validation could not run; not charged", + "value": { + "request_id": "3c1b2a0f-9e8d-4c7b-b6a5-1f0e9d8c7b6a", + "address_validation": { + "status": "unavailable", + "verification_level": null, + "address": null, + "formatted_address": null, + "latitude": null, + "longitude": null + }, + "created_at": "2026-10-27T11:22:00.000000+00:00" + } + } + } + } + } }, - "error": { - "type": "string" + "400": { + "$ref": "#/components/responses/AddressValidationValidationError" }, - "raw_row": { - "type": "object", - "additionalProperties": true + "403": { + "$ref": "#/components/responses/AddressValidationPermissionDenied" }, - "created_at": { - "type": "string", - "format": "date-time" + "429": { + "$ref": "#/components/responses/RateLimitedWrite" } } - }, - "BrandingCustomizationUpdate": { - "type": "object", - "description": "Partial branding update body. Provide only the fields you want to change. Hex colors accept `#RRGGBB` or `#RRGGBBAA`. Custom domain and white-label email are not accepted here.", - "properties": { - "color_primary": { - "type": "string", - "description": "Headers / primary text color." - }, - "color_secondary": { - "type": "string", - "description": "Paragraph / secondary text color." + } + }, + "/v3/travel-rule/wallet-proofs/": { + "get": { + "summary": "List screenshot wallet proofs to review", + "description": "Your review queue for screenshot wallet-ownership proofs. A screenshot is the one proof Didit cannot check itself: once the end user uploads it from the widget it waits PENDING until you approve or reject it with POST /v3/travel-rule/wallet-proofs/{proof_uuid}/decision/. Oldest first, so the queue is worked from the proof that has waited longest. Only uploaded screenshots appear, never a proof whose end user has not uploaded a file yet, and never one on a wallet you deleted from your address book (Didit closes those as OWNERSHIP_CHANGED).", + "operationId": "listTravelRuleWalletProofs", + "x-didit-sdk": { + "resource": "travel_rule.wallet_proofs", + "method": "list", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Which proofs to list. Defaults to PENDING, the ones still to decide.", + "schema": { + "type": "string", + "enum": [ + "PENDING", + "VERIFIED", + "REJECTED", + "EXPIRED" + ], + "default": "PENDING" + } }, - "color_background": { - "type": "string", - "description": "Page background color." + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50, + "minimum": 1 + } }, - "color_panel": { - "type": "string", - "description": "Panel / accent color." + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + } + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl https://verification.didit.me/v3/travel-rule/wallet-proofs/?status=PENDING \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Paginated screenshot proofs. count is exact up to 100 and capped at 100 above it.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "next": { + "type": "string", + "nullable": true + }, + "previous": { + "type": "string", + "nullable": true + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TravelRuleWalletProofReview" + } + } + } + } + } + } }, - "color_panel_10": { - "type": "string", - "description": "Panel color at 10% opacity." + "400": { + "description": "Unknown status." }, - "color_on_panel_1": { - "type": "string", - "description": "Support text color on panels." + "401": { + "description": "Missing or invalid x-api-key." }, - "color_on_panel_2": { - "type": "string", - "description": "Pill / badge color on panels." + "403": { + "description": "The caller lacks the transactions permission." + } + } + } + }, + "/v3/travel-rule/wallet-proofs/{proof_uuid}/decision/": { + "post": { + "summary": "Approve or reject a screenshot wallet proof", + "description": "Decide a screenshot proof from your review queue. approve verifies the proof and the address book entry and applies the ownership to the transfer the widget session was minted for, exactly as a verified signature does. A screenshot never confirms a transfer that another Didit customer sent over the INTERNAL rail: that takes a signed message or a test deposit. reject closes the proof as REJECTED with failure_reason REVIEW_REJECTED; the end user can start a new proof from the widget. The note is kept with the proof.", + "operationId": "decideTravelRuleWalletProof", + "x-didit-sdk": { + "resource": "travel_rule.wallet_proofs", + "method": "decide", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "proof_uuid", + "in": "path", + "required": true, + "description": "proof_id from the review queue.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWalletProofDecision" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/wallet-proofs/{proof_uuid}/decision/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"decision\": \"approve\", \"note\": \"Address matches the wallet app.\"}'" + } + ], + "responses": { + "200": { + "description": "The decided proof.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWalletProofReview" + } + } + } }, - "color_on_background": { - "type": "string", - "description": "Text color on background." + "400": { + "description": "Invalid decision, or a note longer than 512 characters." }, - "color_button_1": { - "type": "string", - "description": "Primary button color." + "401": { + "description": "Missing or invalid x-api-key." }, - "color_button_2": { - "type": "string", - "description": "Secondary button color." + "403": { + "description": "The caller lacks the transactions permission." }, - "color_button_text_1": { - "type": "string", - "description": "Primary button text color." + "404": { + "description": "No submitted screenshot proof with this id in your application." }, - "color_button_text_2": { - "type": "string", - "description": "Secondary button text color." + "409": { + "description": "The proof is already decided (the body carries its status), or code OWNERSHIP_CHANGED: the wallet's holder changed or the wallet was deleted after the widget session was minted, so the proof is closed and the current holder needs a new one.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWidgetProofError" + } + } + } + } + } + } + }, + "/v3/travel-rule/widget/{token}/proofs/": { + "post": { + "summary": "Start a wallet-ownership proof", + "description": "Public endpoint keyed by the widget session token, no API key required: the hosted widget calls it when the end user picks a proof method. A proof exists, and counts in your proof funnel, only from this call. Picking the same method again returns the proof already open (200, same challenge or deposit amount) instead of starting a second one; picking another method closes the unfinished one as METHOD_SWITCHED, except a submitted screenshot or a test deposit still being checked. MESSAGE_SIGNING returns the challenge to sign; SATOSHI_TEST the deposit address, exact amount and window; SELF_DECLARATION is verified at once (status VERIFIED). Signature formats verified per chain family: EVM chains take a personal_sign (EIP-191) signature, hex encoded; Bitcoin takes a BIP-137 signmessage signature (P2PKH, P2SH-P2WPKH and P2WPKH addresses) or a BIP-322 simple signature (P2WPKH and P2TR addresses), base64 encoded; Solana takes an Ed25519 signature over the challenge, base58 encoded; Tron takes a signMessageV2 (TIP-191) signature, hex encoded. A BIP-322 full or proof-of-funds signature, or a multisig address, fails with SIGNATURE_UNSUPPORTED_SCHEME.", + "operationId": "createTravelRuleWidgetProof", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "create_widget_proof", + "auth": "public", + "server": false + }, + "tags": [ + "Travel Rule" + ], + "security": [ + {} + ], + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "description": "Opaque session token returned when the widget session was minted.", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWidgetProofCreate" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/widget/{token}/proofs/ \\\n -H 'Content-Type: application/json' \\\n -d '{\"method\": \"MESSAGE_SIGNING\"}'" + } + ], + "responses": { + "201": { + "description": "A new PENDING proof (VERIFIED for SELF_DECLARATION).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWidgetProofSummary" + } + } + } }, - "color_pill_text": { - "type": "string", - "description": "Text color for pills, inputs and selectors." + "200": { + "description": "The proof already open for this method, resumed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWidgetProofSummary" + } + } + } }, - "border_radius_panel": { - "type": "integer", - "minimum": 0, - "description": "Panel corner radius in pixels." + "400": { + "description": "Invalid body, a wallet that does not match the transfer, SCREENSHOT not allowed, or code CHAIN_UNSUPPORTED when the method is not available on the chain.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWidgetProofError" + } + } + } }, - "border_radius_buttons": { - "type": "integer", - "minimum": 0, - "description": "Button corner radius in pixels." + "403": { + "description": "SELF_DECLARATION is not allowed for this application." }, - "font_family": { - "type": "string", - "description": "Google Font family name (e.g. `Inter`, `AR One Sans`)." + "404": { + "description": "Unknown token." }, - "font_weight": { - "type": "string", - "description": "Font weight available for the selected family (e.g. `regular`, `500`, `700`)." + "409": { + "description": "code OWNERSHIP_CHANGED: the wallet's holder changed or the wallet was deleted after the session was minted; mint a new session for the current holder. Also returned for SATOSHI_TEST when the session has no satoshi_deposit_address, and when the session has no attempts left (no code; the body carries max_attempts, attempts_used and attempts_remaining 0): mint a new session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWidgetProofError" + } + } + } }, - "app_public_name": { - "type": "string", - "nullable": true, - "description": "Public name shown in the verification flow." + "410": { + "description": "The session expired." + } + } + } + }, + "/v3/travel-rule/widget/{token}/proofs/{proof_id}/verify/": { + "post": { + "summary": "Submit wallet-ownership proof evidence", + "description": "Public endpoint keyed by the widget session token, no API key required. Send the evidence the proof's method needs: signature for MESSAGE_SIGNING, txid for SATOSHI_TEST (JSON), or a file for SCREENSHOT (multipart). Every submission counts as an attempt. A signature that does not verify, or a deposit that does not match, answers 400 with a code and leaves the proof PENDING so the end user can try again. A deposit seen but not confirmed yet answers 200 with status PENDING and is rechecked automatically until the window closes. A screenshot answers 200 with status PENDING and waits in your review queue (GET /v3/travel-rule/wallet-proofs/). An EXPIRED or REJECTED proof never verifies later: start a new one. Signature formats verified per chain family: EVM chains take a personal_sign (EIP-191) signature, hex encoded; Bitcoin takes a BIP-137 signmessage signature (P2PKH, P2SH-P2WPKH and P2WPKH addresses) or a BIP-322 simple signature (P2WPKH and P2TR addresses), base64 encoded; Solana takes an Ed25519 signature over the challenge, base58 encoded; Tron takes a signMessageV2 (TIP-191) signature, hex encoded. A BIP-322 full or proof-of-funds signature, or a multisig address, fails with SIGNATURE_UNSUPPORTED_SCHEME.", + "operationId": "verifyTravelRuleWidgetProof", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "verify_widget_proof", + "auth": "public", + "server": false + }, + "tags": [ + "Travel Rule" + ], + "security": [ + {} + ], + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "description": "Opaque session token returned when the widget session was minted.", + "schema": { + "type": "string" + } }, - "privacy_policy_url": { + { + "name": "proof_id", + "in": "path", + "required": true, + "description": "proof_id returned when the proof was started.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { + "type": "string", + "description": "MESSAGE_SIGNING: the wallet's signature over the proof's challenge." + }, + "txid": { + "type": "string", + "description": "SATOSHI_TEST: the transaction id of the test deposit." + } + } + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "file" + ], + "properties": { + "file": { + "type": "string", + "format": "binary", + "description": "SCREENSHOT: an image showing the wallet in the end user's wallet app." + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/widget/{token}/proofs/{proof_id}/verify/ \\\n -H 'Content-Type: application/json' \\\n -d '{\"signature\": \"0x...\"}'" + } + ], + "responses": { + "200": { + "description": "VERIFIED, or still PENDING (a screenshot awaiting review, a deposit not indexed or not confirmed yet, with code DEPOSIT_NOT_FOUND or DEPOSIT_UNCONFIRMED).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWidgetProofVerifyResult" + } + } + } + }, + "400": { + "description": "Missing evidence; a failed attempt that keeps the proof PENDING so the end user can try again (code SIGNATURE_INVALID, SIGNATURE_UNSUPPORTED_SCHEME or DEPOSIT_LOOKUP_FAILED); or a test deposit that rejects the proof (code DEPOSIT_MISMATCH, DEPOSIT_STALE or TXID_REUSED, status REJECTED, with the session's attempt counts): picking a method again starts a new proof while attempts_remaining is above 0.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWidgetProofError" + } + } + } + }, + "404": { + "description": "Unknown token, or no such proof on this session." + }, + "409": { + "description": "code OWNERSHIP_CHANGED (the proof is closed with it); a closed proof (REJECTED or EXPIRED), with its status, the code it closed with and the session's attempt counts: pick a method again to start a new proof; or a SELF_DECLARATION proof, which is verified when started.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWidgetProofError" + } + } + } + }, + "410": { + "description": "The session expired, or code WINDOW_EXPIRED when the satoshi test window closed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWidgetProofError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "DocumentAIDetectedCode": { + "type": "object", + "required": [ + "type", + "page_number", + "position", + "x", + "y", + "width", + "height", + "page_width", + "page_height", + "raw_payload", + "parsed_payload" + ], + "properties": { + "type": { "type": "string", + "description": "Detected symbology, for example `QR_CODE`, `CODE_128`, `EAN_13`, `DATA_MATRIX`, or `PDF_417`." + }, + "page_number": { + "type": "integer", + "minimum": 1, + "description": "1-based page number." + }, + "position": { + "type": "array", "nullable": true, - "description": "Privacy policy URL shown to the end user." + "description": "Detected polygon in rendered-page pixel coordinates. `null` when no reliable location is available.", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "integer" + } + } }, - "skip_welcome_screen": { - "type": "boolean", - "description": "Skip the initial welcome screen." + "x": { + "type": "integer", + "nullable": true, + "description": "Left edge of the bounding box in rendered-page pixels." }, - "disable_login_with_didit": { - "type": "boolean", - "description": "Continue as guest instead of showing the login-with-Didit screen. Cannot be set back to `false` while a custom domain is configured (400)." + "y": { + "type": "integer", + "nullable": true, + "description": "Top edge of the bounding box in rendered-page pixels." }, - "hide_progress_bar": { - "type": "boolean", - "description": "Hide the progress bar during the flow." + "width": { + "type": "integer", + "nullable": true, + "description": "Bounding-box width in rendered-page pixels." }, - "callback_seconds": { + "height": { "type": "integer", - "minimum": 0, - "description": "Delay (seconds) before redirecting to the callback URL." + "nullable": true, + "description": "Bounding-box height in rendered-page pixels." }, - "delete_logo_square": { - "type": "boolean", - "description": "When `true`, removes the current square logo." + "page_width": { + "type": "integer", + "description": "Rendered page width in pixels." }, - "delete_logo_rectangular": { - "type": "boolean", - "description": "When `true`, removes the current rectangular logo." + "page_height": { + "type": "integer", + "description": "Rendered page height in pixels." }, - "delete_logo_rectangular_dark": { - "type": "boolean", - "description": "When `true`, removes the current dark-mode logo, and `logo_rectangular_dark` then reads `null`. If the same multipart request also uploads `image_rectangular_dark`, the upload wins." + "raw_payload": { + "type": "string", + "description": "Raw decoder payload as a base64 string when available; otherwise an empty string." }, - "delete_favicon": { - "type": "boolean", - "description": "When `true`, removes the current favicon." + "parsed_payload": { + "nullable": true, + "description": "Decoded string or structured value. `null` when the code was located but could not be decoded." } } }, - "BrandingCustomization": { + "SessionImportJob": { "type": "object", - "description": "White-label branding for an application: colors, fonts, radii, logo URLs and UI/UX options. Logo URLs are read-only (set them by uploading images). Custom domain and white-label email are not included.", + "required": [ + "uuid", + "status", + "import_type", + "source_format", + "source_filename", + "total_rows", + "processed_rows", + "created_count", + "updated_count", + "skipped_count", + "failed_count" + ], "properties": { - "color_primary": { - "type": "string" - }, - "color_secondary": { - "type": "string" - }, - "color_background": { - "type": "string" - }, - "color_panel": { - "type": "string" - }, - "color_panel_10": { - "type": "string" - }, - "color_on_panel_1": { - "type": "string" + "uuid": { + "type": "string", + "format": "uuid", + "description": "Import job id. Use this value to poll progress and fetch row errors." }, - "color_on_panel_2": { - "type": "string" + "status": { + "type": "string", + "enum": [ + "PENDING", + "PROCESSING", + "COMPLETED", + "FAILED", + "PAUSED", + "CANCELLED", + "READY" + ] }, - "color_on_background": { - "type": "string" + "import_type": { + "type": "string", + "enum": [ + "user_verification", + "business_verification", + "status_rules", + "transactions" + ] }, - "color_button_1": { - "type": "string" + "source_format": { + "type": "string", + "enum": [ + "csv", + "ndjson" + ] }, - "color_button_2": { + "source_filename": { "type": "string" }, - "color_button_text_1": { - "type": "string" + "total_rows": { + "type": "integer", + "format": "int64" }, - "color_button_text_2": { - "type": "string" + "processed_rows": { + "type": "integer", + "format": "int64" }, - "color_pill_text": { - "type": "string" + "checkpoint_row": { + "type": "integer", + "format": "int64" }, - "border_radius_panel": { - "type": "integer" + "created_count": { + "type": "integer", + "format": "int64" }, - "border_radius_buttons": { - "type": "integer" + "updated_count": { + "type": "integer", + "format": "int64" }, - "font_family": { - "type": "string" + "skipped_count": { + "type": "integer", + "format": "int64" }, - "font_weight": { - "type": "string" + "failed_count": { + "type": "integer", + "format": "int64" }, - "font_url": { - "type": "string", - "nullable": true, - "description": "Read-only. Resolved Google Fonts stylesheet URL for the selected family." + "total_media_bytes": { + "type": "integer", + "format": "int64" }, - "logo_square": { + "last_error": { "type": "string", - "nullable": true, - "description": "Read-only URL of the square logo. Upload via `image_square`." + "nullable": true }, - "logo_rectangular": { + "created_at": { "type": "string", - "nullable": true, - "description": "Read-only URL of the rectangular logo. Upload via `image_rectangular`." + "format": "date-time" }, - "logo_rectangular_dark": { + "updated_at": { "type": "string", - "format": "uri", - "nullable": true, - "description": "Read-only. Temporary signed URL of the rectangular logo for dark mode, shown on a dark background. `null` when no dark logo is set or rectangular logos are not shown for your account. Upload it with `image_rectangular_dark`." + "format": "date-time" }, - "favicon": { + "started_at": { "type": "string", - "nullable": true, - "description": "Read-only URL of the favicon. Upload via `image_favicon`." + "format": "date-time", + "nullable": true }, - "app_public_name": { + "completed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "state": { + "type": "string", + "enum": [ + "ready", + "queued", + "running", + "paused", + "completed", + "errors", + "failed", + "cancelled" + ], + "description": "The job's state as the console shows it; `errors` is a completed job with rejected rows." + }, + "source": { + "type": "string", + "enum": [ + "file", + "api", + "provider" + ] + }, + "provider": { + "type": "string", + "description": "Migration source; empty for a file or API import." + }, + "provider_options": { + "type": "object" + }, + "credential_hint": { + "type": "string", + "description": "The first characters of the key the migration holds. The key itself is never returned." + }, + "credentials_expires_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "resumes_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When a job paused by the provider's rate limit resumes by itself." + }, + "status_mapping": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "is_dry_run": { + "type": "boolean" + }, + "dry_run_summary": { + "type": "object", + "description": "What a dry run found: `total`, `by_status`, `already_in_didit`, `same_person`, `not_finished`, `failed`, `unmapped_statuses`." + }, + "face_templates_enabled": { + "type": "boolean" + }, + "lawful_basis_confirmed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "not_finished_count": { + "type": "integer", + "format": "int64" + }, + "same_person_count": { + "type": "integer", + "format": "int64" + }, + "face_templates_count": { + "type": "integer", + "format": "int64" + }, + "face_duplicates_count": { + "type": "integer", + "format": "int64" + }, + "face_blocklist_hits_count": { + "type": "integer", + "format": "int64" + }, + "created_by_name": { + "type": "string", + "nullable": true + }, + "created_by_email": { + "type": "string", + "nullable": true + } + } + }, + "SessionImportError": { + "type": "object", + "required": [ + "uuid", + "row_number", + "error", + "raw_row" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "row_number": { + "type": "integer", + "format": "int64" + }, + "external_id": { + "type": "string", + "nullable": true + }, + "error": { + "type": "string" + }, + "raw_row": { + "type": "object", + "additionalProperties": true + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "BrandingCustomizationUpdate": { + "type": "object", + "description": "Partial branding update body. Provide only the fields you want to change. Hex colors accept `#RRGGBB` or `#RRGGBBAA`. Custom domain and white-label email are not accepted here.", + "properties": { + "color_primary": { + "type": "string", + "description": "Headers / primary text color." + }, + "color_secondary": { + "type": "string", + "description": "Paragraph / secondary text color." + }, + "color_background": { + "type": "string", + "description": "Page background color." + }, + "color_panel": { + "type": "string", + "description": "Panel / accent color." + }, + "color_panel_10": { + "type": "string", + "description": "Panel color at 10% opacity." + }, + "color_on_panel_1": { + "type": "string", + "description": "Support text color on panels." + }, + "color_on_panel_2": { + "type": "string", + "description": "Pill / badge color on panels." + }, + "color_on_background": { + "type": "string", + "description": "Text color on background." + }, + "color_button_1": { + "type": "string", + "description": "Primary button color." + }, + "color_button_2": { + "type": "string", + "description": "Secondary button color." + }, + "color_button_text_1": { + "type": "string", + "description": "Primary button text color." + }, + "color_button_text_2": { + "type": "string", + "description": "Secondary button text color." + }, + "color_pill_text": { + "type": "string", + "description": "Text color for pills, inputs and selectors." + }, + "border_radius_panel": { + "type": "integer", + "minimum": 0, + "description": "Panel corner radius in pixels." + }, + "border_radius_buttons": { + "type": "integer", + "minimum": 0, + "description": "Button corner radius in pixels." + }, + "font_family": { + "type": "string", + "description": "Google Font family name (e.g. `Inter`, `AR One Sans`)." + }, + "font_weight": { + "type": "string", + "description": "Font weight available for the selected family (e.g. `regular`, `500`, `700`)." + }, + "app_public_name": { + "type": "string", + "nullable": true, + "description": "Public name shown in the verification flow." + }, + "privacy_policy_url": { + "type": "string", + "nullable": true, + "description": "Privacy policy URL shown to the end user." + }, + "skip_welcome_screen": { + "type": "boolean", + "description": "Skip the initial welcome screen." + }, + "disable_login_with_didit": { + "type": "boolean", + "description": "Continue as guest instead of showing the login-with-Didit screen. Cannot be set back to `false` while a custom domain is configured (400)." + }, + "hide_progress_bar": { + "type": "boolean", + "description": "Hide the progress bar during the flow." + }, + "callback_seconds": { + "type": "integer", + "minimum": 0, + "description": "Delay (seconds) before redirecting to the callback URL." + }, + "delete_logo_square": { + "type": "boolean", + "description": "When `true`, removes the current square logo." + }, + "delete_logo_rectangular": { + "type": "boolean", + "description": "When `true`, removes the current rectangular logo." + }, + "delete_logo_rectangular_dark": { + "type": "boolean", + "description": "When `true`, removes the current dark-mode logo, and `logo_rectangular_dark` then reads `null`. If the same multipart request also uploads `image_rectangular_dark`, the upload wins." + }, + "delete_favicon": { + "type": "boolean", + "description": "When `true`, removes the current favicon." + } + } + }, + "BrandingCustomization": { + "type": "object", + "description": "White-label branding for an application: colors, fonts, radii, logo URLs and UI/UX options. Logo URLs are read-only (set them by uploading images). Custom domain and white-label email are not included.", + "properties": { + "color_primary": { + "type": "string" + }, + "color_secondary": { + "type": "string" + }, + "color_background": { + "type": "string" + }, + "color_panel": { + "type": "string" + }, + "color_panel_10": { + "type": "string" + }, + "color_on_panel_1": { + "type": "string" + }, + "color_on_panel_2": { + "type": "string" + }, + "color_on_background": { + "type": "string" + }, + "color_button_1": { + "type": "string" + }, + "color_button_2": { + "type": "string" + }, + "color_button_text_1": { + "type": "string" + }, + "color_button_text_2": { + "type": "string" + }, + "color_pill_text": { + "type": "string" + }, + "border_radius_panel": { + "type": "integer" + }, + "border_radius_buttons": { + "type": "integer" + }, + "font_family": { + "type": "string" + }, + "font_weight": { + "type": "string" + }, + "font_url": { + "type": "string", + "nullable": true, + "description": "Read-only. Resolved Google Fonts stylesheet URL for the selected family." + }, + "logo_square": { + "type": "string", + "nullable": true, + "description": "Read-only URL of the square logo. Upload via `image_square`." + }, + "logo_rectangular": { + "type": "string", + "nullable": true, + "description": "Read-only URL of the rectangular logo. Upload via `image_rectangular`." + }, + "logo_rectangular_dark": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Read-only. Temporary signed URL of the rectangular logo for dark mode, shown on a dark background. `null` when no dark logo is set or rectangular logos are not shown for your account. Upload it with `image_rectangular_dark`." + }, + "favicon": { + "type": "string", + "nullable": true, + "description": "Read-only URL of the favicon. Upload via `image_favicon`." + }, + "app_public_name": { "type": "string", "nullable": true }, @@ -77026,123 +78633,19 @@ "networks": { "type": "object", "readOnly": true, - "description": "Effective membership configuration for each network. No network, including GTR, is included by default. This is not a live health or delivery check.", + "description": "Per network, how this application would send and why it cannot. No network, including GTR, is included by default: a network is reachable through your own connected membership, or through Didit's membership once Didit's membership of that network is live and your own participant on it is active. Not a live health or delivery check.", "properties": { "gtr": { - "type": "object", - "properties": { - "rail": { - "type": "string", - "enum": [ - "GTR" - ] - }, - "mode": { - "type": "string", - "enum": [ - "own", - "didit", - "off", - "unavailable" - ], - "description": "own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available." - }, - "platform_available": { - "type": "boolean", - "description": "Whether platform membership is configured; not a live health check." - }, - "connected": { - "type": "boolean", - "description": "Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange." - } - } + "$ref": "#/components/schemas/TravelRuleNetworkState" }, "trust": { - "type": "object", - "properties": { - "rail": { - "type": "string", - "enum": [ - "TRUST" - ] - }, - "mode": { - "type": "string", - "enum": [ - "own", - "didit", - "off", - "unavailable" - ], - "description": "own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available." - }, - "platform_available": { - "type": "boolean", - "description": "Whether platform membership is configured; not a live health check." - }, - "connected": { - "type": "boolean", - "description": "Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange." - } - } + "$ref": "#/components/schemas/TravelRuleNetworkState" }, "verifyvasp": { - "type": "object", - "properties": { - "rail": { - "type": "string", - "enum": [ - "VERIFYVASP" - ] - }, - "mode": { - "type": "string", - "enum": [ - "own", - "didit", - "off", - "unavailable" - ], - "description": "own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available." - }, - "platform_available": { - "type": "boolean", - "description": "Whether platform membership is configured; not a live health check." - }, - "connected": { - "type": "boolean", - "description": "Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange." - } - } + "$ref": "#/components/schemas/TravelRuleNetworkState" }, "sygna": { - "type": "object", - "properties": { - "rail": { - "type": "string", - "enum": [ - "SYGNA" - ] - }, - "mode": { - "type": "string", - "enum": [ - "own", - "didit", - "off", - "unavailable" - ], - "description": "own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available." - }, - "platform_available": { - "type": "boolean", - "description": "Whether platform membership is configured; not a live health check." - }, - "connected": { - "type": "boolean", - "description": "Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange." - } - } + "$ref": "#/components/schemas/TravelRuleNetworkState" } } }, @@ -77332,7 +78835,7 @@ }, "chain": { "type": "string", - "description": "Chain token for the address, for example `ETH` or `BTC`. When Didit routes or registers a transfer, it matches address book entries on the wallet address and, when the transfer carries a chain, on this value too, ignoring case. A transfer's chain is `beneficiary_data.chain`, filled from the transaction's crypto chain or currency symbol (for example `ETH`) when you do not send it, or the `chain` of an after-deposit registration, so use the same token everywhere. An entry with an empty chain matches any chain. A Lightning payee (a node id or a Lightning Address) registered on bitcoin is stored lowercased on `LIGHTNING`, where Lightning transfers are matched." + "description": "Chain token for the address, for example `ETH` or `BTC`. When Didit routes or registers a transfer, it matches address book entries on the wallet address and, when the transfer carries a chain, on this value too, ignoring case. A transfer's chain is `beneficiary_data.chain`, filled from the transaction's crypto chain or currency symbol (for example `ETH`) when you do not send it, or the `chain` of an after-deposit registration, so use the same token everywhere. An entry with an empty chain matches any chain. A Lightning payee (a node id or a Lightning Address) registered on bitcoin is stored lowercased on `LIGHTNING`, where Lightning transfers are matched. Known chains are stored under one canonical identifier whatever spelling you send: \"ethereum\", \"Ethereum\" and \"ERC20\" are all stored as ETH, \"BNB Smart Chain\" and \"BEP20\" as BNB, \"tron\" as TRX. An unknown chain name is kept as given." }, "holder_vendor_data": { "type": "string", @@ -77387,7 +78890,7 @@ "chain": { "type": "string", "maxLength": 32, - "description": "Chain token for the address, for example `ETH` or `BTC`. When Didit routes or registers a transfer, it matches address book entries on the wallet address and, when the transfer carries a chain, on this value too, ignoring case. A transfer's chain is `beneficiary_data.chain`, filled from the transaction's crypto chain or currency symbol (for example `ETH`) when you do not send it, or the `chain` of an after-deposit registration, so use the same token everywhere. An entry with an empty chain matches any chain." + "description": "Chain token for the address, for example `ETH` or `BTC`. When Didit routes or registers a transfer, it matches address book entries on the wallet address and, when the transfer carries a chain, on this value too, ignoring case. A transfer's chain is `beneficiary_data.chain`, filled from the transaction's crypto chain or currency symbol (for example `ETH`) when you do not send it, or the `chain` of an after-deposit registration, so use the same token everywhere. An entry with an empty chain matches any chain. Known chains are stored under one canonical identifier whatever spelling you send: \"ethereum\", \"Ethereum\" and \"ERC20\" are all stored as ETH, \"BNB Smart Chain\" and \"BEP20\" as BNB, \"tron\" as TRX. An unknown chain name is kept as given." }, "holder_vendor_data": { "type": "string", @@ -77677,7 +79180,7 @@ }, "TravelRuleTransferPatchRequest": { "type": "object", - "description": "Provide exactly one of: payment_txn_id (finish), action=cancel, action=resend, action=update_compliance_email with compliance_email, or action=update_originator_wallet with wallet_address.", + "description": "Provide exactly one of: payment_txn_id (finish), action=cancel, action=resend, action=update_compliance_email with compliance_email, action=update_originator_wallet with wallet_address, or action=beneficiary_vasp with beneficiary_vasp.", "properties": { "payment_txn_id": { "type": "string", @@ -77689,9 +79192,10 @@ "cancel", "resend", "update_compliance_email", - "update_originator_wallet" + "update_originator_wallet", + "beneficiary_vasp" ], - "description": "\"cancel\" cancels a non-terminal transfer. \"resend\" re-runs routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA. \"update_compliance_email\" replaces the compliance email the request goes to, on an outbound transfer in one of those three statuses; send \"resend\" afterwards. \"update_originator_wallet\" records the wallet the funds actually left from (`wallet_address`, `memo`)." + "description": "\"cancel\" cancels a non-terminal transfer. \"resend\" re-runs routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA. \"update_compliance_email\" replaces the compliance email the request goes to, on an outbound transfer in one of those three statuses; send \"resend\" afterwards. \"update_originator_wallet\" records the wallet the funds actually left from (`wallet_address`, `memo`). \"beneficiary_vasp\" names the counterparty VASP yourself (requires `beneficiary_vasp`) for an outbound transfer in one of those statuses: it is routed to that VASP and sent again." }, "compliance_email": { "type": "string", @@ -77708,6 +79212,11 @@ "type": "string", "maxLength": 256, "description": "With `action: update_originator_wallet`: the wallet's memo or tag on a memo network. Optional." + }, + "beneficiary_vasp": { + "type": "string", + "format": "uuid", + "description": "Required with action=beneficiary_vasp: the `id` of a directory VASP, from `GET /v3/travel-rule/vasps/`. Your choice outranks every automatic discovery step, which never overrides it." } } }, @@ -78912,7 +80421,7 @@ }, "chain": { "type": "string", - "description": "Chain token for the address, for example `ETH`, `BTC` or `SOL`. Use the same token as in your wallet address book. Message signing and the Satoshi test are offered only for a supported chain.", + "description": "Chain token for the address, for example `ETH`, `BTC` or `SOL`. Use the same token as in your wallet address book. Message signing and the Satoshi test are offered only for a supported chain. The address book entry the proof verifies stores it under its canonical identifier (ETH, BTC, SOL, TRX).", "maxLength": 32 }, "holder_name": { @@ -78982,7 +80491,20 @@ "type": "string" }, "chain": { - "type": "string" + "type": "string", + "description": "The chain the session was minted with, as you sent it." + }, + "chain_family": { + "type": "string", + "nullable": true, + "enum": [ + "EVM", + "BTC", + "SOL", + "TRON", + null + ], + "description": "The key family behind chain: EVM (Ethereum, BNB Chain, Polygon, Arbitrum, Base, Optimism, Avalanche, Linea and the other EVM chains), BTC, SOL or TRON. One EVM key controls the same address on every EVM chain. Null when no signature can be verified on the chain." }, "holder_name": { "type": "string" @@ -78998,7 +80520,7 @@ "SELF_DECLARATION" ] }, - "description": "Proof methods available for this session. MESSAGE_SIGNING requires a supported chain family, SATOSHI_TEST requires a satoshi_deposit_address on the session, SCREENSHOT requires the allow_screenshot_proof setting, and SELF_DECLARATION requires the allow_self_declaration setting and an `ownership_validation_mode` that accepts a declaration for the linked transfer (never in `FULL`; in `SIMPLIFIED` only below `simplified_validation_threshold`). Initiating a SELF_DECLARATION the mode does not accept answers 403." + "description": "Proof methods available for this session. MESSAGE_SIGNING requires a chain with a chain_family, SATOSHI_TEST requires a satoshi_deposit_address on the session and a chain whose deposits Didit can read back, SCREENSHOT requires the allow_screenshot_proof setting, and SELF_DECLARATION requires the allow_self_declaration setting." }, "transfer_status": { "type": "string", @@ -79068,13 +80590,14 @@ "REJECTED", "EXPIRED" ], - "description": "PENDING on a SATOSHI_TEST proof means the deposit has been seen on-chain but has not yet reached enough confirmations; a background job automatically rechecks it and moves it to VERIFIED once it confirms, with no resubmission needed. REJECTED means a genuinely wrong deposit was submitted (absent, wrong sender, or wrong amount). EXPIRED means the proof's own test window closed before a valid deposit was seen." + "description": "PENDING: the end user picked this method and the proof is not decided yet. A failed signature or a test deposit that is not readable or confirmed yet keeps the proof PENDING with a failure_reason, so the end user can try again; a submitted SATOSHI_TEST deposit is rechecked automatically until its window closes, and a submitted SCREENSHOT waits for your review. VERIFIED: ownership is proven. REJECTED: the test deposit is genuinely wrong (another sender, not the expected amount to the deposit address, older than the challenge, or a transaction that already proved another wallet) or your reviewer rejected the screenshot; failure_reason says which. A rejected proof is final and uses one of the session's attempts. EXPIRED: the proof can no longer be decided (its window closed, the end user switched method, the widget session lapsed, or the wallet's holder changed); failure_reason says which." }, "wallet_address": { "type": "string" }, "chain": { - "type": "string" + "type": "string", + "description": "Canonical chain identifier of the address book entry the proof is for, e.g. ETH, BTC, SOL, TRX." }, "deposit_address": { "type": "string", @@ -79099,11 +80622,23 @@ }, "rejection_reason": { "type": "string", - "description": "Why the proof was rejected. Present only when `status` is REJECTED." + "description": "Why the proof was rejected, as an English sentence for the end user. Present only when `status` is REJECTED; empty when your reviewer rejected a screenshot (your note stays with you)." }, "created_at": { "type": "string", "format": "date-time" + }, + "attempts": { + "type": "integer", + "description": "How many times evidence was submitted for this proof (signatures, transaction ids, uploads)." + }, + "failure_reason": { + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleWalletProofFailureReason" + } + ], + "description": "Why the last attempt fell short, or why the proof closed. Empty when nothing failed." } } }, @@ -83559,10 +85094,11 @@ "not_requested", "pending", "delivered", + "not_held", "unavailable", null ], - "description": "`delivered` when the registry published ownership; `unavailable` when it did not (see `unavailable_reason`); `pending` while the registry is still fetching it; `not_requested` for the Basic tier." + "description": "`delivered` when the registry published ownership; `not_held` when the ownership registry answered and does not list the company at all (see `unavailable_reason`); `unavailable` when it did not serve the ownership data for any other reason (see `unavailable_reason`); `pending` while the registry is still fetching it; `not_requested` for the Basic tier." }, "tier_requested": { "type": "string", @@ -91500,6 +93036,15 @@ } } } + }, + "address_validation": { + "allOf": [ + { + "$ref": "#/components/schemas/AddressValidationResult" + } + ], + "nullable": true, + "description": "Address Validation of the address printed on this document: whether it exists and its normalised form, as the document read it (a later correction of `address` does not run it again). Present when this ID verification step enabled the Address Validation add-on; `null` otherwise." } } } @@ -93011,6 +94556,15 @@ "items": { "$ref": "#/components/schemas/DocumentAIDetectedCode" } + }, + "address_validation": { + "allOf": [ + { + "$ref": "#/components/schemas/AddressValidationResult" + } + ], + "nullable": true, + "description": "Address Validation of the address on this proof of address: whether it exists and its normalised form, as the document read it (a later correction of `poa_address` does not run it again). Present when this Proof of Address step enabled the Address Validation add-on and the upload passed the document checks; `null` otherwise." } } } @@ -94478,7 +96032,7 @@ "registry_ownership_status": { "type": "string", "nullable": true, - "description": "`not_requested`, `pending`, `delivered` or `unavailable`. Also in `ownership.status`." + "description": "`not_requested`, `pending`, `delivered`, `not_held` or `unavailable`. Also in `ownership.status`." } } }, @@ -95648,6 +97202,173 @@ } } }, + "biometric_consent": { + "type": "object", + "nullable": true, + "description": "Whether the session required explicit biometric consent before a face capture, why, and the evidence of the answer. User Verification (KYC) sessions only; the same object is in the `decision` of session webhooks. `null` when the session was never evaluated, for example a workflow without a face capture. Fields with no recorded value are `null`. A native SDK below 4.10.0, or one that does not report its version, cannot show the consent step: its face captures run as before, and a session that required consent then reports `status: \"pending\"` with `reason: \"BIOMETRIC_CONSENT_UNSUPPORTED_SDK\"`. See [Biometric consent](/getting-started/biometric-consent).", + "properties": { + "required": { + "type": "boolean", + "description": "Whether the session must collect explicit biometric consent. Never goes back to `false`." + }, + "trigger": { + "type": "string", + "nullable": true, + "enum": [ + "declared_state", + "ip_state", + "ip_unknown", + "document_issuer", + "forced", + "customer_collected" + ], + "description": "Why consent is required, or how it was satisfied. `declared_state`: you declared a covered state (`user_state`, or a US address in `expected_details`). `ip_state`: the person's IP is in a covered state (Illinois, Texas, Colorado). `ip_unknown`: the evidence places the person in the US but the state cannot be told (the IP is in the US with no state, or cannot be located while your details or the document place the person in the US). `document_issuer`: the identity document was issued by a covered state. `forced`: the application's mode is Always. `customer_collected`: you sent `biometric_consent` when creating the session. `null` while not required." + }, + "status": { + "type": "string", + "enum": [ + "not_required", + "pending", + "given", + "declined" + ], + "description": "`not_required`; `pending` (required and not given: the person has not answered yet, or `reason` says why it was not collected); `given`; `declined` (a decline was recorded and the session ended as `Declined`; no Didit screen offers a decline today)." + }, + "given_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When consent was given. For `customer_collected`, the `given_at` you sent." + }, + "declined_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When a decline was recorded." + }, + "text_version": { + "type": "string", + "nullable": true, + "description": "Version id of the consent text the person answered, or the one you sent." + }, + "text_hash": { + "type": "string", + "nullable": true, + "description": "SHA-256 (hex) of the exact consent text shown, in the language it was shown in." + }, + "locale": { + "type": "string", + "nullable": true, + "description": "Language the consent text was shown in." + }, + "policy_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "The biometric data policy the consent step linked to." + }, + "step": { + "type": "string", + "nullable": true, + "description": "Workflow step the session was on when the person answered, for example `LIVENESS`." + }, + "node_id": { + "type": "string", + "nullable": true, + "description": "Workflow graph node the session was on when the person answered." + }, + "ip_address": { + "type": "string", + "nullable": true, + "description": "IP the answer came from." + }, + "ip_country_code": { + "type": "string", + "nullable": true, + "description": "ISO 3166-1 alpha-2 country of the answer's IP." + }, + "ip_state": { + "type": "string", + "nullable": true, + "description": "ISO 3166-2 subdivision of the answer's IP, for example `US-IL`." + }, + "ip_city": { + "type": "string", + "nullable": true, + "description": "City of the answer's IP." + }, + "user_agent": { + "type": "string", + "nullable": true, + "description": "User agent of the answer's request." + }, + "detected_ip_address": { + "type": "string", + "nullable": true, + "description": "The person's IP the requirement was last evaluated from. Can differ from `ip_address` when the person answered on another device." + }, + "detected_country_code": { + "type": "string", + "nullable": true, + "description": "ISO 3166-1 alpha-2 country of the detected IP." + }, + "detected_state": { + "type": "string", + "nullable": true, + "description": "ISO 3166-2 subdivision of the detected IP." + }, + "detected_city": { + "type": "string", + "nullable": true, + "description": "City of the detected IP." + }, + "declared_state": { + "type": "string", + "nullable": true, + "description": "ISO 3166-2 subdivision you declared: `user_state`, or the state of a US address in `expected_details`." + }, + "document_state": { + "type": "string", + "nullable": true, + "description": "ISO 3166-2 subdivision that issued the identity document, read at ID capture." + }, + "jurisdictions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "US states (USPS codes, for example `IL`) whose biometric law covers the person. `US` when the evidence places the person in the US but no state is known. Empty when none applies." + }, + "retention_months": { + "type": "integer", + "nullable": true, + "description": "The shortest biometric retention limit, in months, among `jurisdictions`. `null` when none applies." + }, + "reason": { + "type": "string", + "nullable": true, + "enum": [ + "BIOMETRIC_CONSENT_UNSUPPORTED_SDK", + "BIOMETRIC_CONSENT_DECLINED" + ], + "description": "`BIOMETRIC_CONSENT_UNSUPPORTED_SDK`: consent was required and a native SDK below 4.10.0, or one that did not report its version, captured a face without the consent step; `status` stays `pending`, or `given` if the person later agreed in the hosted flow. `BIOMETRIC_CONSENT_DECLINED`: a decline was recorded (it replaces the SDK reason). `null` otherwise, and always `null` when the session did not require consent." + }, + "decline_reason": { + "type": "string", + "nullable": true, + "enum": [ + "BIOMETRIC_CONSENT_DECLINED" + ], + "description": "`BIOMETRIC_CONSENT_DECLINED` when a recorded decline ended the session, otherwise `null`." + }, + "face_data_deleted_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the decline deleted the session's face images, videos and face templates. `null` unless a decline was recorded." + } + } + }, "investigator": { "$ref": "#/components/schemas/InvestigatorBlock" } @@ -95794,22 +97515,22 @@ }, "is_bank_verification_enabled": { "type": "boolean", - "description": "Whether a simple KYC or KYB workflow runs Bank verification. It runs after the contact steps and before database validation, AML and IP analysis. Set in the console." + "description": "Whether a simple KYC or KYB workflow runs Bank verification. It runs after the contact steps (and Location verification) and before database validation, AML and IP analysis. Bank verification is not generally available yet: turning it on is refused wherever it is not enabled." }, "bank_verification_config": { "type": "object", "additionalProperties": true, - "description": "Bank verification settings of a simple workflow, with the same keys a `BANK_VERIFICATION` node takes (for example `bank_countries` and `bank_method_order`). Every key is optional and an absent key keeps its default, so `{}` means all defaults. See [Bank verification settings](/management-api/workflows/feature-configs#bank-verification).", + "description": "Bank verification settings of a simple workflow, with the same keys a `BANK_VERIFICATION` node takes (for example `bank_countries` and `bank_method_order`). Unknown keys are dropped. Every key is optional and an absent key keeps its default, so `{}` means all defaults. See [Bank verification settings](/management-api/workflows/feature-configs#bank-verification).", "example": {} }, "is_geolocation_enabled": { "type": "boolean", - "description": "Whether a simple KYC workflow runs Location verification. It runs after the contact steps and before Bank verification. Never on KYB workflows. Set in the console." + "description": "Whether a simple KYC workflow runs Location verification. It runs after the contact steps and before Bank verification and database validation. Business (KYB) workflows cannot run it." }, "geolocation_config": { "type": "object", "additionalProperties": true, - "description": "Location verification settings of a simple workflow, with the same keys a `GEOLOCATION` node takes (for example `is_geolocation_geofencing_enabled` and `geolocation_max_attempts`). Every key is optional and an absent key keeps its default, so `{}` means all defaults.", + "description": "Location verification settings of a simple workflow, with the same keys a `GEOLOCATION` node takes (for example `is_geolocation_geofencing_enabled` and `geolocation_max_attempts`). Unknown keys are dropped. Every key is optional and an absent key keeps its default, so `{}` means all defaults.", "example": {} } }, @@ -98368,3492 +100089,4956 @@ } } }, - "score": { - "type": "number", - "format": "float", - "nullable": true, + "score": { + "type": "number", + "format": "float", + "nullable": true, + "minimum": 0, + "maximum": 100, + "description": "Overall AML screening score — the highest risk score across all hits (float, 0-100). `null` until computed." + }, + "screened_data": { + "type": "object", + "properties": { + "full_name": { + "type": "string" + }, + "nationality": { + "type": "string" + }, + "date_of_birth": { + "type": "string", + "format": "date" + }, + "document_number": { + "type": "string", + "nullable": true + } + } + }, + "is_ongoing_monitoring_enabled": { + "type": "boolean", + "description": "Whether AML ongoing monitoring is enabled for this screening. When `true`, the entity is re-screened periodically and any new hits update the report." + }, + "next_ongoing_monitoring_bill_date": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Date (YYYY-MM-DD) when this ongoing-monitoring subscription will next be billed. Computed as the date of the last ongoing-monitoring billing plus 365 days when `is_ongoing_monitoring_enabled = true`; otherwise `null`. This is a billing date, not a screening date - use `last_ongoing_screening_at` for that." + }, + "last_ongoing_screening_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When ongoing monitoring last completed a re-screening of this record, whether or not the result changed. `null` when the record has never been re-screened; it is never back-filled from the enablement or billing date." + }, + "last_ongoing_screening_datasets_updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "The most recent AML dataset update date the provider reported at the time of that screening. `null` when the provider reported no dataset dates; never estimated." + }, + "warnings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "enum": [ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE" + ], + "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Within an AML screening this is typically `AML`, but the enum above is authoritative across all feature warnings on the decision payload." + }, + "risk": { + "type": "string" + }, + "additional_data": { + "type": "object", + "nullable": true + }, + "log_type": { + "type": "string" + }, + "short_description": { + "type": "string" + }, + "long_description": { + "type": "string" + }, + "node_id": { + "type": "string", + "description": "The workflow graph node ID that generated this warning" + } + } + } + } + } + }, + "MonitoringEvent": { + "type": "object", + "description": "One stored monitoring event as returned by the list and subject-timeline endpoints. Restricted evidence (payload, device context, rule evidence) is only returned by `GET /v3/events/{event_uuid}/`.", + "required": [ + "event_uuid", + "event_id", + "event_category", + "action_type", + "occurred_at", + "received_at", + "status", + "score", + "subject_vendor_data", + "subject_entity_type", + "subject_uuid", + "counterparty_vendor_data", + "session_id", + "device_fingerprint", + "ip_address", + "ip_country", + "amount", + "currency", + "game_id", + "round_id", + "bonus_id", + "affiliate_code", + "payment_reference_id", + "retention_class", + "rules_matched_count", + "correlation_pending", + "is_droppable" + ], + "properties": { + "event_uuid": { + "type": "string", + "format": "uuid", + "description": "Didit identifier of the event. Use it as `{event_uuid}` in the retrieve and delete calls." + }, + "event_id": { + "type": "string", + "maxLength": 128, + "description": "Your own identifier, the idempotency key you sent." + }, + "event_category": { + "type": "string", + "enum": [ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom" + ], + "description": "Event category." + }, + "action_type": { + "type": "string", + "maxLength": 128, + "description": "What happened within the category, as you sent it." + }, + "occurred_at": { + "type": "string", + "format": "date-time", + "description": "When the event happened on your side." + }, + "received_at": { + "type": "string", + "format": "date-time", + "description": "When Didit accepted the event." + }, + "status": { + "type": "string", + "enum": [ + "ACCEPTED", + "APPROVED", + "IN_REVIEW", + "DECLINED" + ], + "description": "`ACCEPTED`: stored, no matched rule changed its standing (the normal answer for most events, and always the answer in a sandbox application, where no rules run). `APPROVED`: a rule matched and the score stayed under your review threshold. `IN_REVIEW`: a rule pushed the score over the review threshold or set the status. `DECLINED`: a rule declined it or the score crossed your decline threshold." + }, + "score": { + "type": "integer", + "description": "Accumulated rule score. Starts at 0; rule `add_score` actions move it (negative deltas are allowed)." + }, + "subject_vendor_data": { + "type": "string", + "maxLength": 255, + "description": "`subject.vendor_data` of the event: your id for the user or company." + }, + "subject_entity_type": { + "type": "string", + "description": "`individual` or `company`: which kind of subject `subject_vendor_data` names.", + "example": "individual" + }, + "subject_uuid": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "UUID of the user (individual) or company the event resolved to, or null when that record no longer exists." + }, + "counterparty_vendor_data": { + "type": "string", + "nullable": true, + "description": "The other player of a P2P event (promoted from `payload.counterparty_vendor_data`).", + "maxLength": 255 + }, + "session_id": { + "type": "string", + "nullable": true, + "description": "`session.session_id` sent with the event.", + "maxLength": 255 + }, + "device_fingerprint": { + "type": "string", + "nullable": true, + "description": "`device_context.fingerprint` sent with the event.", + "maxLength": 255 + }, + "ip_address": { + "type": "string", + "nullable": true, + "description": "IP address of the event. On the server lane this is `device_context.network_context.ip_address` as you sent it; on the device lane it is the IP of the request.", + "maxLength": 64 + }, + "ip_country": { + "type": "string", + "nullable": true, + "description": "Country code of `ip_address` from IP enrichment, or null when the address was not enriched (missing, private, or low-value telemetry whose address was not already known).", + "example": "ES" + }, + "amount": { + "type": "string", + "nullable": true, + "description": "Amount as a decimal string with 18 decimal places, from `transaction_context.amount`, `payload.amount` or `payload.stake_amount`, in that order.", + "example": "250.000000000000000000" + }, + "currency": { + "type": "string", + "nullable": true, + "description": "Currency from `transaction_context.currency` or `payload.currency`.", + "maxLength": 10 + }, + "game_id": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.game_id`.", + "maxLength": 128 + }, + "round_id": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.round_id`.", + "maxLength": 128 + }, + "bonus_id": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.bonus_id`.", + "maxLength": 128 + }, + "affiliate_code": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.affiliate_code`.", + "maxLength": 128 + }, + "payment_reference_id": { + "type": "string", + "nullable": true, + "description": "Payment reference from `transaction_context.payment_reference_id` or `payload.payment_reference_id`.", + "maxLength": 255 + }, + "retention_class": { + "type": "string", + "enum": [ + "low_value", + "standard", + "risk_relevant", + "monetary_or_compliance" + ], + "description": "Retention class. Payment and dispute events are `monetary_or_compliance`; security and responsible-gambling events are `risk_relevant`; `session.page_view`, `session.sdk_heartbeat`, `session.sdk_loaded`, `session.device_seen`, `session.bot_signal_observed`, `bonus.promotion_viewed` and `affiliate.referral_clicked` are `low_value`; everything else is `standard`. An event an active rule matched is promoted to `risk_relevant` (never demoted)." + }, + "rules_matched_count": { + "type": "integer", + "minimum": 0, + "description": "Number of rules that matched the event, including rules in `TEST` mode (which record evidence but apply no actions)." + }, + "correlation_pending": { + "type": "boolean", + "description": "`true` while an asynchronous rule (cross-subject correlation, or a sequence longer than 24 hours) still has to run for this event. It resolves within minutes and `status`, `score` and `rules_matched_count` update in place." + }, + "is_droppable": { + "type": "boolean", + "description": "Whether an SDK under backpressure may drop events of this kind. `false` for every `security`, `payment`, `dispute` and `responsible_gambling` event and for `gameplay.bet_placed`, `gameplay.bet_settled` and `gameplay.p2p_settlement`." + } + } + }, + "MonitoringEventDetail": { + "description": "One monitoring event with its full envelope, enrichment and rule evidence. Every field of `MonitoringEvent` plus the fields below.", + "allOf": [ + { + "$ref": "#/components/schemas/MonitoringEvent" + }, + { + "type": "object", + "required": [ + "payload", + "custom_values", + "device_context", + "field_sources", + "session_sequence", + "table_id", + "hand_id", + "match_id", + "market_id", + "side", + "role", + "net_result_amount", + "payment_fingerprint", + "instrument_reference_hash", + "withdrawal_address_hash", + "dispute_id", + "chargeback_id", + "changed_field", + "identity_match_ref", + "schema_version", + "expires_at", + "rules_evaluated_count", + "rule_runs", + "ip_enrichment", + "subject_profile", + "tags" + ], + "properties": { + "payload": { + "type": "object", + "additionalProperties": true, + "description": "The category fields you sent in `payload`." + }, + "custom_values": { + "type": "object", + "additionalProperties": true, + "description": "The scalar `custom_values` you sent. Didit adds `vpn_or_proxy_detected: true` or `is_data_center: true` when IP enrichment flags the address and you did not set them." + }, + "device_context": { + "type": "object", + "additionalProperties": true, + "description": "The device context as stored: your `device_context` (including `fingerprint`, `persistent_device_id` and `bot_score` when sent) with `network_context` completed by IP enrichment (`ip_country`, `ip_country_code`, `ip_city`, `asn_number`, `asn_organization`, `is_vpn_or_tor`, `is_data_center`, `proxy_type`, `connection_type`) for keys you left empty." + }, + "field_sources": { + "type": "object", + "additionalProperties": { + "type": "string", + "enum": [ + "customer", + "sdk", + "server" + ] + }, + "description": "Field path to provenance: `customer` (sent by your backend), `sdk` (sent through the device lane) or `server` (derived by Didit, such as `received_at` and enriched network fields). Check it before acting on a match." + }, + "session_sequence": { + "type": "integer", + "format": "int64", + "nullable": true, + "description": "`session.sequence` sent with the event." + }, + "table_id": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.table_id`.", + "maxLength": 128 + }, + "hand_id": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.hand_id`.", + "maxLength": 128 + }, + "match_id": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.match_id`.", + "maxLength": 128 + }, + "market_id": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.market_id`.", + "maxLength": 128 + }, + "side": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.side`.", + "maxLength": 64 + }, + "role": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.role`.", + "maxLength": 64 + }, + "net_result_amount": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.net_result_amount`, as a decimal string with 18 decimal places.", + "example": "-120.000000000000000000" + }, + "payment_fingerprint": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.payment_fingerprint`.", + "maxLength": 255 + }, + "instrument_reference_hash": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.instrument_reference_hash`.", + "maxLength": 255 + }, + "withdrawal_address_hash": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.withdrawal_address_hash` (or `payload.destination_reference_hash`).", + "maxLength": 255 + }, + "dispute_id": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.dispute_id`.", + "maxLength": 255 + }, + "chargeback_id": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.chargeback_id`.", + "maxLength": 255 + }, + "changed_field": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.changed_field` (security change events).", + "maxLength": 128 + }, + "identity_match_ref": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.identity_match_ref` (responsible-gambling registry matches).", + "maxLength": 255 + }, + "schema_version": { + "type": "integer", + "description": "Version of the event envelope schema. Currently `1`.", + "example": 1 + }, + "expires_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the event expires under its retention class and is deleted: `occurred_at` plus the class period (14, 30 or 90 days by default, adjustable per application). Null for `monetary_or_compliance` events, which follow transaction retention." + }, + "rules_evaluated_count": { + "type": "integer", + "minimum": 0, + "description": "Number of event rules evaluated for this event." + }, + "rule_runs": { + "type": "array", + "description": "Every rule that matched the event, newest first.", + "items": { + "$ref": "#/components/schemas/MonitoringEventRuleRun" + } + }, + "ip_enrichment": { + "type": "object", + "nullable": true, + "description": "Enrichment of the event's IP address, or null when the address was not enriched. See [IP enrichment](https://docs.didit.me/transaction-monitoring/ip-enrichment).", + "properties": { + "ip_country_code": { + "type": "string", + "nullable": true, + "description": "Country code of the address." + }, + "ip_city": { + "type": "string", + "nullable": true, + "description": "City of the address." + }, + "asn_number": { + "type": "integer", + "nullable": true, + "description": "Autonomous system number." + }, + "asn_organization": { + "type": "string", + "nullable": true, + "description": "Organization that operates the autonomous system." + }, + "is_vpn_or_tor": { + "type": "boolean", + "description": "Whether the address is a known VPN or Tor exit." + }, + "is_data_center": { + "type": "boolean", + "description": "Whether the address belongs to a hosting or data-centre network." + }, + "proxy_type": { + "type": "string", + "nullable": true, + "description": "Proxy classification of the address as an opaque string, or null." + }, + "last_enriched_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the address was last enriched." + } + }, + "required": [ + "ip_country_code", + "ip_city", + "asn_number", + "asn_organization", + "is_vpn_or_tor", + "is_data_center", + "proxy_type", + "last_enriched_at" + ] + }, + "subject_profile": { + "type": "object", + "nullable": true, + "description": "Cached email and phone risk signals of the subject, or null when no profile exists for the subject.", + "properties": { + "email_risk_score": { + "type": "integer", + "nullable": true, + "description": "Email risk score." + }, + "email_deliverability": { + "type": "string", + "nullable": true, + "description": "Email deliverability verdict." + }, + "email_is_disposable": { + "type": "boolean", + "nullable": true, + "description": "Whether the email address is disposable." + }, + "email_is_breached": { + "type": "boolean", + "nullable": true, + "description": "Whether the email address appears in known data breaches." + }, + "email_social_count": { + "type": "integer", + "nullable": true, + "description": "Number of online accounts linked to the email address." + }, + "email_social_providers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Names of the online services where an account linked to the email address was found." + }, + "phone_risk_score": { + "type": "integer", + "nullable": true, + "description": "Phone risk score." + }, + "phone_line_type": { + "type": "string", + "nullable": true, + "description": "Phone line type." + }, + "phone_carrier": { + "type": "string", + "nullable": true, + "description": "Phone carrier name." + }, + "phone_social_count": { + "type": "integer", + "nullable": true, + "description": "Number of online accounts linked to the phone number." + }, + "phone_social_providers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Names of the online services where an account linked to the phone number was found." + }, + "source_provider": { + "type": "string", + "nullable": true, + "description": "Opaque identifier of the source of the profile." + }, + "last_enriched_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the profile was last refreshed." + } + }, + "required": [ + "email_risk_score", + "email_deliverability", + "email_is_disposable", + "email_is_breached", + "email_social_count", + "email_social_providers", + "phone_risk_score", + "phone_line_type", + "phone_carrier", + "phone_social_count", + "phone_social_providers", + "source_provider", + "last_enriched_at" + ] + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Names of the tags attached to the event (rule `add_tags` actions)." + } + } + } + ] + }, + "MonitoringEventRuleRun": { + "type": "object", + "properties": { + "rule_uuid": { + "type": "string", + "format": "uuid", + "description": "UUID of the transaction monitoring rule." + }, + "rule_title": { + "type": "string", + "description": "Title of the rule." + }, + "library_key": { + "type": "string", + "nullable": true, + "description": "Library key when the rule was installed from the rules library, otherwise null." + }, + "matched": { + "type": "boolean", + "description": "Always `true`: only matching runs are recorded." + }, + "mode": { + "type": "string", + "enum": [ + "ACTIVE", + "TEST" + ], + "description": "Mode of the rule when it ran. A `TEST` run records evidence but applied no actions." + }, + "is_async": { + "type": "boolean", + "description": "`true` when the run came from the asynchronous correlation pass rather than the ingest request." + }, + "severity": { + "type": "string", + "nullable": true, + "enum": [ + "UNKNOWN", + "LOW", + "MEDIUM", + "HIGH", + "CRITICAL", + null + ], + "description": "Severity of the rule. Null for `TEST` runs and for rules without a severity." + }, + "score_delta": { + "type": "integer", + "description": "How much this run changed the event's score (0 for `TEST` runs)." + }, + "action_summary": { + "type": "string", + "nullable": true, + "description": "Short summary of the actions applied, for example `score +40; add_tag:VPN_LOGIN`. Null when nothing was applied." + }, + "matched_payload": { + "type": "array", + "description": "The conditions and aggregations that matched. Condition entries carry `field`, `operator`, `expected_value`, `current_value`, `matched`, `group_index`, `group_logic` and `field_source` (`server`, `sdk` or `customer`); aggregation entries carry `metric`, `field`, `operator`, `expected_value`, `computed_value`, `window`, `filters`, `matched` and `sample_references`.", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the rule run was recorded." + } + }, + "required": [ + "rule_uuid", + "rule_title", + "library_key", + "matched", + "mode", + "is_async", + "severity", + "score_delta", + "action_summary", + "matched_payload", + "created_at" + ] + }, + "MonitoringEventPage": { + "type": "object", + "description": "One page of monitoring events, newest `occurred_at` first.", + "required": [ + "count", + "next", + "previous", + "results" + ], + "properties": { + "count": { + "type": "integer", + "description": "Number of matching events, exact up to 100 and capped at 100 above that. Do not compute page counts from it; follow `next` until it is null." + }, + "next": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL of the next page, or null on the last page." + }, + "previous": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL of the previous page, or null on the first page." + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MonitoringEvent" + } + } + } + }, + "MonitoringEventCreateRequest": { + "type": "object", + "description": "The event envelope, the same for every category. camelCase aliases are accepted: `eventId`, `eventCategory` (or `category`), `actionType`, `occurredAt`, `timeZone` (or `zoneId`), `deviceContext` (or `device`), `transactionContext`, `customValues` (or `props`); inside `subject`: `vendorData`, `entityType`, `fullName`; inside `session`: `sessionId`, `pageUrl`; inside `transaction_context`: `paymentReferenceId`.", + "required": [ + "event_id", + "event_category", + "action_type", + "occurred_at", + "subject" + ], + "properties": { + "event_id": { + "type": "string", + "maxLength": 128, + "description": "Your identifier for the event, unique per application. It is the idempotency key: resending the same `event_category`, `action_type`, `occurred_at`, `subject.vendor_data` and `payload` under it is a replay, anything different is a conflict.", + "example": "login-2026-10-17-000001" + }, + "event_category": { + "type": "string", + "enum": [ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom" + ], + "description": "Event category.", + "example": "auth" + }, + "action_type": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "What happened within the category. Open vocabulary; the managed actions listed in [Event ingestion](https://docs.didit.me/transaction-monitoring/events) (for example `login`, `deposit_completed`, `chargeback_opened`) unlock typed validation and managed rule fields. Leading and trailing whitespace is trimmed.", + "example": "login" + }, + "occurred_at": { + "type": "string", + "format": "date-time", + "description": "When the event happened on your side, ISO 8601 (a value without an offset is read as UTC). Must be no more than 7 days in the past and no more than 5 minutes in the future.", + "example": "2026-10-17T09:15:02.481Z" + }, + "time_zone": { + "type": "string", + "nullable": true, + "maxLength": 64, + "description": "Time zone of the occurrence on your side, for example `Europe/Madrid`. Stored with the event; not returned by the read endpoints." + }, + "subject": { + "type": "object", + "description": "The user or company the event belongs to. It resolves to the user (or company) with this `vendor_data` in your application; one is created when none exists.", + "required": [ + "vendor_data" + ], + "properties": { + "vendor_data": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Your id for the subject.", + "example": "user-42" + }, + "entity_type": { + "type": "string", + "maxLength": 32, + "default": "individual", + "description": "`company` resolves the subject to a company; any other value (default `individual`) resolves it to an individual. The same `vendor_data` can name one of each.", + "example": "individual" + }, + "full_name": { + "type": "string", + "nullable": true, + "maxLength": 512, + "description": "Display name, used only when this event creates the user or company." + }, + "email": { + "type": "string", + "format": "email", + "nullable": true, + "description": "Validated as an email address; not stored on the event." + }, + "phone": { + "type": "string", + "nullable": true, + "maxLength": 32, + "description": "Validated; not stored on the event." + }, + "country": { + "type": "string", + "nullable": true, + "maxLength": 3, + "description": "Validated; not stored on the event." + } + } + }, + "business": { + "type": "object", + "description": "The business the event belongs to, when it differs from the subject. Only `vendor_data` is read.", + "additionalProperties": true, + "properties": { + "vendor_data": { + "type": "string", + "maxLength": 255, + "description": "Your id for the business. Filterable as `business_vendor_data`.", + "example": "merchant-7" + } + } + }, + "session": { + "type": "object", + "description": "Your session context for the event.", + "properties": { + "session_id": { + "type": "string", + "nullable": true, + "maxLength": 255, + "description": "Your session identifier. Filterable and returned as `session_id`." + }, + "sequence": { + "type": "integer", + "nullable": true, + "description": "Position of the event within the session. Returned as `session_sequence`." + }, + "page_url": { + "type": "string", + "nullable": true, + "maxLength": 2048, + "description": "Validated; not stored on the event." + }, + "referrer": { + "type": "string", + "nullable": true, + "maxLength": 2048, + "description": "Validated; not stored on the event." + } + } + }, + "device_context": { + "type": "object", + "additionalProperties": true, + "description": "Device and network context, stored as sent and returned by `GET /v3/events/{event_uuid}/`. On this server lane Didit does not see the end user's connection, so the IP enriched and used by rules is `network_context.ip_address` as you send it.", + "properties": { + "fingerprint": { + "type": "string", + "maxLength": 255, + "description": "Device fingerprint. Filterable as `device_fingerprint`." + }, + "persistent_device_id": { + "type": "string", + "maxLength": 255, + "description": "Your persistent device identifier." + }, + "bot_score": { + "type": "integer", + "description": "Bot score from your own detection, as an integer." + }, + "network_context": { + "type": "object", + "additionalProperties": true, + "properties": { + "ip_address": { + "type": "string", + "maxLength": 64, + "description": "End user's IP address. Public addresses are enriched once per unique address.", + "example": "203.0.113.42" + } + } + } + } + }, + "transaction_context": { + "type": "object", + "description": "Money context. Non-empty values here also satisfy the required payload keys of the action.", + "properties": { + "amount": { + "type": "string", + "nullable": true, + "description": "Amount as a decimal string (a number is also accepted).", + "example": "1000.00" + }, + "currency": { + "type": "string", + "nullable": true, + "maxLength": 10, + "description": "Currency code." + }, + "direction": { + "type": "string", + "nullable": true, + "maxLength": 16, + "description": "Direction of the money movement, for example `inbound` or `outbound`." + }, + "payment_reference_id": { + "type": "string", + "nullable": true, + "maxLength": 255, + "description": "Your payment reference. Filterable as `payment_reference_id`." + } + } + }, + "payload": { + "type": "object", + "additionalProperties": true, + "description": "The category's domain fields, at most 16384 bytes once JSON-encoded. Some actions require keys (in `payload` or `transaction_context`): `gameplay.p2p_settlement` needs `game_id`, `counterparty_vendor_data`, `stake_amount`, `net_result_amount`, `role` and one of `hand_id`, `round_id`, `match_id`; `gameplay.bet_placed` and `gameplay.bet_settled` need `game_id`; `payment.deposit_initiated`, `payment.deposit_completed`, `payment.withdrawal_requested` and `payment.withdrawal_completed` need `payment_reference_id`, `amount`, `currency`, `payment_method`, `status`; `payment.withdrawal_address_changed` needs `withdrawal_address_hash`; `dispute.chargeback_opened`, `dispute.chargeback_won` and `dispute.chargeback_lost` need `payment_reference_id` and one of `dispute_id`, `chargeback_id`; `session.bot_signal_observed` needs `telemetry_schema_version` and one of `bot_score`, `actions_per_minute`, `timing_variance_ms`, `pointer_entropy`, `keystroke_entropy`, `reaction_time_ms_p50`; `responsible_gambling.self_exclusion_match` needs `registry_source`, `exclusion_status`, `identity_match_ref`. Every `security` action ending in `_changed` needs `changed_field`, plus `previous_reference_hash` or `new_reference_hash` when the field is `password`, `mfa_method`, `mfa`, `email`, `phone`, `withdrawal_address`, `payout_method` or `api_key`. Send hashes and references, never a card number, a full withdrawal address, a password hash or an MFA secret." + }, + "custom_values": { + "type": "object", + "description": "Up to 100 keys of your own, addressable from rules as `custom_values.`. Values must be scalars (string, number, boolean or null); nested objects and arrays are rejected.", + "additionalProperties": { + "nullable": true, + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + } + }, + "example": { + "event_id": "login-2026-10-17-000001", + "event_category": "auth", + "action_type": "login", + "occurred_at": "2026-10-17T09:15:02.481Z", + "time_zone": "Europe/Madrid", + "subject": { + "vendor_data": "user-42", + "entity_type": "individual" + }, + "session": { + "session_id": "web-session-abc", + "sequence": 142 + }, + "device_context": { + "fingerprint": "fp-a8c1", + "network_context": { + "ip_address": "203.0.113.42" + } + }, + "payload": { + "method": "password" + }, + "custom_values": { + "vip_tier": "gold" + } + } + }, + "MonitoringEventIngestResult": { + "type": "object", + "description": "Outcome of ingesting one event. The same shape is returned for a new event and for an idempotent replay.", + "required": [ + "event_uuid", + "event_id", + "status", + "score", + "rules_evaluated_count", + "rules_matched_count", + "correlation_pending", + "retention_class", + "expires_at", + "idempotent_replay", + "rule_matches" + ], + "properties": { + "event_uuid": { + "type": "string", + "format": "uuid", + "description": "Didit identifier of the stored event." + }, + "event_id": { + "type": "string", + "description": "Your `event_id`." + }, + "status": { + "type": "string", + "enum": [ + "ACCEPTED", + "APPROVED", + "IN_REVIEW", + "DECLINED" + ], + "description": "Status after the real-time rules ran. See `MonitoringEvent.status`." + }, + "score": { + "type": "integer", + "description": "Score after the real-time rules ran." + }, + "rules_evaluated_count": { + "type": "integer", + "minimum": 0, + "description": "Number of event rules evaluated in the request. 0 in a sandbox application, where no rules run." + }, + "rules_matched_count": { + "type": "integer", + "minimum": 0, + "description": "Number of rules that matched, including `TEST` mode rules." + }, + "correlation_pending": { + "type": "boolean", + "description": "`true` when an asynchronous rule still has to run. Read `GET /v3/events/{event_uuid}/` a few minutes later for the final `status` and `score`." + }, + "retention_class": { + "type": "string", + "enum": [ + "low_value", + "standard", + "risk_relevant", + "monetary_or_compliance" + ], + "description": "Retention class, after any promotion by a matched rule." + }, + "expires_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the event expires under its retention class and is deleted. Null for `monetary_or_compliance` (payment and dispute) events." + }, + "idempotent_replay": { + "type": "boolean", + "description": "`true` when an event with this `event_id` and the same content was already stored; the values are those of the stored event and no rule ran again." + }, + "rule_matches": { + "type": "array", + "description": "Rules that matched the event.", + "items": { + "type": "object", + "required": [ + "rule_id", + "library_key", + "mode", + "is_async" + ], + "properties": { + "rule_id": { + "type": "string", + "format": "uuid", + "description": "UUID of the rule." + }, + "library_key": { + "type": "string", + "nullable": true, + "description": "Library key when the rule was installed from the rules library, otherwise null." + }, + "mode": { + "type": "string", + "enum": [ + "ACTIVE", + "TEST" + ], + "description": "`TEST` rules record the match but apply no actions." + }, + "is_async": { + "type": "boolean", + "description": "`true` when the match came from the asynchronous pass." + } + } + } + } + } + }, + "ComplianceList": { + "type": "object", + "description": "A blocklist, allowlist or custom list of one application.", + "required": [ + "uuid", + "name", + "description", + "list_type", + "entry_type", + "is_system", + "entry_count", + "last_entry_added_at", + "created_at", + "updated_at" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "List identifier. Use it as `list_uuid` in the list and entry endpoints, and as the value of a workflow or transaction rule condition with `value_type: \"list\"`." + }, + "name": { + "type": "string", + "maxLength": 255, + "description": "List name, unique among the application's lists." + }, + "description": { + "type": "string", + "description": "Free-text description. Empty string when not set." + }, + "list_type": { + "type": "string", + "enum": [ + "blocklist", + "allowlist", + "custom" + ], + "description": "`blocklist`: a match declines the session or transaction (system lists only, one per entry type). `allowlist`: matching items are trusted, for example a face allowlist skips the duplicated-face action; never overrides a blocklist match. `custom`: used in workflow and transaction rule conditions (`value_type: \"list\"`)." + }, + "entry_type": { + "type": "string", + "enum": [ + "face", + "document", + "phone", + "email", + "ip_address", + "device_fingerprint", + "wallet_address", + "bank_account", + "country", + "user", + "business", + "key" + ], + "description": "What the list holds: `face`, `document` (document number), `phone`, `email`, `ip_address` (IP address or CIDR range), `device_fingerprint`, `wallet_address`, `bank_account`, `country` (ISO 3166-1 alpha-3), `user` or `business` (your `vendor_data`), or `key` (any string). It decides how entry values are validated and matched." + }, + "is_system": { + "type": "boolean", + "description": "`true` for the lists Didit provisions: one blocklist per entry type (`face`, `document`, `phone`, `email`, `ip_address`, `device_fingerprint`, `wallet_address`, `bank_account`, `user`, `business`), the **High Risk Countries** custom list, and system allowlists. System lists cannot be renamed, edited or deleted; you can add and delete their entries." + }, + "entry_count": { + "type": "integer", "minimum": 0, - "maximum": 100, - "description": "Overall AML screening score — the highest risk score across all hits (float, 0-100). `null` until computed." + "description": "Number of entries currently in the list." }, - "screened_data": { + "last_entry_added_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the newest current entry was added. `null` for an empty list." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the list was created." + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "When the list was last updated." + } + } + }, + "ComplianceListEntry": { + "type": "object", + "description": "One value in a list.", + "required": [ + "uuid", + "value", + "display_label", + "comment", + "added_by", + "reference_session_id", + "reference_session_number", + "reference_session_type", + "reference_object_uuid", + "metadata", + "created_at" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Entry identifier. Use it as `entry_uuid` in Delete a list entry." + }, + "value": { + "type": "string", + "description": "The listed value: a phone number in E.164, an email address, a document number, an IP address or CIDR range, a device fingerprint, a wallet address, a bank account, an ISO 3166-1 alpha-3 country code, a `vendor_data`, any string on a `key` list, or the UUID of the stored face on a face list. Values you send are stored in lower case, except phone numbers and country codes. Matching is case-insensitive." + }, + "display_label": { + "type": "string", + "nullable": true, + "maxLength": 512, + "description": "Human-readable label. Didit fills it for entries taken from a session (for example the holder's name and document number) when you send none." + }, + "comment": { + "type": "string", + "description": "Free-text note. Empty string when not set." + }, + "added_by": { + "type": "string", + "nullable": true, + "description": "Who added the entry: `api` (an API key), a console user's email address or `console`, `system` or `migration` (entries Didit created), or `rule` (a transaction or monitoring rule action)." + }, + "reference_session_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Verification session the entry was taken from. `null` when the entry was added by value." + }, + "reference_session_number": { + "type": "integer", + "nullable": true, + "description": "Application-scoped number of that session, as shown in the console." + }, + "reference_session_type": { + "type": "string", + "nullable": true, + "enum": [ + "kyc", + "kyb", + null + ], + "description": "`kyc` for a person verification session, `kyb` for a business verification session, `null` when there is no session." + }, + "reference_object_uuid": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "The record the entry stands for: the face, document, phone or email record read from a session, the face created by Upload a face to a list, or the UUID you sent (for example a transaction UUID or a user's `didit_internal_id`)." + }, + "metadata": { "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Extra context. Holds the keys Didit sets (listed here) merged with any `metadata` you sent when creating the entry, returned as stored. `null` when there is none.", "properties": { + "reference_image_url": { + "type": "string", + "description": "Face entries: short-lived signed URL, issued again on every call; `null` for a scoped API key without `media: read`. Absent when no image belongs to the entry." + }, + "front_image_url": { + "type": "string", + "description": "Document entries that carry a document front image: short-lived signed URL, issued again on every call; `null` for a scoped API key without `media: read`." + }, "full_name": { - "type": "string" + "type": "string", + "description": "Document entries taken from a session: holder's full name." }, - "nationality": { - "type": "string" + "issuing_state": { + "type": "string", + "description": "Document entries taken from a session: ISO 3166-1 alpha-3 issuing country." }, - "date_of_birth": { + "document_type": { "type": "string", - "format": "date" + "description": "Document entries taken from a session: document type code, for example `P`, `ID` or `DL`." + }, + "region": { + "type": "string", + "description": "Document entries taken from a session: issuing state or province of a regionally issued document, empty when not applicable. Not set by Add session items to the blocklist." }, "document_number": { "type": "string", - "nullable": true + "description": "Document entries taken from a session: the document number." + }, + "face_image_source": { + "type": "string", + "enum": [ + "front_image_camera_front", + "back_image_camera_front" + ], + "description": "Face entries taken from a document-capture selfie: which selfie. Absent for liveness faces." + }, + "reference_node_id": { + "type": "string", + "nullable": true, + "description": "Face entries taken from a document-capture selfie: the ID verification node the selfie came from." + }, + "fingerprint_tier": { + "type": "string", + "nullable": true, + "enum": [ + "persistent_id", + "composite_hash", + "legacy_fp", + "unknown", + null + ], + "description": "Device fingerprint entries taken from a session: how the fingerprint was derived. `persistent_id` identifies one device; `composite_hash` and `legacy_fp` can be shared by devices of the same model." + }, + "sandbox": { + "type": "boolean", + "description": "`true` on a face entry uploaded in a sandbox application; such an entry matches nothing." + }, + "reference_type": { + "type": "string", + "enum": [ + "transaction", + "vendor_user", + "vendor_business" + ], + "description": "Set by you: what `reference_object_uuid` points to, so the console links back to it." + }, + "reference_id": { + "type": "string", + "description": "Set by you: identifier of the source record." } } }, - "is_ongoing_monitoring_enabled": { - "type": "boolean", - "description": "Whether AML ongoing monitoring is enabled for this screening. When `true`, the entity is re-screened periodically and any new hits update the report." + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the entry was added." + } + } + }, + "BlocklistedItem": { + "type": "object", + "description": "One blocklisted face, identity document, phone number or email address of a verification session. Fields that do not apply to the item's `type` are `null`.", + "required": [ + "type", + "session_id", + "session_number", + "blocklisted_at", + "face_image", + "document_type", + "document_number", + "front_image", + "phone_number", + "country_code", + "email" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "face", + "document", + "phone", + "email" + ], + "description": "What was blocklisted." }, - "next_ongoing_monitoring_bill_date": { + "session_id": { "type": "string", - "format": "date", + "format": "uuid", + "description": "Verification session the item belongs to." + }, + "session_number": { + "type": "integer", "nullable": true, - "description": "Date (YYYY-MM-DD) when this ongoing-monitoring subscription will next be billed. Computed as the date of the last ongoing-monitoring billing plus 365 days when `is_ongoing_monitoring_enabled = true`; otherwise `null`. This is a billing date, not a screening date - use `last_ongoing_screening_at` for that." + "description": "Application-scoped number of that session, as shown in the console." }, - "last_ongoing_screening_at": { + "blocklisted_at": { "type": "string", "format": "date-time", + "description": "When the item was last blocklisted." + }, + "face_image": { + "type": "string", "nullable": true, - "description": "When ongoing monitoring last completed a re-screening of this record, whether or not the result changed. `null` when the record has never been re-screened; it is never back-filled from the enablement or billing date." + "description": "Face items: short-lived signed URL, issued again on every call; `null` for a scoped API key without `media: read`. Also `null` when no reference image is stored." }, - "last_ongoing_screening_datasets_updated_at": { + "document_type": { "type": "string", - "format": "date-time", "nullable": true, - "description": "The most recent AML dataset update date the provider reported at the time of that screening. `null` when the provider reported no dataset dates; never estimated." + "description": "Document items: document type code, for example `P` (passport), `ID` (national ID), `DL` (driver's license) or `RP` (residence permit)." }, - "warnings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feature": { + "document_number": { + "type": "string", + "nullable": true, + "description": "Document items: the document number, or the personal number when the document has none." + }, + "front_image": { + "type": "string", + "nullable": true, + "description": "Document items: front image of the document, short-lived signed URL, issued again on every call; `null` for a scoped API key without `media: read`." + }, + "phone_number": { + "type": "string", + "nullable": true, + "description": "Phone items: the phone number in E.164." + }, + "country_code": { + "type": "string", + "nullable": true, + "description": "Phone items: ISO 3166-1 alpha-2 country of the number." + }, + "email": { + "type": "string", + "nullable": true, + "description": "Email items: the email address." + } + } + }, + "TransactionNote": { + "type": "object", + "description": "One note on a monitored transaction's review timeline.", + "required": [ + "uuid", + "note", + "actor_email", + "actor_name", + "metadata", + "created_at" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Note identifier." + }, + "note": { + "type": "string", + "description": "Plain-text note body." + }, + "actor_email": { + "type": "string", + "format": "email", + "nullable": true, + "description": "Email of the Console user who wrote the note or changed the status. `null` for notes created with an API key and for notes Didit adds automatically." + }, + "actor_name": { + "type": "string", + "nullable": true, + "description": "Display name of the Console user (their email when they have no name). `Rule Engine` on notes Didit adds automatically. `null` for notes created with an API key." + }, + "metadata": { + "type": "object", + "description": "Tells the note types apart. Notes written through the API or the Console carry `mentioned_emails`. A status change made in the Console carries `type: status_change` with `previous_status`, `new_status`, `reason_code` and `reason_label`. Notes Didit adds carry `source`: `rule_action` (with `rule_uuid`) for a rule's `add_note` action, or `remediation_session` for a problem creating the user-action verification session.", + "properties": { + "mentioned_emails": { + "type": "array", + "items": { "type": "string", - "enum": [ - "ID_VERIFICATION", - "NFC", - "LIVENESS", - "FACEMATCH", - "PROOF_OF_ADDRESS", - "PHONE", - "EMAIL", - "AML", - "LOCATION", - "DATABASE_VALIDATION", - "QUESTIONNAIRE", - "DOCUMENT_AI", - "KYB_REGISTRY", - "KYB_DOCUMENTS", - "KYB_KEY_PEOPLE" - ], - "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Within an AML screening this is typically `AML`, but the enum above is authoritative across all feature warnings on the decision payload." - }, - "risk": { - "type": "string" - }, - "additional_data": { - "type": "object", - "nullable": true - }, - "log_type": { - "type": "string" - }, - "short_description": { - "type": "string" - }, - "long_description": { - "type": "string" + "format": "email" }, - "node_id": { - "type": "string", - "description": "The workflow graph node ID that generated this warning" - } + "description": "Mentioned organization members, lower-cased and de-duplicated. Empty when nobody was mentioned." + }, + "type": { + "type": "string", + "enum": [ + "status_change" + ], + "description": "Present on notes recording a status change made in the Console." + }, + "previous_status": { + "type": "string", + "enum": [ + "APPROVED", + "IN_REVIEW", + "DECLINED", + "AWAITING_USER" + ], + "description": "Status before the change." + }, + "new_status": { + "type": "string", + "enum": [ + "APPROVED", + "IN_REVIEW", + "DECLINED", + "AWAITING_USER" + ], + "description": "Status after the change." + }, + "reason_code": { + "type": "string", + "nullable": true, + "description": "Reason code given with the change." + }, + "reason_label": { + "type": "string", + "nullable": true, + "description": "Reason label given with the change." + }, + "source": { + "type": "string", + "enum": [ + "rule_action", + "remediation_session" + ], + "description": "Present on notes Didit adds automatically." + }, + "rule_uuid": { + "type": "string", + "format": "uuid", + "description": "Rule whose `add_note` action wrote the note (with `source: rule_action`)." } } + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the note was stored." } } }, - "MonitoringEvent": { + "TransactionSummary": { "type": "object", - "description": "One stored monitoring event as returned by the list and subject-timeline endpoints. Restricted evidence (payload, device context, rule evidence) is only returned by `GET /v3/events/{event_uuid}/`.", - "required": [ - "event_uuid", - "event_id", - "event_category", - "action_type", - "occurred_at", - "received_at", - "status", - "score", - "subject_vendor_data", - "subject_entity_type", - "subject_uuid", - "counterparty_vendor_data", - "session_id", - "device_fingerprint", - "ip_address", - "ip_country", - "amount", - "currency", - "game_id", - "round_id", - "bonus_id", - "affiliate_code", - "payment_reference_id", - "retention_class", - "rules_matched_count", - "correlation_pending", - "is_droppable" - ], + "description": "Summary of a monitored transaction, the shape `GET /v3/transactions/` lists. Call `GET /v3/transactions/{transaction_id}/` with `uuid` for parties, alerts and rule runs.", "properties": { - "event_uuid": { + "uuid": { "type": "string", "format": "uuid", - "description": "Didit identifier of the event. Use it as `{event_uuid}` in the retrieve and delete calls." + "description": "Didit transaction UUID. Use it as `{transaction_id}` on the transaction endpoints." }, - "event_id": { + "transaction_number": { + "type": "integer", + "nullable": true, + "description": "Application-scoped sequential number shown in the Console." + }, + "txn_id": { "type": "string", - "maxLength": 128, - "description": "Your own identifier, the idempotency key you sent." + "nullable": true, + "description": "Your own identifier from the create call. Unique per application." }, - "event_category": { + "transaction_type": { + "type": "string", + "description": "Category as stored: `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet`, `gamblingLimitChange`, `gamblingBonusChange` or `auditTrailEvent`." + }, + "action_type": { + "type": "string", + "description": "Sub-type within the category, for example `withdrawal`." + }, + "status": { "type": "string", "enum": [ - "auth", - "session", - "gameplay", - "bonus", - "affiliate", - "payment", - "dispute", - "security", - "responsible_gambling", - "custom" + "APPROVED", + "IN_REVIEW", + "DECLINED", + "AWAITING_USER" ], - "description": "Event category." + "description": "Current monitoring verdict." }, - "action_type": { + "source": { "type": "string", - "maxLength": 128, - "description": "What happened within the category, as you sent it." + "enum": [ + "LIVE", + "IMPORTED" + ], + "description": "`LIVE` for transactions submitted for monitoring, `IMPORTED` for historical imports." }, - "occurred_at": { + "direction": { "type": "string", - "format": "date-time", - "description": "When the event happened on your side." + "enum": [ + "INBOUND", + "OUTBOUND" + ], + "description": "Direction relative to the subject." + }, + "amount": { + "type": "string", + "description": "Amount as a decimal string with trailing zeros removed (up to 18 decimal places)." + }, + "currency": { + "type": "string", + "description": "Currency code or crypto asset symbol of `amount`." + }, + "currency_type": { + "type": "string", + "nullable": true, + "description": "`fiat` or `crypto` when known." + }, + "preferred_currency_amount": { + "type": "string", + "nullable": true, + "description": "`amount` converted to the application's preferred currency, when a rate was available." + }, + "preferred_currency_code": { + "type": "string", + "nullable": true, + "description": "Currency of `preferred_currency_amount`." + }, + "payment_details": { + "type": "string", + "nullable": true, + "description": "Free-text payment reference." }, - "received_at": { + "txn_date": { "type": "string", "format": "date-time", - "description": "When Didit accepted the event." + "description": "When the transaction occurred." }, - "status": { + "created_at": { "type": "string", - "enum": [ - "ACCEPTED", - "APPROVED", - "IN_REVIEW", - "DECLINED" - ], - "description": "`ACCEPTED`: stored, no matched rule changed its standing (the normal answer for most events, and always the answer in a sandbox application, where no rules run). `APPROVED`: a rule matched and the score stayed under your review threshold. `IN_REVIEW`: a rule pushed the score over the review threshold or set the status. `DECLINED`: a rule declined it or the score crossed your decline threshold." + "format": "date-time", + "description": "When Didit recorded it." }, "score": { "type": "integer", - "description": "Accumulated rule score. Starts at 0; rule `add_score` actions move it (negative deltas are allowed)." + "description": "Risk score. Higher is riskier." }, - "subject_vendor_data": { + "severity": { "type": "string", - "maxLength": 255, - "description": "`subject.vendor_data` of the event: your id for the user or company." + "nullable": true, + "enum": [ + "UNKNOWN", + "LOW", + "MEDIUM", + "HIGH", + "CRITICAL", + null + ], + "description": "Risk severity, `null` until something sets it." }, - "subject_entity_type": { + "decision_reason_code": { "type": "string", - "description": "`individual` or `company`: which kind of subject `subject_vendor_data` names.", - "example": "individual" + "nullable": true, + "description": "Machine-readable reason for the current `status`." }, - "subject_uuid": { + "decision_reason_label": { "type": "string", - "format": "uuid", "nullable": true, - "description": "UUID of the user (individual) or company the event resolved to, or null when that record no longer exists." + "description": "Human-readable label for `decision_reason_code`." }, - "counterparty_vendor_data": { + "tags": { + "type": "array", + "description": "Tags on the transaction.", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string", + "description": "Hex color, for example `#F59E0B`." + }, + "description": { + "type": "string", + "nullable": true + }, + "source": { + "type": "string", + "enum": [ + "didit", + "custom" + ], + "description": "Didit preset or custom tag." + } + } + } + }, + "subject_name": { "type": "string", "nullable": true, - "description": "The other player of a P2P event (promoted from `payload.counterparty_vendor_data`).", - "maxLength": 255 + "description": "Full name of the `APPLICANT` party." }, - "session_id": { + "subject_vendor_data": { "type": "string", "nullable": true, - "description": "`session.session_id` sent with the event.", - "maxLength": 255 + "description": "Your `vendor_data` for the applicant." }, - "device_fingerprint": { + "subject_type": { "type": "string", "nullable": true, - "description": "`device_context.fingerprint` sent with the event.", - "maxLength": 255 + "enum": [ + "user", + "business", + null + ], + "description": "Whether the applicant is linked to a Didit user or business; `null` when it is linked to neither." }, - "ip_address": { + "subject_uuid": { "type": "string", + "format": "uuid", "nullable": true, - "description": "IP address of the event. On the server lane this is `device_context.network_context.ip_address` as you sent it; on the device lane it is the IP of the request.", - "maxLength": 64 + "description": "UUID of the Didit user or business the applicant is linked to." }, - "ip_country": { + "subject_country": { "type": "string", "nullable": true, - "description": "Country code of `ip_address` from IP enrichment, or null when the address was not enriched (missing, private, or low-value telemetry whose address was not already known).", - "example": "ES" + "description": "Country code of the applicant." }, - "amount": { + "counterparty_name": { "type": "string", "nullable": true, - "description": "Amount as a decimal string with 18 decimal places, from `transaction_context.amount`, `payload.amount` or `payload.stake_amount`, in that order.", - "example": "250.000000000000000000" + "description": "Full name of the `COUNTERPARTY` party, if any." }, - "currency": { + "counterparty_vendor_data": { "type": "string", "nullable": true, - "description": "Currency from `transaction_context.currency` or `payload.currency`.", - "maxLength": 10 + "description": "Your `vendor_data` for the counterparty." }, - "game_id": { + "counterparty_country": { "type": "string", "nullable": true, - "description": "Promoted from `payload.game_id`.", - "maxLength": 128 + "description": "Country code of the counterparty." }, - "round_id": { + "assigned_to_name": { "type": "string", "nullable": true, - "description": "Promoted from `payload.round_id`.", - "maxLength": 128 + "description": "Name of the Console reviewer assigned to the transaction." }, - "bonus_id": { + "travel_rule_status": { "type": "string", "nullable": true, - "description": "Promoted from `payload.bonus_id`.", - "maxLength": 128 + "description": "Status of the transaction's Travel Rule transfer, or `null` when it has none." }, - "affiliate_code": { + "application_id": { "type": "string", - "nullable": true, - "description": "Promoted from `payload.affiliate_code`.", - "maxLength": 128 + "format": "uuid", + "description": "Application that owns the transaction." }, - "payment_reference_id": { + "application_name": { "type": "string", "nullable": true, - "description": "Payment reference from `transaction_context.payment_reference_id` or `payload.payment_reference_id`.", - "maxLength": 255 + "description": "Name of that application." }, - "retention_class": { + "organization_id": { "type": "string", - "enum": [ - "low_value", - "standard", - "risk_relevant", - "monetary_or_compliance" - ], - "description": "Retention class. Payment and dispute events are `monetary_or_compliance`; security and responsible-gambling events are `risk_relevant`; `session.page_view`, `session.sdk_heartbeat`, `session.sdk_loaded`, `session.device_seen`, `session.bot_signal_observed`, `bonus.promotion_viewed` and `affiliate.referral_clicked` are `low_value`; everything else is `standard`. An event an active rule matched is promoted to `risk_relevant` (never demoted)." - }, - "rules_matched_count": { - "type": "integer", - "minimum": 0, - "description": "Number of rules that matched the event, including rules in `TEST` mode (which record evidence but apply no actions)." - }, - "correlation_pending": { - "type": "boolean", - "description": "`true` while an asynchronous rule (cross-subject correlation, or a sequence longer than 24 hours) still has to run for this event. It resolves within minutes and `status`, `score` and `rules_matched_count` update in place." + "format": "uuid", + "description": "Organization that owns the application." }, - "is_droppable": { - "type": "boolean", - "description": "Whether an SDK under backpressure may drop events of this kind. `false` for every `security`, `payment`, `dispute` and `responsible_gambling` event and for `gameplay.bet_placed`, `gameplay.bet_settled` and `gameplay.p2p_settlement`." + "organization_name": { + "type": "string", + "nullable": true, + "description": "Name of that organization." } } }, - "MonitoringEventDetail": { - "description": "One monitoring event with its full envelope, enrichment and rule evidence. Every field of `MonitoringEvent` plus the fields below.", - "allOf": [ - { - "$ref": "#/components/schemas/MonitoringEvent" - }, - { - "type": "object", - "required": [ - "payload", - "custom_values", - "device_context", - "field_sources", - "session_sequence", - "table_id", - "hand_id", - "match_id", - "market_id", - "side", - "role", - "net_result_amount", - "payment_fingerprint", - "instrument_reference_hash", - "withdrawal_address_hash", - "dispute_id", - "chargeback_id", - "changed_field", - "identity_match_ref", - "schema_version", - "expires_at", - "rules_evaluated_count", - "rule_runs", - "ip_enrichment", - "subject_profile", - "tags" - ], - "properties": { - "payload": { - "type": "object", - "additionalProperties": true, - "description": "The category fields you sent in `payload`." - }, - "custom_values": { - "type": "object", - "additionalProperties": true, - "description": "The scalar `custom_values` you sent. Didit adds `vpn_or_proxy_detected: true` or `is_data_center: true` when IP enrichment flags the address and you did not set them." - }, - "device_context": { - "type": "object", - "additionalProperties": true, - "description": "The device context as stored: your `device_context` (including `fingerprint`, `persistent_device_id` and `bot_score` when sent) with `network_context` completed by IP enrichment (`ip_country`, `ip_country_code`, `ip_city`, `asn_number`, `asn_organization`, `is_vpn_or_tor`, `is_data_center`, `proxy_type`, `connection_type`) for keys you left empty." - }, - "field_sources": { - "type": "object", - "additionalProperties": { - "type": "string", - "enum": [ - "customer", - "sdk", - "server" - ] - }, - "description": "Field path to provenance: `customer` (sent by your backend), `sdk` (sent through the device lane) or `server` (derived by Didit, such as `received_at` and enriched network fields). Check it before acting on a match." - }, - "session_sequence": { - "type": "integer", - "format": "int64", - "nullable": true, - "description": "`session.sequence` sent with the event." - }, - "table_id": { - "type": "string", - "nullable": true, - "description": "Promoted from `payload.table_id`.", - "maxLength": 128 - }, - "hand_id": { - "type": "string", - "nullable": true, - "description": "Promoted from `payload.hand_id`.", - "maxLength": 128 - }, - "match_id": { - "type": "string", - "nullable": true, - "description": "Promoted from `payload.match_id`.", - "maxLength": 128 - }, - "market_id": { - "type": "string", - "nullable": true, - "description": "Promoted from `payload.market_id`.", - "maxLength": 128 - }, - "side": { - "type": "string", - "nullable": true, - "description": "Promoted from `payload.side`.", - "maxLength": 64 - }, - "role": { - "type": "string", - "nullable": true, - "description": "Promoted from `payload.role`.", - "maxLength": 64 - }, - "net_result_amount": { - "type": "string", - "nullable": true, - "description": "Promoted from `payload.net_result_amount`, as a decimal string with 18 decimal places.", - "example": "-120.000000000000000000" - }, - "payment_fingerprint": { - "type": "string", - "nullable": true, - "description": "Promoted from `payload.payment_fingerprint`.", - "maxLength": 255 - }, - "instrument_reference_hash": { - "type": "string", - "nullable": true, - "description": "Promoted from `payload.instrument_reference_hash`.", - "maxLength": 255 - }, - "withdrawal_address_hash": { - "type": "string", - "nullable": true, - "description": "Promoted from `payload.withdrawal_address_hash` (or `payload.destination_reference_hash`).", - "maxLength": 255 - }, - "dispute_id": { - "type": "string", - "nullable": true, - "description": "Promoted from `payload.dispute_id`.", - "maxLength": 255 - }, - "chargeback_id": { - "type": "string", - "nullable": true, - "description": "Promoted from `payload.chargeback_id`.", - "maxLength": 255 - }, - "changed_field": { - "type": "string", - "nullable": true, - "description": "Promoted from `payload.changed_field` (security change events).", - "maxLength": 128 - }, - "identity_match_ref": { - "type": "string", - "nullable": true, - "description": "Promoted from `payload.identity_match_ref` (responsible-gambling registry matches).", - "maxLength": 255 - }, - "schema_version": { - "type": "integer", - "description": "Version of the event envelope schema. Currently `1`.", - "example": 1 - }, - "expires_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the event expires under its retention class and is deleted: `occurred_at` plus the class period (14, 30 or 90 days by default, adjustable per application). Null for `monetary_or_compliance` events, which follow transaction retention." - }, - "rules_evaluated_count": { - "type": "integer", - "minimum": 0, - "description": "Number of event rules evaluated for this event." - }, - "rule_runs": { - "type": "array", - "description": "Every rule that matched the event, newest first.", - "items": { - "$ref": "#/components/schemas/MonitoringEventRuleRun" - } - }, - "ip_enrichment": { - "type": "object", - "nullable": true, - "description": "Enrichment of the event's IP address, or null when the address was not enriched. See [IP enrichment](https://docs.didit.me/transaction-monitoring/ip-enrichment).", - "properties": { - "ip_country_code": { - "type": "string", - "nullable": true, - "description": "Country code of the address." - }, - "ip_city": { - "type": "string", - "nullable": true, - "description": "City of the address." - }, - "asn_number": { - "type": "integer", - "nullable": true, - "description": "Autonomous system number." - }, - "asn_organization": { - "type": "string", - "nullable": true, - "description": "Organization that operates the autonomous system." - }, - "is_vpn_or_tor": { - "type": "boolean", - "description": "Whether the address is a known VPN or Tor exit." - }, - "is_data_center": { - "type": "boolean", - "description": "Whether the address belongs to a hosting or data-centre network." - }, - "proxy_type": { - "type": "string", - "nullable": true, - "description": "Proxy classification of the address as an opaque string, or null." - }, - "last_enriched_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the address was last enriched." - } - }, - "required": [ - "ip_country_code", - "ip_city", - "asn_number", - "asn_organization", - "is_vpn_or_tor", - "is_data_center", - "proxy_type", - "last_enriched_at" - ] - }, - "subject_profile": { - "type": "object", - "nullable": true, - "description": "Cached email and phone risk signals of the subject, or null when no profile exists for the subject.", - "properties": { - "email_risk_score": { - "type": "integer", - "nullable": true, - "description": "Email risk score." - }, - "email_deliverability": { - "type": "string", - "nullable": true, - "description": "Email deliverability verdict." - }, - "email_is_disposable": { - "type": "boolean", - "nullable": true, - "description": "Whether the email address is disposable." - }, - "email_is_breached": { - "type": "boolean", - "nullable": true, - "description": "Whether the email address appears in known data breaches." - }, - "email_social_count": { - "type": "integer", - "nullable": true, - "description": "Number of online accounts linked to the email address." - }, - "email_social_providers": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Names of the online services where an account linked to the email address was found." - }, - "phone_risk_score": { - "type": "integer", - "nullable": true, - "description": "Phone risk score." - }, - "phone_line_type": { - "type": "string", - "nullable": true, - "description": "Phone line type." - }, - "phone_carrier": { - "type": "string", - "nullable": true, - "description": "Phone carrier name." - }, - "phone_social_count": { - "type": "integer", - "nullable": true, - "description": "Number of online accounts linked to the phone number." - }, - "phone_social_providers": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Names of the online services where an account linked to the phone number was found." - }, - "source_provider": { - "type": "string", - "nullable": true, - "description": "Opaque identifier of the source of the profile." - }, - "last_enriched_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the profile was last refreshed." - } - }, - "required": [ - "email_risk_score", - "email_deliverability", - "email_is_disposable", - "email_is_breached", - "email_social_count", - "email_social_providers", - "phone_risk_score", - "phone_line_type", - "phone_carrier", - "phone_social_count", - "phone_social_providers", - "source_provider", - "last_enriched_at" - ] - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Names of the tags attached to the event (rule `add_tags` actions)." - } - } - } - ] - }, - "MonitoringEventRuleRun": { + "ApplicationAnalyticsDataPoint": { "type": "object", + "description": "Session counts for one day, by creation date (UTC). Each count reflects the session's current status. Points of the previous period carry at least the required fields.", + "required": [ + "rounded_date", + "total", + "approved", + "declined", + "canceled", + "pending" + ], "properties": { - "rule_uuid": { + "rounded_date": { "type": "string", - "format": "uuid", - "description": "UUID of the transaction monitoring rule." + "format": "date", + "description": "The day, `YYYY-MM-DD`." }, - "rule_title": { - "type": "string", - "description": "Title of the rule." + "total": { + "type": "integer", + "description": "Sessions created that day." }, - "library_key": { - "type": "string", - "nullable": true, - "description": "Library key when the rule was installed from the rules library, otherwise null." + "approved": { + "type": "integer", + "description": "Of those, sessions `Approved` or `Kyc Expired`." }, - "matched": { - "type": "boolean", - "description": "Always `true`: only matching runs are recorded." + "declined": { + "type": "integer", + "description": "Sessions `Declined`." }, - "mode": { - "type": "string", - "enum": [ - "ACTIVE", - "TEST" - ], - "description": "Mode of the rule when it ran. A `TEST` run records evidence but applied no actions." + "canceled": { + "type": "integer", + "description": "Sessions `Expired` or `Abandoned`." }, - "is_async": { - "type": "boolean", - "description": "`true` when the run came from the asynchronous correlation pass rather than the ingest request." + "pending": { + "type": "integer", + "description": "Sessions `Not Started`, `In Progress` or `In Review`." }, - "severity": { - "type": "string", - "nullable": true, - "enum": [ - "UNKNOWN", - "LOW", - "MEDIUM", - "HIGH", - "CRITICAL", - null - ], - "description": "Severity of the rule. Null for `TEST` runs and for rules without a severity." + "in_review": { + "type": "integer", + "description": "Sessions `In Review`." }, - "score_delta": { + "expired": { "type": "integer", - "description": "How much this run changed the event's score (0 for `TEST` runs)." + "description": "Sessions `Expired`." }, - "action_summary": { - "type": "string", - "nullable": true, - "description": "Short summary of the actions applied, for example `score +40; add_tag:VPN_LOGIN`. Null when nothing was applied." + "abandoned": { + "type": "integer", + "description": "Sessions `Abandoned`." }, - "matched_payload": { - "type": "array", - "description": "The conditions and aggregations that matched. Condition entries carry `field`, `operator`, `expected_value`, `current_value`, `matched`, `group_index`, `group_logic` and `field_source` (`server`, `sdk` or `customer`); aggregation entries carry `metric`, `field`, `operator`, `expected_value`, `computed_value`, `window`, `filters`, `matched` and `sample_references`.", - "items": { - "type": "object", - "additionalProperties": true - } + "in_progress": { + "type": "integer", + "description": "Sessions `In Progress`." }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "When the rule run was recorded." + "not_started": { + "type": "integer", + "description": "Sessions `Not Started`." + }, + "manually_approved": { + "type": "integer", + "description": "Sessions a reviewer approved after the workflow gave another verdict." + }, + "manually_declined": { + "type": "integer", + "description": "Sessions a reviewer declined after the workflow gave another verdict." } - }, - "required": [ - "rule_uuid", - "rule_title", - "library_key", - "matched", - "mode", - "is_async", - "severity", - "score_delta", - "action_summary", - "matched_payload", - "created_at" - ] + } }, - "MonitoringEventPage": { + "ApplicationAnalyticsRequestBreakdown": { "type": "object", - "description": "One page of monitoring events, newest `occurred_at` first.", + "description": "Status totals for the range. The previous-period breakdown carries at least the required fields.", "required": [ - "count", - "next", - "previous", - "results" + "total_approved", + "total_in_review", + "total_declined", + "total_canceled", + "total_pending" ], "properties": { - "count": { + "total_approved": { "type": "integer", - "description": "Number of matching events, exact up to 100 and capped at 100 above that. Do not compute page counts from it; follow `next` until it is null." + "description": "Sessions `Approved` or `Kyc Expired`." }, - "next": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "URL of the next page, or null on the last page." + "total_in_review": { + "type": "integer", + "description": "Sessions `In Review`." }, - "previous": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "URL of the previous page, or null on the first page." + "total_declined": { + "type": "integer", + "description": "Sessions `Declined`." }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MonitoringEvent" - } + "total_canceled": { + "type": "integer", + "description": "Sessions `Expired` or `Abandoned`." + }, + "total_pending": { + "type": "integer", + "description": "Sessions `Not Started`, `In Progress` or `In Review` (includes `total_in_review`)." + }, + "total_expired": { + "type": "integer", + "description": "Sessions `Expired`." + }, + "total_abandoned": { + "type": "integer", + "description": "Sessions `Abandoned`." + }, + "total_in_progress": { + "type": "integer", + "description": "Sessions `In Progress`." + }, + "total_not_started": { + "type": "integer", + "description": "Sessions `Not Started`." + }, + "total_manually_approved": { + "type": "integer", + "description": "Approved by a reviewer." + }, + "total_manually_declined": { + "type": "integer", + "description": "Declined by a reviewer." } } }, - "MonitoringEventCreateRequest": { + "ApplicationAnalyticsCountryCount": { "type": "object", - "description": "The event envelope, the same for every category. camelCase aliases are accepted: `eventId`, `eventCategory` (or `category`), `actionType`, `occurredAt`, `timeZone` (or `zoneId`), `deviceContext` (or `device`), `transactionContext`, `customValues` (or `props`); inside `subject`: `vendorData`, `entityType`, `fullName`; inside `session`: `sessionId`, `pageUrl`; inside `transaction_context`: `paymentReferenceId`.", - "required": [ - "event_id", - "event_category", - "action_type", - "occurred_at", - "subject" - ], "properties": { - "event_id": { + "country": { "type": "string", - "maxLength": 128, - "description": "Your identifier for the event, unique per application. It is the idempotency key: resending the same `event_category`, `action_type`, `occurred_at`, `subject.vendor_data` and `payload` under it is a replay, anything different is a conflict.", - "example": "login-2026-10-17-000001" + "description": "Country name." }, - "event_category": { + "country_code": { "type": "string", - "enum": [ - "auth", - "session", - "gameplay", - "bonus", - "affiliate", - "payment", - "dispute", - "security", - "responsible_gambling", - "custom" - ], - "description": "Event category.", - "example": "auth" + "description": "Two-letter country code." }, - "action_type": { - "type": "string", - "minLength": 1, - "maxLength": 128, - "description": "What happened within the category. Open vocabulary; the managed actions listed in [Event ingestion](https://docs.didit.me/transaction-monitoring/events) (for example `login`, `deposit_completed`, `chargeback_opened`) unlock typed validation and managed rule fields. Leading and trailing whitespace is trimmed.", - "example": "login" + "session_count": { + "type": "integer", + "description": "Sessions from that country." + } + } + }, + "ApplicationAnalyticsLocationStats": { + "type": "object", + "properties": { + "ip_countries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsCountryCount" + }, + "description": "Countries of the users' IP addresses, most sessions first." }, - "occurred_at": { - "type": "string", - "format": "date-time", - "description": "When the event happened on your side, ISO 8601 (a value without an offset is read as UTC). Must be no more than 7 days in the past and no more than 5 minutes in the future.", - "example": "2026-10-17T09:15:02.481Z" + "countries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsCountryCount" + }, + "description": "Same list as `ip_countries`." }, - "time_zone": { - "type": "string", - "nullable": true, - "maxLength": 64, - "description": "Time zone of the occurrence on your side, for example `Europe/Madrid`. Stored with the event; not returned by the read endpoints." + "id_countries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsCountryCount" + }, + "description": "Issuing countries of the verified ID documents, most sessions first." }, - "subject": { + "conversion_by_country": { + "type": "array", + "description": "Per document issuing country: finished sessions (approved, declined or in review) and how many were approved. Most finished first.", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string" + }, + "country_code": { + "type": "string" + }, + "finished": { + "type": "integer" + }, + "approved": { + "type": "integer" + } + } + } + }, + "cities": { + "type": "array", + "description": "Up to 100 IP cities, most sessions first.", + "items": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "`City, Country`." + }, + "session_count": { + "type": "integer" + }, + "latitude": { + "type": "number", + "nullable": true + }, + "longitude": { + "type": "number", + "nullable": true + } + } + } + } + } + }, + "ApplicationAnalyticsDemographicStats": { + "type": "object", + "properties": { + "age_distribution": { "type": "object", - "description": "The user or company the event belongs to. It resolves to the user (or company) with this `vendor_data` in your application; one is created when none exists.", - "required": [ - "vendor_data" - ], + "description": "Sessions per age band from the ID document. Empty when there is no data.", "properties": { - "vendor_data": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "description": "Your id for the subject.", - "example": "user-42" + "under_18": { + "type": "integer" }, - "entity_type": { - "type": "string", - "maxLength": 32, - "default": "individual", - "description": "`company` resolves the subject to a company; any other value (default `individual`) resolves it to an individual. The same `vendor_data` can name one of each.", - "example": "individual" + "18_24": { + "type": "integer" }, - "full_name": { - "type": "string", - "nullable": true, - "maxLength": 512, - "description": "Display name, used only when this event creates the user or company." + "25_34": { + "type": "integer" }, - "email": { - "type": "string", - "format": "email", - "nullable": true, - "description": "Validated as an email address; not stored on the event." + "35_44": { + "type": "integer" }, - "phone": { - "type": "string", - "nullable": true, - "maxLength": 32, - "description": "Validated; not stored on the event." + "45_64": { + "type": "integer" }, - "country": { - "type": "string", - "nullable": true, - "maxLength": 3, - "description": "Validated; not stored on the event." + "65_plus": { + "type": "integer" } + }, + "additionalProperties": { + "type": "integer" } }, - "business": { + "gender_distribution": { "type": "object", - "description": "The business the event belongs to, when it differs from the subject. Only `vendor_data` is read.", - "additionalProperties": true, - "properties": { - "vendor_data": { - "type": "string", - "maxLength": 255, - "description": "Your id for the business. Filterable as `business_vendor_data`.", - "example": "merchant-7" + "description": "Sessions per document gender code: `M`, `F` or `U` (unknown).", + "additionalProperties": { + "type": "integer" + } + } + } + }, + "ApplicationAnalyticsDeviceDetails": { + "type": "object", + "description": "Top five of each, most sessions first.", + "properties": { + "device_models": { + "type": "array", + "description": "Device brands: `Apple`, `Android`, `LG`, everything else grouped as `Other`.", + "items": { + "type": "object", + "properties": { + "device": { + "type": "string" + }, + "count": { + "type": "integer" + } } } }, - "session": { - "type": "object", - "description": "Your session context for the event.", - "properties": { - "session_id": { - "type": "string", - "nullable": true, - "maxLength": 255, - "description": "Your session identifier. Filterable and returned as `session_id`." - }, - "sequence": { - "type": "integer", - "nullable": true, - "description": "Position of the event within the session. Returned as `session_sequence`." - }, - "page_url": { - "type": "string", - "nullable": true, - "maxLength": 2048, - "description": "Validated; not stored on the event." - }, - "referrer": { - "type": "string", - "nullable": true, - "maxLength": 2048, - "description": "Validated; not stored on the event." + "browsers": { + "type": "array", + "description": "Browsers: `Chrome`, `Safari`, `Didit App`, `Firefox`, everything else grouped as `Other`.", + "items": { + "type": "object", + "properties": { + "browser": { + "type": "string" + }, + "count": { + "type": "integer" + } } } }, - "device_context": { - "type": "object", - "additionalProperties": true, - "description": "Device and network context, stored as sent and returned by `GET /v3/events/{event_uuid}/`. On this server lane Didit does not see the end user's connection, so the IP enriched and used by rules is `network_context.ip_address` as you send it.", - "properties": { - "fingerprint": { - "type": "string", - "maxLength": 255, - "description": "Device fingerprint. Filterable as `device_fingerprint`." - }, - "persistent_device_id": { - "type": "string", - "maxLength": 255, - "description": "Your persistent device identifier." - }, - "bot_score": { - "type": "integer", - "description": "Bot score from your own detection, as an integer." - }, - "network_context": { - "type": "object", - "additionalProperties": true, - "properties": { - "ip_address": { - "type": "string", - "maxLength": 64, - "description": "End user's IP address. Public addresses are enriched once per unique address.", - "example": "203.0.113.42" - } + "operating_systems": { + "type": "array", + "description": "Operating systems.", + "items": { + "type": "object", + "properties": { + "os": { + "type": "string" + }, + "count": { + "type": "integer" } } } - }, - "transaction_context": { - "type": "object", - "description": "Money context. Non-empty values here also satisfy the required payload keys of the action.", - "properties": { - "amount": { - "type": "string", - "nullable": true, - "description": "Amount as a decimal string (a number is also accepted).", - "example": "1000.00" - }, - "currency": { - "type": "string", - "nullable": true, - "maxLength": 10, - "description": "Currency code." - }, - "direction": { - "type": "string", - "nullable": true, - "maxLength": 16, - "description": "Direction of the money movement, for example `inbound` or `outbound`." - }, - "payment_reference_id": { - "type": "string", - "nullable": true, - "maxLength": 255, - "description": "Your payment reference. Filterable as `payment_reference_id`." + } + } + }, + "ApplicationAnalyticsWorkflowFunnel": { + "type": "object", + "description": "Keyed by workflow ID. `total` is the sessions started on that workflow; `steps` counts the sessions that reached each step (`OCR`, `LIVENESS`, `FACE_MATCH`, `AML`, `POA`, `PHONE`, `EMAIL`, `NFC`, `DATABASE_VALIDATION`, `QUESTIONNAIRE`, `IP_ANALYSIS`, `AGE_ESTIMATION`, `KYB_REGISTRY`, `KYB_DOCUMENTS`, `KYB_KEY_PEOPLE`). Reaching a step is not passing it.", + "additionalProperties": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "steps": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "reached": { + "type": "integer" + } + } } } + } + } + }, + "ApplicationAnalyticsVerificationTimeStats": { + "type": "object", + "description": "Active time users spent verifying, ignoring idle gaps. An empty object when there is no data.", + "properties": { + "overall_avg_seconds": { + "type": "number", + "description": "Average per session, one decimal." }, - "payload": { - "type": "object", - "additionalProperties": true, - "description": "The category's domain fields, at most 16384 bytes once JSON-encoded. Some actions require keys (in `payload` or `transaction_context`): `gameplay.p2p_settlement` needs `game_id`, `counterparty_vendor_data`, `stake_amount`, `net_result_amount`, `role` and one of `hand_id`, `round_id`, `match_id`; `gameplay.bet_placed` and `gameplay.bet_settled` need `game_id`; `payment.deposit_initiated`, `payment.deposit_completed`, `payment.withdrawal_requested` and `payment.withdrawal_completed` need `payment_reference_id`, `amount`, `currency`, `payment_method`, `status`; `payment.withdrawal_address_changed` needs `withdrawal_address_hash`; `dispute.chargeback_opened`, `dispute.chargeback_won` and `dispute.chargeback_lost` need `payment_reference_id` and one of `dispute_id`, `chargeback_id`; `session.bot_signal_observed` needs `telemetry_schema_version` and one of `bot_score`, `actions_per_minute`, `timing_variance_ms`, `pointer_entropy`, `keystroke_entropy`, `reaction_time_ms_p50`; `responsible_gambling.self_exclusion_match` needs `registry_source`, `exclusion_status`, `identity_match_ref`. Every `security` action ending in `_changed` needs `changed_field`, plus `previous_reference_hash` or `new_reference_hash` when the field is `password`, `mfa_method`, `mfa`, `email`, `phone`, `withdrawal_address`, `payout_method` or `api_key`. Send hashes and references, never a card number, a full withdrawal address, a password hash or an MFA secret." + "overall_count": { + "type": "integer", + "description": "Sessions measured." }, - "custom_values": { + "per_feature": { "type": "object", - "description": "Up to 100 keys of your own, addressable from rules as `custom_values.`. Values must be scalars (string, number, boolean or null); nested objects and arrays are rejected.", + "description": "Keyed by feature, for example `OCR` or `LIVENESS`.", "additionalProperties": { - "nullable": true, - "oneOf": [ - { - "type": "string" + "type": "object", + "properties": { + "avg_seconds": { + "type": "number" }, - { + "total_seconds": { "type": "number" }, - { - "type": "boolean" + "count": { + "type": "integer" } - ] + } } - } - }, - "example": { - "event_id": "login-2026-10-17-000001", - "event_category": "auth", - "action_type": "login", - "occurred_at": "2026-10-17T09:15:02.481Z", - "time_zone": "Europe/Madrid", - "subject": { - "vendor_data": "user-42", - "entity_type": "individual" - }, - "session": { - "session_id": "web-session-abc", - "sequence": 142 }, - "device_context": { - "fingerprint": "fp-a8c1", - "network_context": { - "ip_address": "203.0.113.42" + "per_sub_step": { + "type": "object", + "description": "Keyed by feature, then by sub-step, for example `front_id_photo`.", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "avg_seconds": { + "type": "number" + }, + "total_seconds": { + "type": "number" + }, + "count": { + "type": "integer" + } + } + } } - }, - "payload": { - "method": "password" - }, - "custom_values": { - "vip_tier": "gold" } } }, - "MonitoringEventIngestResult": { + "ApplicationAnalyticsResubmissionStats": { "type": "object", - "description": "Outcome of ingesting one event. The same shape is returned for a new event and for an idempotent replay.", - "required": [ - "event_uuid", - "event_id", - "status", - "score", - "rules_evaluated_count", - "rules_matched_count", - "correlation_pending", - "retention_class", - "expires_at", - "idempotent_replay", - "rule_matches" - ], "properties": { - "event_uuid": { - "type": "string", - "format": "uuid", - "description": "Didit identifier of the stored event." - }, - "event_id": { - "type": "string", - "description": "Your `event_id`." - }, - "status": { - "type": "string", - "enum": [ - "ACCEPTED", - "APPROVED", - "IN_REVIEW", - "DECLINED" - ], - "description": "Status after the real-time rules ran. See `MonitoringEvent.status`." - }, - "score": { + "first_pass": { "type": "integer", - "description": "Score after the real-time rules ran." + "description": "Finished sessions with no resubmission." }, - "rules_evaluated_count": { + "second": { "type": "integer", - "minimum": 0, - "description": "Number of event rules evaluated in the request. 0 in a sandbox application, where no rules run." + "description": "Finished sessions resubmitted once." }, - "rules_matched_count": { + "third_plus": { "type": "integer", - "minimum": 0, - "description": "Number of rules that matched, including `TEST` mode rules." - }, - "correlation_pending": { - "type": "boolean", - "description": "`true` when an asynchronous rule still has to run. Read `GET /v3/events/{event_uuid}/` a few minutes later for the final `status` and `score`." - }, - "retention_class": { - "type": "string", - "enum": [ - "low_value", - "standard", - "risk_relevant", - "monetary_or_compliance" - ], - "description": "Retention class, after any promotion by a matched rule." + "description": "Finished sessions resubmitted two or more times." }, - "expires_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the event expires under its retention class and is deleted. Null for `monetary_or_compliance` (payment and dispute) events." + "abandoned": { + "type": "integer", + "description": "Abandoned sessions." + } + } + }, + "ApplicationAnalyticsOriginalStatusStats": { + "type": "object", + "description": "The workflow's automatic verdict, before any reviewer changed it.", + "properties": { + "approved": { + "type": "integer" }, - "idempotent_replay": { - "type": "boolean", - "description": "`true` when an event with this `event_id` and the same content was already stored; the values are those of the stored event and no rule ran again." + "in_review": { + "type": "integer" }, - "rule_matches": { - "type": "array", - "description": "Rules that matched the event.", - "items": { + "declined": { + "type": "integer" + } + } + }, + "ApplicationAnalyticsWarningStats": { + "type": "object", + "description": "Keyed by feature (`ID_VERIFICATION`, `LIVENESS`, `FACEMATCH`, `AML`, `PROOF_OF_ADDRESS`, `PHONE`, `EMAIL`, `NFC`, `LOCATION`, `DATABASE_VALIDATION`, `QUESTIONNAIRE` and the KYB features). `total` is the sessions that reached the feature; `risks` counts sessions per warning code and level.", + "additionalProperties": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "risks": { "type": "object", - "required": [ - "rule_id", - "library_key", - "mode", - "is_async" - ], - "properties": { - "rule_id": { - "type": "string", - "format": "uuid", - "description": "UUID of the rule." - }, - "library_key": { - "type": "string", - "nullable": true, - "description": "Library key when the rule was installed from the rules library, otherwise null." - }, - "mode": { - "type": "string", - "enum": [ - "ACTIVE", - "TEST" - ], - "description": "`TEST` rules record the match but apply no actions." - }, - "is_async": { - "type": "boolean", - "description": "`true` when the match came from the asynchronous pass." + "additionalProperties": { + "type": "object", + "properties": { + "error": { + "type": "integer" + }, + "warning": { + "type": "integer" + }, + "information": { + "type": "integer" + } } } } } } }, - "ComplianceList": { + "ApplicationAnalyticsDemographicPoint": { "type": "object", - "description": "A blocklist, allowlist or custom list of one application.", - "required": [ - "uuid", - "name", - "description", - "list_type", - "entry_type", - "is_system", - "entry_count", - "last_entry_added_at", - "created_at", - "updated_at" - ], "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "List identifier. Use it as `list_uuid` in the list and entry endpoints, and as the value of a workflow or transaction rule condition with `value_type: \"list\"`." - }, - "name": { - "type": "string", - "maxLength": 255, - "description": "List name, unique among the application's lists." - }, - "description": { + "date": { "type": "string", - "description": "Free-text description. Empty string when not set." + "description": "Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`." }, - "list_type": { - "type": "string", - "enum": [ - "blocklist", - "allowlist", - "custom" - ], - "description": "`blocklist`: a match declines the session or transaction (system lists only, one per entry type). `allowlist`: matching items are trusted, for example a face allowlist skips the duplicated-face action; never overrides a blocklist match. `custom`: used in workflow and transaction rule conditions (`value_type: \"list\"`)." + "age_distribution": { + "type": "object", + "additionalProperties": { + "type": "integer" + } }, - "entry_type": { + "gender_distribution": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + } + } + }, + "ApplicationAnalyticsDevicePoint": { + "type": "object", + "properties": { + "date": { "type": "string", - "enum": [ - "face", - "document", - "phone", - "email", - "ip_address", - "device_fingerprint", - "wallet_address", - "bank_account", - "country", - "user", - "business", - "key" - ], - "description": "What the list holds: `face`, `document` (document number), `phone`, `email`, `ip_address` (IP address or CIDR range), `device_fingerprint`, `wallet_address`, `bank_account`, `country` (ISO 3166-1 alpha-3), `user` or `business` (your `vendor_data`), or `key` (any string). It decides how entry values are validated and matched." - }, - "is_system": { - "type": "boolean", - "description": "`true` for the lists Didit provisions: one blocklist per entry type (`face`, `document`, `phone`, `email`, `ip_address`, `device_fingerprint`, `wallet_address`, `bank_account`, `user`, `business`), the **High Risk Countries** custom list, and system allowlists. System lists cannot be renamed, edited or deleted; you can add and delete their entries." - }, - "entry_count": { - "type": "integer", - "minimum": 0, - "description": "Number of entries currently in the list." + "description": "Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`." }, - "last_entry_added_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the newest current entry was added. `null` for an empty list." + "devices": { + "type": "object", + "additionalProperties": { + "type": "integer" + } }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "When the list was created." + "browsers": { + "type": "object", + "additionalProperties": { + "type": "integer" + } }, - "updated_at": { - "type": "string", - "format": "date-time", - "description": "When the list was last updated." + "operating_systems": { + "type": "object", + "additionalProperties": { + "type": "integer" + } } } }, - "ComplianceListEntry": { + "ApplicationAnalyticsVerificationTimePoint": { "type": "object", - "description": "One value in a list.", - "required": [ - "uuid", - "value", - "display_label", - "comment", - "added_by", - "reference_session_id", - "reference_session_number", - "reference_session_type", - "reference_object_uuid", - "metadata", - "created_at" - ], "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Entry identifier. Use it as `entry_uuid` in Delete a list entry." - }, - "value": { - "type": "string", - "description": "The listed value: a phone number in E.164, an email address, a document number, an IP address or CIDR range, a device fingerprint, a wallet address, a bank account, an ISO 3166-1 alpha-3 country code, a `vendor_data`, any string on a `key` list, or the UUID of the stored face on a face list. Values you send are stored in lower case, except phone numbers and country codes. Matching is case-insensitive." - }, - "display_label": { + "date": { "type": "string", - "nullable": true, - "maxLength": 512, - "description": "Human-readable label. Didit fills it for entries taken from a session (for example the holder's name and document number) when you send none." + "description": "Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`." }, - "comment": { - "type": "string", - "description": "Free-text note. Empty string when not set." + "overall_avg": { + "type": "number", + "description": "Average active seconds per session." }, - "added_by": { + "per_feature": { + "type": "object", + "description": "Average active seconds per feature.", + "additionalProperties": { + "type": "number" + } + } + } + }, + "ApplicationAnalyticsResubmissionPoint": { + "type": "object", + "properties": { + "date": { "type": "string", - "nullable": true, - "description": "Who added the entry: `api` (an API key), a console user's email address or `console`, `system` or `migration` (entries Didit created), or `rule` (a transaction or monitoring rule action)." + "description": "Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`." }, - "reference_session_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Verification session the entry was taken from. `null` when the entry was added by value." + "first_pass": { + "type": "integer" }, - "reference_session_number": { - "type": "integer", - "nullable": true, - "description": "Application-scoped number of that session, as shown in the console." + "second": { + "type": "integer" }, - "reference_session_type": { - "type": "string", - "nullable": true, - "enum": [ - "kyc", - "kyb", - null - ], - "description": "`kyc` for a person verification session, `kyb` for a business verification session, `null` when there is no session." + "third_plus": { + "type": "integer" }, - "reference_object_uuid": { + "abandoned": { + "type": "integer" + } + } + }, + "ApplicationAnalyticsOriginalStatusPoint": { + "type": "object", + "properties": { + "date": { "type": "string", - "format": "uuid", - "nullable": true, - "description": "The record the entry stands for: the face, document, phone or email record read from a session, the face created by Upload a face to a list, or the UUID you sent (for example a transaction UUID or a user's `didit_internal_id`)." + "description": "Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`." }, - "metadata": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Extra context. Holds the keys Didit sets (listed here) merged with any `metadata` you sent when creating the entry, returned as stored. `null` when there is none.", - "properties": { - "reference_image_url": { - "type": "string", - "description": "Face entries: short-lived signed URL, issued again on every call; `null` for a scoped API key without `media: read`. Absent when no image belongs to the entry." - }, - "front_image_url": { - "type": "string", - "description": "Document entries that carry a document front image: short-lived signed URL, issued again on every call; `null` for a scoped API key without `media: read`." - }, - "full_name": { - "type": "string", - "description": "Document entries taken from a session: holder's full name." - }, - "issuing_state": { - "type": "string", - "description": "Document entries taken from a session: ISO 3166-1 alpha-3 issuing country." - }, - "document_type": { - "type": "string", - "description": "Document entries taken from a session: document type code, for example `P`, `ID` or `DL`." - }, - "region": { - "type": "string", - "description": "Document entries taken from a session: issuing state or province of a regionally issued document, empty when not applicable. Not set by Add session items to the blocklist." - }, - "document_number": { - "type": "string", - "description": "Document entries taken from a session: the document number." - }, - "face_image_source": { - "type": "string", - "enum": [ - "front_image_camera_front", - "back_image_camera_front" - ], - "description": "Face entries taken from a document-capture selfie: which selfie. Absent for liveness faces." - }, - "reference_node_id": { - "type": "string", - "nullable": true, - "description": "Face entries taken from a document-capture selfie: the ID verification node the selfie came from." - }, - "fingerprint_tier": { - "type": "string", - "nullable": true, - "enum": [ - "persistent_id", - "composite_hash", - "legacy_fp", - "unknown", - null - ], - "description": "Device fingerprint entries taken from a session: how the fingerprint was derived. `persistent_id` identifies one device; `composite_hash` and `legacy_fp` can be shared by devices of the same model." - }, - "sandbox": { - "type": "boolean", - "description": "`true` on a face entry uploaded in a sandbox application; such an entry matches nothing." - }, - "reference_type": { - "type": "string", - "enum": [ - "transaction", - "vendor_user", - "vendor_business" - ], - "description": "Set by you: what `reference_object_uuid` points to, so the console links back to it." - }, - "reference_id": { - "type": "string", - "description": "Set by you: identifier of the source record." - } - } + "approved": { + "type": "integer" }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "When the entry was added." + "in_review": { + "type": "integer" + }, + "declined": { + "type": "integer" } } }, - "BlocklistedItem": { + "ApplicationAnalyticsSummary": { "type": "object", - "description": "One blocklisted face, identity document, phone number or email address of a verification session. Fields that do not apply to the item's `type` are `null`.", + "description": "User verification (KYC) summary, returned when neither `scope` nor `widget_keys` is sent.", "required": [ - "type", - "session_id", - "session_number", - "blocklisted_at", - "face_image", - "document_type", - "document_number", - "front_image", - "phone_number", - "country_code", - "email" + "data_points", + "request_breakdown", + "previous_data_points", + "previous_request_breakdown", + "conversion_rate", + "location_stats", + "demographic_stats", + "device_details", + "workflow_funnel_stats", + "verification_time_stats", + "resubmission_stats", + "warning_stats", + "original_status_stats", + "grouping_mode", + "demographic_time_series", + "device_time_series", + "verification_time_time_series", + "resubmission_time_series", + "original_status_time_series" ], "properties": { - "type": { - "type": "string", - "enum": [ - "face", - "document", - "phone", - "email" - ], - "description": "What was blocklisted." + "data_points": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsDataPoint" + }, + "description": "One point per day, from the first day with sessions in the range to `end_date`. Always daily, whatever `grouping_mode` says." }, - "session_id": { - "type": "string", - "format": "uuid", - "description": "Verification session the item belongs to." + "request_breakdown": { + "$ref": "#/components/schemas/ApplicationAnalyticsRequestBreakdown" }, - "session_number": { - "type": "integer", - "nullable": true, - "description": "Application-scoped number of that session, as shown in the console." + "previous_data_points": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsDataPoint" + }, + "description": "Daily points for the period of the same length right before `start_date`." }, - "blocklisted_at": { - "type": "string", - "format": "date-time", - "description": "When the item was last blocklisted." + "previous_request_breakdown": { + "$ref": "#/components/schemas/ApplicationAnalyticsRequestBreakdown" }, - "face_image": { - "type": "string", + "conversion_rate": { + "type": "number", "nullable": true, - "description": "Face items: short-lived signed URL, issued again on every call; `null` for a scoped API key without `media: read`. Also `null` when no reference image is stored." + "description": "`total_approved / (total_approved + total_declined + total_in_review) x 100`, one decimal. `null` when nothing finished." }, - "document_type": { - "type": "string", - "nullable": true, - "description": "Document items: document type code, for example `P` (passport), `ID` (national ID), `DL` (driver's license) or `RP` (residence permit)." + "location_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsLocationStats" }, - "document_number": { - "type": "string", - "nullable": true, - "description": "Document items: the document number, or the personal number when the document has none." + "demographic_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsDemographicStats" }, - "front_image": { - "type": "string", - "nullable": true, - "description": "Document items: front image of the document, short-lived signed URL, issued again on every call; `null` for a scoped API key without `media: read`." + "device_details": { + "$ref": "#/components/schemas/ApplicationAnalyticsDeviceDetails" }, - "phone_number": { - "type": "string", - "nullable": true, - "description": "Phone items: the phone number in E.164." + "workflow_funnel_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsWorkflowFunnel" }, - "country_code": { - "type": "string", - "nullable": true, - "description": "Phone items: ISO 3166-1 alpha-2 country of the number." + "verification_time_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsVerificationTimeStats" }, - "email": { + "resubmission_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsResubmissionStats" + }, + "warning_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsWarningStats" + }, + "original_status_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsOriginalStatusStats" + }, + "grouping_mode": { "type": "string", - "nullable": true, - "description": "Email items: the email address." + "enum": [ + "day", + "week", + "month" + ], + "description": "How the `*_time_series` lists are grouped: `day` for up to 14 days, `week` up to 90, `month` beyond. Measured from the first day with sessions in the range." + }, + "demographic_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsDemographicPoint" + } + }, + "device_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsDevicePoint" + } + }, + "verification_time_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsVerificationTimePoint" + } + }, + "resubmission_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsResubmissionPoint" + } + }, + "original_status_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsOriginalStatusPoint" + } } } }, - "TransactionNote": { + "ApplicationAnalyticsWidget": { "type": "object", - "description": "One note on a monitored transaction's review timeline.", + "description": "One dashboard widget. Every widget has `kind` and `title`; the other fields depend on `kind`. The session widgets reuse the summary objects (`data_points`, `request_breakdown`, `workflow_funnel_stats` and so on); list widgets put their entries in `items`; transaction metrics use `value`.", "required": [ - "uuid", - "note", - "actor_email", - "actor_name", - "metadata", - "created_at" + "kind", + "title" ], "properties": { - "uuid": { + "kind": { "type": "string", - "format": "uuid", - "description": "Note identifier." + "enum": [ + "sessions_volume", + "workflow_funnel", + "average_verification_time", + "country_distribution", + "warning_insights", + "conversion_rate", + "conversion_by_country", + "resubmission_insights", + "original_status_distribution", + "database_validation_coverage", + "database_validation_latency", + "demographics", + "device_details", + "horizontal_bar", + "kyb_registry_coverage", + "kyb_registry_data_completeness", + "metric", + "time_series", + "breakdown", + "status_time_series", + "leaderboard", + "histogram" + ], + "description": "Widget type." }, - "note": { + "title": { "type": "string", - "description": "Plain-text note body." + "description": "Display title, for example `Verification volume`." }, - "actor_email": { + "grouping_mode": { "type": "string", - "format": "email", - "nullable": true, - "description": "Email of the Console user who wrote the note or changed the status. `null` for notes created with an API key and for notes Didit adds automatically." + "enum": [ + "day", + "week", + "month" + ], + "description": "How the `*_time_series` lists are grouped: `day` for up to 14 days, `week` up to 90, `month` beyond. Measured from the first day with sessions in the range." }, - "actor_name": { + "data_points": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsDataPoint" + } + }, + "request_breakdown": { + "$ref": "#/components/schemas/ApplicationAnalyticsRequestBreakdown" + }, + "previous_data_points": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsDataPoint" + } + }, + "previous_request_breakdown": { + "$ref": "#/components/schemas/ApplicationAnalyticsRequestBreakdown" + }, + "conversion_rate": { + "type": "number", + "nullable": true + }, + "workflow_funnel_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsWorkflowFunnel" + }, + "verification_time_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsVerificationTimeStats" + }, + "verification_time_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsVerificationTimePoint" + } + }, + "warning_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsWarningStats" + }, + "resubmission_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsResubmissionStats" + }, + "resubmission_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsResubmissionPoint" + } + }, + "original_status_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsOriginalStatusStats" + }, + "original_status_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsOriginalStatusPoint" + } + }, + "demographic_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsDemographicStats" + }, + "demographic_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsDemographicPoint" + } + }, + "device_details": { + "$ref": "#/components/schemas/ApplicationAnalyticsDeviceDetails" + }, + "device_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsDevicePoint" + } + }, + "items": { + "type": "array", + "description": "Entries of a list widget. `country_distribution` (KYC): `country`, `country_code`, `session_count`. `conversion_by_country`: `country`, `country_code`, `finished`, `approved`. `time_series`: `date`, `value`. `status_time_series`: `date`, `approved`, `declined`, `flagged`. `breakdown`: `transaction_type` or `action_type` with `count` and `volume`, or `tag_id`, `tag_name`, `count`. `leaderboard`: `rule_title`, `trigger_count` or `assignee`, `handled_count`, `average_processing_seconds`, each with `approved_percentage`, `declined_percentage`, `flagged_percentage`. `histogram`: `score`, `count`.", + "items": { + "type": "object", + "additionalProperties": {} + } + }, + "previous_items": { + "type": "array", + "description": "Transaction time series for the period of the same length right before `start_date`.", + "items": { + "type": "object", + "additionalProperties": {} + } + }, + "value": { + "type": "number", + "description": "`metric` widgets: the figure for the range." + }, + "currency": { "type": "string", "nullable": true, - "description": "Display name of the Console user (their email when they have no name). `Rule Engine` on notes Didit adds automatically. `null` for notes created with an API key." + "description": "Transaction volume and amount widgets: the application's default transaction currency." }, - "metadata": { + "thresholds": { "type": "object", - "description": "Tells the note types apart. Notes written through the API or the Console carry `mentioned_emails`. A status change made in the Console carries `type: status_change` with `previous_status`, `new_status`, `reason_code` and `reason_label`. Notes Didit adds carry `source`: `rule_action` (with `rule_uuid`) for a rule's `add_note` action, or `remediation_session` for a problem creating the user-action verification session.", + "description": "`histogram` widget: the application's review and decline score thresholds.", "properties": { - "mentioned_emails": { - "type": "array", - "items": { - "type": "string", - "format": "email" - }, - "description": "Mentioned organization members, lower-cased and de-duplicated. Empty when nobody was mentioned." - }, - "type": { - "type": "string", - "enum": [ - "status_change" - ], - "description": "Present on notes recording a status change made in the Console." - }, - "previous_status": { - "type": "string", - "enum": [ - "APPROVED", - "IN_REVIEW", - "DECLINED", - "AWAITING_USER" - ], - "description": "Status before the change." - }, - "new_status": { - "type": "string", - "enum": [ - "APPROVED", - "IN_REVIEW", - "DECLINED", - "AWAITING_USER" - ], - "description": "Status after the change." - }, - "reason_code": { - "type": "string", - "nullable": true, - "description": "Reason code given with the change." - }, - "reason_label": { - "type": "string", - "nullable": true, - "description": "Reason label given with the change." - }, - "source": { - "type": "string", - "enum": [ - "rule_action", - "remediation_session" - ], - "description": "Present on notes Didit adds automatically." + "review": { + "type": "integer" }, - "rule_uuid": { - "type": "string", - "format": "uuid", - "description": "Rule whose `add_note` action wrote the note (with `source: rule_action`)." + "decline": { + "type": "integer" } } }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "When the note was stored." + "search": { + "type": "object", + "description": "`kyb_registry_coverage` widget: registry search totals.", + "properties": { + "searches_total": { + "type": "integer" + }, + "searches_with_no_candidates": { + "type": "integer" + } + } } } }, - "TransactionSummary": { + "ApplicationAnalyticsWidgetResponse": { "type": "object", - "description": "Summary of a monitored transaction, the shape `GET /v3/transactions/` lists. Call `GET /v3/transactions/{transaction_id}/` with `uuid` for parties, alerts and rule runs.", + "description": "Dashboard widgets, returned when `scope` or `widget_keys` is sent.", + "required": [ + "scope", + "available_widget_keys", + "requested_widget_keys", + "widgets" + ], "properties": { - "uuid": { + "scope": { "type": "string", - "format": "uuid", - "description": "Didit transaction UUID. Use it as `{transaction_id}` on the transaction endpoints." + "enum": [ + "home", + "kyc", + "kyb", + "transactions" + ], + "description": "Dashboard tab." }, - "transaction_number": { - "type": "integer", - "nullable": true, - "description": "Application-scoped sequential number shown in the Console." + "available_widget_keys": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Every widget key this scope accepts." }, - "txn_id": { - "type": "string", - "nullable": true, - "description": "Your own identifier from the create call. Unique per application." + "requested_widget_keys": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The widget keys answered, in order: your valid `widget_keys`, or the scope's defaults." }, - "transaction_type": { + "widgets": { + "type": "object", + "description": "Keyed by widget key, in the order of `requested_widget_keys`.", + "additionalProperties": { + "$ref": "#/components/schemas/ApplicationAnalyticsWidget" + } + } + } + }, + "WebhookConfigDestination": { + "type": "object", + "description": "A webhook destination as listed in the webhook configuration, without delivery metrics. `GET /v3/webhook/destinations/{destination_uuid}/` adds its delivery summary.", + "required": [ + "uuid", + "label", + "url", + "enabled", + "webhook_version", + "secret_shared_key", + "subscribed_events", + "created_at", + "updated_at" + ], + "properties": { + "uuid": { "type": "string", - "description": "Category as stored: `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet`, `gamblingLimitChange`, `gamblingBonusChange` or `auditTrailEvent`." + "format": "uuid", + "description": "Destination ID." }, - "action_type": { + "label": { "type": "string", - "description": "Sub-type within the category, for example `withdrawal`." + "maxLength": 255, + "description": "Human-readable name." }, - "status": { + "url": { "type": "string", - "enum": [ - "APPROVED", - "IN_REVIEW", - "DECLINED", - "AWAITING_USER" - ], - "description": "Current monitoring verdict." + "format": "uri", + "maxLength": 2000, + "description": "HTTPS endpoint Didit posts events to." }, - "source": { - "type": "string", - "enum": [ - "LIVE", - "IMPORTED" - ], - "description": "`LIVE` for transactions submitted for monitoring, `IMPORTED` for historical imports." + "enabled": { + "type": "boolean", + "description": "`false` pauses deliveries to this destination." }, - "direction": { + "webhook_version": { "type": "string", "enum": [ - "INBOUND", - "OUTBOUND" + "v1", + "v2", + "v3" ], - "description": "Direction relative to the subject." - }, - "amount": { - "type": "string", - "description": "Amount as a decimal string with trailing zeros removed (up to 18 decimal places)." - }, - "currency": { - "type": "string", - "description": "Currency code or crypto asset symbol of `amount`." + "description": "Payload version sent to this destination." }, - "currency_type": { + "secret_shared_key": { "type": "string", - "nullable": true, - "description": "`fiat` or `crypto` when known." + "description": "This destination's signing secret (43-character URL-safe token). Verify each webhook with the secret of the destination it was sent to. API-key callers always receive it in full; Console users without `read:webhooks` receive an empty string." }, - "preferred_currency_amount": { + "subscribed_events": { + "type": "array", + "uniqueItems": true, + "description": "Event types delivered to this destination. Empty when the destination was created without events, in which case it receives nothing.", + "items": { + "$ref": "#/components/schemas/WebhookEvent" + } + }, + "created_at": { "type": "string", - "nullable": true, - "description": "`amount` converted to the application's preferred currency, when a rate was available." + "format": "date-time" }, - "preferred_currency_code": { + "updated_at": { + "type": "string", + "format": "date-time" + } + } + }, + "WebhookConfig": { + "type": "object", + "description": "Application-level webhook configuration, retention policy and Digital ID wallet return hosts, with every webhook destination.", + "required": [ + "webhook_url", + "webhook_version", + "secret_shared_key", + "capture_method", + "data_retention_months", + "face_retention_policy", + "face_retention_days", + "available_events", + "allowed_return_hosts", + "endpoints" + ], + "properties": { + "webhook_url": { "type": "string", + "format": "uri", "nullable": true, - "description": "Currency of `preferred_currency_amount`." + "description": "Application-level (legacy) webhook URL. Didit sends to it only while the application has no enabled destination, and then only `status.updated` and `data.updated`. Every create, update or delete of a destination sets it to the URL of the oldest enabled destination, or `null` when none is enabled." }, - "payment_details": { + "webhook_version": { "type": "string", - "nullable": true, - "description": "Free-text payment reference." + "enum": [ + "v1", + "v2", + "v3" + ], + "description": "Payload version of the legacy `webhook_url`. Each destination has its own `webhook_version`." }, - "txn_date": { + "secret_shared_key": { "type": "string", - "format": "date-time", - "description": "When the transaction occurred." + "description": "Application-level signing secret (43-character URL-safe token). It signs only deliveries to the legacy `webhook_url`; each destination signs with its own `secret_shared_key`. API-key callers always receive it in full; Console users without `read:webhooks` receive an empty string." }, - "created_at": { + "capture_method": { "type": "string", - "format": "date-time", - "description": "When Didit recorded it." + "enum": [ + "MOBILE", + "MOBILE_AND_DESKTOP" + ], + "description": "Legacy application setting. The verification flow does not read it: capture options are configured per workflow." }, - "score": { + "data_retention_months": { "type": "integer", - "description": "Risk score. Higher is riskier." + "nullable": true, + "minimum": 1, + "maximum": 120, + "description": "Automatic deletion window. User verification sessions created more than this many months ago (a month counts as 30 days) are deleted automatically, and that deletion is irreversible. `null` means sessions are never deleted automatically. See [Data retention](https://docs.didit.me/console/data-retention)." }, - "severity": { + "face_retention_policy": { "type": "string", - "nullable": true, "enum": [ - "UNKNOWN", - "LOW", - "MEDIUM", - "HIGH", - "CRITICAL", - null + "delete_with_session", + "retain_until_user_deleted" ], - "description": "Risk severity, `null` until something sets it." + "description": "What happens to a session's face embedding when the session is deleted. `delete_with_session` (default) deletes it with the session. `retain_until_user_deleted` is the opt-in [biometric template retention](https://docs.didit.me/console/data-retention#biometric-template-retention): an image-free biometric template linked to the User is managed separately until its retention period ends, the User is deleted, or you purge it." }, - "decision_reason_code": { - "type": "string", + "face_retention_days": { + "type": "integer", "nullable": true, - "description": "Machine-readable reason for the current `status`." + "minimum": 1, + "maximum": 3650, + "description": "Retention period, in days, of biometric templates under `retain_until_user_deleted`. Required when that policy is set." }, - "decision_reason_label": { + "biometric_consent_mode": { "type": "string", - "nullable": true, - "description": "Human-readable label for `decision_reason_code`." + "enum": [ + "auto", + "always", + "never" + ], + "description": "When the hosted flow and the native SDKs ask the person for explicit biometric consent before a face capture. `auto` (default) asks only where a trigger applies: a declared state, the person's IP, or the identity document's issuer is in Illinois, Texas or Colorado, or the person looks to be in the US without a known state. `always` asks every person whose workflow captures a face. `never` never shows the step, and you collect consent yourself before the selfie. Each change is recorded in the audit log. See [Biometric consent](https://docs.didit.me/getting-started/biometric-consent)." }, - "tags": { + "available_events": { "type": "array", - "description": "Tags on the transaction.", + "description": "Every event type a destination can subscribe to, with a display label.", "items": { "type": "object", + "required": [ + "value", + "label" + ], "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string" - }, - "color": { - "type": "string", - "description": "Hex color, for example `#F59E0B`." - }, - "description": { - "type": "string", - "nullable": true + "value": { + "$ref": "#/components/schemas/WebhookEvent" }, - "source": { + "label": { "type": "string", - "enum": [ - "didit", - "custom" - ], - "description": "Didit preset or custom tag." + "description": "Display label, for example `Session status updated`." } } } }, - "subject_name": { + "allowed_return_hosts": { + "type": "array", + "maxItems": 20, + "description": "Hostnames (exact match, HTTPS only) a standalone Digital ID wallet verification may send the user back to with its `return_url`. Your white-label domain is always allowed as well. See [Register your return destination](https://docs.didit.me/standalone-apis/digital-id-wallets#register-your-return-destination).", + "items": { + "type": "string", + "maxLength": 253 + } + }, + "endpoints": { + "type": "array", + "description": "Every webhook destination of the application, oldest first, each with its signing secret. Read only here: manage destinations with the `/v3/webhook/destinations/` endpoints.", + "items": { + "$ref": "#/components/schemas/WebhookConfigDestination" + } + } + } + }, + "WebhookDestinationDelivery": { + "type": "object", + "description": "One delivery attempt to a webhook destination. The first send of an event, each automatic retry and each resend are separate deliveries.", + "required": [ + "uuid", + "session_id", + "request_url", + "response_status", + "response_time_ms", + "status", + "webhook_type", + "created_at" + ], + "properties": { + "uuid": { "type": "string", - "nullable": true, - "description": "Full name of the `APPLICANT` party." + "format": "uuid", + "description": "Delivery ID, for the detail and resend calls." }, - "subject_vendor_data": { + "session_id": { "type": "string", + "format": "uuid", "nullable": true, - "description": "Your `vendor_data` for the applicant." + "description": "User verification session the event is about. `null` for business session, user, business, transaction, Travel Rule and workflow events." }, - "subject_type": { + "request_url": { "type": "string", + "format": "uri", "nullable": true, - "enum": [ - "user", - "business", - null - ], - "description": "Whether the applicant is linked to a Didit user or business; `null` when it is linked to neither." + "description": "URL the delivery was sent to: the destination's URL when the event was generated." }, - "subject_uuid": { - "type": "string", - "format": "uuid", + "response_status": { + "type": "integer", "nullable": true, - "description": "UUID of the Didit user or business the applicant is linked to." + "description": "HTTP status your endpoint returned. `null` while the delivery is queued and not yet attempted. When no HTTP answer was received Didit records a status of its own: `504` when your endpoint did not answer within 5 seconds, `503` for a connection failure, `400` when the URL was refused as unsafe (for example a private or internal address), and `404` for any other request error. `400` or above counts as a failed delivery." }, - "subject_country": { - "type": "string", + "response_time_ms": { + "type": "integer", "nullable": true, - "description": "Country code of the applicant." + "minimum": 0, + "description": "Time the attempt took, in milliseconds. `null` while the delivery is queued." }, - "counterparty_name": { + "status": { "type": "string", "nullable": true, - "description": "Full name of the `COUNTERPARTY` party, if any." + "description": "Status the event reported when it was generated: the session status for session events (for example `Approved` or `In Review`), the person's or company's status for user and business events, and the transaction status for transaction events. `null` for events that carry no status." }, - "counterparty_vendor_data": { + "webhook_type": { + "$ref": "#/components/schemas/WebhookEvent" + }, + "created_at": { "type": "string", - "nullable": true, - "description": "Your `vendor_data` for the counterparty." + "format": "date-time", + "description": "When the delivery was created." + } + } + }, + "WebhookDestinationDeliveryDetail": { + "type": "object", + "description": "One delivery attempt to a webhook destination, with the exact request headers and body Didit sent.", + "required": [ + "uuid", + "session_id", + "request_url", + "response_status", + "response_time_ms", + "status", + "webhook_type", + "created_at", + "request_header", + "request_body" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Delivery ID, for the detail and resend calls." }, - "counterparty_country": { + "session_id": { "type": "string", + "format": "uuid", "nullable": true, - "description": "Country code of the counterparty." + "description": "User verification session the event is about. `null` for business session, user, business, transaction, Travel Rule and workflow events." }, - "assigned_to_name": { + "request_url": { "type": "string", + "format": "uri", "nullable": true, - "description": "Name of the Console reviewer assigned to the transaction." + "description": "URL the delivery was sent to: the destination's URL when the event was generated." }, - "travel_rule_status": { - "type": "string", + "response_status": { + "type": "integer", "nullable": true, - "description": "Status of the transaction's Travel Rule transfer, or `null` when it has none." + "description": "HTTP status your endpoint returned. `null` while the delivery is queued and not yet attempted. When no HTTP answer was received Didit records a status of its own: `504` when your endpoint did not answer within 5 seconds, `503` for a connection failure, `400` when the URL was refused as unsafe (for example a private or internal address), and `404` for any other request error. `400` or above counts as a failed delivery." }, - "application_id": { - "type": "string", - "format": "uuid", - "description": "Application that owns the transaction." + "response_time_ms": { + "type": "integer", + "nullable": true, + "minimum": 0, + "description": "Time the attempt took, in milliseconds. `null` while the delivery is queued." }, - "application_name": { + "status": { "type": "string", "nullable": true, - "description": "Name of that application." + "description": "Status the event reported when it was generated: the session status for session events (for example `Approved` or `In Review`), the person's or company's status for user and business events, and the transaction status for transaction events. `null` for events that carry no status." }, - "organization_id": { + "webhook_type": { + "$ref": "#/components/schemas/WebhookEvent" + }, + "created_at": { "type": "string", - "format": "uuid", - "description": "Organization that owns the application." + "format": "date-time", + "description": "When the delivery was created." }, - "organization_name": { + "request_header": { "type": "string", - "nullable": true, - "description": "Name of that organization." + "description": "JSON text (a string) of the request headers Didit sent: `Content-Type`, `X-Signature`, `X-Signature-V2`, `X-Signature-Simple`, `X-Timestamp` and `User-Agent`. Parse it with a JSON parser. It holds signatures, never the signing secret." + }, + "request_body": { + "type": "string", + "description": "JSON text (a string) of the exact payload Didit sent, keys sorted, including `event_id` and `timestamp`. Parse it with a JSON parser; see [Payload shape](https://docs.didit.me/integration/webhooks#payload-shape). For a scoped key without `media: read`, media URLs inside it are `null`; without `sessions: write`, session tokens and hosted verification links are `null`. A body changed that way no longer matches its signatures. Didit does not record the response body your endpoint returned." } } }, - "ApplicationAnalyticsDataPoint": { + "SessionWebhookDelivery": { "type": "object", - "description": "Session counts for one day, by creation date (UTC). Each count reflects the session's current status. Points of the previous period carry at least the required fields.", + "description": "One webhook delivery attempt recorded for a verification session, with the exact request headers and body Didit sent.", "required": [ - "rounded_date", - "total", - "approved", - "declined", - "canceled", - "pending" + "uuid", + "session_id", + "request_url", + "response_status", + "status", + "webhook_type", + "request_header", + "request_body", + "created_at" ], "properties": { - "rounded_date": { + "uuid": { "type": "string", - "format": "date", - "description": "The day, `YYYY-MM-DD`." - }, - "total": { - "type": "integer", - "description": "Sessions created that day." - }, - "approved": { - "type": "integer", - "description": "Of those, sessions `Approved` or `Kyc Expired`." - }, - "declined": { - "type": "integer", - "description": "Sessions `Declined`." - }, - "canceled": { - "type": "integer", - "description": "Sessions `Expired` or `Abandoned`." + "format": "uuid", + "description": "Delivery ID. Use it as `{webhook_uuid}` to get or resend this delivery." }, - "pending": { - "type": "integer", - "description": "Sessions `Not Started`, `In Progress` or `In Review`." + "session_id": { + "type": "string", + "format": "uuid", + "description": "The session the delivery belongs to (the session in the path)." }, - "in_review": { - "type": "integer", - "description": "Sessions `In Review`." + "request_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL the delivery was sent to." }, - "expired": { + "response_status": { "type": "integer", - "description": "Sessions `Expired`." + "nullable": true, + "description": "HTTP status your endpoint returned. `null` while the delivery is queued and not yet attempted. When no HTTP answer was received Didit records a status of its own: `504` when your endpoint did not answer within 5 seconds, `503` for a connection failure, `400` when the URL was refused as unsafe (for example a private or internal address), and `404` for any other request error. `400` or above counts as a failed delivery." }, - "abandoned": { - "type": "integer", - "description": "Sessions `Abandoned`." + "status": { + "type": "string", + "nullable": true, + "description": "Session status when the event was generated, for example `In Progress`, `Approved` or `Declined`." }, - "in_progress": { - "type": "integer", - "description": "Sessions `In Progress`." + "webhook_type": { + "$ref": "#/components/schemas/WebhookEvent" }, - "not_started": { - "type": "integer", - "description": "Sessions `Not Started`." + "request_header": { + "type": "string", + "description": "JSON text (a string) of the request headers Didit sent: `Content-Type`, `X-Signature`, `X-Signature-V2`, `X-Signature-Simple`, `X-Timestamp` and `User-Agent`. Parse it with a JSON parser. It holds signatures, never the signing secret." }, - "manually_approved": { - "type": "integer", - "description": "Sessions a reviewer approved after the workflow gave another verdict." + "request_body": { + "type": "string", + "description": "JSON text (a string) of the exact payload Didit sent, keys sorted, including `event_id` and `timestamp`. Parse it with a JSON parser; see [Payload shape](https://docs.didit.me/integration/webhooks#payload-shape). For a scoped key without `media: read`, media URLs inside it are `null`; without `sessions: write`, session tokens and hosted verification links are `null`. A body changed that way no longer matches its signatures. Didit does not record the response body your endpoint returned." }, - "manually_declined": { - "type": "integer", - "description": "Sessions a reviewer declined after the workflow gave another verdict." + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the delivery was created." } } }, - "ApplicationAnalyticsRequestBreakdown": { + "SessionEvent": { "type": "object", - "description": "Status totals for the range. The previous-period breakdown carries at least the required fields.", + "description": "One entry of a verification session's event timeline.", "required": [ - "total_approved", - "total_in_review", - "total_declined", - "total_canceled", - "total_pending" + "uuid", + "event_type", + "created_at", + "component", + "feature_step", + "step", + "details", + "ip_address", + "user_agent", + "device_info", + "file_url", + "sent_at" ], "properties": { - "total_approved": { - "type": "integer", - "description": "Sessions `Approved` or `Kyc Expired`." + "uuid": { + "type": "string", + "format": "uuid", + "description": "Event identifier." }, - "total_in_review": { - "type": "integer", - "description": "Sessions `In Review`." + "event_type": { + "type": "string", + "enum": [ + "SESSION_CREATED", + "SESSION_COMPLETED", + "SESSION_STATUS_CHANGED", + "LINK_OPENED", + "FORM_SUBMITTED", + "BUTTON_CLICKED", + "FILE_UPLOADED", + "ATTEMPT_FAILED", + "FEATURE_STEP_CHANGED", + "STEP_RENDERED", + "NOTE_ADDED", + "MANUAL_REVIEW_STARTED", + "WORKFLOW_BRANCH_EVALUATED", + "WORKFLOW_STATUS_NODE_REACHED", + "LOG" + ], + "description": "What happened. Reported by the device during the hosted flow or SDK: `LINK_OPENED`, `STEP_RENDERED`, `BUTTON_CLICKED`, `FORM_SUBMITTED`, `FILE_UPLOADED`, `ATTEMPT_FAILED` and `LOG` (debugging). Recorded by the server: `SESSION_CREATED`, `SESSION_STATUS_CHANGED` (`details.old_status`, `details.current_status`), `FEATURE_STEP_CHANGED` (`details.old_feature_step`, `details.current_feature_step`), `WORKFLOW_BRANCH_EVALUATED` and `WORKFLOW_STATUS_NODE_REACHED` (the workflow routing trace). `SESSION_COMPLETED`, `NOTE_ADDED` and `MANUAL_REVIEW_STARTED` can appear on older sessions." }, - "total_declined": { - "type": "integer", - "description": "Sessions `Declined`." + "created_at": { + "type": "string", + "format": "date-time", + "description": "When Didit stored the event. Events are stored asynchronously, a few seconds after `sent_at`." }, - "total_canceled": { - "type": "integer", - "description": "Sessions `Expired` or `Abandoned`." + "component": { + "type": "string", + "nullable": true, + "description": "UI component that reported the event (device events only)." }, - "total_pending": { - "type": "integer", - "description": "Sessions `Not Started`, `In Progress` or `In Review` (includes `total_in_review`)." + "feature_step": { + "type": "string", + "nullable": true, + "enum": [ + "OCR", + "OCR_BACK", + "NFC", + "AML", + "FACE", + "LIVENESS", + "FACE_MATCH", + "IP_ANALYSIS", + "AGE_ESTIMATION", + "PROOF_OF_ADDRESS", + "PHONE_VERIFICATION", + "EMAIL_VERIFICATION", + "FACE_SEARCH", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + "DOCUMENT_AI", + "GEOLOCATION", + null + ], + "description": "Verification feature the session was on when the event happened." }, - "total_expired": { - "type": "integer", - "description": "Sessions `Expired`." + "step": { + "type": "string", + "nullable": true, + "description": "Screen within the feature, for example `DOCUMENT_CAPTURE_FRONT` or `FACE_PASSIVE_LIVENESS`." }, - "total_abandoned": { - "type": "integer", - "description": "Sessions `Abandoned`." + "details": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Event-specific data. The keys depend on `event_type`." }, - "total_in_progress": { - "type": "integer", - "description": "Sessions `In Progress`." + "ip_address": { + "type": "string", + "nullable": true, + "description": "IP address the device reported the event from. `null` for server-recorded events." }, - "total_not_started": { - "type": "integer", - "description": "Sessions `Not Started`." + "user_agent": { + "type": "string", + "nullable": true, + "description": "User agent of the device. `null` for server-recorded events." }, - "total_manually_approved": { - "type": "integer", - "description": "Approved by a reviewer." + "device_info": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Device details the SDK or browser reported." + }, + "file_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Short-lived presigned link to a file attached to the event (for example a captured image). `null` when there is none, and always `null` for a scoped API key without `media: read`." }, - "total_manually_declined": { + "sent_at": { "type": "integer", - "description": "Declined by a reviewer." + "format": "int64", + "description": "When the event happened, as a Unix timestamp in milliseconds. Results are ordered by it." } } }, - "ApplicationAnalyticsCountryCount": { + "AMLDatasetSourceFreshness": { "type": "object", + "description": "Freshness of one family of AML datasets.", + "required": [ + "last_updated_at", + "oldest_updated_at", + "dataset_count", + "dated_dataset_count" + ], "properties": { - "country": { + "last_updated_at": { "type": "string", - "description": "Country name." + "format": "date-time", + "description": "Most recent update of any dataset in the family." }, - "country_code": { + "oldest_updated_at": { "type": "string", - "description": "Two-letter country code." + "format": "date-time", + "nullable": true, + "description": "Staleness bound: every dataset in the family is at least this current. `null` when at least one dataset in the family reported no update date, because the bound is then unknown." }, - "session_count": { + "dataset_count": { "type": "integer", - "description": "Sessions from that country." + "description": "Number of datasets in the family." + }, + "dated_dataset_count": { + "type": "integer", + "description": "Number of those datasets that reported an update date." } } }, - "ApplicationAnalyticsLocationStats": { + "AMLDatasetFreshness": { "type": "object", + "description": "How current the AML datasets were when a screening ran. Dates are never estimated.", + "required": [ + "captured_at", + "provider", + "catalog_version", + "sources" + ], "properties": { - "ip_countries": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsCountryCount" - }, - "description": "Countries of the users' IP addresses, most sessions first." - }, - "countries": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsCountryCount" - }, - "description": "Same list as `ip_countries`." + "captured_at": { + "type": "string", + "format": "date-time", + "description": "When Didit read the dataset catalog this freshness comes from." }, - "id_countries": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsCountryCount" - }, - "description": "Issuing countries of the verified ID documents, most sessions first." + "provider": { + "type": "string", + "description": "Opaque identifier of the AML data source. Treat it as an opaque string; do not branch on it.", + "example": "aml_data_source" }, - "conversion_by_country": { - "type": "array", - "description": "Per document issuing country: finished sessions (approved, declined or in review) and how many were approved. Most finished first.", - "items": { - "type": "object", - "properties": { - "country": { - "type": "string" - }, - "country_code": { - "type": "string" - }, - "finished": { - "type": "integer" - }, - "approved": { - "type": "integer" - } - } - } + "catalog_version": { + "type": "string", + "nullable": true, + "description": "Version identifier of the dataset catalog, when the source reports one." }, - "cities": { - "type": "array", - "description": "Up to 100 IP cities, most sessions first.", - "items": { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "`City, Country`." - }, - "session_count": { - "type": "integer" - }, - "latitude": { - "type": "number", - "nullable": true - }, - "longitude": { - "type": "number", - "nullable": true - } - } - } - } - } - }, - "ApplicationAnalyticsDemographicStats": { - "type": "object", - "properties": { - "age_distribution": { + "sources": { "type": "object", - "description": "Sessions per age band from the ID document. Empty when there is no data.", + "description": "Freshness per dataset family. A family whose datasets reported no update date is absent rather than filled with `null`.", "properties": { - "under_18": { - "type": "integer" - }, - "18_24": { - "type": "integer" - }, - "25_34": { - "type": "integer" + "sanctions": { + "$ref": "#/components/schemas/AMLDatasetSourceFreshness" }, - "35_44": { - "type": "integer" + "pep": { + "$ref": "#/components/schemas/AMLDatasetSourceFreshness" }, - "45_64": { - "type": "integer" + "warnings": { + "$ref": "#/components/schemas/AMLDatasetSourceFreshness" }, - "65_plus": { - "type": "integer" + "adverse_media": { + "$ref": "#/components/schemas/AMLDatasetSourceFreshness" } - }, - "additionalProperties": { - "type": "integer" - } - }, - "gender_distribution": { - "type": "object", - "description": "Sessions per document gender code: `M`, `F` or `U` (unknown).", - "additionalProperties": { - "type": "integer" } } } }, - "ApplicationAnalyticsDeviceDetails": { + "AMLScreeningRun": { "type": "object", - "description": "Top five of each, most sessions first.", + "description": "One completed ongoing-monitoring re-screen of an AML check.", + "required": [ + "uuid", + "completed_at", + "started_at", + "outcome", + "trigger", + "provider", + "status", + "score", + "total_hits", + "dataset_freshness" + ], "properties": { - "device_models": { - "type": "array", - "description": "Device brands: `Apple`, `Android`, `LG`, everything else grouped as `Other`.", - "items": { - "type": "object", - "properties": { - "device": { - "type": "string" - }, - "count": { - "type": "integer" - } - } - } + "uuid": { + "type": "string", + "format": "uuid", + "description": "Identifier of this screening run." }, - "browsers": { - "type": "array", - "description": "Browsers: `Chrome`, `Safari`, `Didit App`, `Firefox`, everything else grouped as `Other`.", - "items": { - "type": "object", - "properties": { - "browser": { - "type": "string" - }, - "count": { - "type": "integer" - } - } - } + "completed_at": { + "type": "string", + "format": "date-time", + "description": "When this subject's re-screen finished. Results are ordered by it, newest first." }, - "operating_systems": { - "type": "array", - "description": "Operating systems.", - "items": { - "type": "object", - "properties": { - "os": { - "type": "string" - }, - "count": { - "type": "integer" - } - } - } - } - } - }, - "ApplicationAnalyticsWorkflowFunnel": { - "type": "object", - "description": "Keyed by workflow ID. `total` is the sessions started on that workflow; `steps` counts the sessions that reached each step (`OCR`, `LIVENESS`, `FACE_MATCH`, `AML`, `POA`, `PHONE`, `EMAIL`, `NFC`, `DATABASE_VALIDATION`, `QUESTIONNAIRE`, `IP_ANALYSIS`, `AGE_ESTIMATION`, `KYB_REGISTRY`, `KYB_DOCUMENTS`, `KYB_KEY_PEOPLE`). Reaching a step is not passing it.", - "additionalProperties": { - "type": "object", - "properties": { - "total": { - "type": "integer" - }, - "steps": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "reached": { - "type": "integer" - } - } - } - } - } - } - }, - "ApplicationAnalyticsVerificationTimeStats": { - "type": "object", - "description": "Active time users spent verifying, ignoring idle gaps. An empty object when there is no data.", - "properties": { - "overall_avg_seconds": { - "type": "number", - "description": "Average per session, one decimal." + "started_at": { + "type": "string", + "format": "date-time", + "description": "When the monitoring batch this re-screen belongs to started." }, - "overall_count": { - "type": "integer", - "description": "Sessions measured." + "outcome": { + "type": "string", + "enum": [ + "NO_CHANGE", + "CHANGED", + "FAILED" + ], + "description": "`NO_CHANGE`: completed and the result did not move. `CHANGED`: completed and the score, status, hit count or hits changed. `FAILED`: no usable answer for this subject, so the check was left as it was." }, - "per_feature": { - "type": "object", - "description": "Keyed by feature, for example `OCR` or `LIVENESS`.", - "additionalProperties": { - "type": "object", - "properties": { - "avg_seconds": { - "type": "number" - }, - "total_seconds": { - "type": "number" - }, - "count": { - "type": "integer" - } - } - } + "trigger": { + "type": "string", + "enum": [ + "ONGOING_MONITORING" + ], + "description": "What started the screening. Always `ONGOING_MONITORING` today." }, - "per_sub_step": { - "type": "object", - "description": "Keyed by feature, then by sub-step, for example `front_id_photo`.", - "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "avg_seconds": { - "type": "number" - }, - "total_seconds": { - "type": "number" - }, - "count": { - "type": "integer" - } - } - } - } - } - } - }, - "ApplicationAnalyticsResubmissionStats": { - "type": "object", - "properties": { - "first_pass": { - "type": "integer", - "description": "Finished sessions with no resubmission." + "provider": { + "type": "string", + "description": "Opaque identifier of the AML data source that answered. Treat it as an opaque string; do not branch on it.", + "example": "aml_data_source" }, - "second": { - "type": "integer", - "description": "Finished sessions resubmitted once." + "status": { + "type": "string", + "nullable": true, + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested", + null + ], + "description": "AML check status this run left the check in." }, - "third_plus": { - "type": "integer", - "description": "Finished sessions resubmitted two or more times." + "score": { + "type": "number", + "format": "float", + "nullable": true, + "minimum": 0, + "maximum": 100, + "description": "AML score this run left the check in." }, - "abandoned": { + "total_hits": { "type": "integer", - "description": "Abandoned sessions." - } - } - }, - "ApplicationAnalyticsOriginalStatusStats": { - "type": "object", - "description": "The workflow's automatic verdict, before any reviewer changed it.", - "properties": { - "approved": { - "type": "integer" - }, - "in_review": { - "type": "integer" + "nullable": true, + "description": "Number of hits this run left the check in." }, - "declined": { - "type": "integer" - } - } - }, - "ApplicationAnalyticsWarningStats": { - "type": "object", - "description": "Keyed by feature (`ID_VERIFICATION`, `LIVENESS`, `FACEMATCH`, `AML`, `PROOF_OF_ADDRESS`, `PHONE`, `EMAIL`, `NFC`, `LOCATION`, `DATABASE_VALIDATION`, `QUESTIONNAIRE` and the KYB features). `total` is the sessions that reached the feature; `risks` counts sessions per warning code and level.", - "additionalProperties": { - "type": "object", - "properties": { - "total": { - "type": "integer" - }, - "risks": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "error": { - "type": "integer" - }, - "warning": { - "type": "integer" - }, - "information": { - "type": "integer" - } - } + "dataset_freshness": { + "allOf": [ + { + "$ref": "#/components/schemas/AMLDatasetFreshness" } - } + ], + "nullable": true, + "description": "Dataset freshness behind this run, or `null` when no dates were available." } } }, - "ApplicationAnalyticsDemographicPoint": { + "KYBKeyPeopleParty": { "type": "object", + "description": "A Key People party of a Business Verification (KYB) session, with its child verification. The same party appears in `key_people_checks[].submitted.parties[]` of the business session decision.", + "required": [ + "uuid", + "entity_type", + "source", + "name", + "is_active", + "is_skipped", + "requires_verification", + "roles", + "kyc_session_deleted", + "kyb_sub_session_deleted" + ], "properties": { - "date": { + "uuid": { "type": "string", - "description": "Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`." - }, - "age_distribution": { - "type": "object", - "additionalProperties": { - "type": "integer" - } + "format": "uuid", + "description": "Party identifier." }, - "gender_distribution": { - "type": "object", - "additionalProperties": { - "type": "integer" - } - } - } - }, - "ApplicationAnalyticsDevicePoint": { - "type": "object", - "properties": { - "date": { + "entity_type": { "type": "string", - "description": "Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`." - }, - "devices": { - "type": "object", - "additionalProperties": { - "type": "integer" - } + "enum": [ + "person", + "company" + ], + "description": "Whether the party is an individual or a company." }, - "browsers": { - "type": "object", - "additionalProperties": { - "type": "integer" - } + "source": { + "type": "string", + "enum": [ + "REGISTRY", + "USER" + ], + "description": "`REGISTRY`: taken from the company registry. `USER`: entered in the Key People screen." }, - "operating_systems": { - "type": "object", - "additionalProperties": { - "type": "integer" - } - } - } - }, - "ApplicationAnalyticsVerificationTimePoint": { - "type": "object", - "properties": { - "date": { + "name": { "type": "string", - "description": "Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`." + "description": "Display name of the party." }, - "overall_avg": { - "type": "number", - "description": "Average active seconds per session." + "first_name": { + "type": "string", + "nullable": true, + "description": "First name (persons)." }, - "per_feature": { - "type": "object", - "description": "Average active seconds per feature.", - "additionalProperties": { - "type": "number" - } - } - } - }, - "ApplicationAnalyticsResubmissionPoint": { - "type": "object", - "properties": { - "date": { + "last_name": { + "type": "string", + "nullable": true, + "description": "Last name (persons)." + }, + "company_name": { "type": "string", - "description": "Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`." + "nullable": true, + "description": "Company name (company parties)." }, - "first_pass": { - "type": "integer" + "registration_number": { + "type": "string", + "nullable": true, + "description": "Company registration number (company parties)." }, - "second": { - "type": "integer" + "email": { + "type": "string", + "format": "email", + "nullable": true, + "description": "Email the verification link is sent to." }, - "third_plus": { - "type": "integer" + "nationality": { + "type": "string", + "nullable": true, + "description": "Nationality for persons, country of incorporation for companies." }, - "abandoned": { - "type": "integer" - } - } - }, - "ApplicationAnalyticsOriginalStatusPoint": { - "type": "object", - "properties": { - "date": { + "phone_number": { "type": "string", - "description": "Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`." + "nullable": true, + "description": "Phone number collected for the party." }, - "approved": { - "type": "integer" + "date_of_birth": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Date of birth (persons)." }, - "in_review": { - "type": "integer" + "position": { + "type": "string", + "nullable": true, + "description": "Free-text job title (persons)." }, - "declined": { - "type": "integer" - } - } - }, - "ApplicationAnalyticsSummary": { - "type": "object", - "description": "User verification (KYC) summary, returned when neither `scope` nor `widget_keys` is sent.", - "required": [ - "data_points", - "request_breakdown", - "previous_data_points", - "previous_request_breakdown", - "conversion_rate", - "location_stats", - "demographic_stats", - "device_details", - "workflow_funnel_stats", - "verification_time_stats", - "resubmission_stats", - "warning_stats", - "original_status_stats", - "grouping_mode", - "demographic_time_series", - "device_time_series", - "verification_time_time_series", - "resubmission_time_series", - "original_status_time_series" - ], - "properties": { - "data_points": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsDataPoint" - }, - "description": "One point per day, from the first day with sessions in the range to `end_date`. Always daily, whatever `grouping_mode` says." + "custom_fields": { + "type": "object", + "additionalProperties": true, + "description": "Workflow-defined custom fields collected for the party, keyed by field key." }, - "request_breakdown": { - "$ref": "#/components/schemas/ApplicationAnalyticsRequestBreakdown" + "is_active": { + "type": "boolean", + "description": "Whether the party is active." }, - "previous_data_points": { + "is_skipped": { + "type": "boolean", + "description": "Whether the party is exempt from verification. Regenerating clears it." + }, + "requires_verification": { + "type": "boolean", + "description": "Whether the role and ownership rules require a verification for this party." + }, + "roles": { "type": "array", + "description": "Roles the party holds in the company.", "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsDataPoint" - }, - "description": "Daily points for the period of the same length right before `start_date`." - }, - "previous_request_breakdown": { - "$ref": "#/components/schemas/ApplicationAnalyticsRequestBreakdown" + "type": "object", + "required": [ + "role", + "ownership_percent", + "voting_percent" + ], + "properties": { + "role": { + "type": "string", + "enum": [ + "director", + "non_executive_director", + "secretary", + "chairman", + "ubo", + "shareholder", + "representative", + "founder", + "legal_advisor", + "authorized_signatory", + "trustee", + "beneficiary", + "settlor", + "protector", + "company_officer", + "investor", + "other" + ], + "description": "Role code." + }, + "ownership_percent": { + "type": "number", + "nullable": true, + "description": "Ownership percentage for this role, when known." + }, + "voting_percent": { + "type": "number", + "nullable": true, + "description": "Voting-rights percentage for this role, when known." + } + } + } }, - "conversion_rate": { - "type": "number", + "kyc_session_id": { + "type": "string", + "format": "uuid", "nullable": true, - "description": "`total_approved / (total_approved + total_declined + total_in_review) x 100`, one decimal. `null` when nothing finished." - }, - "location_stats": { - "$ref": "#/components/schemas/ApplicationAnalyticsLocationStats" + "description": "Child individual KYC session (persons)." }, - "demographic_stats": { - "$ref": "#/components/schemas/ApplicationAnalyticsDemographicStats" + "kyc_session_status": { + "type": "string", + "nullable": true, + "enum": [ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + null + ], + "description": "Status of the child KYC session. A deleted session keeps the status it had when it was deleted." }, - "device_details": { - "$ref": "#/components/schemas/ApplicationAnalyticsDeviceDetails" + "kyc_session_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Hosted link of the child KYC session. `null` when there is none or it was deleted, and for a scoped API key without `sessions: write`." }, - "workflow_funnel_stats": { - "$ref": "#/components/schemas/ApplicationAnalyticsWorkflowFunnel" + "kyc_session_deleted": { + "type": "boolean", + "description": "The child KYC session was deleted, so it can never settle." }, - "verification_time_stats": { - "$ref": "#/components/schemas/ApplicationAnalyticsVerificationTimeStats" + "kyb_sub_session_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Child KYB session (company parties)." }, - "resubmission_stats": { - "$ref": "#/components/schemas/ApplicationAnalyticsResubmissionStats" + "kyb_sub_session_status": { + "type": "string", + "nullable": true, + "enum": [ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + null + ], + "description": "Status of the child KYB session." }, - "warning_stats": { - "$ref": "#/components/schemas/ApplicationAnalyticsWarningStats" + "kyb_sub_session_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Hosted link of the child KYB session, with the same `null` rules as `kyc_session_url`." }, - "original_status_stats": { - "$ref": "#/components/schemas/ApplicationAnalyticsOriginalStatusStats" + "kyb_sub_session_deleted": { + "type": "boolean", + "description": "Company-party counterpart of `kyc_session_deleted`." }, - "grouping_mode": { + "verification_workflow_type": { "type": "string", + "nullable": true, "enum": [ - "day", - "week", - "month" + "kyc", + "kyb", + "adaptive_age_verification", + "biometric_authentication", + "address_verification", + "questionnaire_verification", + "email_verification", + "phone_verification", + null ], - "description": "How the `*_time_series` lists are grouped: `day` for up to 14 days, `week` up to 90, `month` beyond. Measured from the first day with sessions in the range." - }, - "demographic_time_series": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsDemographicPoint" - } + "description": "Type of the workflow the child session runs." }, - "device_time_series": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsDevicePoint" - } + "verification_workflow_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Workflow the child session runs." }, - "verification_time_time_series": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsVerificationTimePoint" - } + "verification_workflow_label": { + "type": "string", + "nullable": true, + "description": "Label of that workflow." }, - "resubmission_time_series": { + "verification_workflow_features": { "type": "array", + "nullable": true, + "description": "Features of the child session's workflow with their current status.", "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsResubmissionPoint" + "type": "object", + "required": [ + "feature", + "status" + ], + "properties": { + "feature": { + "type": "string", + "description": "Feature code, for example `ID_VERIFICATION` or `LIVENESS`." + }, + "status": { + "type": "string", + "description": "Feature status, `Not Finished` until it runs." + } + } } }, - "original_status_time_series": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsOriginalStatusPoint" - } + "didit_internal_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Didit's identifier of the verified person, once known." } } }, - "ApplicationAnalyticsWidget": { + "TravelRuleDelivery": { "type": "object", - "description": "One dashboard widget. Every widget has `kind` and `title`; the other fields depend on `kind`. The session widgets reuse the summary objects (`data_points`, `request_breakdown`, `workflow_funnel_stats` and so on); list widgets put their entries in `items`; transaction metrics use `value`.", + "description": "Where the latest message the transfer owes the other side stands. Didit contacts the counterparty after the request that created or changed the transfer has returned, so this object says whether that message got through: queued, being sent, waiting to be retried after an error, delivered, or failed. A failure the network may still recover from (no answer in time, a failed connection, HTTP `408`, `429` or `5xx`) is tried again after 30 seconds, 2 minutes, 10 minutes, 30 minutes and 1 hour. When a first message (`kind: SEND`) fails for good, because the network refused it or the sixth attempt failed too, the transfer moves to `COUNTERPARTY_VASP_NOT_REACHABLE` with `last_error` in its `timeline`; any other message that fails for good is noted in the `timeline`. The transfer's `status` stays the source of truth for the exchange itself.", "required": [ "kind", - "title" + "status", + "rail", + "attempts", + "last_error", + "next_attempt_at", + "delivered_at" ], "properties": { "kind": { "type": "string", "enum": [ - "sessions_volume", - "workflow_funnel", - "average_verification_time", - "country_distribution", - "warning_insights", - "conversion_rate", - "conversion_by_country", - "resubmission_insights", - "original_status_distribution", - "database_validation_coverage", - "database_validation_latency", - "demographics", - "device_details", - "horizontal_bar", - "kyb_registry_coverage", - "kyb_registry_data_completeness", - "metric", - "time_series", - "breakdown", - "status_time_series", - "leaderboard", - "histogram" + "SEND", + "ANSWER", + "TXID", + "CANCEL", + "POLL" ], - "description": "Widget type." + "description": "What the message carries. `SEND`: the transfer's first message to the counterparty (outbound; the rail is chosen when it is sent). `ANSWER`: your accept or decline, owed to the network that asked (inbound). `TXID`: the on-chain transaction hash, after you finish the transfer. `CANCEL`: the cancellation, after you cancel it. `POLL`: a status query to a network that offers one, repeated every 5 minutes while the transfer waits on the counterparty." }, - "title": { + "status": { "type": "string", - "description": "Display title, for example `Verification volume`." + "enum": [ + "QUEUED", + "SENDING", + "RETRYING", + "DELIVERED", + "FAILED" + ], + "description": "`QUEUED`: waiting to be sent. `SENDING`: an attempt is running. `RETRYING`: an attempt failed and the next one runs at `next_attempt_at`. `DELIVERED`: the other side took the message. `FAILED`: the other side refused it, or the last attempt failed." }, - "grouping_mode": { + "rail": { "type": "string", "enum": [ - "day", - "week", - "month" + "INTERNAL", + "TRP", + "GTR", + "TRUST", + "VERIFYVASP", + "SYGNA", + "EMAIL", + "" ], - "description": "How the `*_time_series` lists are grouped: `day` for up to 14 days, `week` up to 90, `month` beyond. Measured from the first day with sessions in the range." + "description": "The rail that carries the message. Empty until Didit has chosen one for a first message." }, - "data_points": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsDataPoint" - } + "attempts": { + "type": "integer", + "minimum": 0, + "description": "Attempts made so far for this message. A message that is not a status poll is attempted at most 6 times." }, - "request_breakdown": { - "$ref": "#/components/schemas/ApplicationAnalyticsRequestBreakdown" + "last_error": { + "type": "string", + "description": "The kind of failure the last attempt hit, `\"\"` when none did: for example `email undeliverable`, `trp identity endpoint unreachable` or `trp transfer inquiry delivery failed`. It never carries the counterparty's own answer, which stays on the transfer's `timeline`.\nWhen GTR answers with an error, its own status code and message are included. Response bodies are never included.", + "example": "email undeliverable" }, - "previous_data_points": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsDataPoint" - } + "next_attempt_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the next attempt runs while the message is `QUEUED` or `RETRYING`." }, - "previous_request_breakdown": { - "$ref": "#/components/schemas/ApplicationAnalyticsRequestBreakdown" + "delivered_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the other side took the message." + } + } + }, + "TravelRuleDiscoveryStep": { + "type": "object", + "description": "One step of the counterparty discovery Didit ran for an outbound transfer, in the order it ran them. The first step that names a VASP with a route ends the chain.", + "properties": { + "step": { + "type": "string", + "enum": [ + "internal_address_book", + "wallet_attribution", + "network_lookup", + "customer_hint", + "end_user" + ], + "description": "`internal_address_book`: another Didit customer that proved control of the destination wallet. `wallet_attribution`: the wallet's blockchain analytics attribution. `network_lookup`: a network that can say which VASP holds the address. `customer_hint`: the VASP name, LEI or jurisdiction you sent in `beneficiary_data`. `end_user`: the counterparty the end user (or you) picked, or the request to pick one." }, - "conversion_rate": { - "type": "number", + "outcome": { + "type": "string", + "enum": [ + "match", + "miss", + "skipped" + ], + "description": "`match`: the step named a counterparty. `miss`: it ran and found none. `skipped`: it could not run, and `reason` says why." + }, + "reason": { + "type": "string", + "description": "Machine-readable reason, `\"\"` on a match: `no_proven_internal_wallet`, `attribution_disabled`, `already_attributed`, `no_attributed_entity`, `directory_match_no_route`, `gtr_address_lookup_runs_on_dispatch`, `no_membership`, `network_switched_off`, `no_network_address_lookup`, `no_directory_match`, `awaiting_end_user` or `picked_member_without_proven_wallet`." + }, + "vasp": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "The directory VASP the step named, when it named one." + }, + "rail": { + "type": "string", + "nullable": true, + "description": "The rail that reaches that counterparty, when the step found one." + }, + "at": { + "type": "string", + "format": "date-time" + } + } + }, + "TravelRuleNextAction": { + "type": "object", + "description": "One thing that moves a `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE` transfer on. See [Why a transfer is stuck](/transaction-monitoring/travel-rule#why-a-transfer-is-stuck).", + "properties": { + "action": { + "type": "string", + "enum": [ + "select_counterparty_vasp", + "declare_self_hosted", + "add_vasp_hint", + "update_compliance_email", + "resend" + ], + "description": "`select_counterparty_vasp`: the end user picks the destination VASP in the wallet-ownership widget, or you PATCH `action=beneficiary_vasp`. `declare_self_hosted`: the end user declares the wallet self-hosted in the widget. `add_vasp_hint`: tell Didit who the counterparty is, with a VASP name, LEI or travel address (for example a `travel_address` on the destination wallet's address-book entry), then resend. `update_compliance_email` and `resend` are `action` values of `PATCH /v3/transactions/{transaction_id}/travel-rule/`: give the counterparty's compliance email again, and re-run routing." + }, + "url": { + "type": "string", + "format": "uri", + "description": "The wallet-ownership widget link, on `select_counterparty_vasp` and `declare_self_hosted` while the transfer has an open widget session." + } + }, + "required": [ + "action" + ] + }, + "TransactionTravelRule": { + "type": "object", + "description": "A transaction's Travel Rule transfer, as `GET /v3/transactions/{transaction_id}/`, the `201` of `POST /v3/transactions/` and each row of `GET /v3/transactions/` return it. `direction`, `rail`, `counterparty_vasp`, `delivery`, `discovery`, `reason` and `next_actions` have the same shape as on `PATCH /v3/transactions/{transaction_id}/travel-rule/`, so one read of a transaction says why its transfer is stuck and what moves it on.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string" + }, + "protocol": { + "type": "string", "nullable": true }, - "workflow_funnel_stats": { - "$ref": "#/components/schemas/ApplicationAnalyticsWorkflowFunnel" + "required": { + "type": "boolean" }, - "verification_time_stats": { - "$ref": "#/components/schemas/ApplicationAnalyticsVerificationTimeStats" + "obligations_count": { + "type": "integer" }, - "verification_time_time_series": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsVerificationTimePoint" - } + "originator_data": { + "type": "object", + "description": "Originator party. For an inbound transfer the counterparty's values are masked until ownership is confirmed." }, - "warning_stats": { - "$ref": "#/components/schemas/ApplicationAnalyticsWarningStats" + "beneficiary_data": { + "type": "object", + "description": "Beneficiary party. For an inbound transfer the counterparty's values are masked until ownership is confirmed." + }, + "name_match": { + "type": "object", + "nullable": true, + "description": "The name comparison that decided the transfer (score and strictness). Null until one ran. The compared names are masked together with the parties." }, - "resubmission_stats": { - "$ref": "#/components/schemas/ApplicationAnalyticsResubmissionStats" + "data_match": { + "$ref": "#/components/schemas/TravelRuleDataMatch" }, - "resubmission_time_series": { + "mismatched_fields": { "type": "array", "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsResubmissionPoint" - } + "type": "string" + }, + "description": "Shortcut for `data_match.mismatched_fields`. Empty when nothing differs." }, - "original_status_stats": { - "$ref": "#/components/schemas/ApplicationAnalyticsOriginalStatusStats" + "data_unmasked": { + "$ref": "#/components/schemas/TravelRuleDataUnmasked" }, - "original_status_time_series": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsOriginalStatusPoint" - } + "data_masked": { + "type": "boolean", + "description": "True while the counterparty's personal data is masked in this response." }, - "demographic_stats": { - "$ref": "#/components/schemas/ApplicationAnalyticsDemographicStats" + "metadata": { + "type": "object" }, - "demographic_time_series": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsDemographicPoint" - } + "direction": { + "type": "string", + "enum": [ + "INBOUND", + "OUTBOUND" + ], + "description": "Which side of the exchange this transfer is." }, - "device_details": { - "$ref": "#/components/schemas/ApplicationAnalyticsDeviceDetails" + "rail": { + "type": "string", + "nullable": true, + "description": "The rail the transfer went over: `INTERNAL`, `TRP`, `GTR`, `TRUST`, `VERIFYVASP`, `SYGNA` or `EMAIL` (`TEST_VASP` on a sandbox application, when the Didit Test VASP answered), or `null` until a rail is chosen." }, - "device_time_series": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApplicationAnalyticsDevicePoint" - } + "counterparty_vasp": { + "type": "object", + "nullable": true, + "properties": { + "name": { + "type": "string" + }, + "lei": { + "type": "string" + }, + "dd_score": { + "type": "integer", + "description": "Due-diligence score from 0 to 100, where higher is better. Same scale as in the VASP directory." + }, + "dd_score_breakdown": { + "type": "object", + "description": "Per-component contributions to dd_score, one object per component as in the VASP directory (jurisdiction, verification, protocol, lei, history, essentials, responsiveness), each {points, max, reason, detail}. {} until the counterparty has been assessed.", + "additionalProperties": { + "type": "object", + "properties": { + "points": { + "type": "number", + "description": "Points awarded for this component." + }, + "max": { + "type": "integer", + "description": "Maximum points this component can award." + }, + "reason": { + "type": "string", + "description": "Machine-readable code of the component's verdict." + }, + "detail": { + "type": "string", + "description": "Human-readable reason for the awarded points." + } + } + } + }, + "dd_assessed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the due-diligence assessment last ran. Null until the counterparty has been assessed." + } + }, + "description": "The counterparty's directory entry, or `null` when the transfer links none." }, - "items": { + "delivery": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleDelivery" + } + ], + "description": "The newest message the transfer owes another party. `null` when it has owed none, for example below `threshold_amount` or for a declared self-hosted wallet." + }, + "discovery": { "type": "array", - "description": "Entries of a list widget. `country_distribution` (KYC): `country`, `country_code`, `session_count`. `conversion_by_country`: `country`, `country_code`, `finished`, `approved`. `time_series`: `date`, `value`. `status_time_series`: `date`, `approved`, `declined`, `flagged`. `breakdown`: `transaction_type` or `action_type` with `count` and `volume`, or `tag_id`, `tag_name`, `count`. `leaderboard`: `rule_title`, `trigger_count` or `assignee`, `handled_count`, `average_processing_seconds`, each with `approved_percentage`, `declined_percentage`, `flagged_percentage`. `histogram`: `score`, `count`.", + "description": "How Didit looked for the counterparty, one entry per step that ran. Empty when no discovery ran, for example on an inbound transfer.", "items": { - "type": "object", - "additionalProperties": {} + "$ref": "#/components/schemas/TravelRuleDiscoveryStep" } }, - "previous_items": { + "reason": { + "type": "string", + "description": "Why a `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE` transfer is stuck; `\"\"` for any other status. A failed delivery decides it first, on `COUNTERPARTY_VASP_NOT_REACHABLE` only: `email_undeliverable` (the mail provider refused the compliance email and no address is left to try: when it was sent, on a reminder, or as a permanent bounce reported later) or `delivery_failed` (the rail that was delivering failed for good: a network rail refused the message or ran out of retries, a `TRP` identity host did not answer, or the counterparty's `TRP` host did not accept the inquiry). Otherwise the `discovery` trace decides it: `awaiting_end_user` (no step named a counterparty Didit can reach), `directory_match_no_route` (a step named a directory VASP that has no route), `picked_member_without_proven_wallet` (the end user picked a Didit customer that has not proven control of the wallet), or `no_directory_match` when the trace gives no other reason. A resend moves the transfer on, so the reason clears. See [Why a transfer is stuck](/transaction-monitoring/travel-rule#why-a-transfer-is-stuck)." + }, + "next_actions": { "type": "array", - "description": "Transaction time series for the period of the same length right before `start_date`.", + "description": "What moves the transfer on; `[]` for any status but `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE`. `email_undeliverable` offers `update_compliance_email`, then `resend`. `delivery_failed` offers `resend`. A discovery reason offers `select_counterparty_vasp`, `declare_self_hosted`, `add_vasp_hint` and `resend`.", "items": { - "type": "object", - "additionalProperties": {} + "$ref": "#/components/schemas/TravelRuleNextAction" } }, - "value": { - "type": "number", - "description": "`metric` widgets: the figure for the range." - }, - "currency": { - "type": "string", - "nullable": true, - "description": "Transaction volume and amount widgets: the application's default transaction currency." - }, - "thresholds": { + "chain_details": { + "$ref": "#/components/schemas/TravelRuleChainDetails" + } + } + }, + "NetworkListCounts": { + "type": "object", + "description": "Network counts for the list's views and filter options, each network counted once. Returned only when `summary` is not `false`. `scopes` count every network visible to the application. `views` count the visible networks inside the selected `scope`. `facets` count the networks in the selected `scope` and `status` view (dismissed networks are left out unless `status` includes them). The other list filters do not change these counts.", + "properties": { + "scopes": { "type": "object", - "description": "`histogram` widget: the application's review and decline score thresholds.", + "description": "Visible networks by product.", "properties": { - "review": { + "all": { "type": "integer" }, - "decline": { - "type": "integer" + "identity": { + "type": "integer", + "description": "Every other network." + }, + "transactions": { + "type": "integer", + "description": "Networks whose members are all transaction parties." } } }, - "search": { + "views": { "type": "object", - "description": "`kyb_registry_coverage` widget: registry search totals.", + "description": "Visible networks inside the selected `scope`, by status, plus two saved views.", "properties": { - "searches_total": { + "all": { "type": "integer" }, - "searches_with_no_candidates": { + "active": { + "type": "integer" + }, + "in_review": { + "type": "integer" + }, + "resolved": { + "type": "integer" + }, + "dismissed": { "type": "integer" + }, + "mine": { + "type": "integer", + "description": "Networks assigned to the calling console member. Always 0 for an API key." + }, + "high_risk": { + "type": "integer", + "description": "Networks with a risk score of 80 or more that are not dismissed." + } + } + }, + "facets": { + "type": "object", + "description": "Counts for each filter option, inside the selected `scope` and status view.", + "properties": { + "total": { + "type": "integer", + "description": "Networks in the selected `scope` and status view." + }, + "last_activity_days": { + "type": "object", + "description": "Networks with activity in the last 7, 30 and 90 days, keyed by the number of days.", + "properties": { + "7": { + "type": "integer" + }, + "30": { + "type": "integer" + }, + "90": { + "type": "integer" + } + } + }, + "risk_min": { + "type": "object", + "description": "Networks with a risk score of at least 50 and at least 80, keyed by the threshold.", + "properties": { + "50": { + "type": "integer" + }, + "80": { + "type": "integer" + } + } + }, + "signal_types": { + "type": "object", + "description": "Networks with at least one signal of each type.", + "properties": { + "ip_address": { + "type": "integer" + }, + "device": { + "type": "integer" + }, + "face": { + "type": "integer" + }, + "document_number": { + "type": "integer" + }, + "phone": { + "type": "integer" + }, + "email": { + "type": "integer" + }, + "address": { + "type": "integer" + } + } + }, + "priority": { + "type": "object", + "description": "Networks by priority. `none` counts networks without a priority.", + "properties": { + "low": { + "type": "integer" + }, + "medium": { + "type": "integer" + }, + "high": { + "type": "integer" + }, + "none": { + "type": "integer" + } + } + }, + "assignee": { + "type": "object", + "description": "Networks by assignee.", + "properties": { + "me": { + "type": "integer", + "description": "Assigned to the calling console member. Always 0 for an API key." + }, + "unassigned": { + "type": "integer", + "description": "Assigned to nobody." + }, + "users": { + "type": "array", + "description": "One entry per assignee, most networks first.", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "full_name": { + "type": "string", + "description": "The assignee's name, or their email when no name is set." + }, + "email": { + "type": "string" + }, + "count": { + "type": "integer" + } + } + } + } + } + }, + "has_case": { + "type": "integer", + "description": "Networks this application opened a case for." + }, + "min_size": { + "type": "object", + "description": "Networks with at least 10 members, keyed by the minimum size.", + "properties": { + "10": { + "type": "integer" + } + } } } } } }, - "ApplicationAnalyticsWidgetResponse": { + "NetworkTag": { "type": "object", - "description": "Dashboard widgets, returned when `scope` or `widget_keys` is sent.", - "required": [ - "scope", - "available_widget_keys", - "requested_widget_keys", - "widgets" - ], + "description": "One of your application's tags on the network.", "properties": { - "scope": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The tag's UUID, the same value you pass as `tag_uuid`." + }, + "name": { + "type": "string" + }, + "color": { + "type": "string", + "description": "Hex color, for example `#6366F1`." + }, + "source": { "type": "string", "enum": [ - "home", - "kyc", - "kyb", - "transactions" + "didit", + "custom" ], - "description": "Dashboard tab." - }, - "available_widget_keys": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Every widget key this scope accepts." + "description": "`didit` for a Didit preset tag, `custom` for a tag your team created." }, - "requested_widget_keys": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The widget keys answered, in order: your valid `widget_keys`, or the scope's defaults." + "added_by": { + "type": "string", + "nullable": true, + "description": "Email of the console member who added the tag. Null when an API key added it." }, - "widgets": { - "type": "object", - "description": "Keyed by widget key, in the order of `requested_widget_keys`.", - "additionalProperties": { - "$ref": "#/components/schemas/ApplicationAnalyticsWidget" - } + "added_at": { + "type": "string", + "format": "date-time" } } }, - "WebhookConfigDestination": { + "NetworkOutcomes": { "type": "object", - "description": "A webhook destination as listed in the webhook configuration, without delivery metrics. `GET /v3/webhook/destinations/{destination_uuid}/` adds its delivery summary.", - "required": [ - "uuid", - "label", - "url", - "enabled", - "webhook_version", - "secret_shared_key", - "subscribed_events", - "created_at", - "updated_at" - ], + "description": "Where the network's people and businesses stand. Each member you may see counts once, by the status of their most recent verification that reached a decision. Transaction parties are not counted.", "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Destination ID." + "approved": { + "type": "integer", + "description": "Members whose most recent decided verification was approved." }, - "label": { - "type": "string", - "maxLength": 255, - "description": "Human-readable name." + "declined": { + "type": "integer", + "description": "Members whose most recent decided verification was declined." }, - "url": { + "in_review": { + "type": "integer", + "description": "Members whose most recent decided verification is in review." + }, + "total": { + "type": "integer", + "description": "Members with at least one verification, decided or not. A member whose verifications are all undecided (not started, in progress, expired or abandoned) counts here and in no other key." + } + } + }, + "NetworkPatternScore": { + "type": "object", + "description": "How alike one kind of the members' data is across the whole network. A pattern is not a signal: no shared value links the members.", + "properties": { + "type": { "type": "string", - "format": "uri", - "maxLength": 2000, - "description": "HTTPS endpoint Didit posts events to." + "enum": [ + "background" + ], + "description": "`background`: the mean similarity of every pair of member selfie backgrounds, with the person masked out. It reads the newest selfie of each person, at most the 50 newest, and leaves out age assurance sessions. It covers the whole network, not only the members of your applications." }, - "enabled": { - "type": "boolean", - "description": "`false` pauses deliveries to this destination." + "score": { + "type": "number", + "nullable": true, + "minimum": 0, + "maximum": 1, + "description": "From 0 to 1, rounded to 4 decimals. Null when `reason` is set." }, - "webhook_version": { + "members": { + "type": "integer", + "nullable": true, + "description": "How many members' selfies the score compared. A selfie that cannot be read, or shows too little background, is not compared. Null while `computing`; 0 or 1 with `not_enough_selfies`." + }, + "reason": { "type": "string", "enum": [ - "v1", - "v2", - "v3" + "computing", + "not_enough_selfies" ], - "description": "Payload version sent to this destination." + "nullable": true, + "description": "Why there is no score. `computing`: the score for the current members is being computed; read the network again later. `not_enough_selfies`: fewer than two members have a usable selfie. Null when there is a score." }, - "secret_shared_key": { + "computed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the score was computed. Null while `computing`." + } + } + }, + "NetworkDetailResponse": { + "description": "The network detail, plus the organization's sharing mode, whether cross-organization signals apply to it, your application's tags on the network, member outcomes and pattern scores.", + "allOf": [ + { + "$ref": "#/components/schemas/NetworkDetail" + }, + { + "type": "object", + "properties": { + "sharing_mode": { + "type": "string", + "enum": [ + "off", + "private", + "global" + ], + "description": "The organization's cross-organization sharing mode. Only `global` compares networks across organizations; `private` and `off` keep them to the organization's own history.", + "example": "global" + }, + "cross_org_available": { + "type": "boolean", + "description": "Whether cross-organization signals apply to this organization. They need both the Enterprise plan and the `global` sharing mode.", + "example": true + }, + "cross_org_unavailable_reason": { + "type": "string", + "enum": [ + "plan", + "sharing_mode" + ], + "nullable": true, + "description": "Why cross-organization signals do not apply, checked in this order. `plan`: the organization's plan does not include the cross-organization fraud network (it needs the Enterprise plan), whatever the sharing mode. `sharing_mode`: the plan includes it, but the sharing mode is `private` or `off`. Null when `cross_org_available` is `true`.", + "example": null + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkTag" + }, + "description": "Your application's tags on the network, oldest first. Each application sees only its own tags. `fraud_tags` is separate and unchanged: the tags the network's members carry." + }, + "outcomes": { + "$ref": "#/components/schemas/NetworkOutcomes" + }, + "patterns": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkPatternScore" + }, + "description": "Pattern scores over the whole network. Today one item, `background`." + } + } + } + ] + }, + "NetworkTagRequest": { + "type": "object", + "required": [ + "tag_uuid" + ], + "properties": { + "tag_uuid": { "type": "string", - "description": "This destination's signing secret (43-character URL-safe token). Verify each webhook with the secret of the destination it was sent to. API-key callers always receive it in full; Console users without `read:webhooks` receive an empty string." - }, - "subscribed_events": { + "format": "uuid", + "description": "UUID of one of your application's tags." + } + }, + "example": { + "tag_uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7" + } + }, + "NetworkTimeline": { + "title": "Default timeline (no contract)", + "type": "object", + "description": "Without `contract`: the network's own events, oldest first, not paginated.", + "properties": { + "results": { "type": "array", - "uniqueItems": true, - "description": "Event types delivered to this destination. Empty when the destination was created without events, in which case it receives nothing.", "items": { - "$ref": "#/components/schemas/WebhookEvent" + "$ref": "#/components/schemas/NetworkTimelineEvent" + } + } + } + }, + "NetworkTimelinePage": { + "title": "Typed timeline page (contract=2)", + "type": "object", + "description": "With `contract=2`: one page of typed events, newest first.", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkTypedTimelineEvent" } }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { + "next_cursor": { "type": "string", - "format": "date-time" + "nullable": true, + "description": "Pass as `cursor` to read the next page. Null on the last page." } } }, - "WebhookConfig": { + "NetworkTypedTimelineEvent": { "type": "object", - "description": "Application-level webhook configuration, retention policy and Digital ID wallet return hosts, with every webhook destination.", - "required": [ - "webhook_url", - "webhook_version", - "secret_shared_key", - "capture_method", - "data_retention_months", - "face_retention_policy", - "face_retention_days", - "available_events", - "allowed_return_hosts", - "endpoints" - ], + "description": "One typed timeline event: the four default fields, plus its id, when Didit stored it, the member account and record it belongs to, and the detail block of its event type. The other detail blocks are null.", "properties": { - "webhook_url": { + "id": { "type": "string", - "format": "uri", - "nullable": true, - "description": "Application-level (legacy) webhook URL. Didit sends to it only while the application has no enabled destination, and then only `status.updated` and `data.updated`. Every create, update or delete of a destination sets it to the URL of the oldest enabled destination, or `null` when none is enabled." + "description": "`:`, unique within the timeline.", + "example": "session:3fa85f64-5717-4562-b3fc-2c963f66afa6" }, - "webhook_version": { + "event_type": { "type": "string", "enum": [ - "v1", - "v2", - "v3" - ], - "description": "Payload version of the legacy `webhook_url`. Each destination has its own `webhook_version`." + "network_detected", + "signal_observed", + "network_active", + "network_in_review", + "network_resolved", + "network_dismissed", + "verification_attempt", + "transaction", + "login", + "rule_hit", + "case_linked", + "note_added" + ] }, - "secret_shared_key": { + "occurred_at": { "type": "string", - "description": "Application-level signing secret (43-character URL-safe token). It signs only deliveries to the legacy `webhook_url`; each destination signs with its own `secret_shared_key`. API-key callers always receive it in full; Console users without `read:webhooks` receive an empty string." + "format": "date-time", + "description": "When it happened." }, - "capture_method": { + "ingested_at": { "type": "string", - "enum": [ - "MOBILE", - "MOBILE_AND_DESKTOP" + "format": "date-time", + "description": "When Didit stored it." + }, + "label": { + "type": "string", + "description": "A short English label, for example `Verification attempt 2 of 3`." + }, + "metadata": { + "type": "object", + "description": "For the network's own events, the same metadata as the default timeline. Empty otherwise." + }, + "account": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineAccount" + } ], - "description": "Legacy application setting. The verification flow does not read it: capture options are configured per workflow." + "nullable": true, + "description": "The member account. Null for the network's own events, `case_linked` and `note_added`." }, - "data_retention_months": { - "type": "integer", + "source_ref": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineSourceRef" + } + ], "nullable": true, - "minimum": 1, - "maximum": 120, - "description": "Automatic deletion window. User verification sessions created more than this many months ago (a month counts as 30 days) are deleted automatically, and that deletion is irreversible. `null` means sessions are never deleted automatically. See [Data retention](https://docs.didit.me/console/data-retention)." + "description": "The source record. Null for the network's own events and `note_added`." }, - "face_retention_policy": { - "type": "string", - "enum": [ - "delete_with_session", - "retain_until_user_deleted" + "verification_attempt": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineAttempt" + } ], - "description": "What happens to a session's face embedding when the session is deleted. `delete_with_session` (default) deletes it with the session. `retain_until_user_deleted` is the opt-in [biometric template retention](https://docs.didit.me/console/data-retention#biometric-template-retention): an image-free biometric template linked to the User is managed separately until its retention period ends, the User is deleted, or you purge it." + "nullable": true, + "description": "Set on `verification_attempt` events." }, - "face_retention_days": { - "type": "integer", + "transaction": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineTransaction" + } + ], "nullable": true, - "minimum": 1, - "maximum": 3650, - "description": "Retention period, in days, of biometric templates under `retain_until_user_deleted`. Required when that policy is set." + "description": "Set on `transaction` events." }, - "available_events": { - "type": "array", - "description": "Every event type a destination can subscribe to, with a display label.", - "items": { - "type": "object", - "required": [ - "value", - "label" - ], - "properties": { - "value": { - "$ref": "#/components/schemas/WebhookEvent" - }, - "label": { - "type": "string", - "description": "Display label, for example `Session status updated`." - } + "login": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineLogin" } - } + ], + "nullable": true, + "description": "Set on `login` events." }, - "allowed_return_hosts": { - "type": "array", - "maxItems": 20, - "description": "Hostnames (exact match, HTTPS only) a standalone Digital ID wallet verification may send the user back to with its `return_url`. Your white-label domain is always allowed as well. See [Register your return destination](https://docs.didit.me/standalone-apis/digital-id-wallets#register-your-return-destination).", - "items": { - "type": "string", - "maxLength": 253 - } + "rule_hit": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineRuleHit" + } + ], + "nullable": true, + "description": "Set on `rule_hit` events." }, - "endpoints": { - "type": "array", - "description": "Every webhook destination of the application, oldest first, each with its signing secret. Read only here: manage destinations with the `/v3/webhook/destinations/` endpoints.", - "items": { - "$ref": "#/components/schemas/WebhookConfigDestination" - } + "case_linked": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineCaseLinked" + } + ], + "nullable": true, + "description": "Set on `case_linked` events." + }, + "note_added": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineNoteAdded" + } + ], + "nullable": true, + "description": "Set on `note_added` events. Never returned to an API key." } } }, - "WebhookDestinationDelivery": { + "NetworkTimelineAccount": { "type": "object", - "description": "One delivery attempt to a webhook destination. The first send of an event, each automatic retry and each resend are separate deliveries.", - "required": [ - "uuid", - "session_id", - "request_url", - "response_status", - "response_time_ms", - "status", - "webhook_type", - "created_at" - ], + "description": "The member account the event belongs to.", "properties": { - "uuid": { + "node_id": { "type": "string", - "format": "uuid", - "description": "Delivery ID, for the detail and resend calls." + "description": "`:`, where kind is `vendor_user`, `vendor_business` or `transaction_party`.", + "example": "vendor_user:3fa85f64-5717-4562-b3fc-2c963f66afa6" }, - "session_id": { + "node_type": { "type": "string", - "format": "uuid", - "nullable": true, - "description": "User verification session the event is about. `null` for business session, user, business, transaction, Travel Rule and workflow events." + "enum": [ + "account", + "business_account", + "transaction_party" + ] }, - "request_url": { + "uuid": { "type": "string", - "format": "uri", - "nullable": true, - "description": "URL the delivery was sent to: the destination's URL when the event was generated." - }, - "response_status": { - "type": "integer", - "nullable": true, - "description": "HTTP status your endpoint returned. `null` while the delivery is queued and not yet attempted. When no HTTP answer was received Didit records a status of its own: `504` when your endpoint did not answer within 5 seconds, `503` for a connection failure, `400` when the URL was refused as unsafe (for example a private or internal address), and `404` for any other request error. `400` or above counts as a failed delivery." - }, - "response_time_ms": { - "type": "integer", - "nullable": true, - "minimum": 0, - "description": "Time the attempt took, in milliseconds. `null` while the delivery is queued." + "format": "uuid" }, - "status": { + "vendor_data": { "type": "string", - "nullable": true, - "description": "Status the event reported when it was generated: the session status for session events (for example `Approved` or `In Review`), the person's or company's status for user and business events, and the transaction status for transaction events. `null` for events that carry no status." + "nullable": true }, - "webhook_type": { - "$ref": "#/components/schemas/WebhookEvent" + "display_name": { + "type": "string", + "nullable": true }, - "created_at": { + "ownership": { "type": "string", - "format": "date-time", - "description": "When the delivery was created." + "enum": [ + "owned", + "organization" + ], + "description": "`owned` when the record belongs to the application you called (for a console member, to an application they were granted). `organization` when it belongs to another application of your organization." + }, + "route": { + "$ref": "#/components/schemas/NetworkTimelineRoute" } } }, - "WebhookDestinationDeliveryDetail": { + "NetworkTimelineSourceRef": { "type": "object", - "description": "One delivery attempt to a webhook destination, with the exact request headers and body Didit sent.", - "required": [ - "uuid", - "session_id", - "request_url", - "response_status", - "response_time_ms", - "status", - "webhook_type", - "created_at", - "request_header", - "request_body" - ], + "description": "The record the event was read from.", "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Delivery ID, for the detail and resend calls." - }, - "session_id": { + "kind": { "type": "string", - "format": "uuid", - "nullable": true, - "description": "User verification session the event is about. `null` for business session, user, business, transaction, Travel Rule and workflow events." + "enum": [ + "session", + "business_session", + "transaction", + "monitoring_event", + "case" + ] }, - "request_url": { + "id": { "type": "string", - "format": "uri", - "nullable": true, - "description": "URL the delivery was sent to: the destination's URL when the event was generated." - }, - "response_status": { - "type": "integer", - "nullable": true, - "description": "HTTP status your endpoint returned. `null` while the delivery is queued and not yet attempted. When no HTTP answer was received Didit records a status of its own: `504` when your endpoint did not answer within 5 seconds, `503` for a connection failure, `400` when the URL was refused as unsafe (for example a private or internal address), and `404` for any other request error. `400` or above counts as a failed delivery." - }, - "response_time_ms": { - "type": "integer", - "nullable": true, - "minimum": 0, - "description": "Time the attempt took, in milliseconds. `null` while the delivery is queued." + "format": "uuid" }, - "status": { + "ownership": { "type": "string", - "nullable": true, - "description": "Status the event reported when it was generated: the session status for session events (for example `Approved` or `In Review`), the person's or company's status for user and business events, and the transaction status for transaction events. `null` for events that carry no status." - }, - "webhook_type": { - "$ref": "#/components/schemas/WebhookEvent" + "enum": [ + "owned", + "organization" + ], + "description": "`owned` when the record belongs to the application you called (for a console member, to an application they were granted). `organization` when it belongs to another application of your organization." }, - "created_at": { + "route": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineRoute" + } + ], + "description": "Absent for `monitoring_event` and `case`." + } + } + }, + "NetworkTimelineRoute": { + "type": "object", + "description": "Where the record opens: its kind, its UUID and the application it belongs to.", + "properties": { + "kind": { "type": "string", - "format": "date-time", - "description": "When the delivery was created." + "description": "`user`, `business` or `transaction_party` for an account; `session`, `business_session` or `transaction` for a source record." }, - "request_header": { + "id": { "type": "string", - "description": "JSON text (a string) of the request headers Didit sent: `Content-Type`, `X-Signature`, `X-Signature-V2`, `X-Signature-Simple`, `X-Timestamp` and `User-Agent`. Parse it with a JSON parser. It holds signatures, never the signing secret." + "format": "uuid" }, - "request_body": { + "application_id": { "type": "string", - "description": "JSON text (a string) of the exact payload Didit sent, keys sorted, including `event_id` and `timestamp`. Parse it with a JSON parser; see [Payload shape](https://docs.didit.me/integration/webhooks#payload-shape). For a scoped key without `media: read`, media URLs inside it are `null`; without `sessions: write`, session tokens and hosted verification links are `null`. A body changed that way no longer matches its signatures. Didit does not record the response body your endpoint returned." + "format": "uuid" } } }, - "SessionWebhookDelivery": { + "NetworkTimelineAttempt": { "type": "object", - "description": "One webhook delivery attempt recorded for a verification session, with the exact request headers and body Didit sent.", - "required": [ - "uuid", - "session_id", - "request_url", - "response_status", - "status", - "webhook_type", - "request_header", - "request_body", - "created_at" - ], + "description": "One verification session of the account.", "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Delivery ID. Use it as `{webhook_uuid}` to get or resend this delivery." - }, "session_id": { "type": "string", "format": "uuid", - "description": "The session the delivery belongs to (the session in the path)." + "description": "The session UUID, or the business session UUID for a company." }, - "request_url": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "URL the delivery was sent to." + "session_number": { + "type": "integer", + "nullable": true }, - "response_status": { + "attempt": { "type": "integer", - "nullable": true, - "description": "HTTP status your endpoint returned. `null` while the delivery is queued and not yet attempted. When no HTTP answer was received Didit records a status of its own: `504` when your endpoint did not answer within 5 seconds, `503` for a connection failure, `400` when the URL was refused as unsafe (for example a private or internal address), and `404` for any other request error. `400` or above counts as a failed delivery." + "description": "This session's position among the account's sessions, in creation order." + }, + "attempt_count": { + "type": "integer", + "description": "How many sessions the account has." }, "status": { "type": "string", - "nullable": true, - "description": "Session status when the event was generated, for example `In Progress`, `Approved` or `Declined`." - }, - "webhook_type": { - "$ref": "#/components/schemas/WebhookEvent" + "description": "The session status, for example `Approved` or `Declined`." }, - "request_header": { + "status_changed_at": { "type": "string", - "description": "JSON text (a string) of the request headers Didit sent: `Content-Type`, `X-Signature`, `X-Signature-V2`, `X-Signature-Simple`, `X-Timestamp` and `User-Agent`. Parse it with a JSON parser. It holds signatures, never the signing secret." + "format": "date-time", + "nullable": true }, - "request_body": { + "decline_reasons": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkTimelineDeclineReason" + }, + "description": "For a declined session only: its warnings, oldest first, once per warning. Empty otherwise." + } + } + }, + "NetworkTimelineDeclineReason": { + "type": "object", + "properties": { + "risk": { "type": "string", - "description": "JSON text (a string) of the exact payload Didit sent, keys sorted, including `event_id` and `timestamp`. Parse it with a JSON parser; see [Payload shape](https://docs.didit.me/integration/webhooks#payload-shape). For a scoped key without `media: read`, media URLs inside it are `null`; without `sessions: write`, session tokens and hosted verification links are `null`. A body changed that way no longer matches its signatures. Didit does not record the response body your endpoint returned." + "description": "The warning code." }, - "created_at": { + "feature": { "type": "string", - "format": "date-time", - "description": "When the delivery was created." + "description": "The feature that raised the warning." + }, + "short_description": { + "type": "string" } } }, - "SessionEvent": { + "NetworkTimelineTransaction": { "type": "object", - "description": "One entry of a verification session's event timeline.", - "required": [ - "uuid", - "event_type", - "created_at", - "component", - "feature_step", - "step", - "details", - "ip_address", - "user_agent", - "device_info", - "file_url", - "sent_at" - ], + "description": "A transaction a member account took part in, dated by its transaction date.", "properties": { - "uuid": { + "transaction_id": { "type": "string", - "format": "uuid", - "description": "Event identifier." + "format": "uuid" }, - "event_type": { - "type": "string", - "enum": [ - "SESSION_CREATED", - "SESSION_COMPLETED", - "SESSION_STATUS_CHANGED", - "LINK_OPENED", - "FORM_SUBMITTED", - "BUTTON_CLICKED", - "FILE_UPLOADED", - "ATTEMPT_FAILED", - "FEATURE_STEP_CHANGED", - "STEP_RENDERED", - "NOTE_ADDED", - "MANUAL_REVIEW_STARTED", - "WORKFLOW_BRANCH_EVALUATED", - "WORKFLOW_STATUS_NODE_REACHED", - "LOG" + "transaction_number": { + "type": "integer", + "nullable": true + }, + "transaction_type": { + "type": "string" + }, + "action_type": { + "type": "string" + }, + "amount": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineAmount" + } ], - "description": "What happened. Reported by the device during the hosted flow or SDK: `LINK_OPENED`, `STEP_RENDERED`, `BUTTON_CLICKED`, `FORM_SUBMITTED`, `FILE_UPLOADED`, `ATTEMPT_FAILED` and `LOG` (debugging). Recorded by the server: `SESSION_CREATED`, `SESSION_STATUS_CHANGED` (`details.old_status`, `details.current_status`), `FEATURE_STEP_CHANGED` (`details.old_feature_step`, `details.current_feature_step`), `WORKFLOW_BRANCH_EVALUATED` and `WORKFLOW_STATUS_NODE_REACHED` (the workflow routing trace). `SESSION_COMPLETED`, `NOTE_ADDED` and `MANUAL_REVIEW_STARTED` can appear on older sessions." + "nullable": true, + "description": "Null when the transaction has no amount." }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "When Didit stored the event. Events are stored asynchronously, a few seconds after `sent_at`." + "status": { + "type": "string" }, - "component": { + "decision_reason_code": { "type": "string", - "nullable": true, - "description": "UI component that reported the event (device events only)." + "nullable": true }, - "feature_step": { + "decision_reason_label": { "type": "string", - "nullable": true, - "enum": [ - "OCR", - "OCR_BACK", - "NFC", - "AML", - "FACE", - "LIVENESS", - "FACE_MATCH", - "IP_ANALYSIS", - "AGE_ESTIMATION", - "PROOF_OF_ADDRESS", - "PHONE_VERIFICATION", - "EMAIL_VERIFICATION", - "FACE_SEARCH", - "DATABASE_VALIDATION", - "QUESTIONNAIRE", - "KYB_REGISTRY", - "KYB_DOCUMENTS", - "KYB_KEY_PEOPLE", - "DOCUMENT_AI", - "GEOLOCATION", - null - ], - "description": "Verification feature the session was on when the event happened." + "nullable": true }, - "step": { + "party_role": { "type": "string", - "nullable": true, - "description": "Screen within the feature, for example `DOCUMENT_CAPTURE_FRONT` or `FACE_PASSIVE_LIVENESS`." + "description": "The account's role in the transaction: `APPLICANT`, `REMITTER`, `BENEFICIARY` or `COUNTERPARTY`." + } + } + }, + "NetworkTimelineAmount": { + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Decimal amount as a string." }, - "details": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Event-specific data. The keys depend on `event_type`." + "currency": { + "type": "string" }, - "ip_address": { + "direction": { "type": "string", - "nullable": true, - "description": "IP address the device reported the event from. `null` for server-recorded events." + "nullable": true + } + } + }, + "NetworkTimelineLogin": { + "type": "object", + "description": "A sign-in event you posted for a person through the events API.", + "properties": { + "action": { + "type": "string", + "description": "`.`: `auth.login` or `security.login_succeeded` for a sign-in, `auth.login_failed` or `security.login_failed` for a failed one." }, - "user_agent": { + "succeeded": { + "type": "boolean" + }, + "status": { "type": "string", - "nullable": true, - "description": "User agent of the device. `null` for server-recorded events." + "description": "The event's status." }, - "device_info": { - "type": "object", + "ip": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineIP" + } + ], "nullable": true, - "additionalProperties": true, - "description": "Device details the SDK or browser reported." + "description": "Null when the event has no IP address." }, - "file_url": { - "type": "string", - "format": "uri", + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineDevice" + } + ], "nullable": true, - "description": "Short-lived presigned link to a file attached to the event (for example a captured image). `null` when there is none, and always `null` for a scoped API key without `media: read`." + "description": "Null when the event has no device fingerprint or context." }, - "sent_at": { - "type": "integer", - "format": "int64", - "description": "When the event happened, as a Unix timestamp in milliseconds. Results are ordered by it." + "is_first_login": { + "type": "boolean", + "description": "The account's first successful sign-in on record." + }, + "is_new_device": { + "type": "boolean", + "description": "A successful sign-in from a device none of the account's earlier successful sign-ins came from. Never true on the first sign-in." } } }, - "AMLDatasetSourceFreshness": { + "NetworkTimelineIP": { "type": "object", - "description": "Freshness of one family of AML datasets.", - "required": [ - "last_updated_at", - "oldest_updated_at", - "dataset_count", - "dated_dataset_count" - ], "properties": { - "last_updated_at": { + "address": { + "type": "string" + }, + "country_code": { "type": "string", - "format": "date-time", - "description": "Most recent update of any dataset in the family." + "nullable": true }, - "oldest_updated_at": { + "city": { + "type": "string", + "nullable": true + }, + "connection_type": { "type": "string", - "format": "date-time", "nullable": true, - "description": "Staleness bound: every dataset in the family is at least this current. `null` when at least one dataset in the family reported no update date, because the bound is then unknown." + "description": "The connection type label for the address, as free text." }, - "dataset_count": { - "type": "integer", - "description": "Number of datasets in the family." + "is_vpn_or_tor": { + "type": "boolean" }, - "dated_dataset_count": { - "type": "integer", - "description": "Number of those datasets that reported an update date." + "is_data_center": { + "type": "boolean" } } }, - "AMLDatasetFreshness": { + "NetworkTimelineDevice": { "type": "object", - "description": "How current the AML datasets were when a screening ran. Dates are never estimated.", - "required": [ - "captured_at", - "provider", - "catalog_version", - "sources" - ], "properties": { - "captured_at": { - "type": "string", - "format": "date-time", - "description": "When Didit read the dataset catalog this freshness comes from." - }, - "provider": { + "fingerprint": { "type": "string", - "description": "Opaque identifier of the AML data source. Treat it as an opaque string; do not branch on it.", - "example": "aml_data_source" + "nullable": true }, - "catalog_version": { + "label": { "type": "string", "nullable": true, - "description": "Version identifier of the dataset catalog, when the source reports one." - }, - "sources": { - "type": "object", - "description": "Freshness per dataset family. A family whose datasets reported no update date is absent rather than filled with `null`.", - "properties": { - "sanctions": { - "$ref": "#/components/schemas/AMLDatasetSourceFreshness" - }, - "pep": { - "$ref": "#/components/schemas/AMLDatasetSourceFreshness" - }, - "warnings": { - "$ref": "#/components/schemas/AMLDatasetSourceFreshness" - }, - "adverse_media": { - "$ref": "#/components/schemas/AMLDatasetSourceFreshness" - } - } + "description": "A readable device label built from the event's device context." } } }, - "AMLScreeningRun": { + "NetworkTimelineRuleHit": { "type": "object", - "description": "One completed ongoing-monitoring re-screen of an AML check.", - "required": [ - "uuid", - "completed_at", - "started_at", - "outcome", - "trigger", - "provider", - "status", - "score", - "total_hits", - "dataset_freshness" - ], + "description": "A live rule that matched a member's transaction or event. Test-mode matches are not included.", "properties": { - "uuid": { + "rule_id": { "type": "string", - "format": "uuid", - "description": "Identifier of this screening run." + "format": "uuid" }, - "completed_at": { - "type": "string", - "format": "date-time", - "description": "When this subject's re-screen finished. Results are ordered by it, newest first." + "rule_title": { + "type": "string" }, - "started_at": { - "type": "string", - "format": "date-time", - "description": "When the monitoring batch this re-screen belongs to started." + "rule_category": { + "type": "string" }, - "outcome": { - "type": "string", - "enum": [ - "NO_CHANGE", - "CHANGED", - "FAILED" - ], - "description": "`NO_CHANGE`: completed and the result did not move. `CHANGED`: completed and the score, status, hit count or hits changed. `FAILED`: no usable answer for this subject, so the check was left as it was." + "rule_source": { + "type": "string" }, - "trigger": { + "severity": { "type": "string", - "enum": [ - "ONGOING_MONITORING" - ], - "description": "What started the screening. Always `ONGOING_MONITORING` today." + "nullable": true }, - "provider": { - "type": "string", - "description": "Opaque identifier of the AML data source that answered. Treat it as an opaque string; do not branch on it.", - "example": "aml_data_source" + "score_delta": { + "type": "integer" }, - "status": { + "status_target": { "type": "string", - "nullable": true, - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Resub Requested", - null - ], - "description": "AML check status this run left the check in." - }, - "score": { - "type": "number", - "format": "float", - "nullable": true, - "minimum": 0, - "maximum": 100, - "description": "AML score this run left the check in." - }, - "total_hits": { - "type": "integer", - "nullable": true, - "description": "Number of hits this run left the check in." + "nullable": true }, - "dataset_freshness": { - "allOf": [ - { - "$ref": "#/components/schemas/AMLDatasetFreshness" - } - ], - "nullable": true, - "description": "Dataset freshness behind this run, or `null` when no dates were available." + "action_summary": { + "type": "string", + "nullable": true } } }, - "KYBKeyPeopleParty": { + "NetworkTimelineCaseLinked": { "type": "object", - "description": "A Key People party of a Business Verification (KYB) session, with its child verification. The same party appears in `key_people_checks[].submitted.parties[]` of the business session decision.", - "required": [ - "uuid", - "entity_type", - "source", - "name", - "is_active", - "is_skipped", - "requires_verification", - "roles", - "kyc_session_deleted", - "kyb_sub_session_deleted" - ], + "description": "A case opened for the network in your organization. `source_ref.ownership` says whether it belongs to the application you called.", "properties": { - "uuid": { + "case_id": { "type": "string", - "format": "uuid", - "description": "Party identifier." + "format": "uuid" }, - "entity_type": { + "case_number": { "type": "string", - "enum": [ - "person", - "company" - ], - "description": "Whether the party is an individual or a company." + "nullable": true }, - "source": { - "type": "string", - "enum": [ - "REGISTRY", - "USER" - ], - "description": "`REGISTRY`: taken from the company registry. `USER`: entered in the Key People screen." + "status": { + "type": "string" }, - "name": { + "author_email": { "type": "string", - "description": "Display name of the party." + "nullable": true }, - "first_name": { + "author_name": { "type": "string", - "nullable": true, - "description": "First name (persons)." - }, - "last_name": { + "nullable": true + } + } + }, + "NetworkTimelineNoteAdded": { + "type": "object", + "description": "A note left on the network. The note text is not included.", + "properties": { + "note_id": { "type": "string", - "nullable": true, - "description": "Last name (persons)." + "format": "uuid" }, - "company_name": { + "author_email": { + "type": "string" + } + } + }, + "FeatureStatusOverride": { + "type": "object", + "description": "One override of a check (feature) status made with `PATCH /v3/session/{sessionId}/features/{nodeId}/update-status/`: what it changes, whether it applied or still waits for the person to finish the flow, who asked and why.", + "required": [ + "id", + "node_id", + "feature_type", + "feature_model", + "previous_status", + "new_status", + "state", + "forced", + "reason", + "requested_by", + "requested_at", + "applied_at", + "cancelled_at", + "cancel_reason" + ], + "properties": { + "id": { "type": "string", - "nullable": true, - "description": "Company name (company parties)." + "format": "uuid", + "description": "Identifier of the override. Pass it to the cancel endpoint.", + "example": "7c9e6679-7425-40de-944b-e07fc1f90ae7" }, - "registration_number": { + "node_id": { "type": "string", - "nullable": true, - "description": "Company registration number (company parties)." + "description": "The workflow node the request named (`default` for a step without a node).", + "example": "feature_ocr_1" }, - "email": { + "feature_type": { "type": "string", - "format": "email", - "nullable": true, - "description": "Email the verification link is sent to." + "description": "The `feature_type` the request passed, or an empty string when it passed none.", + "example": "" }, - "nationality": { + "feature_model": { "type": "string", - "nullable": true, - "description": "Nationality for persons, country of incorporation for companies." + "description": "The kind of check that was overridden, for example `KYC`, `Face`, `AML` or `GeolocationCheck`.", + "example": "KYC" }, - "phone_number": { + "previous_status": { "type": "string", - "nullable": true, - "description": "Phone number collected for the party." + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ], + "description": "The check's status when the override was requested.", + "example": "In Review" }, - "date_of_birth": { + "new_status": { "type": "string", - "format": "date", - "nullable": true, - "description": "Date of birth (persons)." + "enum": [ + "Approved", + "Declined", + "In Review" + ], + "description": "The status the override sets.", + "example": "Approved" }, - "position": { + "state": { "type": "string", - "nullable": true, - "description": "Free-text job title (persons)." - }, - "custom_fields": { - "type": "object", - "additionalProperties": true, - "description": "Workflow-defined custom fields collected for the party, keyed by field key." - }, - "is_active": { - "type": "boolean", - "description": "Whether the party is active." + "enum": [ + "applied", + "pending_until_flow_finishes", + "cancelled" + ], + "description": "`applied`: the check has the new status. `pending_until_flow_finishes`: the override waits and applies when the person finishes the flow. `cancelled`: it waited and never applied (see `cancel_reason`).", + "example": "applied" }, - "is_skipped": { + "forced": { "type": "boolean", - "description": "Whether the party is exempt from verification. Regenerating clears it." + "description": "`true` when the override applied at once while the person was still in the flow.", + "example": false }, - "requires_verification": { - "type": "boolean", - "description": "Whether the role and ownership rules require a verification for this party." + "reason": { + "type": "string", + "nullable": true, + "description": "The `comment` sent with the request, or `null`.", + "example": "Document manually verified by compliance." }, - "roles": { - "type": "array", - "description": "Roles the party holds in the company.", - "items": { - "type": "object", - "required": [ - "role", - "ownership_percent", - "voting_percent" - ], - "properties": { - "role": { - "type": "string", - "enum": [ - "director", - "non_executive_director", - "secretary", - "chairman", - "ubo", - "shareholder", - "representative", - "founder", - "legal_advisor", - "authorized_signatory", - "trustee", - "beneficiary", - "settlor", - "protector", - "company_officer", - "investor", - "other" - ], - "description": "Role code." - }, - "ownership_percent": { - "type": "number", - "nullable": true, - "description": "Ownership percentage for this role, when known." - }, - "voting_percent": { - "type": "number", - "nullable": true, - "description": "Voting-rights percentage for this role, when known." - } + "requested_by": { + "type": "object", + "nullable": true, + "description": "Who requested the override, or `null` when that is not known.", + "properties": { + "email": { + "type": "string", + "format": "email", + "nullable": true, + "description": "Email of the console user, or `null` for API keys and the system.", + "example": "reviewer@example.com" + }, + "name": { + "type": "string", + "nullable": true, + "description": "Display name of the requester, or `null`.", + "example": "Alex Reviewer" + }, + "type": { + "type": "string", + "enum": [ + "CONSOLE_USER", + "API_KEY", + "SYSTEM" + ], + "description": "Who made the request: a console user, an API key, or Didit itself.", + "example": "CONSOLE_USER" } } }, - "kyc_session_id": { + "requested_at": { "type": "string", - "format": "uuid", + "format": "date-time", + "description": "When the override was requested.", + "example": "2026-10-13T09:15:00Z" + }, + "applied_at": { + "type": "string", + "format": "date-time", "nullable": true, - "description": "Child individual KYC session (persons)." + "description": "When the check took the new status, or `null` while it waits or once it is cancelled.", + "example": "2026-10-13T09:15:00Z" }, - "kyc_session_status": { + "cancelled_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When a waiting override was cancelled, or `null`.", + "example": null + }, + "cancel_reason": { "type": "string", "nullable": true, "enum": [ - "Not Started", - "In Progress", + "cancelled_by_reviewer", + "replaced", + "check_changed", + "session_closed", + null + ], + "description": "Why a waiting override never applied, or `null`. `cancelled_by_reviewer`: cancelled with the cancel endpoint. `replaced`: a newer override on the same check replaced it. `check_changed`: the person redid the check before finishing the flow. `session_closed`: the session left the flow before the person finished it (abandoned, expired, or decided mid-flow).", + "example": null + } + } + }, + "SandboxSimulateRequest": { + "type": "object", + "description": "Force a sandbox session's status. Every field is optional, but at least one of `new_status`, `scenario` or `risk` is required.", + "properties": { + "new_status": { + "type": "string", + "enum": [ "Approved", "Declined", "In Review", "Expired", - "Abandoned", - "Kyc Expired", - "Resubmitted", - "Awaiting User", - null + "Abandoned" ], - "description": "Status of the child KYC session. A deleted session keeps the status it had when it was deleted." + "description": "The status to set. When you leave it out, it defaults to the default status of the risk case (`risk`, or the first risk the scenario's magic values carry), else to the scenario's default status.", + "example": "Declined" }, - "kyc_session_url": { + "comment": { "type": "string", - "format": "uri", - "nullable": true, - "description": "Hosted link of the child KYC session. `null` when there is none or it was deleted, and for a scoped API key without `sessions: write`." + "maxLength": 500, + "description": "Note added to the activity entry the status change records.", + "example": "Testing the decline path" }, - "kyc_session_deleted": { - "type": "boolean", - "description": "The child KYC session was deleted, so it can never settle." + "scenario": { + "type": "string", + "maxLength": 100, + "description": "A scenario slug from the catalog (`GET /v1/sandbox/scenarios/`). It supplies the default status and, without `risk`, the warning its magic values carry. An unknown slug returns `400`.", + "example": "decline_aml_hit" }, - "kyb_sub_session_id": { + "risk": { + "type": "string", + "maxLength": 128, + "description": "A risk case from the catalog. Its warning is added to the session, with a log type that follows `new_status`. An unknown value returns `400`.", + "example": "POSSIBLE_MATCH_FOUND" + }, + "node_id": { + "type": "string", + "maxLength": 128, + "description": "Workflow node the risk warning is attached to. Defaults to the session's current node.", + "example": "feature_aml_1" + }, + "send_webhooks": { + "type": "boolean", + "default": true, + "description": "`true` sends exactly the webhooks a real status change sends to the sandbox application's destinations: `status.updated`, plus the `user.status.updated` / `user.data.updated` (or `business.status.updated` / `business.data.updated`) events the change causes, for destinations subscribed to them. `false` changes the status without logging or sending any webhook: the missed-webhook recovery never re-sends that decision, the console still updates live, and the next status change of the session sends webhooks as usual.", + "example": true + } + } + }, + "SandboxSimulateResponse": { + "type": "object", + "required": [ + "session_id", + "previous_status", + "status", + "scenario", + "risk", + "send_webhooks" + ], + "properties": { + "session_id": { "type": "string", "format": "uuid", - "nullable": true, - "description": "Child KYB session (company parties)." + "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }, - "kyb_sub_session_status": { + "previous_status": { "type": "string", - "nullable": true, "enum": [ "Not Started", "In Progress", @@ -101864,4315 +105049,3833 @@ "Abandoned", "Kyc Expired", "Resubmitted", - "Awaiting User", - null + "Awaiting User" ], - "description": "Status of the child KYB session." - }, - "kyb_sub_session_url": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "Hosted link of the child KYB session, with the same `null` rules as `kyc_session_url`." - }, - "kyb_sub_session_deleted": { - "type": "boolean", - "description": "Company-party counterpart of `kyc_session_deleted`." + "description": "The session's status before the simulation.", + "example": "In Progress" }, - "verification_workflow_type": { + "status": { "type": "string", - "nullable": true, "enum": [ - "kyc", - "kyb", - "adaptive_age_verification", - "biometric_authentication", - "address_verification", - "questionnaire_verification", - "email_verification", - "phone_verification", - null + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned" ], - "description": "Type of the workflow the child session runs." + "description": "The status the simulation set.", + "example": "Declined" }, - "verification_workflow_id": { + "scenario": { "type": "string", - "format": "uuid", "nullable": true, - "description": "Workflow the child session runs." + "description": "The scenario slug you passed, or `null`.", + "example": "decline_aml_hit" }, - "verification_workflow_label": { + "risk": { "type": "string", "nullable": true, - "description": "Label of that workflow." + "description": "The risk case applied (`risk`, or the one taken from the scenario), or `null` when none.", + "example": "POSSIBLE_MATCH_FOUND" }, - "verification_workflow_features": { - "type": "array", - "nullable": true, - "description": "Features of the child session's workflow with their current status.", - "items": { - "type": "object", - "required": [ - "feature", - "status" - ], - "properties": { - "feature": { - "type": "string", - "description": "Feature code, for example `ID_VERIFICATION` or `LIVENESS`." - }, - "status": { - "type": "string", - "description": "Feature status, `Not Finished` until it runs." - } - } - } + "send_webhooks": { + "type": "boolean", + "description": "The `send_webhooks` value the simulation applied.", + "example": false + } + } + }, + "CaseTeamSummary": { + "type": "object", + "nullable": true, + "description": "The review team that owns the case, or null when no team owns it. Routing sets it when a routing rule places the case with one of its teams. Change or clear it with `PATCH .../cases/{case_uuid}/` (`team`).", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" }, - "didit_internal_id": { + "name": { + "type": "string" + } + } + }, + "CaseChecker": { + "type": "object", + "nullable": true, + "description": "The member named to approve or reject the pending resolution. Set only while a four-eyes resolution waits on a named checker. Null when nothing is pending, while the pending resolution waits on the checker queue, and once the resolution is approved, rejected, or returned for a new review because the evidence changed.", + "properties": { + "member_uuid": { "type": "string", "format": "uuid", + "description": "The member's user uuid: the value you pass as `checker` when you resolve the case." + }, + "name": { + "type": "string", "nullable": true, - "description": "Didit's identifier of the verified person, once known." + "description": "The member's full name, or null when it is not known." + }, + "email": { + "type": "string", + "format": "email" } } }, - "TravelRuleDelivery": { + "ServiceStatusFeed": { "type": "object", - "description": "Where the latest message the transfer owes the other side stands. Didit contacts the counterparty after the request that created or changed the transfer has returned, so this object says whether that message got through: queued, being sent, waiting to be retried after an error, delivered, or failed. A failure the network may still recover from (no answer in time, a failed connection, HTTP `408`, `429` or `5xx`) is tried again after 30 seconds, 2 minutes, 10 minutes, 30 minutes and 1 hour. When a first message (`kind: SEND`) fails for good, because the network refused it or the sixth attempt failed too, the transfer moves to `COUNTERPARTY_VASP_NOT_REACHABLE` with `last_error` in its `timeline`; any other message that fails for good is noted in the `timeline`. The transfer's `status` stays the source of truth for the exchange itself.", + "description": "Public service status: the state of each Didit service, the overall state, and the incidents that are ongoing or were resolved in the last 7 days.", "required": [ - "kind", "status", - "rail", - "attempts", - "last_error", - "next_attempt_at", - "delivered_at" + "updated_at", + "services", + "incidents" ], "properties": { - "kind": { - "type": "string", - "enum": [ - "SEND", - "ANSWER", - "TXID", - "CANCEL", - "POLL" - ], - "description": "What the message carries. `SEND`: the transfer's first message to the counterparty (outbound; the rail is chosen when it is sent). `ANSWER`: your accept or decline, owed to the network that asked (inbound). `TXID`: the on-chain transaction hash, after you finish the transfer. `CANCEL`: the cancellation, after you cancel it. `POLL`: a status query to a network that offers one, repeated every 5 minutes while the transfer waits on the counterparty." - }, "status": { "type": "string", "enum": [ - "QUEUED", - "SENDING", - "RETRYING", - "DELIVERED", - "FAILED" - ], - "description": "`QUEUED`: waiting to be sent. `SENDING`: an attempt is running. `RETRYING`: an attempt failed and the next one runs at `next_attempt_at`. `DELIVERED`: the other side took the message. `FAILED`: the other side refused it, or the last attempt failed." - }, - "rail": { - "type": "string", - "enum": [ - "INTERNAL", - "TRP", - "GTR", - "TRUST", - "VERIFYVASP", - "SYGNA", - "EMAIL", - "" + "operational", + "degraded", + "down" ], - "description": "The rail that carries the message. Empty until Didit has chosen one for a first message." - }, - "attempts": { - "type": "integer", - "minimum": 0, - "description": "Attempts made so far for this message. A message that is not a status poll is attempted at most 6 times." - }, - "last_error": { - "type": "string", - "description": "The kind of failure the last attempt hit, `\"\"` when none did: for example `email undeliverable`, `trp identity endpoint unreachable` or `trp transfer inquiry delivery failed`. It never carries the counterparty's own answer, which stays on the transfer's `timeline`.\nWhen GTR answers with an error, its own status code and message are included. Response bodies are never included.", - "example": "email undeliverable" + "description": "The worst state of any service right now, decided by the ongoing incidents only.", + "example": "degraded" }, - "next_attempt_at": { + "updated_at": { "type": "string", "format": "date-time", - "nullable": true, - "description": "When the next attempt runs while the message is `QUEUED` or `RETRYING`." + "description": "When Didit built this response (ISO 8601, UTC). Responses are cached for 30 seconds, so it can be up to 30 seconds old.", + "example": "2026-10-13T09:41:07.512345Z" }, - "delivered_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the other side took the message." + "services": { + "type": "array", + "description": "One entry per service, every service always listed.", + "items": { + "$ref": "#/components/schemas/ServiceStatusService" + } + }, + "incidents": { + "type": "array", + "description": "Newest `started_at` first. Every ongoing incident, plus the incidents resolved in the last 7 days (at most 50 resolved incidents). Empty when nothing happened in that time.", + "items": { + "$ref": "#/components/schemas/ServiceStatusIncident" + } } } }, - "TravelRuleDiscoveryStep": { + "ServiceStatusService": { "type": "object", - "description": "One step of the counterparty discovery Didit ran for an outbound transfer, in the order it ran them. The first step that names a VASP with a route ends the chain.", + "required": [ + "key", + "name", + "status" + ], "properties": { - "step": { - "type": "string", - "enum": [ - "internal_address_book", - "wallet_attribution", - "network_lookup", - "customer_hint", - "end_user" - ], - "description": "`internal_address_book`: another Didit customer that proved control of the destination wallet. `wallet_attribution`: the wallet's blockchain analytics attribution. `network_lookup`: a network that can say which VASP holds the address. `customer_hint`: the VASP name, LEI or jurisdiction you sent in `beneficiary_data`. `end_user`: the counterparty the end user (or you) picked, or the request to pick one." - }, - "outcome": { + "key": { "type": "string", "enum": [ - "match", - "miss", - "skipped" + "service_id_verification", + "service_liveness_face_match", + "service_aml_screening", + "service_kyb_registries", + "service_database_validation", + "service_phone_delivery", + "service_email_delivery", + "service_transaction_monitoring" ], - "description": "`match`: the step named a counterparty. `miss`: it ran and found none. `skipped`: it could not run, and `reason` says why." - }, - "reason": { - "type": "string", - "description": "Machine-readable reason, `\"\"` on a match: `no_proven_internal_wallet`, `attribution_disabled`, `already_attributed`, `no_attributed_entity`, `directory_match_no_route`, `gtr_address_lookup_runs_on_dispatch`, `no_membership`, `network_switched_off`, `no_network_address_lookup`, `no_directory_match`, `awaiting_end_user` or `picked_member_without_proven_wallet`." - }, - "vasp": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "The directory VASP the step named, when it named one." + "description": "Stable service key. Match on this, never on the display name. `service_id_verification`: ID verification, `service_liveness_face_match`: Liveness and face match, `service_aml_screening`: AML screening, `service_kyb_registries`: KYB registries, `service_database_validation`: Database validation, `service_phone_delivery`: Phone verification delivery, `service_email_delivery`: Email verification delivery, `service_transaction_monitoring`: Transaction monitoring.", + "example": "service_database_validation" }, - "rail": { + "name": { "type": "string", - "nullable": true, - "description": "The rail that reaches that counterparty, when the step found one." + "description": "English display name of the service.", + "example": "Database validation" }, - "at": { + "status": { "type": "string", - "format": "date-time" + "enum": [ + "operational", + "degraded", + "down" + ], + "description": "`operational` when no incident is ongoing for the service, otherwise the worst state of its ongoing incidents.", + "example": "degraded" } } }, - "TravelRuleNextAction": { + "ServiceStatusIncident": { "type": "object", - "description": "One thing that moves a `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE` transfer on. See [Why a transfer is stuck](/transaction-monitoring/travel-rule#why-a-transfer-is-stuck).", + "required": [ + "id", + "service", + "service_name", + "state", + "status", + "title", + "body", + "affected", + "country_name", + "started_at", + "resolved_at", + "duration_minutes" + ], "properties": { - "action": { + "id": { + "type": "string", + "format": "uuid", + "description": "Incident ID.", + "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + }, + "service": { "type": "string", "enum": [ - "select_counterparty_vasp", - "declare_self_hosted", - "add_vasp_hint", - "update_compliance_email", - "resend" + "service_id_verification", + "service_liveness_face_match", + "service_aml_screening", + "service_kyb_registries", + "service_database_validation", + "service_phone_delivery", + "service_email_delivery", + "service_transaction_monitoring" ], - "description": "`select_counterparty_vasp`: the end user picks the destination VASP in the wallet-ownership widget. `declare_self_hosted`: the end user declares the wallet self-hosted in the widget. `add_vasp_hint`: tell Didit who the counterparty is, with a VASP name, LEI or travel address (for example a `travel_address` on the destination wallet's address-book entry), then resend. `update_compliance_email` and `resend` are `action` values of `PATCH /v3/transactions/{transaction_id}/travel-rule/`: give the counterparty's compliance email again, and re-run routing." + "description": "Key of the affected service (see `services[].key`).", + "example": "service_database_validation" }, - "url": { + "service_name": { "type": "string", - "format": "uri", - "description": "The wallet-ownership widget link, on `select_counterparty_vasp` and `declare_self_hosted` while the transfer has an open widget session." - } - }, - "required": [ - "action" - ] - }, - "TransactionTravelRule": { - "type": "object", - "description": "A transaction's Travel Rule transfer, as `GET /v3/transactions/{transaction_id}/`, the `201` of `POST /v3/transactions/` and each row of `GET /v3/transactions/` return it. `direction`, `rail`, `counterparty_vasp`, `delivery`, `discovery`, `reason` and `next_actions` have the same shape as on `PATCH /v3/transactions/{transaction_id}/travel-rule/`, so one read of a transaction says why its transfer is stuck and what moves it on.", - "properties": { - "uuid": { + "description": "English display name of the affected service.", + "example": "Database validation" + }, + "state": { "type": "string", - "format": "uuid" + "enum": [ + "degraded", + "down" + ], + "description": "How the service is affected. A resolved incident keeps the last state it had.", + "example": "degraded" }, "status": { - "type": "string" + "type": "string", + "enum": [ + "ongoing", + "resolved" + ], + "description": "`ongoing` until the service recovers, then `resolved`.", + "example": "ongoing" }, - "protocol": { + "title": { "type": "string", - "nullable": true + "description": "English headline. To translate it, build your own text from `service`, `state` and `country_name`.", + "example": "Database validation is degraded in Brazil." }, - "required": { - "type": "boolean" + "body": { + "type": "string", + "description": "English detail line: since when checks may be slow or fail, or when it recovered.", + "example": "Checks against Brazil (CPF) may be slow or fail since 08:55 UTC." }, - "obligations_count": { - "type": "integer" + "affected": { + "type": "string", + "description": "Countries or registries affected, for example `Brazil (CPF)`. Empty when the incident is not tied to a country or registry.", + "example": "Brazil (CPF)" }, - "originator_data": { - "type": "object", - "description": "Originator party. For an inbound transfer the counterparty's values are masked until ownership is confirmed." + "country_name": { + "type": "string", + "description": "Country affected, in English. Empty when the incident is not tied to a country.", + "example": "Brazil" }, - "beneficiary_data": { - "type": "object", - "description": "Beneficiary party. For an inbound transfer the counterparty's values are masked until ownership is confirmed." + "started_at": { + "type": "string", + "format": "date-time", + "description": "When the incident started (ISO 8601, UTC).", + "example": "2026-10-13T08:55:00Z" }, - "name_match": { - "type": "object", + "resolved_at": { + "type": "string", + "format": "date-time", "nullable": true, - "description": "The name comparison that decided the transfer (score and strictness). Null until one ran. The compared names are masked together with the parties." - }, - "data_match": { - "$ref": "#/components/schemas/TravelRuleDataMatch" - }, - "mismatched_fields": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Shortcut for `data_match.mismatched_fields`. Empty when nothing differs." - }, - "data_unmasked": { - "$ref": "#/components/schemas/TravelRuleDataUnmasked" + "description": "When the service recovered (ISO 8601, UTC). `null` while the incident is ongoing." }, - "data_masked": { - "type": "boolean", - "description": "True while the counterparty's personal data is masked in this response." + "duration_minutes": { + "type": "integer", + "nullable": true, + "description": "Whole minutes from `started_at` to `resolved_at`. `null` while the incident is ongoing." + } + } + }, + "TravelRuleJoinInvitation": { + "type": "object", + "description": "What the join page shows before anything is typed.", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "The address the invitation went to." }, - "metadata": { - "type": "object" + "vasp_name": { + "type": "string" }, - "direction": { + "source": { "type": "string", - "enum": [ - "INBOUND", - "OUTBOUND" - ], - "description": "Which side of the exchange this transfer is." + "description": "Where the invitation came from, for example `pickup`." }, - "rail": { + "expires_at": { "type": "string", - "nullable": true, - "description": "The rail the transfer went over: `INTERNAL`, `TRP`, `GTR`, `TRUST`, `VERIFYVASP`, `SYGNA` or `EMAIL` (`TEST_VASP` on a sandbox application, when the Didit Test VASP answered), or `null` until a rail is chosen." + "format": "date-time", + "nullable": true }, - "counterparty_vasp": { + "prefill": { "type": "object", - "nullable": true, "properties": { - "name": { + "vasp_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "legal_name": { "type": "string" }, "lei": { "type": "string" }, - "dd_score": { - "type": "integer", - "description": "Due-diligence score from 0 to 100, where higher is better. Same scale as in the VASP directory." + "jurisdiction": { + "type": "string" }, - "dd_score_breakdown": { - "type": "object", - "description": "Per-component contributions to dd_score, one object per component as in the VASP directory (jurisdiction, verification, protocol, lei, history, essentials, responsiveness), each {points, max, reason, detail}. {} until the counterparty has been assessed.", - "additionalProperties": { - "type": "object", - "properties": { - "points": { - "type": "number", - "description": "Points awarded for this component." - }, - "max": { - "type": "integer", - "description": "Maximum points this component can award." - }, - "reason": { - "type": "string", - "description": "Machine-readable code of the component's verdict." - }, - "detail": { - "type": "string", - "description": "Human-readable reason for the awarded points." - } - } + "domains": { + "type": "array", + "items": { + "type": "string" } - }, - "dd_assessed_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the due-diligence assessment last ran. Null until the counterparty has been assessed." - } - }, - "description": "The counterparty's directory entry, or `null` when the transfer links none." - }, - "delivery": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/TravelRuleDelivery" } - ], - "description": "The newest message the transfer owes another party. `null` when it has owed none, for example below `threshold_amount` or for a declared self-hosted wallet." - }, - "discovery": { - "type": "array", - "description": "How Didit looked for the counterparty, one entry per step that ran. Empty when no discovery ran, for example on an inbound transfer.", - "items": { - "$ref": "#/components/schemas/TravelRuleDiscoveryStep" } - }, - "reason": { + } + } + }, + "TravelRuleJoinRegisterRequest": { + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { "type": "string", - "description": "Why a `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE` transfer is stuck; `\"\"` for any other status. A failed delivery decides it first, on `COUNTERPARTY_VASP_NOT_REACHABLE` only: `email_undeliverable` (the mail provider refused the compliance email and no address is left to try: when it was sent, on a reminder, or as a permanent bounce reported later) or `delivery_failed` (the rail that was delivering failed for good: a network rail refused the message or ran out of retries, a `TRP` identity host did not answer, or the counterparty's `TRP` host did not accept the inquiry). Otherwise the `discovery` trace decides it: `awaiting_end_user` (no step named a counterparty Didit can reach), `directory_match_no_route` (a step named a directory VASP that has no route), `picked_member_without_proven_wallet` (the end user picked a Didit customer that has not proven control of the wallet), or `no_directory_match` when the trace gives no other reason. A resend moves the transfer on, so the reason clears. See [Why a transfer is stuck](/transaction-monitoring/travel-rule#why-a-transfer-is-stuck)." + "format": "email", + "maxLength": 254, + "description": "Must be the address the invitation went to." }, - "next_actions": { - "type": "array", - "description": "What moves the transfer on; `[]` for any status but `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE`. `email_undeliverable` offers `update_compliance_email`, then `resend`. `delivery_failed` offers `resend`. A discovery reason offers `select_counterparty_vasp`, `declare_self_hosted`, `add_vasp_hint` and `resend`.", - "items": { - "$ref": "#/components/schemas/TravelRuleNextAction" - } + "password": { + "type": "string", + "minLength": 8, + "maxLength": 128 + } + } + }, + "TravelRuleJoinVerifyRequest": { + "type": "object", + "required": [ + "email", + "code" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "maxLength": 254 }, - "chain_details": { - "$ref": "#/components/schemas/TravelRuleChainDetails" + "code": { + "type": "string", + "minLength": 6, + "maxLength": 6, + "description": "The 6-character code emailed after registering." } } }, - "NetworkListCounts": { + "TravelRuleJoinCredentials": { "type": "object", - "description": "Network counts for the list's views and filter options, each network counted once. Returned only when `summary` is not `false`. `scopes` count every network visible to the application. `views` count the visible networks inside the selected `scope`. `facets` count the networks in the selected `scope` and `status` view (dismissed networks are left out unless `status` includes them). The other list filters do not change these counts.", + "description": "The new free organization and the credentials its application calls the API with.", "properties": { - "scopes": { + "organization": { "type": "object", - "description": "Visible networks by product.", "properties": { - "all": { - "type": "integer" - }, - "identity": { - "type": "integer", - "description": "Every other network." - }, - "transactions": { - "type": "integer", - "description": "Networks whose members are all transaction parties." + "uuid": { + "type": "string", + "format": "uuid" } } }, - "views": { + "application": { "type": "object", - "description": "Visible networks inside the selected `scope`, by status, plus two saved views.", "properties": { - "all": { - "type": "integer" + "uuid": { + "type": "string", + "format": "uuid" + } + } + }, + "client_id": { + "type": "string" + }, + "api_key": { + "type": "string", + "description": "Send as the `x-api-key` header on the workspace endpoints." + } + } + }, + "TravelRuleWorkspaceClaimRequest": { + "type": "object", + "required": [ + "vasp_id" + ], + "properties": { + "vasp_id": { + "type": "string", + "format": "uuid", + "description": "The directory entry this workspace is. Read ids from `GET /v3/travel-rule/vasps/`." + } + } + }, + "TravelRuleWorkspaceDomainConfirmRequest": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string", + "minLength": 6, + "maxLength": 6 + } + } + }, + "TravelRuleWorkspaceBillingRequest": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Only `true` is accepted: outbound billing cannot be turned back off through the API." + } + } + }, + "TravelRuleWorkspaceDomainCodeResponse": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "unverified", + "code_sent", + "verified" + ] + }, + "txt": { + "$ref": "#/components/schemas/TravelRuleWorkspaceTxtRecord" + } + } + }, + "TravelRuleWorkspaceTxtRecord": { + "type": "object", + "description": "The TXT record to publish for the DNS proof. Empty until a proof has been started.", + "properties": { + "host": { + "type": "string", + "example": "didit-tr-verify.example.com" + }, + "value": { + "type": "string" + } + } + }, + "TravelRuleWorkspace": { + "type": "object", + "description": "The free Travel Rule workspace of an application that joined free.", + "properties": { + "joined_free": { + "type": "boolean" + }, + "work_email": { + "type": "string", + "format": "email" + }, + "joined_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "claim": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "unclaimed", + "pending_review", + "claimed", + "rejected" + ] }, - "active": { - "type": "integer" + "proved_by_domain": { + "type": "boolean" }, - "in_review": { - "type": "integer" + "claimed_at": { + "type": "string", + "format": "date-time", + "nullable": true }, - "resolved": { - "type": "integer" + "reviewed_at": { + "type": "string", + "format": "date-time", + "nullable": true }, - "dismissed": { - "type": "integer" + "review_note": { + "type": "string" }, - "mine": { - "type": "integer", - "description": "Networks assigned to the calling console member. Always 0 for an API key." + "vasp_id": { + "type": "string", + "format": "uuid", + "nullable": true }, - "high_risk": { - "type": "integer", - "description": "Networks with a risk score of 80 or more that are not dismissed." + "vasp_name": { + "type": "string" } } }, - "facets": { + "prefill": { + "type": "object", + "description": "Profile values taken from the directory entry: `vasp_id`, `legal_name`, `lei`, `jurisdiction`, `compliance_email`, `domains`. Nothing is written until the claim is made." + }, + "domain": { "type": "object", - "description": "Counts for each filter option, inside the selected `scope` and status view.", "properties": { - "total": { - "type": "integer", - "description": "Networks in the selected `scope` and status view." - }, - "last_activity_days": { - "type": "object", - "description": "Networks with activity in the last 7, 30 and 90 days, keyed by the number of days.", - "properties": { - "7": { - "type": "integer" - }, - "30": { - "type": "integer" - }, - "90": { - "type": "integer" - } - } - }, - "risk_min": { - "type": "object", - "description": "Networks with a risk score of at least 50 and at least 80, keyed by the threshold.", - "properties": { - "50": { - "type": "integer" - }, - "80": { - "type": "integer" - } - } - }, - "signal_types": { - "type": "object", - "description": "Networks with at least one signal of each type.", - "properties": { - "ip_address": { - "type": "integer" - }, - "device": { - "type": "integer" - }, - "face": { - "type": "integer" - }, - "document_number": { - "type": "integer" - }, - "phone": { - "type": "integer" - }, - "email": { - "type": "integer" - }, - "address": { - "type": "integer" - } - } - }, - "priority": { - "type": "object", - "description": "Networks by priority. `none` counts networks without a priority.", - "properties": { - "low": { - "type": "integer" - }, - "medium": { - "type": "integer" - }, - "high": { - "type": "integer" - }, - "none": { - "type": "integer" - } - } + "domain": { + "type": "string" }, - "assignee": { - "type": "object", - "description": "Networks by assignee.", - "properties": { - "me": { - "type": "integer", - "description": "Assigned to the calling console member. Always 0 for an API key." - }, - "unassigned": { - "type": "integer", - "description": "Assigned to nobody." - }, - "users": { - "type": "array", - "description": "One entry per assignee, most networks first.", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "full_name": { - "type": "string", - "description": "The assignee's name, or their email when no name is set." - }, - "email": { - "type": "string" - }, - "count": { - "type": "integer" - } - } - } - } - } + "status": { + "type": "string", + "enum": [ + "unverified", + "code_sent", + "verified" + ] }, - "has_case": { - "type": "integer", - "description": "Networks this application opened a case for." + "verified_at": { + "type": "string", + "format": "date-time", + "nullable": true }, - "min_size": { - "type": "object", - "description": "Networks with at least 10 members, keyed by the minimum size.", - "properties": { - "10": { - "type": "integer" - } - } + "txt": { + "$ref": "#/components/schemas/TravelRuleWorkspaceTxtRecord" } } - } - } - }, - "NetworkTag": { - "type": "object", - "description": "One of your application's tags on the network.", - "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "The tag's UUID, the same value you pass as `tag_uuid`." - }, - "name": { - "type": "string" }, - "color": { - "type": "string", - "description": "Hex color, for example `#6366F1`." + "billing_enabled": { + "type": "boolean", + "description": "Receiving Travel Rule data is free. Sending is billed only after billing is enabled." }, - "source": { + "billing_enabled_at": { "type": "string", - "enum": [ - "didit", - "custom" - ], - "description": "`didit` for a Didit preset tag, `custom` for a tag your team created." + "format": "date-time", + "nullable": true }, - "added_by": { - "type": "string", - "nullable": true, - "description": "Email of the console member who added the tag. Null when an API key added it." + "didit_network": { + "type": "object", + "description": "Whether the Didit network lists this VASP and, when not, each condition it still misses." }, - "added_at": { - "type": "string", - "format": "date-time" + "settings": { + "$ref": "#/components/schemas/TravelRuleSettingsDetail" } } }, - "NetworkOutcomes": { + "InvestigatorDecision": { "type": "object", - "description": "Where the network's people and businesses stand. Each member you may see counts once, by the status of their most recent verification that reached a decision. Transaction parties are not counted.", + "description": "How the item left the Investigator queue. `decided_by`, `original_outcome` and `overrides` are returned by the accept and override endpoints and the report; the `investigator` block of a session decision carries the other fields.", "properties": { - "approved": { - "type": "integer", - "description": "Members whose most recent decided verification was approved." + "outcome": { + "type": "string", + "enum": [ + "accepted", + "edited", + "overridden", + "decided_elsewhere", + "automated" + ], + "description": "`accepted` or `edited` took the recommendation (`edited` with a rewritten narrative), `overridden` took another action, `decided_elsewhere` is a decision taken outside the Investigator while the report was open, `automated` is a decision the Investigator took on its own." + }, + "action": { + "type": "string", + "enum": [ + "approve", + "decline", + "escalate", + "request" + ] }, - "declined": { - "type": "integer", - "description": "Members whose most recent decided verification was declined." + "actor_email": { + "type": "string", + "nullable": true }, - "in_review": { - "type": "integer", - "description": "Members whose most recent decided verification is in review." + "actor_name": { + "type": "string", + "nullable": true }, - "total": { - "type": "integer", - "description": "Members with at least one verification, decided or not. A member whose verifications are all undecided (not started, in progress, expired or abandoned) counts here and in no other key." - } - } - }, - "NetworkPatternScore": { - "type": "object", - "description": "How alike one kind of the members' data is across the whole network. A pattern is not a signal: no shared value links the members.", - "properties": { - "type": { + "override_category": { "type": "string", "enum": [ - "background" + "missing_context", + "needs_information", + "policy_differs", + "evidence_misread", + "other", + "" ], - "description": "`background`: the mean similarity of every pair of member selfie backgrounds, with the person masked out. It reads the newest selfie of each person, at most the 50 newest, and leaves out age assurance sessions. It covers the whole network, not only the members of your applications." + "nullable": true }, - "score": { - "type": "number", - "nullable": true, - "minimum": 0, - "maximum": 1, - "description": "From 0 to 1, rounded to 4 decimals. Null when `reason` is set." + "override_reason": { + "type": "string", + "nullable": true }, - "members": { - "type": "integer", - "nullable": true, - "description": "How many members' selfies the score compared. A selfie that cannot be read, or shows too little background, is not compared. Null while `computing`; 0 or 1 with `not_enough_selfies`." + "final_status": { + "type": "string", + "description": "The item's status after the decision." }, - "reason": { + "agreed": { + "type": "boolean", + "description": "Whether the decision matched the recommendation.", + "nullable": true + }, + "decided_at": { "type": "string", - "enum": [ - "computing", - "not_enough_selfies" - ], - "nullable": true, - "description": "Why there is no score. `computing`: the score for the current members is being computed; read the network again later. `not_enough_selfies`: fewer than two members have a usable selfie. Null when there is a score." + "format": "date-time" }, - "computed_at": { + "billed": { + "type": "boolean", + "description": "The review was charged." + }, + "decided_by": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the score was computed. Null while `computing`." - } - } - }, - "NetworkDetailResponse": { - "description": "The network detail, plus the organization's sharing mode, whether cross-organization signals apply to it, your application's tags on the network, member outcomes and pattern scores.", - "allOf": [ - { - "$ref": "#/components/schemas/NetworkDetail" + "enum": [ + "investigator", + "member" + ] }, - { + "original_outcome": { "type": "object", + "description": "The decision the Investigator took on its own, unchanged by later overrides. Null for a decision the team took.", "properties": { - "sharing_mode": { - "type": "string", - "enum": [ - "off", - "private", - "global" - ], - "description": "The organization's cross-organization sharing mode. Only `global` compares networks across organizations; `private` and `off` keep them to the organization's own history.", - "example": "global" + "action": { + "type": "string" }, - "cross_org_available": { - "type": "boolean", - "description": "Whether cross-organization signals apply to this organization. They need both the Identity Enterprise plan and the `global` sharing mode.", - "example": true + "final_status": { + "type": "string" }, - "cross_org_unavailable_reason": { + "decided_at": { "type": "string", - "enum": [ - "plan", - "sharing_mode" - ], - "nullable": true, - "description": "Why cross-organization signals do not apply, checked in this order. `plan`: the organization's Identity plan does not include the cross-organization fraud network (it needs Identity Enterprise), whatever the sharing mode. `sharing_mode`: the plan includes it, but the sharing mode is `private` or `off`. Null when `cross_org_available` is `true`.", - "example": null - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NetworkTag" - }, - "description": "Your application's tags on the network, oldest first. Each application sees only its own tags. `fraud_tags` is separate and unchanged: the tags the network's members carry." - }, - "outcomes": { - "$ref": "#/components/schemas/NetworkOutcomes" - }, - "patterns": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NetworkPatternScore" - }, - "description": "Pattern scores over the whole network. Today one item, `background`." + "format": "date-time" } - } - } - ] - }, - "NetworkTagRequest": { - "type": "object", - "required": [ - "tag_uuid" - ], - "properties": { - "tag_uuid": { - "type": "string", - "format": "uuid", - "description": "UUID of one of your application's tags." - } - }, - "example": { - "tag_uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7" - } - }, - "NetworkTimeline": { - "title": "Default timeline (no contract)", - "type": "object", - "description": "Without `contract`: the network's own events, oldest first, not paginated.", - "properties": { - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NetworkTimelineEvent" - } - } - } - }, - "NetworkTimelinePage": { - "title": "Typed timeline page (contract=2)", - "type": "object", - "description": "With `contract=2`: one page of typed events, newest first.", - "properties": { - "results": { + }, + "nullable": true + }, + "overrides": { "type": "array", + "description": "Every override of a decision the Investigator took on its own, oldest first.", "items": { - "$ref": "#/components/schemas/NetworkTypedTimelineEvent" + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "actor_email": { + "type": "string", + "nullable": true + }, + "actor_name": { + "type": "string", + "nullable": true + }, + "override_category": { + "type": "string" + }, + "override_reason": { + "type": "string" + }, + "previous_status": { + "type": "string" + }, + "final_status": { + "type": "string" + }, + "agreed": { + "type": "boolean", + "nullable": true + }, + "overridden_at": { + "type": "string", + "format": "date-time" + } + } } - }, - "next_cursor": { - "type": "string", - "nullable": true, - "description": "Pass as `cursor` to read the next page. Null on the last page." } } }, - "NetworkTypedTimelineEvent": { + "InvestigatorBlock": { "type": "object", - "description": "One typed timeline event: the four default fields, plus its id, when Didit stored it, the member account and record it belongs to, and the detail block of its event type. The other detail blocks are null.", + "nullable": true, + "description": "The item's latest Investigator review, or null when it never had one. Carried by the V3 session and business session decision (and so by `status.updated` webhooks).", "properties": { - "id": { + "run_id": { "type": "string", - "description": "`:`, unique within the timeline.", - "example": "session:3fa85f64-5717-4562-b3fc-2c963f66afa6" + "format": "uuid" }, - "event_type": { + "status": { "type": "string", "enum": [ - "network_detected", - "signal_observed", - "network_active", - "network_in_review", - "network_resolved", - "network_dismissed", - "verification_attempt", - "transaction", - "login", - "rule_hit", - "case_linked", - "note_added" + "running", + "completed", + "failed" ] }, - "occurred_at": { + "stage": { "type": "string", - "format": "date-time", - "description": "When it happened." + "enum": [ + "shadow", + "live" + ] }, - "ingested_at": { + "queue": { "type": "string", - "format": "date-time", - "description": "When Didit stored it." + "enum": [ + "kyc", + "kyb", + "aml", + "transactions", + "cases" + ] }, - "label": { + "is_open": { + "type": "boolean" + }, + "recommendation": { "type": "string", - "description": "A short English label, for example `Verification attempt 2 of 3`." + "enum": [ + "approve", + "decline", + "escalate", + "request" + ], + "nullable": true }, - "metadata": { - "type": "object", - "description": "For the network's own events, the same metadata as the default timeline. Empty otherwise." + "recommendation_detail": { + "type": "string", + "nullable": true }, - "account": { - "allOf": [ - { - "$ref": "#/components/schemas/NetworkTimelineAccount" - } + "confidence": { + "type": "string", + "enum": [ + "high", + "moderate", + "low" ], - "nullable": true, - "description": "The member account. Null for the network's own events, `case_linked` and `note_added`." + "nullable": true }, - "source_ref": { - "allOf": [ - { - "$ref": "#/components/schemas/NetworkTimelineSourceRef" - } - ], - "nullable": true, - "description": "The source record. Null for the network's own events and `note_added`." + "summary": { + "type": "string", + "nullable": true }, - "verification_attempt": { - "allOf": [ - { - "$ref": "#/components/schemas/NetworkTimelineAttempt" - } - ], - "nullable": true, - "description": "Set on `verification_attempt` events." + "ready_to_accept": { + "type": "boolean" }, - "transaction": { - "allOf": [ - { - "$ref": "#/components/schemas/NetworkTimelineTransaction" - } - ], - "nullable": true, - "description": "Set on `transaction` events." + "prepared_at": { + "type": "string", + "format": "date-time", + "nullable": true }, - "login": { - "allOf": [ - { - "$ref": "#/components/schemas/NetworkTimelineLogin" - } - ], - "nullable": true, - "description": "Set on `login` events." + "automated": { + "type": "boolean", + "description": "The Investigator decided on its own, even if the decision was later overridden." }, - "rule_hit": { - "allOf": [ - { - "$ref": "#/components/schemas/NetworkTimelineRuleHit" - } + "decided_by": { + "type": "string", + "enum": [ + "investigator", + "member" ], - "nullable": true, - "description": "Set on `rule_hit` events." + "nullable": true }, - "case_linked": { - "allOf": [ - { - "$ref": "#/components/schemas/NetworkTimelineCaseLinked" - } - ], + "decision": { "nullable": true, - "description": "Set on `case_linked` events." - }, - "note_added": { "allOf": [ { - "$ref": "#/components/schemas/NetworkTimelineNoteAdded" + "$ref": "#/components/schemas/InvestigatorDecision" } ], - "nullable": true, - "description": "Set on `note_added` events. Never returned to an API key." + "type": "object" } } }, - "NetworkTimelineAccount": { + "InvestigatorQueueRow": { "type": "object", - "description": "The member account the event belongs to.", "properties": { - "node_id": { + "run_id": { "type": "string", - "description": "`:`, where kind is `vendor_user`, `vendor_business` or `transaction_party`.", - "example": "vendor_user:3fa85f64-5717-4562-b3fc-2c963f66afa6" + "format": "uuid" }, - "node_type": { + "kind": { "type": "string", "enum": [ - "account", - "business_account", - "transaction_party" + "session", + "business_session", + "transaction", + "case" ] }, - "uuid": { + "queue": { + "type": "string", + "enum": [ + "kyc", + "kyb", + "aml", + "transactions", + "cases" + ] + }, + "stage": { + "type": "string", + "enum": [ + "shadow", + "live" + ] + }, + "subject": { + "type": "object", + "description": "The item under review: `id`, `number`, `status`, `name` and, depending on the kind, `country`, `workflow`, `vendor_data`, `txn_id`, `amount`, `currency`, `direction`, `title` or `priority`.", + "additionalProperties": true + }, + "recommendation": { + "type": "string", + "enum": [ + "approve", + "decline", + "escalate", + "request" + ] + }, + "recommendation_detail": { + "type": "string" + }, + "confidence": { + "type": "string", + "enum": [ + "high", + "moderate", + "low" + ] + }, + "summary": { + "type": "string" + }, + "sources_count": { + "type": "integer" + }, + "ready_to_accept": { + "type": "boolean" + }, + "guardrails": { + "description": "The locked guardrails that hold this item, if any." + }, + "waiting_since": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "due_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "prepared_at": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, + "InvestigatorRunDetail": { + "allOf": [ + { + "$ref": "#/components/schemas/InvestigatorQueueRow" + }, + { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "is_open": { + "type": "boolean" + }, + "lookups": { + "type": "integer", + "description": "How many sections and linked records it read." + }, + "duration_ms": { + "type": "integer", + "nullable": true + }, + "agent_version": { + "type": "string" + }, + "prepared_before_opened": { + "type": "boolean", + "nullable": true + }, + "report": { + "type": "object", + "additionalProperties": true, + "description": "The report: reasoning, evidence points with their verdict (`ok`, `risk`, `look`, `explained`, `context`) and cited sources, screening hit verdicts (`same`, `not_same`, `unclear`) and suggested next steps." + }, + "trail": { + "description": "What I checked: the overview, sections and records it read, in order." + }, + "decision": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/InvestigatorDecision" + } + ], + "type": "object" + }, + "autonomy": { + "type": "object", + "additionalProperties": true, + "description": "What autonomy made of the report (`result`, `reason`, `tier`, `policy_version`, `rule`, `agreement`, `decisions`, `threshold`, `at`). Null when the queue is not automated.", + "nullable": true + } + } + } + ] + }, + "InvestigatorAcceptRequest": { + "type": "object", + "properties": { + "narrative": { "type": "string", - "format": "uuid" + "maxLength": 4000, + "description": "A rewritten narrative. Sending one records the decision as edited and accepted." }, - "vendor_data": { - "type": "string", - "nullable": true + "send_email": { + "type": "boolean", + "default": false, + "description": "Email the person about the decision." }, - "display_name": { + "email_address": { "type": "string", - "nullable": true + "format": "email", + "description": "Required when `send_email` is true." }, - "ownership": { + "email_language": { "type": "string", - "enum": [ - "owned", - "organization" - ], - "description": "`owned` when the record belongs to the application you called (for a console member, to an application they were granted). `organization` when it belongs to another application of your organization." - }, - "route": { - "$ref": "#/components/schemas/NetworkTimelineRoute" + "default": "en", + "maxLength": 16 } } }, - "NetworkTimelineSourceRef": { + "InvestigatorOverrideRequest": { "type": "object", - "description": "The record the event was read from.", + "required": [ + "action", + "category", + "reason" + ], "properties": { - "kind": { + "action": { "type": "string", "enum": [ - "session", - "business_session", - "transaction", - "monitoring_event", - "case" + "approve", + "decline", + "escalate", + "request" ] }, - "id": { - "type": "string", - "format": "uuid" - }, - "ownership": { + "category": { "type": "string", "enum": [ - "owned", - "organization" - ], - "description": "`owned` when the record belongs to the application you called (for a console member, to an application they were granted). `organization` when it belongs to another application of your organization." + "missing_context", + "needs_information", + "policy_differs", + "evidence_misread", + "other" + ] }, - "route": { - "allOf": [ - { - "$ref": "#/components/schemas/NetworkTimelineRoute" - } - ], - "description": "Absent for `monitoring_event` and `case`." - } - } - }, - "NetworkTimelineRoute": { - "type": "object", - "description": "Where the record opens: its kind, its UUID and the application it belongs to.", - "properties": { - "kind": { + "reason": { "type": "string", - "description": "`user`, `business` or `transaction_party` for an account; `session`, `business_session` or `transaction` for a source record." + "minLength": 10, + "maxLength": 500 }, - "id": { + "send_email": { + "type": "boolean", + "default": false, + "description": "Email the person about the decision." + }, + "email_address": { "type": "string", - "format": "uuid" + "format": "email", + "description": "Required when `send_email` is true." }, - "application_id": { + "email_language": { "type": "string", - "format": "uuid" + "default": "en", + "maxLength": 16 } } }, - "NetworkTimelineAttempt": { + "InvestigatorBatchAcceptRequest": { "type": "object", - "description": "One verification session of the account.", + "required": [ + "run_ids" + ], "properties": { - "session_id": { - "type": "string", - "format": "uuid", - "description": "The session UUID, or the business session UUID for a company." - }, - "session_number": { - "type": "integer", - "nullable": true - }, - "attempt": { - "type": "integer", - "description": "This session's position among the account's sessions, in creation order." - }, - "attempt_count": { - "type": "integer", - "description": "How many sessions the account has." - }, - "status": { - "type": "string", - "description": "The session status, for example `Approved` or `Declined`." - }, - "status_changed_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "decline_reasons": { + "run_ids": { "type": "array", + "minItems": 1, + "maxItems": 50, "items": { - "$ref": "#/components/schemas/NetworkTimelineDeclineReason" - }, - "description": "For a declined session only: its warnings, oldest first, once per warning. Empty otherwise." + "type": "string", + "format": "uuid" + } } } }, - "NetworkTimelineDeclineReason": { + "InvestigatorBatchAcceptResponse": { "type": "object", "properties": { - "risk": { - "type": "string", - "description": "The warning code." + "accepted": { + "type": "integer" }, - "feature": { - "type": "string", - "description": "The feature that raised the warning." + "skipped": { + "type": "integer" }, - "short_description": { - "type": "string" + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "run_id": { + "type": "string", + "format": "uuid" + }, + "result": { + "type": "string", + "enum": [ + "accepted", + "skipped" + ] + }, + "reason": { + "type": "string", + "enum": [ + "not_found", + "forbidden", + "already_decided", + "not_ready", + "guardrail", + "invalid" + ] + }, + "detail": {}, + "action": { + "type": "string" + }, + "final_status": { + "type": "string" + } + } + } } } }, - "NetworkTimelineTransaction": { + "InvestigatorSettings": { "type": "object", - "description": "A transaction a member account took part in, dated by its transaction date.", "properties": { - "transaction_id": { - "type": "string", - "format": "uuid" - }, - "transaction_number": { - "type": "integer", - "nullable": true - }, - "transaction_type": { - "type": "string" - }, - "action_type": { - "type": "string" - }, - "amount": { - "allOf": [ - { - "$ref": "#/components/schemas/NetworkTimelineAmount" - } - ], - "nullable": true, - "description": "Null when the transaction has no amount." - }, - "status": { - "type": "string" - }, - "decision_reason_code": { + "stage": { "type": "string", - "nullable": true + "enum": [ + "none", + "shadow", + "live" + ] }, - "decision_reason_label": { + "stage_changed_at": { "type": "string", + "format": "date-time", + "readOnly": true, "nullable": true }, - "party_role": { - "type": "string", - "description": "The account's role in the transaction: `APPLICANT`, `REMITTER`, `BENEFICIARY` or `COUNTERPARTY`." - } - } - }, - "NetworkTimelineAmount": { - "type": "object", - "properties": { - "value": { + "stage_changed_by": { "type": "string", - "description": "Decimal amount as a string." - }, - "currency": { - "type": "string" + "readOnly": true }, - "direction": { + "monthly_spend_limit": { "type": "string", + "format": "decimal", + "description": "Monthly limit in your billing currency, 0.01 to 1,000,000. Null turns it off.", "nullable": true - } - } - }, - "NetworkTimelineLogin": { - "type": "object", - "description": "A sign-in event you posted for a person through the events API.", - "properties": { - "action": { - "type": "string", - "description": "`.`: `auth.login` or `security.login_succeeded` for a sign-in, `auth.login_failed` or `security.login_failed` for a failed one." - }, - "succeeded": { - "type": "boolean" - }, - "status": { - "type": "string", - "description": "The event's status." - }, - "ip": { - "allOf": [ - { - "$ref": "#/components/schemas/NetworkTimelineIP" - } - ], - "nullable": true, - "description": "Null when the event has no IP address." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/NetworkTimelineDevice" + "spend_limit": { + "type": "object", + "readOnly": true, + "properties": { + "enabled": { + "type": "boolean" + }, + "limit": { + "type": "string", + "format": "decimal", + "nullable": true + }, + "spent": { + "type": "string", + "format": "decimal" + }, + "reviews": { + "type": "integer" + }, + "reserved": { + "type": "string", + "format": "decimal", + "description": "The price of every open live report, held back because accepting it charges it." + }, + "currency": { + "type": "string" + }, + "paused": { + "type": "boolean", + "description": "At the limit: no new review starts." } - ], - "nullable": true, - "description": "Null when the event has no device fingerprint or context." - }, - "is_first_login": { - "type": "boolean", - "description": "The account's first successful sign-in on record." - }, - "is_new_device": { - "type": "boolean", - "description": "A successful sign-in from a device none of the account's earlier successful sign-ins came from. Never true on the first sign-in." + } } } }, - "NetworkTimelineIP": { + "TravelRuleUtxoEntry": { "type": "object", + "description": "One input or output of a UTXO transaction, as the chain recorded it.", + "required": [ + "address" + ], "properties": { "address": { - "type": "string" - }, - "country_code": { - "type": "string", - "nullable": true - }, - "city": { "type": "string", - "nullable": true + "maxLength": 128 }, - "connection_type": { + "amount": { "type": "string", "nullable": true, - "description": "The connection type label for the address, as free text." + "description": "Decimal string in the coin's own unit (BTC, not satoshis). Optional.", + "example": "0.0125" }, - "is_vpn_or_tor": { - "type": "boolean" + "txid": { + "type": "string", + "maxLength": 128, + "description": "Optional. The transaction an input spends." }, - "is_data_center": { - "type": "boolean" + "vout": { + "type": "integer", + "nullable": true, + "minimum": 0, + "description": "Optional. The output index an input spends." } } }, - "NetworkTimelineDevice": { + "TravelRuleUtxo": { "type": "object", + "description": "The inputs and outputs of a UTXO transfer (BTC, LTC, BCH, DOGE). Only accepted on those chains: on any other chain it is a `400`. From them Didit infers the originator wallet (the declared one, else the input address that contributed the most) and the beneficiary output (the declared one, else the only output that does not pay an input address back as change, else the one output paying the transfer amount). The result is on the transfer's `chain_details`.", + "required": [ + "outputs" + ], "properties": { - "fingerprint": { - "type": "string", - "nullable": true + "inputs": { + "type": "array", + "maxItems": 1000, + "items": { + "$ref": "#/components/schemas/TravelRuleUtxoEntry" + }, + "description": "Optional. Without inputs no originator is inferred and no change output is recognised." }, - "label": { - "type": "string", - "nullable": true, - "description": "A readable device label built from the event's device context." + "outputs": { + "type": "array", + "minItems": 1, + "maxItems": 1000, + "items": { + "$ref": "#/components/schemas/TravelRuleUtxoEntry" + } } } }, - "NetworkTimelineRuleHit": { + "TravelRuleChainSide": { "type": "object", - "description": "A live rule that matched a member's transaction or event. Test-mode matches are not included.", + "description": "One side's wallet as Didit resolved it from the chain data.", "properties": { - "rule_id": { - "type": "string", - "format": "uuid" - }, - "rule_title": { - "type": "string" - }, - "rule_category": { - "type": "string" - }, - "rule_source": { - "type": "string" - }, - "severity": { + "address": { "type": "string", - "nullable": true - }, - "score_delta": { - "type": "integer" + "description": "The wallet. On a memo network the classic address (an XRP X-address is split into its classic address and its tag); on Lightning the payee node id or Lightning Address." }, - "status_target": { + "memo": { "type": "string", - "nullable": true + "description": "The destination tag or memo that is part of the wallet; `\"\"` for none." }, - "action_summary": { + "source": { "type": "string", - "nullable": true + "enum": [ + "declared", + "x_address", + "largest_input", + "only_non_change_output", + "amount_match", + "invoice_payee", + "updated" + ], + "description": "Where the wallet came from: `declared` (your request), `x_address` (split from an XRP X-address), `largest_input` (the UTXO input address that contributed the most), `only_non_change_output` (the only UTXO output that is not change), `amount_match` (the one UTXO output paying the transfer amount), `invoice_payee` (the payee a Lightning payment request names), `updated` (set later with `action: update_originator_wallet`)." } } }, - "NetworkTimelineCaseLinked": { + "TravelRuleChainDetails": { "type": "object", - "description": "A case opened for the network in your organization. `source_ref.ownership` says whether it belongs to the application you called.", + "description": "How the transfer's chain names its two wallets, when that is more than one address paying another. Empty (`{}`) for an account-chain transfer (EVM, Solana, Tron), where the two addresses are the wallets. Matching, routing and the address book read the wallets from here first.", "properties": { - "case_id": { + "chain": { "type": "string", - "format": "uuid" + "description": "Canonical chain, e.g. `BTC`, `XRP`, or `LIGHTNING` for a Lightning transfer." }, - "case_number": { + "shape": { "type": "string", - "nullable": true + "enum": [ + "utxo", + "memo", + "lightning" + ] }, - "status": { - "type": "string" + "originator": { + "$ref": "#/components/schemas/TravelRuleChainSide" }, - "author_email": { - "type": "string", - "nullable": true + "beneficiary": { + "$ref": "#/components/schemas/TravelRuleChainSide" }, - "author_name": { - "type": "string", - "nullable": true - } - } - }, - "NetworkTimelineNoteAdded": { - "type": "object", - "description": "A note left on the network. The note text is not included.", - "properties": { - "note_id": { - "type": "string", - "format": "uuid" + "utxo": { + "type": "object", + "description": "On a UTXO transfer: the inputs and outputs as stored (amounts as decimal strings) and which outputs Didit read as what.", + "properties": { + "inputs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TravelRuleUtxoEntry" + } + }, + "outputs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TravelRuleUtxoEntry" + } + }, + "change_output_indexes": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "Outputs that pay an input address back." + }, + "beneficiary_output_index": { + "type": "integer", + "nullable": true, + "description": "The output that pays the beneficiary; null when none could be told apart." + } + } }, - "author_email": { - "type": "string" + "lightning": { + "type": "object", + "description": "On a Lightning transfer: what the payment request says. Always `kind` (`bolt11`, `bolt12_offer`, `bolt12_invoice`, `bolt12_invoice_request`, `lnurl`, `lightning_address`, `node_id` or `unknown`); a BOLT11 invoice adds `network`, `payee_node_id`, `payment_hash`, `amount_msat`, `created_at` and `expires_at`; a BOLT12 string `blinded_paths` and `payee_node_id`; an LNURL or a Lightning Address its `domain`. Nothing is resolved over the network: the request is decoded only.", + "additionalProperties": true + }, + "warnings": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "declared_originator_not_an_input", + "declared_beneficiary_not_an_output", + "beneficiary_output_ambiguous", + "beneficiary_output_amount_differs" + ] + }, + "description": "UTXO inconsistencies worth a look: the declared originator is not an input, the declared beneficiary is not an output, no single beneficiary output could be told apart, or the beneficiary output pays a different amount than the transaction states." + }, + "not_applicable_reason": { + "type": "string", + "enum": [ + "LIGHTNING_NO_PAYEE_IDENTIFIER", + "LIGHTNING_BOLT12_BLINDED_PAYEE", + "LIGHTNING_LNURL_NAMES_NO_PAYEE" + ], + "description": "On an outbound Lightning transfer whose payment request names no payee: why the transfer is `NOT_APPLICABLE`. `LIGHTNING_NO_PAYEE_IDENTIFIER` (the request names no payee node, or could not be decoded), `LIGHTNING_BOLT12_BLINDED_PAYEE` (the offer hides its payee behind blinded paths), `LIGHTNING_LNURL_NAMES_NO_PAYEE` (an LNURL names a service, not a payee). The same code opens the transfer's last `timeline` entry." } } }, - "FeatureStatusOverride": { + "WalletAddressImportRow": { "type": "object", - "description": "One override of a check (feature) status made with `PATCH /v3/session/{sessionId}/features/{nodeId}/update-status/`: what it changes, whether it applied or still waits for the person to finish the flow, who asked and why.", - "required": [ - "id", - "node_id", - "feature_type", - "feature_model", - "previous_status", - "new_status", - "state", - "forced", - "reason", - "requested_by", - "requested_at", - "applied_at", - "cancelled_at", - "cancel_reason" - ], + "description": "One wallet to import: by its `address`, its `address_hash`, or both. The camelCase forms of these fields are accepted too, as are `walletAddress`, `walletAddressHash`, `destinationTag` and `externalUserId`.", "properties": { - "id": { + "address": { "type": "string", - "format": "uuid", - "description": "Identifier of the override. Pass it to the cancel endpoint.", - "example": "7c9e6679-7425-40de-944b-e07fc1f90ae7" + "maxLength": 128, + "description": "The wallet address. Alias: `walletAddress`." }, - "node_id": { + "address_hash": { "type": "string", - "description": "The workflow node the request named (`default` for a step without a node).", - "example": "feature_ocr_1" + "description": "SHA-256 of the lowercased address, as 64 hex characters or 44 base64 characters. Send it alone to register a wallet without disclosing its address; sent with `address`, it must be that address's hash. Alias: `walletAddressHash`." }, - "feature_type": { + "chain": { "type": "string", - "description": "The `feature_type` the request passed, or an empty string when it passed none.", - "example": "" + "maxLength": 32 }, - "feature_model": { + "memo": { "type": "string", - "description": "The kind of check that was overridden, for example `KYC`, `Face`, `AML` or `GeolocationCheck`.", - "example": "KYC" + "maxLength": 256, + "description": "The destination tag or memo. XRP: a destination tag, a whole number from 0 to 4294967295 (a JSON number is accepted). XLM: a text memo of up to 28 bytes, a 64-bit id or a 32-byte hash in hex. EOS: up to 256 bytes. ATOM: up to 256 characters. HBAR: up to 100 bytes. Bitcoin, EVM, Solana, Tron, Litecoin, Bitcoin Cash, Dogecoin and Lightning wallets take no memo: one there is a `400`." }, - "previous_status": { + "holder_name": { "type": "string", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Resub Requested" - ], - "description": "The check's status when the override was requested.", - "example": "In Review" + "maxLength": 255 }, - "new_status": { + "holder_vendor_data": { "type": "string", - "enum": [ - "Approved", - "Declined", - "In Review" - ], - "description": "The status the override sets.", - "example": "Approved" + "maxLength": 255, + "description": "Your internal identifier for the holder. Alias: `externalUserId`." }, - "state": { + "entity_type": { "type": "string", "enum": [ - "applied", - "pending_until_flow_finishes", - "cancelled" + "individual", + "company" ], - "description": "`applied`: the check has the new status. `pending_until_flow_finishes`: the override waits and applies when the person finishes the flow. `cancelled`: it waited and never applied (see `cancel_reason`).", - "example": "applied" - }, - "forced": { - "type": "boolean", - "description": "`true` when the override applied at once while the person was still in the flow.", - "example": false - }, - "reason": { - "type": "string", - "nullable": true, - "description": "The `comment` sent with the request, or `null`.", - "example": "Document manually verified by compliance." - }, - "requested_by": { - "type": "object", - "nullable": true, - "description": "Who requested the override, or `null` when that is not known.", - "properties": { - "email": { - "type": "string", - "format": "email", - "nullable": true, - "description": "Email of the console user, or `null` for API keys and the system.", - "example": "reviewer@example.com" - }, - "name": { - "type": "string", - "nullable": true, - "description": "Display name of the requester, or `null`.", - "example": "Alex Reviewer" - }, - "type": { - "type": "string", - "enum": [ - "CONSOLE_USER", - "API_KEY", - "SYSTEM" - ], - "description": "Who made the request: a console user, an API key, or Didit itself.", - "example": "CONSOLE_USER" - } - } + "default": "individual" }, - "requested_at": { + "label": { "type": "string", - "format": "date-time", - "description": "When the override was requested.", - "example": "2026-10-13T09:15:00Z" + "maxLength": 255 }, - "applied_at": { + "owner_type": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the check took the new status, or `null` while it waits or once it is cancelled.", - "example": "2026-10-13T09:15:00Z" + "enum": [ + "customer", + "internal", + "vasp" + ], + "default": "customer" }, - "cancelled_at": { + "travel_address": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "When a waiting override was cancelled, or `null`.", - "example": null + "maxLength": 512 }, - "cancel_reason": { - "type": "string", - "nullable": true, - "enum": [ - "cancelled_by_reviewer", - "replaced", - "check_changed", - "session_closed", - null - ], - "description": "Why a waiting override never applied, or `null`. `cancelled_by_reviewer`: cancelled with the cancel endpoint. `replaced`: a newer override on the same check replaced it. `check_changed`: the person redid the check before finishing the flow. `session_closed`: the session left the flow before the person finished it (abandoned, expired, or decided mid-flow).", - "example": null + "self_declared": { + "type": "boolean", + "default": false, + "description": "Create the entry already verified through a self-declaration, when your settings allow it." } } }, - "SandboxSimulateRequest": { + "WalletAddressImportReport": { "type": "object", - "description": "Force a sandbox session's status. Every field is optional, but at least one of `new_status`, `scenario` or `risk` is required.", + "description": "What happened to each row of an import, in the order sent.", "properties": { - "new_status": { - "type": "string", - "enum": [ - "Approved", - "Declined", - "In Review", - "Expired", - "Abandoned" - ], - "description": "The status to set. When you leave it out, it defaults to the default status of the risk case (`risk`, or the first risk the scenario's magic values carry), else to the scenario's default status.", - "example": "Declined" + "total": { + "type": "integer" }, - "comment": { - "type": "string", - "maxLength": 500, - "description": "Note added to the activity entry the status change records.", - "example": "Testing the decline path" + "created": { + "type": "integer" }, - "scenario": { - "type": "string", - "maxLength": 100, - "description": "A scenario slug from the catalog (`GET /v1/sandbox/scenarios/`). It supplies the default status and, without `risk`, the warning its magic values carry. An unknown slug returns `400`.", - "example": "decline_aml_hit" + "existing": { + "type": "integer" }, - "risk": { - "type": "string", - "maxLength": 128, - "description": "A risk case from the catalog. Its warning is added to the session, with a log type that follows `new_status`. An unknown value returns `400`.", - "example": "POSSIBLE_MATCH_FOUND" + "duplicate": { + "type": "integer" }, - "node_id": { - "type": "string", - "maxLength": 128, - "description": "Workflow node the risk warning is attached to. Defaults to the session's current node.", - "example": "feature_aml_1" + "failed": { + "type": "integer" }, - "send_webhooks": { - "type": "boolean", - "default": true, - "description": "`true` sends exactly the webhooks a real status change sends to the sandbox application's destinations: `status.updated`, plus the `user.status.updated` / `user.data.updated` (or `business.status.updated` / `business.data.updated`) events the change causes, for destinations subscribed to them. `false` changes the status without logging or sending any webhook: the missed-webhook recovery never re-sends that decision, the console still updates live, and the next status change of the session sends webhooks as usual.", - "example": true + "rows": { + "type": "array", + "items": { + "type": "object", + "properties": { + "row": { + "type": "integer", + "description": "The row's number, from 1." + }, + "status": { + "type": "string", + "enum": [ + "created", + "existing", + "duplicate", + "failed" + ], + "description": "`created`: added to the book. `existing`: the wallet is already in the book and was left untouched. `duplicate`: repeats an earlier row of this import. `failed`: see `errors`." + }, + "entry_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "The address-book entry the row created or found." + }, + "errors": { + "type": "object", + "description": "Field errors of a `failed` row; empty otherwise." + }, + "duplicate_of_row": { + "type": "integer", + "nullable": true, + "description": "On a `duplicate` row, the row it repeats." + } + } + } } } }, - "SandboxSimulateResponse": { + "TravelRuleCounterpartyContact": { "type": "object", - "required": [ - "session_id", - "previous_status", - "status", - "scenario", - "risk", - "send_webhooks" - ], + "description": "A compliance address the EMAIL rail writes to for one directory entry.", "properties": { - "session_id": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "counterparty": { "type": "string", "format": "uuid", - "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + "description": "The directory entry's `id`, as GET /v3/travel-rule/vasps/ shows it." }, - "previous_status": { + "counterparty_name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "name": { + "type": "string", + "description": "Optional label, for example the team name." + }, + "source": { "type": "string", "enum": [ - "Not Started", - "In Progress", - "Approved", - "Declined", - "In Review", - "Expired", - "Abandoned", - "Kyc Expired", - "Resubmitted", - "Awaiting User" + "staff", + "application", + "learned" ], - "description": "The session's status before the simulation.", - "example": "In Progress" + "description": "staff: curated by Didit. learned: an address at one of the VASP's own domains that answered a request through its response link. application: added by your application." }, - "status": { + "scope": { "type": "string", "enum": [ - "Approved", - "Declined", - "In Review", - "Expired", - "Abandoned" + "platform", + "application" ], - "description": "The status the simulation set.", - "example": "Declined" + "description": "platform: curated by Didit or learned from a verified answer, used for every Didit customer, read-only for you. application: private to your application." }, - "scenario": { + "created_by": { "type": "string", - "nullable": true, - "description": "The scenario slug you passed, or `null`.", - "example": "decline_aml_hit" + "description": "Empty for platform contacts and for contacts an API key added." }, - "risk": { + "verified_at": { "type": "string", + "format": "date-time", "nullable": true, - "description": "The risk case applied (`risk`, or the one taken from the scenario), or `null` when none.", - "example": "POSSIBLE_MATCH_FOUND" + "description": "When this address last answered a request through its response link." }, - "send_webhooks": { + "is_invalid": { "type": "boolean", - "description": "The `send_webhooks` value the simulation applied.", - "example": false - } - } - }, - "CaseTeamSummary": { - "type": "object", - "nullable": true, - "description": "The review team that owns the case, or null when no team owns it. Routing sets it when a routing rule places the case with one of its teams. Change or clear it with `PATCH .../cases/{case_uuid}/` (`team`).", - "properties": { - "uuid": { + "description": "true once the address bounced or complained; the EMAIL rail skips it." + }, + "invalid_reason": { "type": "string", - "format": "uuid" + "enum": [ + "", + "hard_bounce", + "soft_bounce", + "spam_complaint", + "suppressed" + ] }, - "name": { - "type": "string" - } - } - }, - "CaseChecker": { - "type": "object", - "nullable": true, - "description": "The member named to approve or reject the pending resolution. Set only while a four-eyes resolution waits on a named checker. Null when nothing is pending, while the pending resolution waits on the checker queue, and once the resolution is approved, rejected, or returned for a new review because the evidence changed.", - "properties": { - "member_uuid": { + "invalid_detail": { "type": "string", - "format": "uuid", - "description": "The member's user uuid: the value you pass as `checker` when you resolve the case." + "description": "The receiving mail server's reason, when it gave one." }, - "name": { + "invalid_at": { "type": "string", - "nullable": true, - "description": "The member's full name, or null when it is not known." + "format": "date-time", + "nullable": true }, - "email": { + "created_at": { "type": "string", - "format": "email" + "format": "date-time" } } }, - "ServiceStatusFeed": { + "TravelRuleCounterpartyContactCreate": { "type": "object", - "description": "Public service status: the state of each Didit service, the overall state, and the incidents that are ongoing or were resolved in the last 7 days.", "required": [ - "status", - "updated_at", - "services", - "incidents" + "counterparty", + "email" ], "properties": { - "status": { + "counterparty": { "type": "string", - "enum": [ - "operational", - "degraded", - "down" - ], - "description": "The worst state of any service right now, decided by the ongoing incidents only.", - "example": "degraded" + "format": "uuid", + "description": "The directory entry's `id`." }, - "updated_at": { + "email": { "type": "string", - "format": "date-time", - "description": "When Didit built this response (ISO 8601, UTC). Responses are cached for 30 seconds, so it can be up to 30 seconds old.", - "example": "2026-10-13T09:41:07.512345Z" - }, - "services": { - "type": "array", - "description": "One entry per service, every service always listed.", - "items": { - "$ref": "#/components/schemas/ServiceStatusService" - } + "format": "email", + "maxLength": 254 }, - "incidents": { - "type": "array", - "description": "Newest `started_at` first. Every ongoing incident, plus the incidents resolved in the last 7 days (at most 50 resolved incidents). Empty when nothing happened in that time.", - "items": { - "$ref": "#/components/schemas/ServiceStatusIncident" - } + "name": { + "type": "string", + "description": "Optional label.", + "maxLength": 255 } } }, - "ServiceStatusService": { + "TravelRuleNameMatch": { "type": "object", - "required": [ - "key", - "name", - "status" - ], + "nullable": true, + "description": "The name comparison that decided the transfer, or null until one ran. While the parties are masked, and on the other customer's copy of an INTERNAL exchange, `transfer_name`, `compared_name`, `matched_names` and `mismatched_names` are hidden. `score` and `matched` can disagree: `matched` follows the strictness, `score` is the similarity of the two full names.", "properties": { - "key": { + "score": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "Similarity of the two full names, 0 to 100. 0 when either name is blank." + }, + "matched": { + "type": "boolean", + "description": "Whether the names satisfy `strictness`. The transfer's outcome follows `data_match`, which compares every field, so a transfer can be COUNTERPARTY_MISMATCHED_DATA while this is true." + }, + "method": { "type": "string", "enum": [ - "service_id_verification", - "service_liveness_face_match", - "service_aml_screening", - "service_kyb_registries", - "service_database_validation", - "service_phone_delivery", - "service_email_delivery", - "service_transaction_monitoring" + "none", + "exact_token", + "token_subset", + "token_containment", + "sequence_ratio" ], - "description": "Stable service key. Match on this, never on the display name. `service_id_verification`: ID verification, `service_liveness_face_match`: Liveness and face match, `service_aml_screening`: AML screening, `service_kyb_registries`: KYB registries, `service_database_validation`: Database validation, `service_phone_delivery`: Phone verification delivery, `service_email_delivery`: Email verification delivery, `service_transaction_monitoring`: Transaction monitoring.", - "example": "service_database_validation" + "description": "How `matched` was decided: none (`NONE`), exact_token (`STRICT`), token_subset (`DEFAULT`), token_containment (`CONTAINS`) or sequence_ratio (`FUZZY`)." }, - "name": { + "strictness": { "type": "string", - "description": "English display name of the service.", - "example": "Database validation" + "enum": [ + "NONE", + "STRICT", + "CONTAINS", + "DEFAULT", + "FUZZY" + ] }, - "status": { + "compared_with": { "type": "string", "enum": [ - "operational", - "degraded", - "down" + "wallet_holder", + "counterparty_vasp" ], - "description": "`operational` when no incident is ongoing for the service, otherwise the worst state of its ongoing incidents.", - "example": "degraded" - } - } - }, - "ServiceStatusIncident": { - "type": "object", - "required": [ - "id", - "service", - "service_name", - "state", - "status", - "title", - "body", - "affected", - "country_name", - "started_at", - "resolved_at", - "duration_minutes" - ], + "description": "wallet_holder: the holder recorded for the wallet. counterparty_vasp: the name the counterparty answered on the email response page, or the Didit Test VASP's answer in sandbox." + }, + "transfer_name": { + "type": "string", + "description": "The name on the transfer." + }, + "compared_name": { + "type": "string", + "description": "The name it was compared with." + }, + "matched_names": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Words of `transfer_name` found in `compared_name`." + }, + "mismatched_names": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Words of `transfer_name` missing from `compared_name`, then words of `compared_name` missing from `transfer_name`." + } + } + }, + "TravelRuleTransferRow": { + "type": "object", + "description": "One Travel Rule transfer as the transfers list shows it: VASP names, wallets and memos only, never the originator or beneficiary person data.", "properties": { "id": { "type": "string", "format": "uuid", - "description": "Incident ID.", - "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + "description": "The transfer's id." }, - "service": { + "transaction_id": { "type": "string", - "enum": [ - "service_id_verification", - "service_liveness_face_match", - "service_aml_screening", - "service_kyb_registries", - "service_database_validation", - "service_phone_delivery", - "service_email_delivery", - "service_transaction_monitoring" - ], - "description": "Key of the affected service (see `services[].key`).", - "example": "service_database_validation" + "format": "uuid", + "description": "The Didit transaction the transfer belongs to." }, - "service_name": { + "txn_id": { "type": "string", - "description": "English display name of the affected service.", - "example": "Database validation" + "nullable": true, + "description": "Your own id for the transaction. Transfers Didit books for you, such as a Didit Test VASP inbound transfer, carry a generated one." }, - "state": { + "txn_date": { + "type": "string", + "format": "date-time" + }, + "kind": { "type": "string", "enum": [ - "degraded", - "down" + "withdrawal", + "deposit", + "message" ], - "description": "How the service is affected. A resolved incident keeps the last state it had.", - "example": "degraded" + "description": "withdrawal: outbound. message: an inbound transfer a rail created from a counterparty's message (a TRP inquiry, the INTERNAL inbound leg, or a GTR, VerifyVASP or Sygna Bridge message). deposit: any other inbound transfer." + }, + "direction": { + "type": "string", + "enum": [ + "INBOUND", + "OUTBOUND" + ] }, "status": { "type": "string", "enum": [ - "ongoing", - "resolved" - ], - "description": "`ongoing` until the service recovers, then `resolved`.", - "example": "ongoing" + "AWAITING_COUNTERPARTY", + "ON_HOLD", + "COMPLETED", + "FINISHED", + "CANCELLED", + "EXPIRED", + "COUNTERPARTY_MISMATCHED_DATA", + "COUNTERPARTY_VASP_NOT_FOUND", + "COUNTERPARTY_VASP_NOT_REACHABLE", + "UNCONFIRMED_OWNERSHIP", + "COUNTERPARTY_VASP_GENERAL_DECLINE", + "NOT_ENOUGH_COUNTERPARTY_DATA", + "NOT_APPLICABLE" + ] }, - "title": { + "reason": { "type": "string", - "description": "English headline. To translate it, build your own text from `service`, `state` and `country_name`.", - "example": "Database validation is degraded in Brazil." + "description": "Why a `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE` transfer is stuck, the same value as the transfer's own `reason`, so the list tells a bounced compliance email from a counterparty with no route without opening each transfer. `\"\"` for any other status. See [Why a transfer is stuck](/transaction-monitoring/travel-rule#why-a-transfer-is-stuck)." }, - "body": { + "rail": { "type": "string", - "description": "English detail line: since when checks may be slow or fail, or when it recovered.", - "example": "Checks against Brazil (CPF) may be slow or fail since 08:55 UTC." + "nullable": true, + "enum": [ + "INTERNAL", + "TRP", + "GTR", + "TRUST", + "VERIFYVASP", + "SYGNA", + "EMAIL", + "TEST_VASP", + null + ] }, - "affected": { + "asset": { "type": "string", - "description": "Countries or registries affected, for example `Brazil (CPF)`. Empty when the incident is not tied to a country or registry.", - "example": "Brazil (CPF)" + "description": "The transaction's currency." }, - "country_name": { + "amount": { "type": "string", - "description": "Country affected, in English. Empty when the incident is not tied to a country.", - "example": "Brazil" + "description": "The amount as a plain decimal, for example \"1.25\"." }, - "started_at": { + "originator_vasp": { "type": "string", - "format": "date-time", - "description": "When the incident started (ISO 8601, UTC).", - "example": "2026-10-13T08:55:00Z" + "description": "Outbound: your VASP's legal name. Inbound: the counterparty's name, from its directory entry, the Didit network member, the TRP sender, the Didit Test VASP, or the name the transfer declared. Empty when none is known." }, - "resolved_at": { + "beneficiary_vasp": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the service recovered (ISO 8601, UTC). `null` while the incident is ongoing." + "description": "Outbound: the counterparty's name, worked out the same way. Inbound: your VASP's legal name." }, - "duration_minutes": { - "type": "integer", - "nullable": true, - "description": "Whole minutes from `started_at` to `resolved_at`. `null` while the incident is ongoing." - } - } - }, - "TravelRuleJoinInvitation": { - "type": "object", - "description": "What the join page shows before anything is typed.", - "properties": { - "email": { + "originator_wallet": { + "type": "string" + }, + "beneficiary_wallet": { + "type": "string" + }, + "originator_memo": { "type": "string", - "format": "email", - "description": "The address the invitation went to." + "description": "Memo or destination tag, empty on chains without one." }, - "vasp_name": { + "beneficiary_memo": { "type": "string" }, - "source": { + "originator_wallet_type": { "type": "string", - "description": "Where the invitation came from, for example `pickup`." + "enum": [ + "vasp", + "self_hosted", + "own" + ], + "description": "vasp, self_hosted (declared unhosted) or own (a wallet your address book records as yours). Your own side is always vasp." }, - "expires_at": { + "beneficiary_wallet_type": { + "type": "string", + "enum": [ + "vasp", + "self_hosted", + "own" + ] + }, + "matched_transaction_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "The other side of a deposit you matched to an announced transfer by hand." + }, + "deadline_at": { "type": "string", "format": "date-time", - "nullable": true + "nullable": true, + "description": "When the transfer expires if it is still waiting on its counterparty. Set on every transfer you create with `POST /v3/transactions/` that needs an exchange, outbound or inbound. Null below the threshold, on a Lightning payment the Travel Rule does not apply to, and on inbound transfers Didit creates when a counterparty reaches you (an inbound TRP inquiry, a network message or the INTERNAL inbound leg)." }, - "prefill": { - "type": "object", - "properties": { - "vasp_id": { - "type": "string", - "format": "uuid", - "nullable": true - }, - "legal_name": { - "type": "string" - }, - "lei": { - "type": "string" - }, - "jurisdiction": { - "type": "string" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - } - } + "created_at": { + "type": "string", + "format": "date-time" } } }, - "TravelRuleJoinRegisterRequest": { - "type": "object", - "required": [ - "email", - "password" - ], - "properties": { - "email": { - "type": "string", - "format": "email", - "maxLength": 254, - "description": "Must be the address the invitation went to." + "BankAccountCheckHolder": { + "description": "The name the account is checked against. Send exactly one of `full_name` and `business_name`.", + "oneOf": [ + { + "$ref": "#/components/schemas/BankAccountCheckPersonHolder" }, - "password": { - "type": "string", - "minLength": 8, - "maxLength": 128 + { + "$ref": "#/components/schemas/BankAccountCheckBusinessHolder" } - } + ] }, - "TravelRuleJoinVerifyRequest": { + "BankAccountCheckRequest": { "type": "object", "required": [ - "email", - "code" + "country", + "account", + "holder" ], "properties": { - "email": { + "country": { "type": "string", - "format": "email", - "maxLength": 254 + "minLength": 3, + "maxLength": 3, + "description": "The account's country, as an ISO 3166-1 alpha-3 code, for example `GBR`. Lowercase is accepted.", + "example": "GBR" }, - "code": { - "type": "string", - "minLength": 6, - "maxLength": 6, - "description": "The 6-character code emailed after registering." - } - } - }, - "TravelRuleJoinCredentials": { - "type": "object", - "description": "The new free organization and the credentials its application calls the API with.", - "properties": { - "organization": { + "account": { "type": "object", + "description": "The account the way its country writes it: an `iban`, or an `account_number` with the bank or branch code the country needs (`sort_code`, `routing_number`, `transit_institution`, `bsb`, `ifsc`, `bank_code` or `bic`). Mexico takes a `clabe` and Argentina a `cbu_cvu`, which already name the bank. A business account adds a `registration_id` where the country asks for one. Spaces, dashes and dots are ignored. Keys the country's check does not use are ignored; a key it needs and does not get is a `400` that names it.", "properties": { - "uuid": { + "iban": { "type": "string", - "format": "uuid" - } - } - }, - "application": { - "type": "object", - "properties": { - "uuid": { + "maxLength": 64, + "description": "The IBAN, in a country that uses IBANs. It must belong to `country`." + }, + "account_number": { "type": "string", - "format": "uuid" + "maxLength": 64, + "description": "The account number, in a country that writes the account with a separate bank or branch code." + }, + "sort_code": { + "type": "string", + "maxLength": 64, + "description": "United Kingdom: the 6-digit sort code." + }, + "routing_number": { + "type": "string", + "maxLength": 64, + "description": "United States: the 9-digit ABA routing number." + }, + "transit_institution": { + "type": "string", + "maxLength": 64, + "description": "Canada: the 8-digit transit and institution number." + }, + "bsb": { + "type": "string", + "maxLength": 64, + "description": "Australia: the 6-digit BSB." + }, + "ifsc": { + "type": "string", + "maxLength": 64, + "description": "India: the 11-character IFSC." + }, + "bank_code": { + "type": "string", + "maxLength": 64, + "description": "The bank's code, where the country needs one, for example in Nigeria." + }, + "bic": { + "type": "string", + "maxLength": 64, + "description": "The bank's BIC, where the country's check asks for one." + }, + "clabe": { + "type": "string", + "maxLength": 64, + "description": "Mexico: the 18-digit CLABE. It already names the bank." + }, + "cbu_cvu": { + "type": "string", + "maxLength": 64, + "description": "Argentina: the 22-digit CBU or CVU. It already names the bank." + }, + "registration_id": { + "type": "string", + "maxLength": 64, + "description": "A business account's registration number, where the country asks for one." } + }, + "additionalProperties": { + "type": "string", + "maxLength": 64 + }, + "example": { + "sort_code": "20-45-67", + "account_number": "31926819" } }, - "client_id": { - "type": "string" + "holder": { + "$ref": "#/components/schemas/BankAccountCheckHolder" }, - "api_key": { + "vendor_data": { "type": "string", - "description": "Send as the `x-api-key` header on the workspace endpoints." + "nullable": true, + "description": "Your own reference for this check, returned unchanged.", + "example": "payout_20481" } } }, - "TravelRuleWorkspaceClaimRequest": { + "BankAccountCheckResponse": { "type": "object", "required": [ - "vasp_id" + "request_id", + "result", + "holder_name", + "account", + "vendor_data", + "created_at" ], "properties": { - "vasp_id": { + "request_id": { "type": "string", "format": "uuid", - "description": "The directory entry this workspace is. Read ids from `GET /v3/travel-rule/vasps/`." + "description": "The ID of this check." + }, + "result": { + "type": "string", + "enum": [ + "match", + "close_match", + "no_match", + "cannot_verify" + ], + "description": "`match`: the bank holds the account under the name you sent. `close_match`: under a close name, for example a missing middle name or a typo. `no_match`: under another name, or there is no such account. `cannot_verify`: the bank answered without comparing the name, for example for a blocked or closed account." + }, + "holder_name": { + "type": "string", + "nullable": true, + "description": "Only on a `close_match`, and only when the bank returned it: the name on the account, as the bank holds it. `null` otherwise." + }, + "account": { + "type": "string", + "description": "The account, masked to its last 4 characters." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your own reference, as you sent it." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the check ran, in UTC." } } }, - "TravelRuleWorkspaceDomainConfirmRequest": { + "BankAccountCheckError": { "type": "object", "required": [ - "code" + "detail" ], "properties": { + "detail": { + "type": "string", + "description": "What went wrong, in English." + }, "code": { "type": "string", - "minLength": 6, - "maxLength": 6 - } - } - }, - "TravelRuleWorkspaceBillingRequest": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": true, - "description": "Only `true` is accepted: outbound billing cannot be turned back off through the API." + "description": "A stable code to branch on. Absent on `401` and `403`." + }, + "field": { + "type": "string", + "description": "`400` only: the body field to correct, as a dotted path, for example `account.sort_code`." + }, + "reason": { + "type": "string", + "enum": [ + "required", + "length", + "format", + "checksum", + "country_mismatch", + "rejected" + ], + "description": "`400` only: why the field was refused. `required`, `length`, `format`, `checksum` and `country_mismatch` (an IBAN of another country) are Didit's own checks; `rejected` means the bank refused the account as sent." } } }, - "TravelRuleWorkspaceDomainCodeResponse": { + "BankVerificationResult": { "type": "object", + "description": "One Bank Verification step's result, for User and Business Verification. `method` says how the account was verified, and `none` that the person continued without a bank before any attempt was evaluated. `continued_without_bank` says the person continued without a bank, also after an evaluated attempt. See [Bank verification methods](/core-technology/bank-verification/verification-methods) and [When the person cannot verify their bank](/core-technology/bank-verification/fallbacks).", "properties": { - "domain": { - "type": "string" + "schema_version": { + "type": "integer", + "description": "Version of this result shape. `1` today." + }, + "bank_verification_id": { + "type": "string", + "format": "uuid", + "description": "The ID of this result." + }, + "node_id": { + "type": "string", + "description": "The workflow node this result belongs to." }, "status": { "type": "string", "enum": [ - "unverified", - "code_sent", - "verified" - ] + "Not Finished", + "Approved", + "Declined", + "In Review", + "Expired" + ], + "description": "The step's verdict, after its actions and status rules." }, - "txt": { - "$ref": "#/components/schemas/TravelRuleWorkspaceTxtRecord" - } - } - }, - "TravelRuleWorkspaceTxtRecord": { - "type": "object", - "description": "The TXT record to publish for the DNS proof. Empty until a proof has been started.", - "properties": { - "host": { + "original_status": { "type": "string", - "example": "didit-tr-verify.example.com" + "nullable": true, + "description": "The first verdict the step reached, before any later change such as a manual review." }, - "value": { - "type": "string" - } - } - }, - "TravelRuleWorkspace": { - "type": "object", - "description": "The free Travel Rule workspace of an application that joined free.", - "properties": { - "joined_free": { - "type": "boolean" + "method": { + "type": "string", + "nullable": true, + "enum": [ + "open_banking", + "account_lookup", + "microdeposits", + "statement", + "none" + ], + "description": "How the account was verified: `open_banking` (bank sign-in: the person signed in at their bank), `account_lookup` (account check: the person typed the account details and the bank confirmed the name), `microdeposits` or `statement` (a statement upload). `none` when the person continued without a bank before any attempt was evaluated, so no account was verified. After an evaluated attempt, it stays the method of that attempt. `null` until an account is evaluated, and when the workflow narrows the data it returns. Workflow rules read the same value as `bank.method`." }, - "work_email": { + "provider": { "type": "string", - "format": "email" + "nullable": true, + "description": "The key of the route that ran the connection. Read `method`, not this key, to tell how the account was verified." }, - "joined_at": { + "credential_source": { "type": "string", - "format": "date-time", - "nullable": true + "nullable": true, + "enum": [ + "native", + "byok" + ], + "description": "`native`: Didit's own route, billed to your Didit balance. `byok`: a marketplace provider you connected with your own credentials." + }, + "route": { + "type": "object", + "nullable": true, + "description": "The route chosen before consent. It never changes afterwards.", + "properties": { + "provider_key": { + "type": "string", + "nullable": true, + "description": "The route's key." + }, + "credential_source": { + "type": "string", + "enum": [ + "native", + "byok" + ] + }, + "is_fallback": { + "type": "boolean", + "description": "Whether Didit's own route stood in for a marketplace provider that could not connect." + }, + "provider_connection_id": { + "type": "string", + "nullable": true, + "description": "The marketplace connection used, for `byok`." + }, + "requested_scopes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The data the connection asked for, for example `ownership` or `balances`." + } + } }, - "claim": { + "connection": { "type": "object", + "nullable": true, + "description": "The latest attempt's connection. `null` before the first attempt.", "properties": { "status": { "type": "string", "enum": [ - "unclaimed", - "pending_review", - "claimed", - "rejected" + "pending", + "connected", + "cancelled", + "timeout", + "failed", + "unsupported", + "revoked", + "expired" ] }, - "proved_by_domain": { - "type": "boolean" + "institution_id": { + "type": "string", + "nullable": true, + "description": "The bank's ID. `null` for an account check, where no bank is picked." }, - "claimed_at": { + "institution_name": { "type": "string", - "format": "date-time", - "nullable": true + "nullable": true, + "description": "The bank's name. `null` for an account check." }, - "reviewed_at": { + "institution_country": { + "type": "string", + "nullable": true, + "description": "The account's country, ISO 3166-1 alpha-3." + }, + "oauth": { + "type": "boolean", + "nullable": true, + "description": "Whether the bank sign-in ran over OAuth or app-to-app. `null` for an account check." + }, + "connected_at": { "type": "string", "format": "date-time", - "nullable": true + "nullable": true, + "description": "When the connection succeeded." }, - "review_note": { - "type": "string" + "consent_expires_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the bank consent ends, for bank sign-in." }, - "vasp_id": { + "last_refreshed_at": { "type": "string", - "format": "uuid", - "nullable": true + "format": "date-time", + "nullable": true, + "description": "When the financial data was last read again." }, - "vasp_name": { - "type": "string" + "attempts": { + "type": "integer", + "description": "The attempts the person spent on this step, every method together." + }, + "reason_code": { + "type": "string", + "nullable": true, + "description": "Why the connection is not simply `connected`, for example `USER_CANCELLED`, `INSTITUTION_DOWN`, `ACCOUNT_NOT_FOUND`, `REFERENCE_IDENTITY_MISSING` or `VERIFICATION_COMPLETED`." } } }, - "prefill": { - "type": "object", - "description": "Profile values taken from the directory entry: `vasp_id`, `legal_name`, `lei`, `jurisdiction`, `compliance_email`, `domains`. Nothing is written until the claim is made." - }, - "domain": { + "account": { "type": "object", + "nullable": true, + "description": "The evaluated account. `null` until an account is evaluated.", "properties": { - "domain": { - "type": "string" + "account_id_masked": { + "type": "string", + "nullable": true, + "description": "The account number, masked." }, - "status": { + "iban_masked": { + "type": "string", + "nullable": true, + "description": "The IBAN, masked." + }, + "sort_code": { + "type": "string", + "nullable": true, + "description": "The sort code, in the United Kingdom." + }, + "routing_number_masked": { + "type": "string", + "nullable": true, + "description": "The routing number, masked." + }, + "type": { "type": "string", + "nullable": true, "enum": [ - "unverified", - "code_sent", - "verified" + "checking", + "current", + "savings", + "business", + "other" ] }, - "verified_at": { + "subtype": { "type": "string", - "format": "date-time", + "nullable": true, + "description": "The bank's own account subtype." + }, + "currency": { + "type": "string", + "nullable": true, + "description": "ISO 4217 currency of the account." + }, + "holder_type": { + "type": "string", + "nullable": true, + "enum": [ + "person", + "business", + "unknown" + ] + }, + "holders_count": { + "type": "integer", "nullable": true }, - "txt": { - "$ref": "#/components/schemas/TravelRuleWorkspaceTxtRecord" + "is_joint": { + "type": "boolean", + "nullable": true + }, + "is_business": { + "type": "boolean", + "nullable": true } } }, - "billing_enabled": { - "type": "boolean", - "description": "Receiving Travel Rule data is free. Sending is billed only after billing is enabled." - }, - "billing_enabled_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "didit_network": { - "type": "object", - "description": "Whether the Didit network lists this VASP and, when not, each condition it still misses." - }, - "settings": { - "$ref": "#/components/schemas/TravelRuleSettingsDetail" - } - } - }, - "InvestigatorDecision": { - "type": "object", - "description": "How the item left the Investigator queue. `decided_by`, `original_outcome` and `overrides` are returned by the accept and override endpoints and the report; the `investigator` block of a session decision carries the other fields.", - "properties": { - "outcome": { - "type": "string", - "enum": [ - "accepted", - "edited", - "overridden", - "decided_elsewhere", - "automated" - ], - "description": "`accepted` or `edited` took the recommendation (`edited` with a rewritten narrative), `overridden` took another action, `decided_elsewhere` is a decision taken outside the Investigator while the report was open, `automated` is a decision the Investigator took on its own." - }, - "action": { - "type": "string", - "enum": [ - "approve", - "decline", - "escalate", - "request" - ] - }, - "actor_email": { - "type": "string", - "nullable": true - }, - "actor_name": { - "type": "string", - "nullable": true - }, - "override_category": { - "type": "string", - "enum": [ - "missing_context", - "needs_information", - "policy_differs", - "evidence_misread", - "other", - "" - ], - "nullable": true - }, - "override_reason": { - "type": "string", - "nullable": true - }, - "final_status": { - "type": "string", - "description": "The item's status after the decision." - }, - "agreed": { - "type": "boolean", - "description": "Whether the decision matched the recommendation.", - "nullable": true - }, - "decided_at": { - "type": "string", - "format": "date-time" - }, - "billed": { - "type": "boolean", - "description": "The review was charged." - }, - "decided_by": { - "type": "string", - "enum": [ - "investigator", - "member" - ] - }, - "original_outcome": { + "ownership": { "type": "object", - "description": "The decision the Investigator took on its own, unchanged by later overrides. Null for a decision the team took.", + "nullable": true, + "description": "How the account holder compared with the verified identity. `null` until an account is evaluated.", "properties": { - "action": { - "type": "string" + "result": { + "type": "string", + "enum": [ + "match", + "partial_match", + "no_match", + "unknown", + "not_verifiable" + ], + "description": "`partial_match` is a close match. `not_verifiable`: the bank answered with no holder to compare. `unknown`: the evidence could not be read, or there was no identity to compare." }, - "final_status": { - "type": "string" + "reference_identity_source": { + "type": "string", + "nullable": true, + "description": "The step whose identity the holder was compared against: `kyc`, `database_validation`, `expected_details`, `questionnaire`, `document_ai` or `kyb`." }, - "decided_at": { + "name_score": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "nullable": true, + "description": "Didit's holder name score." + }, + "provider_name_score": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "nullable": true, + "description": "The bank's own holder name score, when it returned one." + }, + "address_score": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "nullable": true, + "description": "The holder address score, when address matching ran." + }, + "email_score": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "nullable": true, + "description": "The holder email score, when email matching ran." + }, + "phone_score": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "nullable": true, + "description": "The holder phone score, when phone matching ran." + }, + "dob_score": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "nullable": true, + "description": "The holder date of birth score, when the bank shared one." + }, + "close_match": { + "type": "boolean", + "nullable": true, + "description": "Whether the name landed in the close-match band." + }, + "holder_confirmation": { "type": "string", - "format": "date-time" - } - }, - "nullable": true - }, - "overrides": { - "type": "array", - "description": "Every override of a decision the Investigator took on its own, oldest first.", - "items": { - "type": "object", - "properties": { - "action": { - "type": "string" - }, - "actor_email": { - "type": "string", - "nullable": true - }, - "actor_name": { - "type": "string", - "nullable": true - }, - "override_category": { - "type": "string" - }, - "override_reason": { - "type": "string" - }, - "previous_status": { - "type": "string" - }, - "final_status": { - "type": "string" - }, - "agreed": { - "type": "boolean", - "nullable": true + "nullable": true, + "enum": [ + "pending", + "confirmed", + "rejected" + ], + "description": "The person's answer when asked to confirm a close-match name. `null` when never asked." + }, + "method": { + "type": "string", + "nullable": true, + "enum": [ + "exact", + "token_set", + "nickname", + "transliteration", + "fuzzy", + "provider_only", + "microdeposits" + ], + "description": "Which name comparison produced the score. This is not the verification method: read the top-level `method` for that." + }, + "matched_holder_index": { + "type": "integer", + "nullable": true + }, + "holders": { + "type": "array", + "items": { + "type": "object" }, - "overridden_at": { - "type": "string", - "format": "date-time" - } + "description": "Each holder the bank listed, with its scores." + }, + "owner_identity_available": { + "type": "boolean", + "nullable": true + }, + "evidence_freshness": { + "type": "string", + "nullable": true, + "enum": [ + "live", + "cached", + "refreshed" + ] + }, + "evaluated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the verdict was computed." } } - } - } - }, - "InvestigatorBlock": { - "type": "object", - "nullable": true, - "description": "The item's latest Investigator review, or null when it never had one. Carried by the V3 session and business session decision (and so by `status.updated` webhooks).", - "properties": { - "run_id": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ - "running", - "completed", - "failed" - ] - }, - "stage": { - "type": "string", - "enum": [ - "shadow", - "live" - ] - }, - "queue": { - "type": "string", - "enum": [ - "kyc", - "kyb", - "aml", - "transactions", - "cases" - ] - }, - "is_open": { - "type": "boolean" - }, - "recommendation": { - "type": "string", - "enum": [ - "approve", - "decline", - "escalate", - "request" - ], - "nullable": true - }, - "recommendation_detail": { - "type": "string", - "nullable": true - }, - "confidence": { - "type": "string", - "enum": [ - "high", - "moderate", - "low" - ], - "nullable": true - }, - "summary": { - "type": "string", - "nullable": true - }, - "ready_to_accept": { - "type": "boolean" - }, - "prepared_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "automated": { - "type": "boolean", - "description": "The Investigator decided on its own, even if the decision was later overridden." }, - "decided_by": { - "type": "string", - "enum": [ - "investigator", - "member" - ], - "nullable": true + "financial_data": { + "type": "object", + "description": "The financial data the step read. Only bank sign-in reads it.", + "properties": { + "balances": { + "type": "object", + "nullable": true, + "description": "With the `balances` scope." + }, + "transactions": { + "type": "object", + "nullable": true, + "description": "With the `transactions` scope." + }, + "income": { + "type": "object", + "nullable": true, + "description": "With the `income` scope." + }, + "categorization": { + "type": "object", + "nullable": true, + "description": "Didit's categorization of the transactions read. `null` without them." + }, + "not_collected": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "balances", + "income", + "transactions" + ] + }, + "description": "The data the step asked for that the method which verified the account does not collect. Balances, transactions and income come from bank sign-in only, so an account verified by account check lists them here. Never a failure. Empty otherwise." + } + } }, - "decision": { + "billing": { + "type": "object", "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/InvestigatorDecision" + "properties": { + "route": { + "type": "string", + "enum": [ + "native", + "byok" + ] + }, + "billed": { + "type": "boolean" + }, + "marketplace_fee": { + "type": "string", + "nullable": true, + "description": "The marketplace platform fee charged, for `byok`." + }, + "usage_type": { + "type": "string", + "nullable": true, + "description": "The usage line the charge was booked on: `bank_verification` for a bank sign-in or an account check, `bank_statement_upload` for a statement upload with no charged bank attempt before it, and `bank_byok_fee` for the marketplace fee on your own provider. `null` when nothing was billed." } - ], - "type": "object" - } - } - }, - "InvestigatorQueueRow": { - "type": "object", - "properties": { - "run_id": { - "type": "string", - "format": "uuid" - }, - "kind": { - "type": "string", - "enum": [ - "session", - "business_session", - "transaction", - "case" - ] - }, - "queue": { - "type": "string", - "enum": [ - "kyc", - "kyb", - "aml", - "transactions", - "cases" - ] - }, - "stage": { - "type": "string", - "enum": [ - "shadow", - "live" - ] + } }, - "subject": { + "reference_identity": { "type": "object", - "description": "The item under review: `id`, `number`, `status`, `name` and, depending on the kind, `country`, `workflow`, `vendor_data`, `txn_id`, `amount`, `currency`, `direction`, `title` or `priority`.", - "additionalProperties": true - }, - "recommendation": { - "type": "string", - "enum": [ - "approve", - "decline", - "escalate", - "request" - ] - }, - "recommendation_detail": { - "type": "string" - }, - "confidence": { - "type": "string", - "enum": [ - "high", - "moderate", - "low" - ] - }, - "summary": { - "type": "string" - }, - "sources_count": { - "type": "integer" - }, - "ready_to_accept": { - "type": "boolean" + "nullable": true, + "description": "The identity the holder was compared against, with its source." }, - "guardrails": { - "description": "The locked guardrails that hold this item, if any." + "signals": { + "type": "object", + "nullable": true, + "description": "Account signals read from the transaction history, when the step read one." }, - "waiting_since": { - "type": "string", - "format": "date-time", - "nullable": true + "connection_abandoned": { + "type": "boolean", + "nullable": true, + "description": "Whether the step ended without a match after the person left at their bank." }, - "due_at": { - "type": "string", - "format": "date-time", - "nullable": true + "not_covered": { + "type": "boolean", + "nullable": true, + "description": "Whether the person's bank or country could not be reached on this step. Also `true` when the person continued without a bank because no method could verify their country or bank." }, - "prepared_at": { - "type": "string", - "format": "date-time", - "nullable": true - } - } - }, - "InvestigatorRunDetail": { - "allOf": [ - { - "$ref": "#/components/schemas/InvestigatorQueueRow" + "continued_without_bank": { + "type": "boolean", + "description": "Whether the person left the step with **Continue without your bank**. `continuation` then says why. `method` is `none` when no attempt was evaluated before they continued. After an evaluated attempt, such as a holder that does not match, `method` stays the method of that attempt and its verdict and reason codes stand. Returned even when the workflow narrows the data it returns." }, - { + "continuation": { "type": "object", + "nullable": true, + "description": "How the person continued without a bank. `null` when they did not. When the workflow narrows the data it returns, `reason` is still returned and every other key is `null`.", "properties": { - "status": { - "type": "string" - }, - "is_open": { - "type": "boolean" - }, - "lookups": { - "type": "integer", - "description": "How many sections and linked records it read." - }, - "duration_ms": { - "type": "integer", - "nullable": true - }, - "agent_version": { - "type": "string" + "reason": { + "type": "string", + "enum": [ + "no_route", + "institution_unsupported", + "attempts_exhausted", + "person_choice" + ], + "description": "Why, as Didit worked it out when the person continued: `no_route` (no bank sign-in or account check serves their country), `institution_unsupported` (the bank they picked cannot be used on this step), `attempts_exhausted` (every attempt was already spent and none was in progress), or `person_choice` (a way to verify was there and they chose to continue, also when their bank could not be reached right now). `no_route` and `institution_unsupported` follow `bank_unsupported_action`, `attempts_exhausted` follows `bank_attempts_exhausted_action`, and `person_choice` follows `bank_skipped_action`, unless an evaluated attempt already decided the step." }, - "prepared_before_opened": { - "type": "boolean", - "nullable": true + "country": { + "type": "string", + "nullable": true, + "description": "The country the person was verifying in, ISO 3166-1 alpha-3. `null` when they continued before picking one." }, - "report": { - "type": "object", - "additionalProperties": true, - "description": "The report: reasoning, evidence points with their verdict (`ok`, `risk`, `look`, `explained`, `context`) and cited sources, screening hit verdicts (`same`, `not_same`, `unclear`) and suggested next steps." + "offered": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "statement", + "microdeposits" + ] + }, + "nullable": true, + "description": "The other ways to verify that country offered the person when they continued, in the order it offered them: `statement` (a statement upload), then `microdeposits`. Empty when it offered none." }, - "trail": { - "description": "What I checked: the overview, sections and records it read, in order." + "institution_id": { + "type": "string", + "nullable": true, + "description": "For `institution_unsupported`, the ID of the bank the person could not use: the bank they picked, or the bank their last attempt ended on. Didit takes it from its own list of banks, never from the person's device, so a bank that list does not have is `null`. `null` for every other reason." }, - "decision": { + "institution_name": { + "type": "string", "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/InvestigatorDecision" - } - ], - "type": "object" + "description": "For `institution_unsupported`, the name of that bank, from the same list as `institution_id`. `null` when that list does not have the bank, and for every other reason." }, - "autonomy": { - "type": "object", - "additionalProperties": true, - "description": "What autonomy made of the report (`result`, `reason`, `tier`, `policy_version`, `rule`, `agreement`, `decisions`, `threshold`, `at`). Null when the queue is not automated.", - "nullable": true + "continued_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the person continued." } } - } - ] - }, - "InvestigatorAcceptRequest": { - "type": "object", - "properties": { - "narrative": { + }, + "evidence_purged_at": { "type": "string", - "maxLength": 4000, - "description": "A rewritten narrative. Sending one records the decision as edited and accepted." + "format": "date-time", + "nullable": true, + "description": "When the bank evidence was deleted under `bank_evidence_retention_days`. `null` otherwise." }, - "send_email": { - "type": "boolean", - "default": false, - "description": "Email the person about the decision." + "warnings": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Informational codes, for example `holder_declared`. A warning never changes the status on its own." }, - "email_address": { + "reason_codes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Why the verdict came out the way it did, for example `OWNER_NAME_PARTIAL`. Several may apply. `NOT_COVERED`: the person continued without a bank because no method could verify their country or bank. `SKIPPED`: the person chose to continue without a bank while a way to verify was there. Neither is added when an evaluated attempt already decided the step." + }, + "created_at": { "type": "string", - "format": "email", - "description": "Required when `send_email` is true." + "format": "date-time" }, - "email_language": { + "updated_at": { "type": "string", - "default": "en", - "maxLength": 16 + "format": "date-time" } } }, - "InvestigatorOverrideRequest": { + "BankAccountCheckPersonHolder": { "type": "object", + "description": "The person who should hold the account.", "required": [ - "action", - "category", - "reason" + "full_name" ], "properties": { - "action": { - "type": "string", - "enum": [ - "approve", - "decline", - "escalate", - "request" - ] - }, - "category": { - "type": "string", - "enum": [ - "missing_context", - "needs_information", - "policy_differs", - "evidence_misread", - "other" - ] - }, - "reason": { - "type": "string", - "minLength": 10, - "maxLength": 500 - }, - "send_email": { - "type": "boolean", - "default": false, - "description": "Email the person about the decision." - }, - "email_address": { - "type": "string", - "format": "email", - "description": "Required when `send_email` is true." - }, - "email_language": { + "full_name": { "type": "string", - "default": "en", - "maxLength": 16 + "maxLength": 255, + "description": "The full name of the person who should hold the account.", + "example": "Maria Oliveira" } } }, - "InvestigatorBatchAcceptRequest": { + "BankAccountCheckBusinessHolder": { "type": "object", + "description": "The company that should hold the account. Checks a business account.", "required": [ - "run_ids" + "business_name" ], "properties": { - "run_ids": { - "type": "array", - "minItems": 1, - "maxItems": 50, - "items": { - "type": "string", - "format": "uuid" - } + "business_name": { + "type": "string", + "maxLength": 255, + "description": "The name of the company that should hold the account. Checks a business account." } } }, - "InvestigatorBatchAcceptResponse": { + "BankVerificationPricing": { "type": "object", + "description": "Public Bank Verification list prices: the statement upload, the own provider key fee and each country's methods.", + "required": [ + "currency", + "statement_upload", + "own_provider_fee_usd", + "countries" + ], "properties": { - "accepted": { - "type": "integer" + "currency": { + "type": "string", + "enum": [ + "USD" + ], + "description": "Currency of every price in the response.", + "example": "USD" }, - "skipped": { - "type": "integer" + "statement_upload": { + "$ref": "#/components/schemas/BankStatementUploadPricing" }, - "results": { - "type": "array", - "items": { - "type": "object", - "properties": { - "run_id": { - "type": "string", - "format": "uuid" - }, - "result": { - "type": "string", - "enum": [ - "accepted", - "skipped" - ] - }, - "reason": { - "type": "string", - "enum": [ - "not_found", - "forbidden", - "already_decided", - "not_ready", - "guardrail", - "invalid" - ] - }, - "detail": {}, - "action": { - "type": "string" - }, - "final_status": { - "type": "string" - } - } + "own_provider_fee_usd": { + "type": "string", + "description": "Fee for each billable ownership check on a route that uses your own provider contract instead of Didit's, charged whatever the result. A cancelled, timed out or abandoned connection is not billed. Decimal string with 4 decimals, in USD.", + "example": "0.0200" + }, + "countries": { + "type": "object", + "description": "Every country, keyed by ISO 3166-1 alpha-3 code.", + "additionalProperties": { + "$ref": "#/components/schemas/BankVerificationPricingCountry" } } } }, - "InvestigatorSettings": { + "BankStatementUploadPricing": { "type": "object", + "description": "How the bank statement upload is priced.", + "required": [ + "price_usd", + "included_after_charged_bank_attempt", + "standalone_available" + ], "properties": { - "stage": { + "price_usd": { "type": "string", - "enum": [ - "none", - "shadow", - "live" - ] + "description": "Price of a statement upload after a charged bank attempt. Decimal string with 4 decimals, in USD.", + "example": "0.0000" }, - "stage_changed_at": { - "type": "string", - "format": "date-time", - "readOnly": true, - "nullable": true + "included_after_charged_bank_attempt": { + "type": "boolean", + "description": "`true` when the upload is included at no extra charge after a charged bank attempt in the same session.", + "example": true }, - "stage_changed_by": { + "standalone_available": { + "type": "boolean", + "description": "`true` when a statement upload can run on its own, with no bank attempt first. `false` in production; sandbox sessions can test it without billing.", + "example": false + } + } + }, + "BankVerificationPricingCountry": { + "type": "object", + "description": "One country's Bank Verification methods and their list prices.", + "required": [ + "name", + "methods" + ], + "properties": { + "name": { "type": "string", - "readOnly": true + "description": "Country name in English.", + "example": "United Kingdom" }, - "monthly_spend_limit": { - "type": "string", - "format": "decimal", - "description": "Monthly limit in your billing currency, 0.01 to 1,000,000. Null turns it off.", - "nullable": true + "methods": { + "$ref": "#/components/schemas/BankVerificationPricingMethods" + } + } + }, + "BankVerificationPricingMethods": { + "type": "object", + "description": "The two Bank Verification methods in a country.", + "required": [ + "open_banking", + "account_lookup" + ], + "properties": { + "open_banking": { + "$ref": "#/components/schemas/BankSignInPricing" }, - "spend_limit": { - "type": "object", - "readOnly": true, - "properties": { - "enabled": { - "type": "boolean" - }, - "limit": { - "type": "string", - "format": "decimal", - "nullable": true - }, - "spent": { - "type": "string", - "format": "decimal" - }, - "reviews": { - "type": "integer" - }, - "reserved": { - "type": "string", - "format": "decimal", - "description": "The price of every open live report, held back because accepting it charges it." - }, - "currency": { - "type": "string" - }, - "paused": { - "type": "boolean", - "description": "At the limit: no new review starts." - } - } + "account_lookup": { + "$ref": "#/components/schemas/BankAccountCheckPricing" } } }, - "TravelRuleUtxoEntry": { + "BankSignInPricing": { "type": "object", - "description": "One input or output of a UTXO transaction, as the chain recorded it.", + "description": "Bank sign-in: the person signs in to their bank. Priced by market, the same for every bank in it.", "required": [ - "address" + "available", + "market", + "price_usd", + "add_ons" ], "properties": { - "address": { - "type": "string", - "maxLength": 128 + "available": { + "type": "boolean", + "description": "`true` only where Didit serves bank sign-in in this country in production.", + "example": true }, - "amount": { + "market": { "type": "string", "nullable": true, - "description": "Decimal string in the coin's own unit (BTC, not satoshis). Optional.", - "example": "0.0125" + "description": "The market whose price applies. `null` when not available.", + "example": "UK and EEA" }, - "txid": { + "price_usd": { "type": "string", - "maxLength": 128, - "description": "Optional. The transaction an input spends." - }, - "vout": { - "type": "integer", "nullable": true, - "minimum": 0, - "description": "Optional. The output index an input spends." + "description": "List price of one bank sign-in. `null` when not available. Decimal string with 4 decimals, in USD.", + "example": "0.7900" + }, + "add_ons": { + "type": "object", + "description": "Add-ons priced in this country, each a decimal string with 4 decimals in USD, added to the bank sign-in price. Only the add-ons the country offers are listed; empty when bank sign-in is not available.", + "properties": { + "balance": { + "type": "string", + "description": "Account balance add-on.", + "example": "0.3500" + }, + "transactions": { + "type": "string", + "description": "Transactions add-on.", + "example": "1.3000" + } + } } } }, - "TravelRuleUtxo": { + "BankAccountCheckPricing": { "type": "object", - "description": "The inputs and outputs of a UTXO transfer (BTC, LTC, BCH, DOGE). Only accepted on those chains: on any other chain it is a `400`. From them Didit infers the originator wallet (the declared one, else the input address that contributed the most) and the beneficiary output (the declared one, else the only output that does not pay an input address back as change, else the one output paying the transfer amount). The result is on the transfer's `chain_details`.", + "description": "Account check: the person types their account number and Didit checks it with the bank. Priced per country.", "required": [ - "outputs" + "available", + "price_usd" ], "properties": { - "inputs": { - "type": "array", - "maxItems": 1000, - "items": { - "$ref": "#/components/schemas/TravelRuleUtxoEntry" - }, - "description": "Optional. Without inputs no originator is inferred and no change output is recognised." + "available": { + "type": "boolean", + "description": "`true` only where Didit serves the account check in this country in production.", + "example": true }, - "outputs": { - "type": "array", - "minItems": 1, - "maxItems": 1000, - "items": { - "$ref": "#/components/schemas/TravelRuleUtxoEntry" - } + "price_usd": { + "type": "string", + "nullable": true, + "description": "List price of one account check the bank answers. `null` when not available. Decimal string with 4 decimals, in USD.", + "example": "0.4000" } } }, - "TravelRuleChainSide": { + "ImportProvider": { "type": "object", - "description": "One side's wallet as Didit resolved it from the chain data.", "properties": { - "address": { + "key": { "type": "string", - "description": "The wallet. On a memo network the classic address (an XRP X-address is split into its classic address and its tag); on Lightning the payee node id or Lightning Address." + "enum": [ + "sumsub", + "jumio", + "veriff", + "onfido", + "persona", + "metamap", + "socure", + "idnow", + "shufti", + "idenfy", + "ondato", + "trulioo", + "au10tix", + "complycube", + "other" + ] }, - "memo": { - "type": "string", - "description": "The destination tag or memo that is part of the wallet; `\"\"` for none." + "name": { + "type": "string" }, - "source": { + "mode": { "type": "string", "enum": [ - "declared", - "x_address", - "largest_input", - "only_non_change_output", - "amount_match", - "invoice_payee", - "updated" + "api_list", + "api_ids", + "export_file" ], - "description": "Where the wallet came from: `declared` (your request), `x_address` (split from an XRP X-address), `largest_input` (the UTXO input address that contributed the most), `only_non_change_output` (the only UTXO output that is not change), `amount_match` (the one UTXO output paying the transfer amount), `invoice_payee` (the payee a Lightning payment request names), `updated` (set later with `action: update_originator_wallet`)." - } - } - }, - "TravelRuleChainDetails": { - "type": "object", - "description": "How the transfer's chain names its two wallets, when that is more than one address paying another. Empty (`{}`) for an account-chain transfer (EVM, Solana, Tron), where the two addresses are the wallets. Matching, routing and the address book read the wallets from here first.", - "properties": { - "chain": { - "type": "string", - "description": "Canonical chain, e.g. `BTC`, `XRP`, or `LIGHTNING` for a Lightning transfer." - }, - "shape": { - "type": "string", - "enum": [ - "utxo", - "memo", - "lightning" - ] - }, - "originator": { - "$ref": "#/components/schemas/TravelRuleChainSide" + "description": "`api_list`: Didit lists records with the key. `api_ids`: upload the record IDs from an export. `export_file`: upload an export in Didit's format." }, - "beneficiary": { - "$ref": "#/components/schemas/TravelRuleChainSide" + "record_noun": { + "type": "string" }, - "utxo": { - "type": "object", - "description": "On a UTXO transfer: the inputs and outputs as stored (amounts as decimal strings) and which outputs Didit read as what.", - "properties": { - "inputs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TravelRuleUtxoEntry" - } - }, - "outputs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TravelRuleUtxoEntry" - } - }, - "change_output_indexes": { - "type": "array", - "items": { - "type": "integer" + "credential_fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" }, - "description": "Outputs that pay an input address back." - }, - "beneficiary_output_index": { - "type": "integer", - "nullable": true, - "description": "The output that pays the beneficiary; null when none could be told apart." + "label": { + "type": "string" + }, + "secret": { + "type": "boolean" + } } } }, - "lightning": { - "type": "object", - "description": "On a Lightning transfer: what the payment request says. Always `kind` (`bolt11`, `bolt12_offer`, `bolt12_invoice`, `bolt12_invoice_request`, `lnurl`, `lightning_address`, `node_id` or `unknown`); a BOLT11 invoice adds `network`, `payee_node_id`, `payment_hash`, `amount_msat`, `created_at` and `expires_at`; a BOLT12 string `blinded_paths` and `payee_node_id`; an LNURL or a Lightning Address its `domain`. Nothing is resolved over the network: the request is decoded only.", - "additionalProperties": true + "regions": { + "type": "array", + "items": { + "type": "string" + } }, - "warnings": { + "required_permissions": { "type": "array", "items": { + "type": "string" + } + }, + "docs_url": { + "type": "string", + "nullable": true + }, + "status_mapping": { + "type": "object", + "additionalProperties": { "type": "string", "enum": [ - "declared_originator_not_an_input", - "declared_beneficiary_not_an_output", - "beneficiary_output_ambiguous", - "beneficiary_output_amount_differs" + "Approved", + "Declined", + "In Review", + "Expired", + "Not Finished", + "Abandoned" ] - }, - "description": "UTXO inconsistencies worth a look: the declared originator is not an input, the declared beneficiary is not an output, no single beneficiary output could be told apart, or the beneficiary output pays a different amount than the transaction states." - }, - "not_applicable_reason": { - "type": "string", - "enum": [ - "LIGHTNING_NO_PAYEE_IDENTIFIER", - "LIGHTNING_BOLT12_BLINDED_PAYEE", - "LIGHTNING_LNURL_NAMES_NO_PAYEE" - ], - "description": "On an outbound Lightning transfer whose payment request names no payee: why the transfer is `NOT_APPLICABLE`. `LIGHTNING_NO_PAYEE_IDENTIFIER` (the request names no payee node, or could not be decoded), `LIGHTNING_BOLT12_BLINDED_PAYEE` (the offer hides its payee behind blinded paths), `LIGHTNING_LNURL_NAMES_NO_PAYEE` (an LNURL names a service, not a payee). The same code opens the transfer's last `timeline` entry." + } } } }, - "WalletAddressImportRow": { + "ImportJobSummary": { "type": "object", - "description": "One wallet to import: by its `address`, its `address_hash`, or both. The camelCase forms of these fields are accepted too, as are `walletAddress`, `walletAddressHash`, `destinationTag` and `externalUserId`.", + "description": "One import job of any kind, in the shape the console's Import jobs drawer reads.", "properties": { - "address": { + "id": { "type": "string", - "maxLength": 128, - "description": "The wallet address. Alias: `walletAddress`." + "format": "uuid" }, - "address_hash": { + "kind": { "type": "string", - "description": "SHA-256 of the lowercased address, as 64 hex characters or 44 base64 characters. Send it alone to register a wallet without disclosing its address; sent with `address`, it must be that address's hash. Alias: `walletAddressHash`." + "enum": [ + "sessions", + "businesses", + "users", + "status_rules", + "transactions" + ] }, - "chain": { + "source": { "type": "string", - "maxLength": 32 + "enum": [ + "file", + "api", + "provider" + ] }, - "memo": { + "provider": { "type": "string", - "maxLength": 256, - "description": "The destination tag or memo. XRP: a destination tag, a whole number from 0 to 4294967295 (a JSON number is accepted). XLM: a text memo of up to 28 bytes, a 64-bit id or a 32-byte hash in hex. EOS: up to 256 bytes. ATOM: up to 256 characters. HBAR: up to 100 bytes. Bitcoin, EVM, Solana, Tron, Litecoin, Bitcoin Cash, Dogecoin and Lightning wallets take no memo: one there is a `400`." + "nullable": true }, - "holder_name": { + "provider_name": { "type": "string", - "maxLength": 255 + "nullable": true }, - "holder_vendor_data": { + "filename": { "type": "string", - "maxLength": 255, - "description": "Your internal identifier for the holder. Alias: `externalUserId`." + "nullable": true }, - "entity_type": { + "state": { "type": "string", "enum": [ - "individual", - "company" + "ready", + "queued", + "running", + "paused", + "completed", + "errors", + "failed", + "cancelled" ], - "default": "individual" + "description": "The job's state as the console shows it; `errors` is a completed job with rejected rows." }, - "label": { + "status": { + "type": "string" + }, + "is_dry_run": { + "type": "boolean" + }, + "credential_hint": { "type": "string", - "maxLength": 255 + "nullable": true }, - "owner_type": { + "resumes_at": { "type": "string", - "enum": [ - "customer", - "internal", - "vasp" - ], - "default": "customer" + "format": "date-time", + "nullable": true }, - "travel_address": { + "last_error": { "type": "string", - "maxLength": 512 + "nullable": true }, - "self_declared": { - "type": "boolean", - "default": false, - "description": "Create the entry already verified through a self-declaration, when your settings allow it." - } - } - }, - "WalletAddressImportReport": { - "type": "object", - "description": "What happened to each row of an import, in the order sent.", - "properties": { - "total": { - "type": "integer" + "started_by": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "nullable": true + } + } }, - "created": { - "type": "integer" + "progress": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "processed": { + "type": "integer", + "format": "int64" + }, + "imported": { + "type": "integer", + "format": "int64" + }, + "updated": { + "type": "integer", + "format": "int64" + }, + "already_in_didit": { + "type": "integer", + "format": "int64" + }, + "failed": { + "type": "integer", + "format": "int64" + }, + "not_finished": { + "type": "integer", + "format": "int64" + }, + "same_person": { + "type": "integer", + "format": "int64" + } + } }, - "existing": { - "type": "integer" + "face_templates": { + "type": "object", + "nullable": true, + "properties": { + "enabled": { + "type": "boolean" + }, + "built": { + "type": "integer", + "format": "int64" + }, + "duplicates": { + "type": "integer", + "format": "int64" + }, + "blocklisted": { + "type": "integer", + "format": "int64" + } + } }, - "duplicate": { - "type": "integer" + "eta_seconds": { + "type": "integer", + "nullable": true }, - "failed": { - "type": "integer" + "created_at": { + "type": "string", + "format": "date-time" }, - "rows": { - "type": "array", - "items": { - "type": "object", - "properties": { - "row": { - "type": "integer", - "description": "The row's number, from 1." - }, - "status": { - "type": "string", - "enum": [ - "created", - "existing", - "duplicate", - "failed" - ], - "description": "`created`: added to the book. `existing`: the wallet is already in the book and was left untouched. `duplicate`: repeats an earlier row of this import. `failed`: see `errors`." - }, - "entry_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "The address-book entry the row created or found." - }, - "errors": { - "type": "object", - "description": "Field errors of a `failed` row; empty otherwise." - }, - "duplicate_of_row": { - "type": "integer", - "nullable": true, - "description": "On a `duplicate` row, the row it repeats." - } - } - } + "started_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "completed_at": { + "type": "string", + "format": "date-time", + "nullable": true } } }, - "TravelRuleCounterpartyContact": { + "FiledRegulatoryReport": { "type": "object", - "description": "A compliance address the EMAIL rail writes to for one directory entry.", + "description": "A finalized regulatory report that prevents the requested deletion.", + "required": [ + "uuid", + "report_number", + "report_type", + "case_uuid" + ], "properties": { "uuid": { - "type": "string", - "format": "uuid" - }, - "counterparty": { "type": "string", "format": "uuid", - "description": "The directory entry's `id`, as GET /v3/travel-rule/vasps/ shows it." - }, - "counterparty_name": { - "type": "string" - }, - "email": { - "type": "string", - "format": "email" - }, - "name": { - "type": "string", - "description": "Optional label, for example the team name." - }, - "source": { - "type": "string", - "enum": [ - "staff", - "application", - "learned" - ], - "description": "staff: curated by Didit. learned: an address at one of the VASP's own domains that answered a request through its response link. application: added by your application." - }, - "scope": { - "type": "string", - "enum": [ - "platform", - "application" - ], - "description": "platform: curated by Didit or learned from a verified answer, used for every Didit customer, read-only for you. application: private to your application." - }, - "created_by": { - "type": "string", - "description": "Empty for platform contacts and for contacts an API key added." + "description": "Report UUID." }, - "verified_at": { + "report_number": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "When this address last answered a request through its response link." - }, - "is_invalid": { - "type": "boolean", - "description": "true once the address bounced or complained; the EMAIL rail skips it." + "description": "Report number shown in the Console (for example, SAR-0001)." }, - "invalid_reason": { + "report_type": { "type": "string", "enum": [ - "", - "hard_bounce", - "soft_bounce", - "spam_complaint", - "suppressed" - ] - }, - "invalid_detail": { - "type": "string", - "description": "The receiving mail server's reason, when it gave one." - }, - "invalid_at": { - "type": "string", - "format": "date-time", - "nullable": true + "SAR", + "STR", + "CTR", + "TTR", + "UTR", + "IFT", + "CBR", + "TFR" + ], + "description": "Regulatory report type." }, - "created_at": { + "case_uuid": { "type": "string", - "format": "date-time" + "format": "uuid", + "description": "Case the report belongs to." } } }, - "TravelRuleCounterpartyContactCreate": { + "FiledReportTransaction": { "type": "object", + "description": "A transaction that cannot be deleted because a finalized regulatory report lists it.", "required": [ - "counterparty", - "email" + "uuid", + "transaction_id", + "reports" ], "properties": { - "counterparty": { + "uuid": { "type": "string", "format": "uuid", - "description": "The directory entry's `id`." + "description": "Didit transaction UUID." }, - "email": { + "transaction_id": { "type": "string", - "format": "email", - "maxLength": 254 + "description": "The transaction identifier supplied when the transaction was created." }, - "name": { - "type": "string", - "description": "Optional label.", - "maxLength": 255 + "reports": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FiledRegulatoryReport" + }, + "description": "Finalized reports that list this transaction." } } }, - "TravelRuleNameMatch": { + "InFiledReportError": { "type": "object", - "nullable": true, - "description": "The name comparison that decided the transfer, or null until one ran. While the parties are masked, and on the other customer's copy of an INTERNAL exchange, `transfer_name`, `compared_name`, `matched_names` and `mismatched_names` are hidden. `score` and `matched` can disagree: `matched` follows the strictness, `score` is the similarity of the two full names.", + "description": "The deletion is refused because the selected data is in a finalized regulatory report. Nothing is deleted. Delete the report, then repeat the request.", + "required": [ + "detail", + "code" + ], "properties": { - "score": { - "type": "integer", - "minimum": 0, - "maximum": 100, - "description": "Similarity of the two full names, 0 to 100. 0 when either name is blank." - }, - "matched": { - "type": "boolean", - "description": "Whether the names satisfy `strictness`. The transfer's outcome follows `data_match`, which compares every field, so a transfer can be COUNTERPARTY_MISMATCHED_DATA while this is true." - }, - "method": { - "type": "string", - "enum": [ - "none", - "exact_token", - "token_subset", - "token_containment", - "sequence_ratio" - ], - "description": "How `matched` was decided: none (`NONE`), exact_token (`STRICT`), token_subset (`DEFAULT`), token_containment (`CONTAINS`) or sequence_ratio (`FUZZY`)." - }, - "strictness": { - "type": "string", - "enum": [ - "NONE", - "STRICT", - "CONTAINS", - "DEFAULT", - "FUZZY" - ] + "detail": { + "type": "string" }, - "compared_with": { + "code": { "type": "string", "enum": [ - "wallet_holder", - "counterparty_vasp" + "in_filed_report" ], - "description": "wallet_holder: the holder recorded for the wallet. counterparty_vasp: the name the counterparty answered on the email response page, or the Didit Test VASP's answer in sandbox." - }, - "transfer_name": { - "type": "string", - "description": "The name on the transfer." - }, - "compared_name": { - "type": "string", - "description": "The name it was compared with." + "description": "Stable error code for a deletion blocked by a finalized regulatory report." }, - "matched_names": { + "reports": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/FiledRegulatoryReport" }, - "description": "Words of `transfer_name` found in `compared_name`." + "description": "Reports that block a person, company, case or session deletion." }, - "mismatched_names": { + "transactions": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/FiledReportTransaction" }, - "description": "Words of `transfer_name` missing from `compared_name`, then words of `compared_name` missing from `transfer_name`." + "description": "Transactions that block a transaction deletion, each with the reports that list it." } } }, - "TravelRuleTransferRow": { + "TravelRuleNetworkParticipantSummary": { "type": "object", - "description": "One Travel Rule transfer as the transfers list shows it: VASP names, wallets and memos only, never the originator or beneficiary person data.", + "nullable": true, + "description": "This application's own participant on the network, or null when it has none.", "properties": { - "id": { - "type": "string", - "format": "uuid", - "description": "The transfer's id." - }, - "transaction_id": { - "type": "string", - "format": "uuid", - "description": "The Didit transaction the transfer belongs to." - }, - "txn_id": { - "type": "string", - "nullable": true, - "description": "Your own id for the transaction. Transfers Didit books for you, such as a Didit Test VASP inbound transfer, carry a generated one." - }, - "txn_date": { + "mode": { "type": "string", - "format": "date-time" + "enum": [ + "platform", + "own" + ], + "description": "platform: carried by Didit's membership; own: onboarded under the application's own membership." }, - "kind": { + "status": { "type": "string", "enum": [ - "withdrawal", - "deposit", - "message" + "not_requested", + "questionnaire_pending", + "submitted_to_network", + "active", + "suspended", + "rejected" ], - "description": "withdrawal: outbound. message: an inbound transfer a rail created from a counterparty's message (a TRP inquiry, the INTERNAL inbound leg, or a GTR, VerifyVASP or Sygna Bridge message). deposit: any other inbound transfer." + "description": "not_requested: never requested, or the network refused the first request (see last_error); questionnaire_pending: waiting for the due-diligence questionnaire to be approved; submitted_to_network: handed to the network's onboarding; active: the network gave this application its own participant code; suspended / rejected: Didit suspended or rejected it." }, - "direction": { + "participant_code": { "type": "string", - "enum": [ - "INBOUND", - "OUTBOUND" - ] + "description": "The code the network gave this application (for GTR, its VASP code). Outbound messages carry this code, never Didit's own." }, - "status": { + "requested_at": { "type": "string", - "enum": [ - "AWAITING_COUNTERPARTY", - "ON_HOLD", - "COMPLETED", - "FINISHED", - "CANCELLED", - "EXPIRED", - "COUNTERPARTY_MISMATCHED_DATA", - "COUNTERPARTY_VASP_NOT_FOUND", - "COUNTERPARTY_VASP_NOT_REACHABLE", - "UNCONFIRMED_OWNERSHIP", - "COUNTERPARTY_VASP_GENERAL_DECLINE", - "NOT_ENOUGH_COUNTERPARTY_DATA", - "NOT_APPLICABLE" - ] + "format": "date-time", + "nullable": true }, - "reason": { + "activated_at": { "type": "string", - "description": "Why a `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE` transfer is stuck, the same value as the transfer's own `reason`, so the list tells a bounced compliance email from a counterparty with no route without opening each transfer. `\"\"` for any other status. See [Why a transfer is stuck](/transaction-monitoring/travel-rule#why-a-transfer-is-stuck)." + "format": "date-time", + "nullable": true }, + "last_error": { + "type": "string", + "description": "The network's answer when it refused the registration; empty otherwise." + } + } + }, + "TravelRuleNetworkState": { + "type": "object", + "description": "How Travel Rule messages on one network would be sent for this application, and why they cannot be. Configuration status, not a live health or delivery check.", + "properties": { "rail": { "type": "string", - "nullable": true, "enum": [ - "INTERNAL", - "TRP", "GTR", "TRUST", "VERIFYVASP", - "SYGNA", - "EMAIL", - "TEST_VASP", - null + "SYGNA" ] }, - "asset": { - "type": "string", - "description": "The transaction's currency." - }, - "amount": { + "mode": { "type": "string", - "description": "The amount as a plain decimal, for example \"1.25\"." + "enum": [ + "own", + "didit", + "off", + "unavailable" + ], + "description": "own: the application's connected membership is used; didit: Didit's membership carries the application as its own active participant; off: platform routing disabled in disabled_networks with no own membership; unavailable: nothing can send on this network yet (see state and reason)." }, - "originator_vasp": { + "state": { "type": "string", - "description": "Outbound: your VASP's legal name. Inbound: the counterparty's name, from its directory entry, the Didit network member, the TRP sender, the Didit Test VASP, or the name the transfer declared. Empty when none is known." + "enum": [ + "own_membership", + "live_through_didit", + "off", + "didit_onboarding", + "questionnaire_needed", + "questionnaire_in_review", + "request_access", + "request_sent", + "unavailable", + "error" + ], + "description": "own_membership: your connected membership sends; live_through_didit: your own participant on Didit's membership sends; off: you switched platform routing off; didit_onboarding: Didit's own membership of this network is not live yet, so it cannot carry anyone; questionnaire_needed / questionnaire_in_review: the due-diligence questionnaire must be submitted or is with Didit; request_access: the questionnaire is approved, request the network; request_sent: your participant is being onboarded; unavailable: see reason; error: see reason." }, - "beneficiary_vasp": { + "reason": { "type": "string", - "description": "Outbound: the counterparty's name, worked out the same way. Inbound: your VASP's legal name." + "enum": [ + "own_membership_connected", + "platform_network_switched_off", + "own_membership_only", + "sandbox_application", + "didit_membership_not_live", + "participant_suspended", + "participant_rejected", + "questionnaire_not_submitted", + "questionnaire_submitted", + "questionnaire_changes_requested", + "questionnaire_rejected", + "participant_not_requested", + "participant_active", + "participant_key_missing", + "participant_key_unresolved", + "provisioning_failed", + "questionnaire_pending", + "submitted_to_network" + ], + "description": "Machine-readable detail of state. own_membership_only: Didit does not carry applications on this network (TRUST), connect your own membership; sandbox_application: sandbox transfers never reach a network; provisioning_failed: the network refused the registration, the participant's last_error says why and requesting the network again retries." }, - "originator_wallet": { - "type": "string" + "platform_available": { + "type": "boolean", + "description": "Whether Didit's own membership of this network is configured; not a live health check." }, - "beneficiary_wallet": { - "type": "string" + "connected": { + "type": "boolean", + "description": "Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange." }, - "originator_memo": { - "type": "string", - "description": "Memo or destination tag, empty on chains without one." + "participant": { + "$ref": "#/components/schemas/TravelRuleNetworkParticipantSummary" }, - "beneficiary_memo": { - "type": "string" + "vasps_reachable": { + "type": "integer", + "description": "Directory VASPs reachable on this network." }, - "originator_wallet_type": { + "routed_30d": { + "type": "integer", + "description": "This application's transfers carried on this network in the last 30 days." + } + } + }, + "TravelRuleNetworkQuestionnaireAnswers": { + "type": "object", + "description": "Didit's network due-diligence questionnaire. Saves are partial: a save keeps every answer it does not name. Submitting requires every field except lei and data_protection_contact_email, and the three attestations true.", + "properties": { + "legal_name": { "type": "string", - "enum": [ - "vasp", - "self_hosted", - "own" - ], - "description": "vasp, self_hosted (declared unhosted) or own (a wallet your address book records as yours). Your own side is always vasp." + "maxLength": 255 }, - "beneficiary_wallet_type": { + "registration_number": { "type": "string", - "enum": [ - "vasp", - "self_hosted", - "own" - ] + "maxLength": 255 }, - "matched_transaction_id": { + "incorporation_country": { "type": "string", - "format": "uuid", - "nullable": true, - "description": "The other side of a deposit you matched to an announced transfer by hand." + "minLength": 2, + "maxLength": 2, + "description": "ISO 3166-1 alpha-2 country code." }, - "deadline_at": { + "registered_address": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the transfer expires if it is still waiting on its counterparty. Set on every transfer you create with `POST /v3/transactions/` that needs an exchange, outbound or inbound. Null below the threshold, on a Lightning payment the Travel Rule does not apply to, and on inbound transfers Didit creates when a counterparty reaches you (an inbound TRP inquiry, a network message or the INTERNAL inbound leg)." + "maxLength": 1000 }, - "created_at": { + "website": { "type": "string", - "format": "date-time" - } - } - }, - "BankAccountCheckHolder": { - "description": "The name the account is checked against. Send exactly one of `full_name` and `business_name`.", - "oneOf": [ - { - "$ref": "#/components/schemas/BankAccountCheckPersonHolder" + "format": "uri", + "maxLength": 500 }, - { - "$ref": "#/components/schemas/BankAccountCheckBusinessHolder" - } - ] - }, - "BankAccountCheckRequest": { - "type": "object", - "required": [ - "country", - "account", - "holder" - ], - "properties": { - "country": { + "lei": { "type": "string", - "minLength": 3, - "maxLength": 3, - "description": "The account's country, as an ISO 3166-1 alpha-3 code, for example `GBR`. Lowercase is accepted.", - "example": "GBR" + "maxLength": 20, + "description": "Optional. A valid ISO 17442 LEI when given." }, - "account": { - "type": "object", - "description": "The account the way its country writes it: an `iban`, or an `account_number` with the bank or branch code the country needs (`sort_code`, `routing_number`, `transit_institution`, `bsb`, `ifsc`, `bank_code` or `bic`). Mexico takes a `clabe` and Argentina a `cbu_cvu`, which already name the bank. A business account adds a `registration_id` where the country asks for one. Spaces, dashes and dots are ignored. Keys the country's check does not use are ignored; a key it needs and does not get is a `400` that names it.", - "properties": { - "iban": { - "type": "string", - "maxLength": 64, - "description": "The IBAN, in a country that uses IBANs. It must belong to `country`." - }, - "account_number": { - "type": "string", - "maxLength": 64, - "description": "The account number, in a country that writes the account with a separate bank or branch code." - }, - "sort_code": { - "type": "string", - "maxLength": 64, - "description": "United Kingdom: the 6-digit sort code." - }, - "routing_number": { - "type": "string", - "maxLength": 64, - "description": "United States: the 9-digit ABA routing number." - }, - "transit_institution": { - "type": "string", - "maxLength": 64, - "description": "Canada: the 8-digit transit and institution number." - }, - "bsb": { - "type": "string", - "maxLength": 64, - "description": "Australia: the 6-digit BSB." - }, - "ifsc": { - "type": "string", - "maxLength": 64, - "description": "India: the 11-character IFSC." - }, - "bank_code": { - "type": "string", - "maxLength": 64, - "description": "The bank's code, where the country needs one, for example in Nigeria." - }, - "bic": { - "type": "string", - "maxLength": 64, - "description": "The bank's BIC, where the country's check asks for one." - }, - "clabe": { - "type": "string", - "maxLength": 64, - "description": "Mexico: the 18-digit CLABE. It already names the bank." - }, - "cbu_cvu": { - "type": "string", - "maxLength": 64, - "description": "Argentina: the 22-digit CBU or CVU. It already names the bank." - }, - "registration_id": { - "type": "string", - "maxLength": 64, - "description": "A business account's registration number, where the country asks for one." + "licences": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "regulator", + "jurisdiction", + "licence_type", + "licence_number" + ], + "properties": { + "regulator": { + "type": "string", + "maxLength": 255 + }, + "jurisdiction": { + "type": "string", + "minLength": 2, + "maxLength": 2, + "description": "ISO 3166-1 alpha-2 country code." + }, + "licence_type": { + "type": "string", + "maxLength": 255 + }, + "licence_number": { + "type": "string", + "maxLength": 255 + } } }, - "additionalProperties": { + "description": "Every licence or registration the VASP holds." + }, + "operating_jurisdictions": { + "type": "array", + "minItems": 1, + "maxItems": 250, + "items": { "type": "string", - "maxLength": 64 - }, - "example": { - "sort_code": "20-45-67", - "account_number": "31926819" + "minLength": 2, + "maxLength": 2, + "description": "ISO 3166-1 alpha-2 country code." } }, - "holder": { - "$ref": "#/components/schemas/BankAccountCheckHolder" + "compliance_officer_name": { + "type": "string", + "maxLength": 255 }, - "vendor_data": { + "compliance_officer_email": { "type": "string", - "nullable": true, - "description": "Your own reference for this check, returned unchanged.", - "example": "payout_20481" + "format": "email" + }, + "aml_ctf_policy_attested": { + "type": "boolean", + "description": "Must be true to submit." + }, + "sanctions_screening_attested": { + "type": "boolean", + "description": "Must be true to submit." + }, + "data_protection_attested": { + "type": "boolean", + "description": "Must be true to submit." + }, + "data_protection_contact_email": { + "type": "string", + "format": "email", + "description": "Optional." + }, + "expected_monthly_transfers": { + "type": "integer", + "minimum": 0 + }, + "expected_monthly_volume_usd": { + "type": "string", + "format": "decimal", + "description": "Expected monthly volume in USD, up to 2 decimal places. Returned as a string." + }, + "assets": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "type": "string", + "maxLength": 32 + }, + "description": "Asset tickers, stored upper-case, sorted and de-duplicated." } } }, - "BankAccountCheckResponse": { + "TravelRuleNetworkQuestionnaire": { "type": "object", - "required": [ - "request_id", - "result", - "holder_name", - "account", - "vendor_data", - "created_at" - ], "properties": { - "request_id": { - "type": "string", - "format": "uuid", - "description": "The ID of this check." - }, - "result": { + "status": { "type": "string", "enum": [ - "match", - "close_match", - "no_match", - "cannot_verify" + "draft", + "submitted", + "changes_requested", + "approved", + "rejected" ], - "description": "`match`: the bank holds the account under the name you sent. `close_match`: under a close name, for example a missing middle name or a typo. `no_match`: under another name, or there is no such account. `cannot_verify`: the bank answered without comparing the name, for example for a blocked or closed account." + "description": "Answers can be saved while draft or changes_requested; submitted, approved and rejected are Didit's to move." }, - "holder_name": { + "answers": { + "$ref": "#/components/schemas/TravelRuleNetworkQuestionnaireAnswers" + }, + "submitted_at": { "type": "string", - "nullable": true, - "description": "Only on a `close_match`, and only when the bank returned it: the name on the account, as the bank holds it. `null` otherwise." + "format": "date-time", + "nullable": true }, - "account": { + "reviewed_at": { "type": "string", - "description": "The account, masked to its last 4 characters." + "format": "date-time", + "nullable": true }, - "vendor_data": { + "review_note": { "type": "string", - "nullable": true, - "description": "Your own reference, as you sent it." + "description": "Didit's note on its last decision, for example what to change." }, - "created_at": { + "history": { + "type": "array", + "readOnly": true, + "description": "Every status change, oldest first.", + "items": { + "type": "object", + "properties": { + "at": { + "type": "string", + "format": "date-time" + }, + "from": { + "type": "string" + }, + "to": { + "type": "string" + }, + "note": { + "type": "string", + "description": "Review note or the network's answer, when there is one." + } + } + } + }, + "updated_at": { "type": "string", "format": "date-time", - "description": "When the check ran, in UTC." + "nullable": true } } }, - "BankAccountCheckError": { + "TravelRuleNetworkParticipant": { "type": "object", - "required": [ - "detail" - ], + "description": "This application's own participant on one network.", "properties": { - "detail": { + "network": { "type": "string", - "description": "What went wrong, in English." + "enum": [ + "gtr", + "trust", + "verifyvasp", + "sygna" + ] }, - "code": { + "rail": { "type": "string", - "description": "A stable code to branch on. Absent on `401` and `403`." + "enum": [ + "GTR", + "TRUST", + "VERIFYVASP", + "SYGNA" + ] }, - "field": { + "mode": { "type": "string", - "description": "`400` only: the body field to correct, as a dotted path, for example `account.sort_code`." + "enum": [ + "platform", + "own" + ] }, - "reason": { + "status": { "type": "string", "enum": [ - "required", - "length", - "format", - "checksum", - "country_mismatch", + "not_requested", + "questionnaire_pending", + "submitted_to_network", + "active", + "suspended", "rejected" ], - "description": "`400` only: why the field was refused. `required`, `length`, `format`, `checksum` and `country_mismatch` (an IBAN of another country) are Didit's own checks; `rejected` means the bank refused the account as sent." - } - } - }, - "BankVerificationResult": { - "type": "object", - "description": "One Bank Verification step's result, for User and Business Verification. `method` says how the account was verified, and `none` that the person continued without a bank before any attempt was evaluated. `continued_without_bank` says the person continued without a bank, also after an evaluated attempt. See [Bank verification methods](/core-technology/bank-verification/verification-methods) and [When the person cannot verify their bank](/core-technology/bank-verification/fallbacks).", - "properties": { - "schema_version": { - "type": "integer", - "description": "Version of this result shape. `1` today." - }, - "bank_verification_id": { - "type": "string", - "format": "uuid", - "description": "The ID of this result." + "description": "not_requested: never requested, or the network refused the first request (see last_error); questionnaire_pending: waiting for the due-diligence questionnaire to be approved; submitted_to_network: handed to the network's onboarding; active: the network gave this application its own participant code; suspended / rejected: Didit suspended or rejected it." }, - "node_id": { - "type": "string", - "description": "The workflow node this result belongs to." + "participant_code": { + "type": "string" }, - "status": { + "requested_at": { "type": "string", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Expired" - ], - "description": "The step's verdict, after its actions and status rules." + "format": "date-time", + "nullable": true }, - "original_status": { - "type": "string", - "nullable": true, - "description": "The first verdict the step reached, before any later change such as a manual review." + "submitted_at": { + "type": "string", + "format": "date-time", + "nullable": true }, - "method": { + "activated_at": { "type": "string", - "nullable": true, - "enum": [ - "open_banking", - "account_lookup", - "microdeposits", - "statement", - "none" - ], - "description": "How the account was verified: `open_banking` (bank sign-in: the person signed in at their bank), `account_lookup` (account check: the person typed the account details and the bank confirmed the name), `microdeposits` or `statement` (a statement upload). `none` when the person continued without a bank before any attempt was evaluated, so no account was verified. After an evaluated attempt, it stays the method of that attempt. `null` until an account is evaluated, and when the workflow narrows the data it returns. Workflow rules read the same value as `bank.method`." + "format": "date-time", + "nullable": true }, - "provider": { + "suspended_at": { "type": "string", - "nullable": true, - "description": "The key of the route that ran the connection. Read `method`, not this key, to tell how the account was verified." + "format": "date-time", + "nullable": true }, - "credential_source": { + "last_error": { "type": "string", - "nullable": true, - "enum": [ - "native", - "byok" - ], - "description": "`native`: Didit's own route, billed to your Didit balance. `byok`: a marketplace provider you connected with your own credentials." + "description": "The network's answer when it refused the registration; empty otherwise." }, - "route": { - "type": "object", - "nullable": true, - "description": "The route chosen before consent. It never changes afterwards.", - "properties": { - "provider_key": { - "type": "string", - "nullable": true, - "description": "The route's key." - }, - "credential_source": { - "type": "string", - "enum": [ - "native", - "byok" - ] - }, - "is_fallback": { - "type": "boolean", - "description": "Whether Didit's own route stood in for a marketplace provider that could not connect." - }, - "provider_connection_id": { - "type": "string", - "nullable": true, - "description": "The marketplace connection used, for `byok`." - }, - "requested_scopes": { - "type": "array", - "items": { + "history": { + "type": "array", + "readOnly": true, + "description": "Every status change, oldest first.", + "items": { + "type": "object", + "properties": { + "at": { + "type": "string", + "format": "date-time" + }, + "from": { "type": "string" }, - "description": "The data the connection asked for, for example `ownership` or `balances`." + "to": { + "type": "string" + }, + "note": { + "type": "string", + "description": "Review note or the network's answer, when there is one." + } } } + } + } + }, + "TravelRuleNetworksOverview": { + "type": "object", + "properties": { + "platform_networks_enabled": { + "type": "boolean", + "description": "Whether network access through Didit's membership is open. While false, questionnaire saves, submissions and network requests answer 403." }, - "connection": { - "type": "object", - "nullable": true, - "description": "The latest attempt's connection. `null` before the first attempt.", - "properties": { - "status": { - "type": "string", - "enum": [ - "pending", - "connected", - "cancelled", - "timeout", - "failed", - "unsupported", - "revoked", - "expired" - ] - }, - "institution_id": { - "type": "string", - "nullable": true, - "description": "The bank's ID. `null` for an account check, where no bank is picked." - }, - "institution_name": { - "type": "string", - "nullable": true, - "description": "The bank's name. `null` for an account check." - }, - "institution_country": { - "type": "string", - "nullable": true, - "description": "The account's country, ISO 3166-1 alpha-3." - }, - "oauth": { - "type": "boolean", - "nullable": true, - "description": "Whether the bank sign-in ran over OAuth or app-to-app. `null` for an account check." - }, - "connected_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the connection succeeded." - }, - "consent_expires_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the bank consent ends, for bank sign-in." - }, - "last_refreshed_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the financial data was last read again." - }, - "attempts": { - "type": "integer", - "description": "The attempts the person spent on this step, every method together." - }, - "reason_code": { - "type": "string", - "nullable": true, - "description": "Why the connection is not simply `connected`, for example `USER_CANCELLED`, `INSTITUTION_DOWN`, `ACCOUNT_NOT_FOUND`, `REFERENCE_IDENTITY_MISSING` or `VERIFICATION_COMPLETED`." - } - } + "questionnaire": { + "$ref": "#/components/schemas/TravelRuleNetworkQuestionnaire" }, - "account": { - "type": "object", - "nullable": true, - "description": "The evaluated account. `null` until an account is evaluated.", - "properties": { - "account_id_masked": { - "type": "string", - "nullable": true, - "description": "The account number, masked." - }, - "iban_masked": { - "type": "string", - "nullable": true, - "description": "The IBAN, masked." - }, - "sort_code": { - "type": "string", - "nullable": true, - "description": "The sort code, in the United Kingdom." - }, - "routing_number_masked": { - "type": "string", - "nullable": true, - "description": "The routing number, masked." - }, - "type": { - "type": "string", - "nullable": true, - "enum": [ - "checking", - "current", - "savings", - "business", - "other" - ] - }, - "subtype": { - "type": "string", - "nullable": true, - "description": "The bank's own account subtype." - }, - "currency": { - "type": "string", - "nullable": true, - "description": "ISO 4217 currency of the account." - }, - "holder_type": { - "type": "string", - "nullable": true, - "enum": [ - "person", - "business", - "unknown" - ] - }, - "holders_count": { - "type": "integer", - "nullable": true - }, - "is_joint": { - "type": "boolean", - "nullable": true - }, - "is_business": { - "type": "boolean", - "nullable": true - } + "participants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TravelRuleNetworkParticipant" } }, - "ownership": { + "networks": { "type": "object", - "nullable": true, - "description": "How the account holder compared with the verified identity. `null` until an account is evaluated.", + "readOnly": true, + "description": "Per network, how this application would send and why it cannot. No network, including GTR, is included by default: a network is reachable through your own connected membership, or through Didit's membership once Didit's membership of that network is live and your own participant on it is active. Not a live health or delivery check.", "properties": { - "result": { - "type": "string", - "enum": [ - "match", - "partial_match", - "no_match", - "unknown", - "not_verifiable" - ], - "description": "`partial_match` is a close match. `not_verifiable`: the bank answered with no holder to compare. `unknown`: the evidence could not be read, or there was no identity to compare." - }, - "reference_identity_source": { - "type": "string", - "nullable": true, - "description": "The step whose identity the holder was compared against: `kyc`, `database_validation`, `expected_details`, `questionnaire`, `document_ai` or `kyb`." - }, - "name_score": { - "type": "integer", - "minimum": 0, - "maximum": 100, - "nullable": true, - "description": "Didit's holder name score." - }, - "provider_name_score": { - "type": "integer", - "minimum": 0, - "maximum": 100, - "nullable": true, - "description": "The bank's own holder name score, when it returned one." - }, - "address_score": { - "type": "integer", - "minimum": 0, - "maximum": 100, - "nullable": true, - "description": "The holder address score, when address matching ran." - }, - "email_score": { - "type": "integer", - "minimum": 0, - "maximum": 100, - "nullable": true, - "description": "The holder email score, when email matching ran." - }, - "phone_score": { - "type": "integer", - "minimum": 0, - "maximum": 100, - "nullable": true, - "description": "The holder phone score, when phone matching ran." - }, - "dob_score": { - "type": "integer", - "minimum": 0, - "maximum": 100, - "nullable": true, - "description": "The holder date of birth score, when the bank shared one." - }, - "close_match": { - "type": "boolean", - "nullable": true, - "description": "Whether the name landed in the close-match band." - }, - "holder_confirmation": { - "type": "string", - "nullable": true, - "enum": [ - "pending", - "confirmed", - "rejected" - ], - "description": "The person's answer when asked to confirm a close-match name. `null` when never asked." - }, - "method": { - "type": "string", - "nullable": true, - "enum": [ - "exact", - "token_set", - "nickname", - "transliteration", - "fuzzy", - "provider_only", - "microdeposits" - ], - "description": "Which name comparison produced the score. This is not the verification method: read the top-level `method` for that." - }, - "matched_holder_index": { - "type": "integer", - "nullable": true - }, - "holders": { - "type": "array", - "items": { - "type": "object" - }, - "description": "Each holder the bank listed, with its scores." + "gtr": { + "$ref": "#/components/schemas/TravelRuleNetworkState" }, - "owner_identity_available": { - "type": "boolean", - "nullable": true + "trust": { + "$ref": "#/components/schemas/TravelRuleNetworkState" }, - "evidence_freshness": { - "type": "string", - "nullable": true, - "enum": [ - "live", - "cached", - "refreshed" - ] + "verifyvasp": { + "$ref": "#/components/schemas/TravelRuleNetworkState" }, - "evaluated_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the verdict was computed." + "sygna": { + "$ref": "#/components/schemas/TravelRuleNetworkState" } } + } + } + }, + "AddressValidationResult": { + "type": "object", + "description": "The result of one Address Validation: whether the address exists, and its normalised form. It confirms the address only; it never links the address to a person.", + "required": [ + "status", + "verification_level", + "address", + "formatted_address", + "latitude", + "longitude" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "verified", + "partially_verified", + "not_verified", + "unavailable" + ], + "description": "`verified`: the address was confirmed down to the building (or the flat or PO box). `partially_verified`: it was confirmed only to the street, or to the most precise level the postal reference data holds for that place. `not_verified`: only the region or the city matched, or nothing did. `unavailable`: the validation could not run (for example the reference service did not answer); it is not charged and never affects a verification." }, - "financial_data": { - "type": "object", - "description": "The financial data the step read. Only bank sign-in reads it.", - "properties": { - "balances": { - "type": "object", - "nullable": true, - "description": "With the `balances` scope." - }, - "transactions": { - "type": "object", - "nullable": true, - "description": "With the `transactions` scope." - }, - "income": { - "type": "object", - "nullable": true, - "description": "With the `income` scope." - }, - "categorization": { - "type": "object", - "nullable": true, - "description": "Didit's categorization of the transactions read. `null` without them." - }, - "not_collected": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "balances", - "income", - "transactions" - ] - }, - "description": "The data the step asked for that the method which verified the account does not collect. Balances, transactions and income come from bank sign-in only, so an account verified by account check lists them here. Never a failure. Empty otherwise." - } - } + "verification_level": { + "type": "string", + "nullable": true, + "enum": [ + "administrative_area", + "locality", + "thoroughfare", + "premise", + "delivery_point", + null + ], + "description": "The most precise level the address was matched to. `null` when nothing matched or `status` is `unavailable`." }, - "billing": { + "address": { "type": "object", "nullable": true, + "description": "The normalised address. `null` when `status` is `unavailable`.", "properties": { - "route": { - "type": "string", - "enum": [ - "native", - "byok" - ] - }, - "billed": { - "type": "boolean" - }, - "marketplace_fee": { + "address_line_1": { "type": "string", "nullable": true, - "description": "The marketplace platform fee charged, for `byok`." + "description": "First delivery line, written the way the country writes it.", + "example": "1600 Amphitheatre Pkwy" }, - "usage_type": { + "address_line_2": { "type": "string", "nullable": true, - "description": "The usage line the charge was booked on: `bank_verification` for a bank sign-in or an account check, `bank_statement_upload` for a statement upload with no charged bank attempt before it, and `bank_byok_fee` for the marketplace fee on your own provider. `null` when nothing was billed." - } - } - }, - "reference_identity": { - "type": "object", - "nullable": true, - "description": "The identity the holder was compared against, with its source." - }, - "signals": { - "type": "object", - "nullable": true, - "description": "Account signals read from the transaction history, when the step read one." - }, - "connection_abandoned": { - "type": "boolean", - "nullable": true, - "description": "Whether the step ended without a match after the person left at their bank." - }, - "not_covered": { - "type": "boolean", - "nullable": true, - "description": "Whether the person's bank or country could not be reached on this step. Also `true` when the person continued without a bank because no method could verify their country or bank." - }, - "continued_without_bank": { - "type": "boolean", - "description": "Whether the person left the step with **Continue without your bank**. `continuation` then says why. `method` is `none` when no attempt was evaluated before they continued. After an evaluated attempt, such as a holder that does not match, `method` stays the method of that attempt and its verdict and reason codes stand. Returned even when the workflow narrows the data it returns." - }, - "continuation": { - "type": "object", - "nullable": true, - "description": "How the person continued without a bank. `null` when they did not. When the workflow narrows the data it returns, `reason` is still returned and every other key is `null`.", - "properties": { - "reason": { - "type": "string", - "enum": [ - "no_route", - "institution_unsupported", - "attempts_exhausted", - "person_choice" - ], - "description": "Why, as Didit worked it out when the person continued: `no_route` (no bank sign-in or account check serves their country), `institution_unsupported` (the bank they picked cannot be used on this step), `attempts_exhausted` (every attempt was already spent and none was in progress), or `person_choice` (a way to verify was there and they chose to continue, also when their bank could not be reached right now). `no_route` and `institution_unsupported` follow `bank_unsupported_action`, `attempts_exhausted` follows `bank_attempts_exhausted_action`, and `person_choice` follows `bank_skipped_action`, unless an evaluated attempt already decided the step." + "description": "Second delivery line (a flat, building or neighbourhood), when the address has one." }, - "country": { + "city": { "type": "string", "nullable": true, - "description": "The country the person was verifying in, ISO 3166-1 alpha-3. `null` when they continued before picking one." - }, - "offered": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "statement", - "microdeposits" - ] - }, - "nullable": true, - "description": "The other ways to verify that country offered the person when they continued, in the order it offered them: `statement` (a statement upload), then `microdeposits`. Empty when it offered none." + "example": "Mountain View" }, - "institution_id": { + "region": { "type": "string", "nullable": true, - "description": "For `institution_unsupported`, the ID of the bank the person could not use: the bank they picked, or the bank their last attempt ended on. Didit takes it from its own list of banks, never from the person's device, so a bank that list does not have is `null`. `null` for every other reason." + "description": "State, province or region.", + "example": "CA" }, - "institution_name": { + "postal_code": { "type": "string", "nullable": true, - "description": "For `institution_unsupported`, the name of that bank, from the same list as `institution_id`. `null` when that list does not have the bank, and for every other reason." + "example": "94043-1351" }, - "continued_at": { + "country": { "type": "string", - "format": "date-time", "nullable": true, - "description": "When the person continued." + "description": "ISO 3166-1 alpha-2 country code.", + "example": "US" } } }, - "evidence_purged_at": { + "formatted_address": { "type": "string", - "format": "date-time", "nullable": true, - "description": "When the bank evidence was deleted under `bank_evidence_retention_days`. `null` otherwise." - }, - "warnings": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Informational codes, for example `holder_declared`. A warning never changes the status on its own." - }, - "reason_codes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Why the verdict came out the way it did, for example `OWNER_NAME_PARTIAL`. Several may apply. `NOT_COVERED`: the person continued without a bank because no method could verify their country or bank. `SKIPPED`: the person chose to continue without a bank while a way to verify was there. Neither is added when an evaluated attempt already decided the step." - }, - "created_at": { - "type": "string", - "format": "date-time" + "description": "The normalised address on one line.", + "example": "1600 Amphitheatre Pkwy, Mountain View CA 94043-1351" }, - "updated_at": { - "type": "string", - "format": "date-time" - } - } - }, - "BankAccountCheckPersonHolder": { - "type": "object", - "description": "The person who should hold the account.", - "required": [ - "full_name" - ], - "properties": { - "full_name": { - "type": "string", - "maxLength": 255, - "description": "The full name of the person who should hold the account.", - "example": "Maria Oliveira" + "latitude": { + "type": "number", + "nullable": true, + "example": 37.422022 + }, + "longitude": { + "type": "number", + "nullable": true, + "example": -122.08429 } } }, - "BankAccountCheckBusinessHolder": { + "AddressValidationRequest": { "type": "object", - "description": "The company that should hold the account. Checks a business account.", "required": [ - "business_name" + "country" ], + "description": "One address: on one line (`address`) or as structured fields with at least `address_line_1`, never both.", "properties": { - "business_name": { + "address": { + "type": "string", + "maxLength": 500, + "description": "The whole address on one line.", + "example": "Paseo de la Castellana 259, 28046 Madrid" + }, + "address_line_1": { "type": "string", "maxLength": 255, - "description": "The name of the company that should hold the account. Checks a business account." - } - } - }, - "BankVerificationPricing": { - "type": "object", - "description": "Public Bank Verification list prices: the statement upload, the own provider key fee and each country's methods.", - "required": [ - "currency", - "statement_upload", - "own_provider_fee_usd", - "countries" - ], - "properties": { - "currency": { + "description": "Street and number.", + "example": "1600 Amphitheatre Pkwy" + }, + "address_line_2": { "type": "string", - "enum": [ - "USD" - ], - "description": "Currency of every price in the response.", - "example": "USD" + "maxLength": 255, + "description": "Apartment, suite, floor or building." }, - "statement_upload": { - "$ref": "#/components/schemas/BankStatementUploadPricing" + "city": { + "type": "string", + "maxLength": 128, + "description": "City or locality.", + "example": "Mountain View" }, - "own_provider_fee_usd": { + "region": { "type": "string", - "description": "Fee for each billable ownership check on a route that uses your own provider contract instead of Didit's, charged whatever the result. A cancelled, timed out or abandoned connection is not billed. Decimal string with 4 decimals, in USD.", - "example": "0.0200" + "maxLength": 128, + "description": "State, province or region.", + "example": "CA" }, - "countries": { - "type": "object", - "description": "Every country, keyed by ISO 3166-1 alpha-3 code.", - "additionalProperties": { - "$ref": "#/components/schemas/BankVerificationPricingCountry" - } + "postal_code": { + "type": "string", + "maxLength": 32, + "example": "94043" + }, + "country": { + "type": "string", + "maxLength": 3, + "description": "ISO 3166-1 alpha-2 or alpha-3 code of the address's country.", + "example": "US" } } }, - "BankStatementUploadPricing": { + "AddressValidationResponse": { "type": "object", - "description": "How the bank statement upload is priced.", "required": [ - "price_usd", - "included_after_charged_bank_attempt", - "standalone_available" + "request_id", + "address_validation", + "created_at" ], "properties": { - "price_usd": { + "request_id": { "type": "string", - "description": "Price of a statement upload after a charged bank attempt. Decimal string with 4 decimals, in USD.", - "example": "0.0000" + "format": "uuid", + "description": "Identifier of this request; quote it to support. No session is created." }, - "included_after_charged_bank_attempt": { - "type": "boolean", - "description": "`true` when the upload is included at no extra charge after a charged bank attempt in the same session.", - "example": true + "address_validation": { + "$ref": "#/components/schemas/AddressValidationResult" }, - "standalone_available": { - "type": "boolean", - "description": "`true` when a statement upload can run on its own, with no bank attempt first. `false` in production; sandbox sessions can test it without billing.", - "example": false + "created_at": { + "type": "string", + "format": "date-time" } } }, - "BankVerificationPricingCountry": { + "TravelRuleWalletProofFailureReason": { + "type": "string", + "enum": [ + "", + "SIGNATURE_INVALID", + "SIGNATURE_UNSUPPORTED_SCHEME", + "DEPOSIT_NOT_FOUND", + "DEPOSIT_UNCONFIRMED", + "DEPOSIT_LOOKUP_FAILED", + "DEPOSIT_MISMATCH", + "DEPOSIT_STALE", + "TXID_REUSED", + "CHAIN_UNSUPPORTED", + "WINDOW_EXPIRED", + "OWNERSHIP_CHANGED", + "METHOD_SWITCHED", + "REVIEW_REJECTED", + "ABANDONED" + ], + "description": "Why a wallet-ownership proof, or its last attempt, fell short of VERIFIED. Empty when nothing failed. A PENDING proof can carry one: the last attempt failed and the end user can try again. DEPOSIT_MISMATCH, DEPOSIT_STALE, TXID_REUSED and REVIEW_REJECTED close the proof as REJECTED. SIGNATURE_INVALID: the signature does not match the wallet and challenge. SIGNATURE_UNSUPPORTED_SCHEME: the wallet signed in a format Didit does not verify. DEPOSIT_NOT_FOUND, DEPOSIT_UNCONFIRMED, DEPOSIT_LOOKUP_FAILED: the test deposit is not readable or not confirmed yet; Didit keeps checking it until the test window closes. DEPOSIT_MISMATCH: the transaction does not send the expected amount from the wallet to the deposit address. DEPOSIT_STALE: the transaction predates the challenge. TXID_REUSED: the transaction already proved another wallet. CHAIN_UNSUPPORTED: the method is not available on this chain. WINDOW_EXPIRED: the test window closed. OWNERSHIP_CHANGED: the wallet's holder changed, or the wallet was deleted from your address book, after the widget session was minted. METHOD_SWITCHED: the end user picked another method before finishing this one. REVIEW_REJECTED: your reviewer rejected the screenshot. ABANDONED: the widget session lapsed before the end user finished." + }, + "TravelRuleWidgetProofError": { "type": "object", - "description": "One country's Bank Verification methods and their list prices.", - "required": [ - "name", - "methods" - ], "properties": { - "name": { + "detail": { "type": "string", - "description": "Country name in English.", - "example": "United Kingdom" + "description": "Human-readable message in English." }, - "methods": { - "$ref": "#/components/schemas/BankVerificationPricingMethods" - } - } - }, - "BankVerificationPricingMethods": { - "type": "object", - "description": "The two Bank Verification methods in a country.", - "required": [ - "open_banking", - "account_lookup" - ], - "properties": { - "open_banking": { - "$ref": "#/components/schemas/BankSignInPricing" + "code": { + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleWalletProofFailureReason" + } + ], + "description": "Machine-readable reason, so a client can show its own translated message." }, - "account_lookup": { - "$ref": "#/components/schemas/BankAccountCheckPricing" - } - } - }, - "BankSignInPricing": { - "type": "object", - "description": "Bank sign-in: the person signs in to their bank. Priced by market, the same for every bank in it.", - "required": [ - "available", - "market", - "price_usd", - "add_ons" - ], - "properties": { - "available": { - "type": "boolean", - "description": "`true` only where Didit serves bank sign-in in this country in production.", - "example": true + "status": { + "type": "string", + "enum": [ + "PENDING", + "VERIFIED", + "REJECTED", + "EXPIRED" + ], + "description": "Present when the proof is closed: REJECTED by the deposit just submitted (400), or REJECTED or EXPIRED when evidence is sent to a proof that is already closed (409)." }, - "market": { + "proof_id": { "type": "string", - "nullable": true, - "description": "The market whose price applies. `null` when not available.", - "example": "UK and EEA" + "format": "uuid", + "description": "Present with status: the closed proof." }, - "price_usd": { + "method": { "type": "string", - "nullable": true, - "description": "List price of one bank sign-in. `null` when not available. Decimal string with 4 decimals, in USD.", - "example": "0.7900" + "enum": [ + "MESSAGE_SIGNING", + "SATOSHI_TEST", + "SCREENSHOT", + "SELF_DECLARATION" + ], + "description": "Present with status: the closed proof's method." }, - "add_ons": { - "type": "object", - "description": "Add-ons priced in this country, each a decimal string with 4 decimals in USD, added to the bank sign-in price. Only the add-ons the country offers are listed; empty when bank sign-in is not available.", - "properties": { - "balance": { - "type": "string", - "description": "Account balance add-on.", - "example": "0.3500" - }, - "transactions": { - "type": "string", - "description": "Transactions add-on.", - "example": "1.3000" - } - } + "max_attempts": { + "type": "integer", + "description": "Present with status: rejected wallet proofs this session allows." + }, + "attempts_used": { + "type": "integer", + "description": "Present with status: rejected proofs so far." + }, + "attempts_remaining": { + "type": "integer", + "description": "Present with status: when 0, starting another proof answers 409 and you mint a new widget session." } } }, - "BankAccountCheckPricing": { + "TravelRuleWidgetProofCreate": { "type": "object", - "description": "Account check: the person types their account number and Didit checks it with the bank. Priced per country.", "required": [ - "available", - "price_usd" + "method" ], "properties": { - "available": { - "type": "boolean", - "description": "`true` only where Didit serves the account check in this country in production.", - "example": true + "method": { + "type": "string", + "enum": [ + "MESSAGE_SIGNING", + "SATOSHI_TEST", + "SCREENSHOT", + "SELF_DECLARATION" + ], + "description": "The proof method the end user picked. It must be one of the context's allowed_methods." }, - "price_usd": { + "wallet_address": { "type": "string", - "nullable": true, - "description": "List price of one account check the bank answers. `null` when not available. Decimal string with 4 decimals, in USD.", - "example": "0.4000" + "maxLength": 128, + "description": "Defaults to the session's wallet_address. Must match the transfer's wallet when the session is linked to a transfer." + }, + "chain": { + "type": "string", + "maxLength": 32, + "description": "Defaults to the session's chain." } } }, - "ImportProvider": { + "TravelRuleWidgetProofVerifyResult": { "type": "object", "properties": { - "key": { + "proof_id": { + "type": "string", + "format": "uuid" + }, + "method": { "type": "string", "enum": [ - "sumsub", - "jumio", - "veriff", - "onfido", - "persona", - "metamap", - "socure", - "idnow", - "shufti", - "idenfy", - "ondato", - "trulioo", - "au10tix", - "complycube", - "other" + "MESSAGE_SIGNING", + "SATOSHI_TEST", + "SCREENSHOT", + "SELF_DECLARATION" ] }, - "name": { - "type": "string" - }, - "mode": { + "status": { "type": "string", "enum": [ - "api_list", - "api_ids", - "export_file" - ], - "description": "`api_list`: Didit lists records with the key. `api_ids`: upload the record IDs from an export. `export_file`: upload an export in Didit's format." - }, - "record_noun": { - "type": "string" - }, - "credential_fields": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "label": { - "type": "string" - }, - "secret": { - "type": "boolean" - } - } - } + "PENDING", + "VERIFIED", + "REJECTED", + "EXPIRED" + ] }, - "regions": { - "type": "array", - "items": { - "type": "string" - } + "is_ownership_verified": { + "type": "boolean", + "description": "Present when the proof is VERIFIED: the address book entry is now ownership-verified." }, - "required_permissions": { - "type": "array", - "items": { - "type": "string" - } + "transfer_status": { + "type": "string", + "nullable": true, + "description": "Present when the proof is VERIFIED: the linked transfer's status, null without one." }, - "docs_url": { + "detail": { "type": "string", - "nullable": true + "description": "Present while the proof stays PENDING: a submitted screenshot awaiting review, or a test deposit that is not confirmed yet." }, - "status_mapping": { - "type": "object", - "additionalProperties": { - "type": "string", - "enum": [ - "Approved", - "Declined", - "In Review", - "Expired", - "Not Finished", - "Abandoned" - ] - } + "code": { + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleWalletProofFailureReason" + } + ], + "description": "Present on a pending test deposit: why it is not confirmed yet." } } }, - "ImportJobSummary": { + "TravelRuleWalletProofReview": { "type": "object", - "description": "One import job of any kind, in the shape the console's Import jobs drawer reads.", "properties": { - "id": { + "proof_id": { "type": "string", "format": "uuid" }, - "kind": { + "method": { "type": "string", "enum": [ - "sessions", - "businesses", - "users", - "status_rules", - "transactions" + "SCREENSHOT" ] }, - "source": { + "status": { "type": "string", "enum": [ - "file", - "api", - "provider" + "PENDING", + "VERIFIED", + "REJECTED", + "EXPIRED" ] }, - "provider": { - "type": "string", - "nullable": true - }, - "provider_name": { - "type": "string", - "nullable": true - }, - "filename": { - "type": "string", - "nullable": true + "wallet_address": { + "type": "string" }, - "state": { + "chain": { "type": "string", - "enum": [ - "ready", - "queued", - "running", - "paused", - "completed", - "errors", - "failed", - "cancelled" - ], - "description": "The job's state as the console shows it; `errors` is a completed job with rejected rows." + "description": "Canonical chain identifier, e.g. ETH, BTC, SOL, TRX." }, - "status": { + "holder_name": { "type": "string" }, - "is_dry_run": { - "type": "boolean" + "is_ownership_verified": { + "type": "boolean", + "description": "Whether the address book entry is ownership-verified, by this proof or another one." }, - "credential_hint": { + "transaction_id": { "type": "string", - "nullable": true + "format": "uuid", + "nullable": true, + "description": "The transaction whose Travel Rule transfer the widget session was minted for. Null for a session minted without a transaction_id." }, - "resumes_at": { + "file_name": { "type": "string", - "format": "date-time", "nullable": true }, - "last_error": { + "content_type": { "type": "string", "nullable": true }, - "started_by": { - "type": "object", - "properties": { - "name": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string", - "nullable": true - } - } + "screenshot_url": { + "type": "string", + "nullable": true, + "description": "Time-limited link to the uploaded screenshot. List the queue again for a fresh link." }, - "progress": { - "type": "object", - "properties": { - "total": { - "type": "integer", - "format": "int64" - }, - "processed": { - "type": "integer", - "format": "int64" - }, - "imported": { - "type": "integer", - "format": "int64" - }, - "updated": { - "type": "integer", - "format": "int64" - }, - "already_in_didit": { - "type": "integer", - "format": "int64" - }, - "failed": { - "type": "integer", - "format": "int64" - }, - "not_finished": { - "type": "integer", - "format": "int64" - }, - "same_person": { - "type": "integer", - "format": "int64" + "attempts": { + "type": "integer", + "description": "How many files the end user uploaded for this proof." + }, + "failure_reason": { + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleWalletProofFailureReason" } - } + ], + "description": "REVIEW_REJECTED after a rejection, OWNERSHIP_CHANGED when the wallet changed holder or was deleted before a decision. Empty otherwise." }, - "face_templates": { - "type": "object", + "method_chosen_at": { + "type": "string", + "format": "date-time", "nullable": true, - "properties": { - "enabled": { - "type": "boolean" - }, - "built": { - "type": "integer", - "format": "int64" - }, - "duplicates": { - "type": "integer", - "format": "int64" - }, - "blocklisted": { - "type": "integer", - "format": "int64" - } - } + "description": "When the end user picked the screenshot method." }, - "eta_seconds": { - "type": "integer", + "decided_at": { + "type": "string", + "format": "date-time", "nullable": true }, + "decided_by": { + "type": "string", + "description": "Who closed the proof: review: for a console reviewer, review for an API-key decision, widget or sweeper when Didit closed it automatically. Empty while PENDING." + }, + "review_note": { + "type": "string", + "description": "The note sent with the decision. Empty when none." + }, "created_at": { "type": "string", "format": "date-time" - }, - "started_at": { + } + } + }, + "TravelRuleWalletProofDecision": { + "type": "object", + "required": [ + "decision" + ], + "properties": { + "decision": { "type": "string", - "format": "date-time", - "nullable": true + "enum": [ + "approve", + "reject" + ] }, - "completed_at": { + "note": { "type": "string", - "format": "date-time", - "nullable": true + "maxLength": 512, + "description": "Optional. Kept with the proof and returned as review_note." } } } @@ -106402,6 +109105,56 @@ } } } + }, + "AddressValidationValidationError": { + "description": "The request body is invalid: a missing or unknown `country`, no address, or both `address` and the structured fields.", + "content": { + "application/json": { + "examples": { + "Unknown country": { + "value": { + "country": [ + "Use an ISO 3166-1 alpha-2 or alpha-3 country code, e.g. 'US' or 'USA'." + ] + } + }, + "No address": { + "value": { + "address": [ + "Send the address: 'address' on one line, or the structured fields with 'address_line_1'." + ] + } + }, + "Both shapes": { + "value": { + "address": [ + "Send either 'address' or the structured fields (address_line_1, ...), not both." + ] + } + } + } + } + } + }, + "AddressValidationPermissionDenied": { + "description": "The API key is missing or invalid, a scoped key lacks `sessions` write access, or the organization cannot cover the validation. This API never returns `401`.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Not enough credits": { + "value": { + "detail": "You don't have enough credits to perform this request. Please top up at https://business.didit.me", + "code": "insufficient_credits" + } + } + } + } + } } }, "parameters": { diff --git a/src/resources/account.ts b/src/resources/account.ts index 7590b56..0f648c8 100644 --- a/src/resources/account.ts +++ b/src/resources/account.ts @@ -200,7 +200,7 @@ export class Account extends APIResource { /** * Register Account * - * Step 1 of programmatic onboarding. Creates a pending account and emails a 6-char code (10 min TTL). Rate limit: 5/IP/hour. + * Step 1 of programmatic onboarding. Creates a pending account and emails a 6-char code (10 min TTL). The address is checked before any account is created: one that cannot receive the code returns 400 `email_undeliverable`. Rate limits: 5/IP/hour, plus a shared hourly budget per network (/24 for IPv4, /64 for IPv6). * * `POST /programmatic/register/` */ @@ -213,7 +213,7 @@ export class Account extends APIResource { * * Resend the 6-character verification code for an unverified programmatic account. Requires the account's email and password so only the owner can trigger a new code (no console Basic Auth). A fresh code replaces the previous one and is valid for 10 minutes. * - * Rate limits: a per-email cooldown of 60 seconds between resends (HTTP 429 with `Retry-After`), and the same 5-per-IP-per-hour budget as registration. + * Rate limits: a per-email cooldown of 60 seconds between resends (HTTP 429 with `Retry-After`), and the same 5-per-IP-per-hour and per-network hourly budgets as registration. * * `POST /programmatic/resend-otp/` */ diff --git a/src/resources/address-validation.ts b/src/resources/address-validation.ts new file mode 100644 index 0000000..00961e7 --- /dev/null +++ b/src/resources/address-validation.ts @@ -0,0 +1,44 @@ +// Code generated by codegen from openapi/*.json. DO NOT EDIT. + +import type { APIPromise } from "../core/api-promise.js"; +import type { OperationSpec } from "../core/operation.js"; +import type { RequestOptions } from "../core/request-options.js"; +import { APIResource } from "../core/resource.js"; +import type { AddressValidationValidateParams } from "../types/address-validation.js"; +import type { AddressValidationResponse } from "../types/components.js"; + +const ADDRESS_VALIDATION_VALIDATE: OperationSpec = { + method: "POST", + path: "/v3/address-validation/", + body: "json", + dates: "AddressValidationResponse", +}; + +/** `address_validation` */ +export class AddressValidation extends APIResource { + /** + * Validate an address + * + * Confirm that an address exists and get it back normalised: its delivery lines, components and coordinates. It confirms the address only; it never links the address to a person. + * + * **Request.** Send the address on one line (`address`) or as structured fields (`address_line_1` and, optionally, `address_line_2`, `city`, `region`, `postal_code`), never both, and always its `country` (ISO 3166-1 alpha-2 or alpha-3). + * + * **Result.** `address_validation.status` is `verified` (confirmed to the building), `partially_verified` (confirmed only to the street or above), `not_verified` (only the region or city matched, or nothing did) or `unavailable` (the validation could not run). HTTP is `200` for every completed request, `unavailable` included; retry an `unavailable` result later. + * + * **Billing.** A live call costs $0.10 per validated address. An `unavailable` result is not charged. Nothing is stored apart from the usage record: no session is created and the result is returned only in this response. + * + * **Sandbox.** Sandbox keys validate the request and always return a deterministic `verified` result that echoes the address sent, without consuming credits. + * + * **Authentication.** Send your application's API key in `x-api-key`. Missing or invalid credentials return `403` - this API never returns `401`. + * + * **Rate limit.** Shared write budget of 300 requests per minute per API key across all POST/PATCH/DELETE endpoints. + * + * `POST /v3/address-validation/` + */ + validate( + params: AddressValidationValidateParams, + options?: RequestOptions, + ): APIPromise { + return this._client.request(ADDRESS_VALIDATION_VALIDATE, [], params, options); + } +} diff --git a/src/resources/age-estimation.ts b/src/resources/age-estimation.ts index 1c84eab..f8c955b 100644 --- a/src/resources/age-estimation.ts +++ b/src/resources/age-estimation.ts @@ -28,7 +28,7 @@ export class AgeEstimation extends APIResource { * * **Decision logic.** Any warning declines: `status` is `Approved` only when the `warnings` array is empty. A liveness `score` at or below `face_liveness_score_decline_threshold` (default `30`) adds `LOW_LIVENESS_SCORE`; a detected attack adds `LIVENESS_FACE_ATTACK`; no face found by the liveness model adds `NO_FACE_DETECTED`; no estimable face age adds `AGE_NOT_DETECTED` (`age_estimation` is `null`); an estimated age below `age_estimation_decline_threshold` (default `18`) adds `AGE_BELOW_MINIMUM`. Borderline ages near your threshold deserve a fallback to document-based verification - the model's error grows for faces close to the limit. * - * **Billing.** Each `200` response consumes one Age Estimation API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing. + * **Billing.** Each `200` response consumes one Age Estimation API credit (paid from your plan credit or top-up balance). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing. * * **Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the result is exposed there in `liveness_checks[]`, with `features: ["AGE_ESTIMATION"]`), the face crop and reference image are stored, the face embedding is added to your application's face index (used by face search and duplicate detection), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later. * diff --git a/src/resources/billing.ts b/src/resources/billing.ts index cb70f67..4f80868 100644 --- a/src/resources/billing.ts +++ b/src/resources/billing.ts @@ -29,7 +29,7 @@ export class Billing extends APIResource { /** * Top up credits * - * Add credits via a hosted checkout session (**$50 minimum**); not available on annual plans. Redirect the customer to the returned `checkout_session_url`. + * Add credits via a hosted Stripe Checkout session (**\$50 to \$10,000** per top-up). Top-ups are available on the Free, Growth and Scale plans, added at face value with no discount or bonus, and last 12 months; Enterprise annual contracts cannot top up. Redirect the customer to the returned `checkout_session_url`. * * `POST /v3/billing/top-up/` */ diff --git a/src/resources/businesses.ts b/src/resources/businesses.ts index be9e1e6..55e9d1e 100644 --- a/src/resources/businesses.ts +++ b/src/resources/businesses.ts @@ -110,6 +110,8 @@ export class Businesses extends APIResource { * * **Permanently delete** businesses - this cannot be undone. For blocking prefer [Update Business Status](#patch-/v3/businesses/-vendor_data-/update-status/) with `BLOCKED`. * + * **Filed regulatory reports:** if the selected data is in a finalized regulatory report, the whole request is refused with `409` and nothing is deleted. The body uses `code: in_filed_report` and names each report with `uuid`, `report_number`, `report_type` and `case_uuid`. Delete the reports, then repeat the request. + * * `POST /v3/businesses/delete/` */ batchDelete( diff --git a/src/resources/cases.ts b/src/resources/cases.ts index 6cdfc03..c23a958 100644 --- a/src/resources/cases.ts +++ b/src/resources/cases.ts @@ -1374,7 +1374,9 @@ export class Cases extends APIResource { /** * Delete case * - * Delete a case. There is no undelete endpoint. + * Delete a case permanently. Its draft regulatory reports are deleted with it. Linked verifications, people, companies and transactions are not deleted. There is no undelete endpoint. + * + * If the case has a finalized regulatory report, the whole request is refused with `409` and nothing is deleted. The body uses `code: in_filed_report` and names each report with `uuid`, `report_number`, `report_type` and `case_uuid`. Delete the reports, then repeat the request. * * `DELETE /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/` * diff --git a/src/resources/date-plans.ts b/src/resources/date-plans.ts index 08802dd..1812da2 100644 --- a/src/resources/date-plans.ts +++ b/src/resources/date-plans.ts @@ -38,6 +38,7 @@ export const DATE_PLANS: Readonly> = { AMLScreeningRun: { p: { completed_at: 1, started_at: 1, dataset_freshness: "AMLDatasetFreshness" }, }, + AddressValidationResponse: { p: { created_at: 1 } }, AgeEstimationEstimateResponse: { p: { created_at: 1 } }, AmlMonitoringRetrieveJobResponse: { p: { created_at: 1, completed_at: 1 } }, AmlRetrieveDatasetFreshnessResponse: { p: { dataset_freshness: "AMLDatasetFreshness" } }, @@ -372,6 +373,7 @@ export const DATE_PLANS: Readonly> = { }, document_verifications: { i: { p: { items: { i: { p: { created_at: 1 } } } } } }, resubmissions: { i: { p: { created_at: 1 } } }, + biometric_consent: { p: { given_at: 1, declined_at: 1, face_data_deleted_at: 1 } }, investigator: "InvestigatorBlock", }, }, @@ -500,6 +502,46 @@ export const DATE_PLANS: Readonly> = { p: { fulfilled_at: 1, attachment: "TravelRuleMessageAttachment", created_at: 1 }, }, TravelRuleMessageAttachment: { p: { created_at: 1 } }, + TravelRuleNetworkParticipant: { + p: { + requested_at: 1, + submitted_at: 1, + activated_at: 1, + suspended_at: 1, + history: { i: { p: { at: 1 } } }, + }, + }, + TravelRuleNetworkParticipantSummary: { p: { requested_at: 1, activated_at: 1 } }, + TravelRuleNetworkQuestionnaire: { + p: { submitted_at: 1, reviewed_at: 1, history: { i: { p: { at: 1 } } }, updated_at: 1 }, + }, + TravelRuleNetworkState: { p: { participant: "TravelRuleNetworkParticipantSummary" } }, + TravelRuleNetworksOverview: { + p: { + questionnaire: "TravelRuleNetworkQuestionnaire", + participants: { i: "TravelRuleNetworkParticipant" }, + networks: { + p: { + gtr: "TravelRuleNetworkState", + trust: "TravelRuleNetworkState", + verifyvasp: "TravelRuleNetworkState", + sygna: "TravelRuleNetworkState", + }, + }, + }, + }, + TravelRuleSettingsDetail: { + p: { + networks: { + p: { + gtr: "TravelRuleNetworkState", + trust: "TravelRuleNetworkState", + verifyvasp: "TravelRuleNetworkState", + sygna: "TravelRuleNetworkState", + }, + }, + }, + }, TravelRuleTestVaspInboundResponse: { p: { travel_rule: "TravelRuleTransferDetail" } }, TravelRuleTransferDetail: { p: { @@ -515,6 +557,8 @@ export const DATE_PLANS: Readonly> = { TravelRuleTransferRow: { p: { txn_date: 1, deadline_at: 1, created_at: 1 } }, TravelRuleTransfersListResponse: { p: { results: { i: "TravelRuleTransferRow" } } }, TravelRuleWalletAddressesListResponse: { p: { results: { i: "WalletAddressEntry" } } }, + TravelRuleWalletProofReview: { p: { method_chosen_at: 1, decided_at: 1, created_at: 1 } }, + TravelRuleWalletProofsListResponse: { p: { results: { i: "TravelRuleWalletProofReview" } } }, TravelRuleWidgetSessionResponse: { p: { expires_at: 1 } }, TravelRuleWorkspace: { p: { @@ -522,6 +566,7 @@ export const DATE_PLANS: Readonly> = { claim: { p: { claimed_at: 1, reviewed_at: 1 } }, domain: { p: { verified_at: 1 } }, billing_enabled_at: 1, + settings: "TravelRuleSettingsDetail", }, }, UserDetailItem: { diff --git a/src/resources/document-ai.ts b/src/resources/document-ai.ts index 4da283a..1eb6115 100644 --- a/src/resources/document-ai.ts +++ b/src/resources/document-ai.ts @@ -29,7 +29,7 @@ export class DocumentAi extends APIResource { * * **Decision logic.** Each risk maps to a configurable action: `DECLINE` → `status: "Declined"`, `REVIEW` → `status: "In Review"`, `NO_ACTION` → informational only. Defaults mirror the workflow step: unreadable document `REVIEW`, missing required fields `REVIEW`, suspected manipulation `DECLINE`, name mismatch `NO_ACTION`, unsupported file `REVIEW`. A document that cannot be processed at all returns `400`; a readable but problematic document returns `200` with `Declined`/`In Review` - always inspect `document_ai.status` and `document_ai.warnings`, not just the HTTP code. * - * **Billing.** Each `200` response consumes one Document AI API credit - **$0.20 per document, no free tier** (standalone APIs never have one). When the organization's balance cannot cover the call, the endpoint returns `403` before any processing. Sandbox API keys return a deterministic mock response and are not charged. + * **Billing.** Each `200` response consumes one Document AI API credit - **$0.20 per document** at list, paid from your plan credit or top-up balance. When the organization's balance cannot cover the call, the endpoint returns `403` before any processing. Sandbox API keys return a deterministic mock response and are not charged. * * **Persistence.** With `save_api_request=true` (default) the call is stored as an API-type session: the document, the extracted values, the field definitions and the name-match snapshot are kept, the session shows up in the Business Console, and `request_id` works with `GET /v3/session/{sessionId}/decision/`. * diff --git a/src/resources/face-match.ts b/src/resources/face-match.ts index 2eb7688..7b38d2b 100644 --- a/src/resources/face-match.ts +++ b/src/resources/face-match.ts @@ -23,7 +23,7 @@ export class FaceMatch extends APIResource { * * **How it works.** Both images are decoded and analyzed by a biometric model (set `rotate_image=true` if captures may be sideways or upside down), then a similarity score between the two faces is computed. A score **strictly above** `face_match_score_decline_threshold` (default `30`) returns `status: "Approved"`; a score at or below it returns `Declined` with a `LOW_FACE_MATCH_SIMILARITY` warning. If no face pair can be scored at all, the call still returns `200` with `status: "Declined"` and a `NO_REFERENCE_IMAGE` warning - `score` is `null` on the default save path, or `0` with `save_api_request=false`. Submit one clear, front-facing face per image for best results. * - * **Billing.** Each `200` response consumes one Face Match API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing. + * **Billing.** Each `200` response consumes one Face Match API credit (paid from your plan credit or top-up balance). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing. * * **Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, both images are stored as the session's face-match source/target images, and a `status.updated` webhook is emitted to your configured webhook endpoints. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later. * diff --git a/src/resources/face-search.ts b/src/resources/face-search.ts index 1ab0e80..949f245 100644 --- a/src/resources/face-search.ts +++ b/src/resources/face-search.ts @@ -30,7 +30,7 @@ export class FaceSearch extends APIResource { * * `status` is `Declined` only when a `FACE_IN_BLOCKLIST` or `POSSIBLE_FACE_IN_BLOCKLIST` warning fires; `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` (`information`) and `MULTIPLE_FACES_DETECTED` (`warning`) never decline. Unlike Passive Liveness, this endpoint does **not** exclude prior sessions with the same `vendor_data` from matching. A passive-liveness analysis also runs internally and is stored with the persisted session, but its result is not part of this response. * - * **Billing.** Each `200` response consumes one Face Search API credit (standalone APIs have no free tier). Insufficient balance returns `403` before any image processing. + * **Billing.** Each `200` response consumes one Face Search API credit (paid from your plan credit or top-up balance). Insufficient balance returns `403` before any image processing. * * **Session persistence and face enrollment (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session (Business Console, `GET /v3/session/{sessionId}/decision/` via the returned `request_id`, `status.updated` webhook) and the searched face is enrolled into your face search index. Faces enrolled by Face Search calls are excluded from future Face Search results, so repeated searches never match each other. When `false`, the call is one-shot: nothing is stored, the face is not enrolled, and `match_image_url` values are internal storage paths rather than downloadable URLs. * diff --git a/src/resources/id-verification.ts b/src/resources/id-verification.ts index d72d88b..d3a64c5 100644 --- a/src/resources/id-verification.ts +++ b/src/resources/id-verification.ts @@ -22,13 +22,13 @@ export class IdVerification extends APIResource { /** * ID Verification (document OCR + fraud checks) * - * OCR an identity document and run document fraud checks in one call - returns the extracted holder fields, the parsed MRZ, an `Approved`/`Declined` `status`, and a `warnings` list explaining every issue found. Supports 14,000+ document types from 220+ countries and territories. + * OCR an identity document and run document fraud checks in one call - returns the extracted holder fields, the parsed MRZ, an `Approved`/`Declined`/`In Review` `status`, and a `warnings` list explaining every issue found. Supports 14,000+ document types from 220+ countries and territories. * * **How it works.** Send the document's `front_image` (required) and, for two-sided documents, the `back_image`. Upload uncropped photos with all four corners of the document visible - the service detects, aligns, and crops the document itself, so do not pre-crop. The document is classified automatically (you never declare the country or document type), the visual zone is read with OCR, and the MRZ and any barcodes are decoded. Cross-checks then validate dates, number formats, and MRZ check digits, and compare the visual zone against the MRZ. With `perform_document_liveness=true`, the images are additionally screened for screen replays, printed copies, and portrait manipulation. * - * **Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Fraud and hard-failure risks always decline: `DOCUMENT_EXPIRED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, `PUBLIC_DOCUMENT_IMAGE_DETECTED`, plus extraction failures (`NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`, `DOCUMENT_NUMBER_NOT_DETECTED`). (`PORTRAIT_IMAGE_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `INVALID_DATE`, and `MRZ_NOT_DETECTED` exist in workflow sessions but are never produced by this standalone endpoint.) Three risk groups are configurable per request via `invalid_mrz_action`, `inconsistent_data_action`, and `expiration_date_not_detected_action` (`DECLINE` or `NO_ACTION`). All other warnings (e.g. `POSSIBLE_DUPLICATED_USER`) are informational and never decline on their own. A document that cannot be processed at all returns `400` with `{"error": "COULD_NOT_RECOGNIZE_DOCUMENT"}`; a readable but problematic document returns `200` with `status: "Declined"` - always inspect `id_verification.status` and `id_verification.warnings`, not just the HTTP code. + * **Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Fraud and hard-failure risks always decline: `DOCUMENT_EXPIRED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, `PUBLIC_DOCUMENT_IMAGE_DETECTED`, plus extraction failures (`NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`, `DOCUMENT_NUMBER_NOT_DETECTED`). (`PORTRAIT_IMAGE_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `INVALID_DATE`, and `MRZ_NOT_DETECTED` exist in workflow sessions but are never produced by this standalone endpoint.) Three risk groups are configurable per request via `invalid_mrz_action`, `inconsistent_data_action`, and `expiration_date_not_detected_action` (`DECLINE` or `NO_ACTION`). All other warnings (e.g. `POSSIBLE_DUPLICATED_USER`) are informational and never decline on their own. When `perform_document_liveness=true` (the default) and the forgery check returns no usable score - the provider failed, answered non-200, or answered HTTP 200 with `status="ERR"` - the call returns `200` with `status: "In Review"` and a `DOCUMENT_LIVENESS_NOT_COMPUTED` warning (`log_type: "warning"`) instead of `Approved`: a check that did not run is not evidence that the document is genuine. This escalation is not configurable and never overrides a `Declined`; with `perform_document_liveness=false` the check is skipped and the status follows the remaining warnings as before. A document that cannot be processed at all returns `400` with `{"error": "COULD_NOT_RECOGNIZE_DOCUMENT"}`; a readable but problematic document returns `200` with `status: "Declined"` - always inspect `id_verification.status` and `id_verification.warnings`, not just the HTTP code. * - * **Billing.** Each `200` response consumes one ID Verification API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing. + * **Billing.** Each `200` response consumes one ID Verification API credit (paid from your plan credit or top-up balance). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing. * * **Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the cropped document/portrait images are stored and returned as short-lived media URLs (`https:///ocr/...`), and a `status.updated` webhook is emitted to your configured webhook endpoints. When `false`, nothing is stored, `request_id` is a one-off correlation UUID, and `portrait_image`/`front_image`/`back_image` are returned inline as base64-encoded JPEG strings. * diff --git a/src/resources/index.ts b/src/resources/index.ts index 86b9d74..8aa3ca5 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -4,6 +4,7 @@ import { APIClient } from "../core/api-client.js"; import type { DatePlan } from "../core/dates.js"; import { DATE_PLANS } from "./date-plans.js"; import { Account } from "./account.js"; +import { AddressValidation } from "./address-validation.js"; import { AgeEstimation } from "./age-estimation.js"; import { Aml } from "./aml.js"; import { AmlMonitoring } from "./aml-monitoring.js"; @@ -49,6 +50,7 @@ import { Workflows } from "./workflows.js"; export abstract class DiditResources extends APIClient { protected override readonly datePlans: Readonly> = DATE_PLANS; readonly account: Account = new Account(this); + readonly addressValidation: AddressValidation = new AddressValidation(this); readonly ageEstimation: AgeEstimation = new AgeEstimation(this); readonly aml: Aml = new Aml(this); readonly amlMonitoring: AmlMonitoring = new AmlMonitoring(this); @@ -92,6 +94,7 @@ export abstract class DiditResources extends APIClient { } export { Account, AccountApplications, AccountOrganizations } from "./account.js"; +export { AddressValidation } from "./address-validation.js"; export { AgeEstimation } from "./age-estimation.js"; export { Aml } from "./aml.js"; export { AmlMonitoring } from "./aml-monitoring.js"; @@ -151,8 +154,10 @@ export { export { TravelRule, TravelRuleCounterpartyContacts, + TravelRuleNetworks, TravelRuleTransfers, TravelRuleWalletAddresses, + TravelRuleWalletProofs, TravelRuleWorkspaceResource, } from "./travel-rule.js"; export { Users } from "./users.js"; diff --git a/src/resources/passive-liveness.ts b/src/resources/passive-liveness.ts index 6e758e3..d427d8b 100644 --- a/src/resources/passive-liveness.ts +++ b/src/resources/passive-liveness.ts @@ -34,7 +34,7 @@ export class PassiveLiveness extends APIResource { * * **Blocklist and duplicate screening.** Runs on **every** call - with or without `save_api_request` - by matching the detected face against your application's face search index. Prior faces with the same `vendor_data` are excluded from the entire index search - so re-running liveness for the same user does not trigger `DUPLICATED_FACE`, and a blocklisted face originating from a session with the caller's own `vendor_data` is also NOT flagged (`FACE_IN_BLOCKLIST` screening is bypassed for same-`vendor_data` faces). * - * **Billing.** Each `200` response consumes one Passive Liveness API credit (standalone APIs have no free tier). Insufficient balance returns `403` before any image processing. + * **Billing.** Each `200` response consumes one Passive Liveness API credit (paid from your plan credit or top-up balance). Insufficient balance returns `403` before any image processing. * * **Session persistence and face enrollment (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session (Business Console, `GET /v3/session/{sessionId}/decision/` via the returned `request_id`, `status.updated` webhook), the reference image and face crop are stored, and the detected face is **enrolled into your face search index** so later Face Search calls and duplicate checks can match it. When `false`, the call is one-shot: nothing is stored and the face is not enrolled (blocklist/duplicate screening still runs). * diff --git a/src/resources/proof-of-address.ts b/src/resources/proof-of-address.ts index 218d48c..9aa4ffd 100644 --- a/src/resources/proof-of-address.ts +++ b/src/resources/proof-of-address.ts @@ -30,7 +30,7 @@ export class ProofOfAddress extends APIResource { * * **Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Hard failures always decline: `MISSING_ADDRESS_INFORMATION`, `POA_DOCUMENT_EXPIRED`, `INVALID_DOCUMENT_TYPE`, `UNABLE_TO_VALIDATE_DOCUMENT_AGE`, plus `UNABLE_TO_EXTRACT_ISSUE_DATE` and `POA_NAME_NOT_DETECTED` (not configurable on this endpoint). Five risk groups are configurable per request via the `poa_*_action` options (`DECLINE` or `NO_ACTION`, all defaulting to `DECLINE`). `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION` and `UNPARSABLE_OR_INVALID_ADDRESS` are informational on this endpoint and never decline. A document that cannot be processed at all returns `400`; a readable but problematic document returns `200` with `status: "Declined"` - always inspect `poa.status` and `poa.warnings`, not just the HTTP code. * - * **Billing.** Each `200` response consumes one Proof of Address API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any document processing. + * **Billing.** Each `200` response consumes one Proof of Address API credit (paid from your plan credit or top-up balance). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any document processing. * * **Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the uploaded document is stored, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the decision additionally exposes `document_file` and the `document_metadata` forensics, which this response omits), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later. * diff --git a/src/resources/regulatory-reports.ts b/src/resources/regulatory-reports.ts index 6e0329d..9dd3999 100644 --- a/src/resources/regulatory-reports.ts +++ b/src/resources/regulatory-reports.ts @@ -8,6 +8,7 @@ import { APIResource } from "../core/resource.js"; import type { RegulatoryReportDetail, RegulatoryReportList } from "../types/components.js"; import type { RegulatoryReportsCreateParams, + RegulatoryReportsDeleteParams, RegulatoryReportsListParams, RegulatoryReportsRetrieveDownloadUrlParams, RegulatoryReportsRetrieveDownloadUrlResponse, @@ -26,6 +27,7 @@ const REGULATORY_REPORTS_DELETE: OperationSpec = { method: "DELETE", path: "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/", scope: "application", + body: "json", response: "none", }; const REGULATORY_REPORTS_FINALIZE: OperationSpec = { @@ -85,14 +87,20 @@ export class RegulatoryReports extends APIResource { /** * Delete report * - * Delete a draft or validated report. Finalized reports cannot be deleted (409). + * Delete a regulatory report permanently. The report, its copied narrative and reasons, its transaction links, and its generated goAML XML and PDF are erased, including every stored version of those files. The report disappears from lists and download URLs stop working. Other records, including the case, transactions, people and companies, are not deleted. There is no undo. + * + * A draft or validated report can be deleted without a request body. Deleting a finalized report requires `confirm_delete: true` in the request body. If the report is missing, the request returns `404`; if a finalized report is sent without confirmation, it returns `400` and nothing is deleted. * * `DELETE /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/` * * @param reportUuid Report UUID. */ - delete(reportUuid: string, options?: RequestOptions): APIPromise { - return this._client.request(REGULATORY_REPORTS_DELETE, [reportUuid], undefined, options); + delete( + reportUuid: string, + params: RegulatoryReportsDeleteParams = {}, + options?: RequestOptions, + ): APIPromise { + return this._client.request(REGULATORY_REPORTS_DELETE, [reportUuid], params, options); } /** diff --git a/src/resources/sessions.ts b/src/resources/sessions.ts index b1f333e..13d7de3 100644 --- a/src/resources/sessions.ts +++ b/src/resources/sessions.ts @@ -853,6 +853,8 @@ export class Sessions extends APIResource { * * This endpoint accepts API keys only. Business Console user Bearer tokens are rejected with `403` (unlike the single-session delete, which accepts both). * + * **Filed regulatory reports:** if the selected data is in a finalized regulatory report, the whole request is refused with `409` and nothing is deleted. The body uses `code: in_filed_report` and names each report with `uuid`, `report_number`, `report_type` and `case_uuid`. Delete the reports, then repeat the request. + * * `POST /v3/sessions/delete/` */ batchDelete( @@ -962,6 +964,8 @@ export class Sessions extends APIResource { * * To bulk-delete KYC sessions by their numeric `session_number`, use `POST /v3/sessions/delete/` instead. * + * **Filed regulatory reports:** if the selected data is in a finalized regulatory report, the whole request is refused with `409` and nothing is deleted. The body uses `code: in_filed_report` and names each report with `uuid`, `report_number`, `report_type` and `case_uuid`. Delete the reports, then repeat the request. + * * `DELETE /v3/session/{sessionId}/delete/` * * @param sessionId UUID (`session_id`) of the User Verification (KYC) or Business Verification (KYB) session to delete, as returned when the session was created. Must be a canonical hyphenated UUID - a non-UUID value does not match the route and returns `404`. diff --git a/src/resources/transactions.ts b/src/resources/transactions.ts index cadf305..c4463af 100644 --- a/src/resources/transactions.ts +++ b/src/resources/transactions.ts @@ -19,6 +19,8 @@ import type { TravelRuleTransferDetail, } from "../types/components.js"; import type { + TransactionsBatchDeleteParamsAllTransactions, + TransactionsBatchDeleteParamsSelectedTransactions, TransactionsCreateParams, TransactionsCreateSdkTokenParams, TransactionsCreateSdkTokenResponse, @@ -134,6 +136,13 @@ const TRANSACTIONS_TRAVEL_RULE_UPDATE_TRANSFER: OperationSpec = { body: "json", dates: "TravelRuleTransferDetail", }; +const TRANSACTIONS_BATCH_DELETE: OperationSpec = { + method: "DELETE", + path: "/v3/organization/{organization_id}/application/{application_id}/transactions/delete/", + scope: "application", + body: "whole", + response: "none", +}; const TRANSACTIONS_CREATE: OperationSpec = { method: "POST", path: "/v3/transactions/", @@ -448,7 +457,7 @@ export class TransactionsTravelRule extends APIResource { /** * Finish, cancel, resend, or update the originator wallet of a Travel Rule transfer * - * Provide `payment_txn_id` to report the on-chain hash and move an outbound COMPLETED transfer to FINISHED (its INTERNAL-rail counterpart also moves to FINISHED), `{"action": "cancel"}` to cancel a non-terminal transfer, or `{"action": "resend"}` to route an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA again (improve the counterparty data first, e.g. add a travel_address to the destination wallet entry). resend applies to outbound transfers only. `{"action": "update_compliance_email", "compliance_email": "desk@example.com"}` replaces the compliance email of an outbound transfer in one of those three statuses, the fix for `reason: email_undeliverable`: the status does not change, and the next `resend` sends the request to that address. A resend is queued, not run inside the request: the response shows the transfer back in AWAITING_COUNTERPARTY with a `resend queued` timeline entry and a queued `delivery`, and where it went arrives as a `travel_rule.status.updated` webhook. When the transfer went over GTR, VerifyVASP or Sygna Bridge, the network learns the on-chain hash or the cancellation after the response, with retries; `delivery` shows how that message stands. `{"action": "update_originator_wallet", "wallet_address": "..."}` records the wallet the funds actually left from on an outbound transfer, for example the address a UTXO withdrawal was broadcast from rather than the main wallet you declared. It changes the record only: nothing is sent to the counterparty again and the status stays. The originator becomes `chain_details.originator` with `source: updated`, the change is added to the `timeline` with the previous and the new wallet, and the INTERNAL-rail counterpart of the transfer is updated the same way. + * Provide `payment_txn_id` to report the on-chain hash and move an outbound COMPLETED transfer to FINISHED (its INTERNAL-rail counterpart also moves to FINISHED), `{"action": "cancel"}` to cancel a non-terminal transfer, or `{"action": "resend"}` to route an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA again (improve the counterparty data first, e.g. add a travel_address to the destination wallet entry). resend applies to outbound transfers only. `{"action": "update_compliance_email", "compliance_email": "desk@example.com"}` replaces the compliance email of an outbound transfer in one of those three statuses, the fix for `reason: email_undeliverable`: the status does not change, and the next `resend` sends the request to that address. A resend is queued, not run inside the request: the response shows the transfer back in AWAITING_COUNTERPARTY with a `resend queued` timeline entry and a queued `delivery`, and where it went arrives as a `travel_rule.status.updated` webhook. When the transfer went over GTR, VerifyVASP or Sygna Bridge, the network learns the on-chain hash or the cancellation after the response, with retries; `delivery` shows how that message stands. `{"action": "update_originator_wallet", "wallet_address": "..."}` records the wallet the funds actually left from on an outbound transfer, for example the address a UTXO withdrawal was broadcast from rather than the main wallet you declared. It changes the record only: nothing is sent to the counterparty again and the status stays. The originator becomes `chain_details.originator` with `source: updated`, the change is added to the `timeline` with the previous and the new wallet, and the INTERNAL-rail counterpart of the transfer is updated the same way. `{"action": "beneficiary_vasp", "beneficiary_vasp": ""}` names the counterparty yourself when discovery could not, on an outbound transfer in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE or NOT_ENOUGH_COUNTERPARTY_DATA: the transfer is routed to that VASP and sent again. * * `PATCH /v3/transactions/{transaction_id}/travel-rule/` * @@ -474,6 +483,26 @@ export class Transactions extends APIResource { readonly rules: TransactionsRules = new TransactionsRules(this._client); readonly travelRule: TransactionsTravelRule = new TransactionsTravelRule(this._client); + /** + * Batch delete transactions + * + * Delete one or more transactions permanently. Select them with `transaction_uuids`, or delete every transaction in the application with `delete_all: true`. + * + * A deleted transaction goes with the records that are only its own: its parties, tags, notes, payment method links, documents and every stored version of their files, rule runs, alerts, provider results, network snapshot and Travel Rule data. Cases, people and companies stay; their link to the deleted transaction is cleared. Draft regulatory reports stop listing it and are rebuilt without it. Deletion is permanent and irreversible; there is no undo endpoint. + * + * If any selected transaction is listed in a finalized regulatory report, the whole request is refused with `409` and nothing is deleted. The body uses `code: in_filed_report` and names each transaction and the reports that list it. Delete those reports, then repeat the request. + * + * `DELETE /v3/organization/{organization_id}/application/{application_id}/transactions/delete/` + */ + batchDelete( + params: + | TransactionsBatchDeleteParamsSelectedTransactions + | TransactionsBatchDeleteParamsAllTransactions, + options?: RequestOptions, + ): APIPromise { + return this._client.request(TRANSACTIONS_BATCH_DELETE, [], params, options); + } + /** * Create transaction * diff --git a/src/resources/travel-rule.ts b/src/resources/travel-rule.ts index 9e18814..c8d7ea1 100644 --- a/src/resources/travel-rule.ts +++ b/src/resources/travel-rule.ts @@ -13,8 +13,12 @@ import type { TravelRuleCounterpartyScreeningResult, TravelRuleCounterpartyScreeningStatus, TravelRuleInboundResponse, + TravelRuleNetworkParticipant, + TravelRuleNetworkQuestionnaire, + TravelRuleNetworksOverview, TravelRuleSettingsDetail, TravelRuleTransferRow, + TravelRuleWalletProofReview, TravelRuleWidgetSessionResponse, TravelRuleWorkspace, TravelRuleWorkspaceDomainCodeResponse, @@ -30,6 +34,7 @@ import type { TravelRuleCreateWidgetSessionParams, TravelRuleListVaspsParams, TravelRuleListVaspsResponse, + TravelRuleNetworksUpdateQuestionnaireParams, TravelRuleRegisterInboundParams, TravelRuleScreenVaspParams, TravelRuleSendComplianceEmailConfirmationResponse, @@ -43,6 +48,9 @@ import type { TravelRuleWalletAddressesListParams, TravelRuleWalletAddressesListResponse, TravelRuleWalletAddressesUpdateParams, + TravelRuleWalletProofsDecideParams, + TravelRuleWalletProofsListParams, + TravelRuleWalletProofsListResponse, TravelRuleWorkspaceClaimParams, TravelRuleWorkspaceConfirmDomainParams, TravelRuleWorkspaceEnableBillingParams, @@ -65,6 +73,32 @@ const TRAVEL_RULE_COUNTERPARTY_CONTACTS_LIST: OperationSpec = { query: ["counterparty", "limit", "offset"], dates: { p: { results: { i: "TravelRuleCounterpartyContact" } } }, }; +const TRAVEL_RULE_NETWORKS_REQUEST_ACCESS: OperationSpec = { + method: "POST", + path: "/v3/travel-rule/networks/{network}/request/", + dates: "TravelRuleNetworkParticipant", +}; +const TRAVEL_RULE_NETWORKS_RETRIEVE: OperationSpec = { + method: "GET", + path: "/v3/travel-rule/networks/", + dates: "TravelRuleNetworksOverview", +}; +const TRAVEL_RULE_NETWORKS_RETRIEVE_QUESTIONNAIRE: OperationSpec = { + method: "GET", + path: "/v3/travel-rule/networks/questionnaire/", + dates: "TravelRuleNetworkQuestionnaire", +}; +const TRAVEL_RULE_NETWORKS_SUBMIT_QUESTIONNAIRE: OperationSpec = { + method: "POST", + path: "/v3/travel-rule/networks/questionnaire/submit/", + dates: "TravelRuleNetworkQuestionnaire", +}; +const TRAVEL_RULE_NETWORKS_UPDATE_QUESTIONNAIRE: OperationSpec = { + method: "PUT", + path: "/v3/travel-rule/networks/questionnaire/", + body: "json", + dates: "TravelRuleNetworkQuestionnaire", +}; const TRAVEL_RULE_TRANSFERS_LIST: OperationSpec = { method: "GET", path: "/v3/travel-rule/transfers/", @@ -110,6 +144,18 @@ const TRAVEL_RULE_WALLET_ADDRESSES_UPDATE: OperationSpec = { path: "/v3/travel-rule/wallet-addresses/{entry_uuid}/", body: "json", }; +const TRAVEL_RULE_WALLET_PROOFS_DECIDE: OperationSpec = { + method: "POST", + path: "/v3/travel-rule/wallet-proofs/{proof_uuid}/decision/", + body: "json", + dates: "TravelRuleWalletProofReview", +}; +const TRAVEL_RULE_WALLET_PROOFS_LIST: OperationSpec = { + method: "GET", + path: "/v3/travel-rule/wallet-proofs/", + query: ["status", "limit", "offset"], + dates: { p: { results: { i: "TravelRuleWalletProofReview" } } }, +}; const TRAVEL_RULE_WORKSPACE_CLAIM: OperationSpec = { method: "POST", path: "/v3/travel-rule/workspace/claim/", @@ -183,6 +229,7 @@ const TRAVEL_RULE_REGISTER_INBOUND: OperationSpec = { const TRAVEL_RULE_RETRIEVE_SETTINGS: OperationSpec = { method: "GET", path: "/v3/travel-rule/settings/", + dates: "TravelRuleSettingsDetail", }; const TRAVEL_RULE_RETRIEVE_VASP: OperationSpec = { method: "GET", @@ -219,6 +266,7 @@ const TRAVEL_RULE_UPDATE_SETTINGS: OperationSpec = { method: "PUT", path: "/v3/travel-rule/settings/", body: "json", + dates: "TravelRuleSettingsDetail", }; const TRAVEL_RULE_UPDATE_VASP_DECISION: OperationSpec = { method: "PUT", @@ -276,6 +324,77 @@ export class TravelRuleCounterpartyContacts extends APIResource { } } +/** `travel_rule.networks` */ +export class TravelRuleNetworks extends APIResource { + /** + * Request a Travel Rule network + * + * Ask for this application's own participant on a network Didit's membership carries (GTR or Sygna). Without an approved questionnaire the participant waits for it (questionnaire_pending); with one it goes to the network's onboarding (submitted_to_network) and becomes active once the network gives it a code. Idempotent: a pending, onboarding or live participant is returned unchanged. If the network refused the registration (last_error is set), requesting again retries. TRUST and VerifyVASP are reachable only through your own membership: TRUST admits members per VASP, and a VerifyVASP Enclave serves one VASP id. + * + * `POST /v3/travel-rule/networks/{network}/request/` + * + * @param network The network to join through Didit's membership. + */ + requestAccess( + network: string, + options?: RequestOptions, + ): APIPromise { + return this._client.request(TRAVEL_RULE_NETWORKS_REQUEST_ACCESS, [network], undefined, options); + } + + /** + * Get Travel Rule networks + * + * Read, for this application, every network's true state: whether it sends through your own membership or through Didit's, and why it cannot yet. Also returns the due-diligence questionnaire and your participants. Reading creates nothing. + * + * `GET /v3/travel-rule/networks/` + */ + retrieve(options?: RequestOptions): APIPromise { + return this._client.request(TRAVEL_RULE_NETWORKS_RETRIEVE, [], undefined, options); + } + + /** + * Get the network questionnaire + * + * Read Didit's network due-diligence questionnaire for this application, its status and Didit's review note. An application that never saved one reads an empty draft. + * + * `GET /v3/travel-rule/networks/questionnaire/` + */ + retrieveQuestionnaire(options?: RequestOptions): APIPromise { + return this._client.request( + TRAVEL_RULE_NETWORKS_RETRIEVE_QUESTIONNAIRE, + [], + undefined, + options, + ); + } + + /** + * Submit the network questionnaire + * + * Send the saved questionnaire to Didit's review. The saved answers must be complete: every field except lei and data_protection_contact_email, with the three attestations true. Didit approves it, asks for changes (edit and submit again) or rejects it. Networks you requested while it was pending are handed to their onboarding when it is approved. + * + * `POST /v3/travel-rule/networks/questionnaire/submit/` + */ + submitQuestionnaire(options?: RequestOptions): APIPromise { + return this._client.request(TRAVEL_RULE_NETWORKS_SUBMIT_QUESTIONNAIRE, [], undefined, options); + } + + /** + * Save network questionnaire answers + * + * Save answers to the questionnaire while it is draft or changes_requested. Saves are partial: answers you do not send are kept. + * + * `PUT /v3/travel-rule/networks/questionnaire/` + */ + updateQuestionnaire( + params: TravelRuleNetworksUpdateQuestionnaireParams = {}, + options?: RequestOptions, + ): APIPromise { + return this._client.request(TRAVEL_RULE_NETWORKS_UPDATE_QUESTIONNAIRE, [], params, options); + } +} + /** `travel_rule.transfers` */ export class TravelRuleTransfers extends APIResource { /** @@ -373,6 +492,40 @@ export class TravelRuleWalletAddresses extends APIResource { } } +/** `travel_rule.wallet_proofs` */ +export class TravelRuleWalletProofs extends APIResource { + /** + * Approve or reject a screenshot wallet proof + * + * Decide a screenshot proof from your review queue. approve verifies the proof and the address book entry and applies the ownership to the transfer the widget session was minted for, exactly as a verified signature does. A screenshot never confirms a transfer that another Didit customer sent over the INTERNAL rail: that takes a signed message or a test deposit. reject closes the proof as REJECTED with failure_reason REVIEW_REJECTED; the end user can start a new proof from the widget. The note is kept with the proof. + * + * `POST /v3/travel-rule/wallet-proofs/{proof_uuid}/decision/` + * + * @param proofUuid proof_id from the review queue. + */ + decide( + proofUuid: string, + params: TravelRuleWalletProofsDecideParams, + options?: RequestOptions, + ): APIPromise { + return this._client.request(TRAVEL_RULE_WALLET_PROOFS_DECIDE, [proofUuid], params, options); + } + + /** + * List screenshot wallet proofs to review + * + * Your review queue for screenshot wallet-ownership proofs. A screenshot is the one proof Didit cannot check itself: once the end user uploads it from the widget it waits PENDING until you approve or reject it with POST /v3/travel-rule/wallet-proofs/{proof_uuid}/decision/. Oldest first, so the queue is worked from the proof that has waited longest. Only uploaded screenshots appear, never a proof whose end user has not uploaded a file yet, and never one on a wallet you deleted from your address book (Didit closes those as OWNERSHIP_CHANGED). + * + * `GET /v3/travel-rule/wallet-proofs/` + */ + list( + params: TravelRuleWalletProofsListParams = {}, + options?: RequestOptions, + ): PagePromise { + return this._client.requestPage(TRAVEL_RULE_WALLET_PROOFS_LIST, [], params, options); + } +} + /** `travel_rule.workspace` */ export class TravelRuleWorkspaceResource extends APIResource { /** @@ -455,8 +608,10 @@ export class TravelRuleWorkspaceResource extends APIResource { export class TravelRule extends APIResource { readonly counterpartyContacts: TravelRuleCounterpartyContacts = new TravelRuleCounterpartyContacts(this._client); + readonly networks: TravelRuleNetworks = new TravelRuleNetworks(this._client); readonly transfers: TravelRuleTransfers = new TravelRuleTransfers(this._client); readonly walletAddresses: TravelRuleWalletAddresses = new TravelRuleWalletAddresses(this._client); + readonly walletProofs: TravelRuleWalletProofs = new TravelRuleWalletProofs(this._client); readonly workspace: TravelRuleWorkspaceResource = new TravelRuleWorkspaceResource(this._client); /** diff --git a/src/resources/users.ts b/src/resources/users.ts index 2a31a92..347c4c9 100644 --- a/src/resources/users.ts +++ b/src/resources/users.ts @@ -119,6 +119,8 @@ export class Users extends APIResource { * * **Biometric templates:** every image-free biometric template retained for these users after session deletion (see `/v3/biometric-templates/`) is purged before the user is deleted, so the person stops matching in duplicate detection and Face Search. If a template cannot be purged because a biometric store is unavailable, the call returns `503`, no user in the request is deleted, and repeating the request finishes the job. * + * **Filed regulatory reports:** if the selected data is in a finalized regulatory report, the whole request is refused with `409` and nothing is deleted. The body uses `code: in_filed_report` and names each report with `uuid`, `report_number`, `report_type` and `case_uuid`. Delete the reports, then repeat the request. + * * `POST /v3/users/delete/` */ batchDelete( diff --git a/src/types/address-validation.ts b/src/types/address-validation.ts new file mode 100644 index 0000000..3dc0db0 --- /dev/null +++ b/src/types/address-validation.ts @@ -0,0 +1,18 @@ +// Code generated by codegen from openapi/*.json. DO NOT EDIT. + +/** One address: on one line (`address`) or as structured fields with at least `address_line_1`, never both. */ +export interface AddressValidationValidateParams { + /** The whole address on one line. */ + address?: string; + /** Street and number. */ + address_line_1?: string; + /** Apartment, suite, floor or building. */ + address_line_2?: string; + /** City or locality. */ + city?: string; + /** State, province or region. */ + region?: string; + postal_code?: string; + /** ISO 3166-1 alpha-2 or alpha-3 code of the address's country. */ + country: string; +} diff --git a/src/types/billing.ts b/src/types/billing.ts index c3f5134..2813f73 100644 --- a/src/types/billing.ts +++ b/src/types/billing.ts @@ -5,14 +5,14 @@ export interface BillingRetrieveBalanceResponse { balance: string; /** When `true`, Didit will automatically charge the saved payment method to add credits once the balance crosses `auto_refill_threshold`. Configured from Console → Billing. */ auto_refill_enabled: boolean; - /** USD amount that will be charged on each auto-refill. `null` when auto-refill is disabled or unset. */ + /** USD amount that will be charged and added to the balance on each auto-refill, at face value. Set from \$50 to \$10,000 in Console → Billing. `null` when auto-refill is disabled or unset. */ auto_refill_amount?: string | null; /** Trigger threshold in USD. Auto-refill fires when `balance` falls below this value. `null` when auto-refill is disabled or unset. */ auto_refill_threshold?: string | null; } export interface BillingTopUpParams { - /** Amount to charge, in USD. **Must be at least 50.** Tax is added automatically at checkout based on the customer's billing address. */ + /** Amount to charge, in USD. **Must be from 50 to 10,000.** The balance receives the same amount, with no discount or bonus. Tax is added automatically at checkout based on the customer's billing address. */ amount_in_dollars: number; /** URL the hosted checkout redirects the customer to after a successful payment. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. Append the literal placeholder `{CHECKOUT_SESSION_ID}` if you want the hosted checkout to substitute the session ID. */ success_url?: string; diff --git a/src/types/components.ts b/src/types/components.ts index 51e326e..4d0a045 100644 --- a/src/types/components.ts +++ b/src/types/components.ts @@ -608,6 +608,49 @@ export interface AMLScreeningWarnings { node_id?: string; } +export interface AddressValidationResponse { + /** Identifier of this request; quote it to support. No session is created. */ + request_id: string; + /** The result of one Address Validation: whether the address exists, and its normalised form. It confirms the address only; it never links the address to a person. */ + address_validation: AddressValidationResult; + created_at: Date; +} + +/** The result of one Address Validation: whether the address exists, and its normalised form. It confirms the address only; it never links the address to a person. */ +export interface AddressValidationResult { + /** `verified`: the address was confirmed down to the building (or the flat or PO box). `partially_verified`: it was confirmed only to the street, or to the most precise level the postal reference data holds for that place. `not_verified`: only the region or the city matched, or nothing did. `unavailable`: the validation could not run (for example the reference service did not answer); it is not charged and never affects a verification. */ + status: "verified" | "partially_verified" | "not_verified" | "unavailable" | (string & {}); + /** The most precise level the address was matched to. `null` when nothing matched or `status` is `unavailable`. */ + verification_level: + | "administrative_area" + | "locality" + | "thoroughfare" + | "premise" + | "delivery_point" + | (string & {}) + | null; + /** The normalised address. `null` when `status` is `unavailable`. */ + address: AddressValidationResultAddress | null; + /** The normalised address on one line. */ + formatted_address: string | null; + latitude: number | null; + longitude: number | null; +} + +/** The normalised address. `null` when `status` is `unavailable`. */ +export interface AddressValidationResultAddress { + /** First delivery line, written the way the country writes it. */ + address_line_1?: string | null; + /** Second delivery line (a flat, building or neighbourhood), when the address has one. */ + address_line_2?: string | null; + city?: string | null; + /** State, province or region. */ + region?: string | null; + postal_code?: string | null; + /** ISO 3166-1 alpha-2 country code. */ + country?: string | null; +} + /** Full application record. `uuid`, `client_id`, and `api_key` never change after creation. */ export interface Application { /** Application UUID. Use as `{app_id}` in subsequent calls. */ @@ -616,7 +659,7 @@ export interface Application { name?: string; /** Public client identifier, safe to embed in OAuth-style flows. */ client_id?: string; - /** Long-lived secret (also called `client_secret`). Use as the `x-api-key` header for every call to `https://verification.didit.me/v3/...` (sessions, workflows, AML, etc.). Treat as a credential; never expose client-side. New keys start with `sk_live_` (live application) or `sk_test_` (sandbox application), followed by 43 URL-safe characters (letters, digits, `-` and `_`). Keys created before this format have no prefix and keep working unchanged. */ + /** Long-lived secret (also called `client_secret`). Use as the `x-api-key` header for every call to `https://verification.didit.me/v3/...` (sessions, workflows, AML, etc.). Treat as a credential; never expose client-side. New keys start with `sk_live_` (live application) or `sk_test_` (sandbox application), followed by 43 URL-safe characters (letters, digits, `-` and `_`). Keys created before this format have no prefix and keep working unchanged. For 72 hours after you recovered your account, list, retrieve and update return an empty string here instead of the key. */ api_key?: string; /** Website or app URL associated with this application. */ website_url?: string | null; @@ -3964,8 +4007,9 @@ export interface KYBRegistryIndustry { /** The outcome of the ownership (Shareholders / UBO) tier, in the same shape for every country and registry source. */ export interface KYBRegistryOwnership { - /** `delivered` when the registry published ownership; `unavailable` when it did not (see `unavailable_reason`); `pending` while the registry is still fetching it; `not_requested` for the Basic tier. */ - status: "not_requested" | "pending" | "delivered" | "unavailable" | (string & {}) | null; + /** `delivered` when the registry published ownership; `not_held` when the ownership registry answered and does not list the company at all (see `unavailable_reason`); `unavailable` when it did not serve the ownership data for any other reason (see `unavailable_reason`); `pending` while the registry is still fetching it; `not_requested` for the Basic tier. */ + status: + "not_requested" | "pending" | "delivered" | "not_held" | "unavailable" | (string & {}) | null; /** The tier the workflow ordered. */ tier_requested: "basic" | "shareholders" | "ubo" | (string & {}) | null; /** The tier the registry delivered (and that is billed). A UBO request answered with direct shareholders only is `shareholders`. */ @@ -4582,9 +4626,9 @@ export interface NetworkDetailResponse { notes_count?: number; /** The organization's cross-organization sharing mode. Only `global` compares networks across organizations; `private` and `off` keep them to the organization's own history. */ sharing_mode?: "off" | "private" | "global" | (string & {}); - /** Whether cross-organization signals apply to this organization. They need both the Identity Enterprise plan and the `global` sharing mode. */ + /** Whether cross-organization signals apply to this organization. They need both the Enterprise plan and the `global` sharing mode. */ cross_org_available?: boolean; - /** Why cross-organization signals do not apply, checked in this order. `plan`: the organization's Identity plan does not include the cross-organization fraud network (it needs Identity Enterprise), whatever the sharing mode. `sharing_mode`: the plan includes it, but the sharing mode is `private` or `off`. Null when `cross_org_available` is `true`. */ + /** Why cross-organization signals do not apply, checked in this order. `plan`: the organization's plan does not include the cross-organization fraud network (it needs the Enterprise plan), whatever the sharing mode. `sharing_mode`: the plan includes it, but the sharing mode is `private` or `off`. Null when `cross_org_available` is `true`. */ cross_org_unavailable_reason?: "plan" | "sharing_mode" | (string & {}) | null; /** Your application's tags on the network, oldest first. Each application sees only its own tags. `fraud_tags` is separate and unchanged: the tags the network's members carry. */ tags?: NetworkTag[]; @@ -5984,10 +6028,79 @@ export interface SessionDecision { environment?: "live" | "sandbox" | (string & {}); /** Returned only with `include=resubmissions`, for user and business sessions. One entry per resubmission request, oldest first. Each entry carries the results the resubmitted steps returned before the request, so earlier submissions stay readable. Deleting the session deletes every submission. */ resubmissions?: SessionDecisionResubmissions[]; + /** Whether the session required explicit biometric consent before a face capture, why, and the evidence of the answer. User Verification (KYC) sessions only; the same object is in the `decision` of session webhooks. `null` when the session was never evaluated, for example a workflow without a face capture. Fields with no recorded value are `null`. A native SDK below 4.10.0, or one that does not report its version, cannot show the consent step: its face captures run as before, and a session that required consent then reports `status: "pending"` with `reason: "BIOMETRIC_CONSENT_UNSUPPORTED_SDK"`. See [Biometric consent](/getting-started/biometric-consent). */ + biometric_consent?: SessionDecisionBiometricConsent | null; /** The item's latest Investigator review, or null when it never had one. Carried by the V3 session and business session decision (and so by `status.updated` webhooks). */ investigator?: InvestigatorBlock | null; } +/** Whether the session required explicit biometric consent before a face capture, why, and the evidence of the answer. User Verification (KYC) sessions only; the same object is in the `decision` of session webhooks. `null` when the session was never evaluated, for example a workflow without a face capture. Fields with no recorded value are `null`. A native SDK below 4.10.0, or one that does not report its version, cannot show the consent step: its face captures run as before, and a session that required consent then reports `status: "pending"` with `reason: "BIOMETRIC_CONSENT_UNSUPPORTED_SDK"`. See [Biometric consent](/getting-started/biometric-consent). */ +export interface SessionDecisionBiometricConsent { + /** Whether the session must collect explicit biometric consent. Never goes back to `false`. */ + required?: boolean; + /** Why consent is required, or how it was satisfied. `declared_state`: you declared a covered state (`user_state`, or a US address in `expected_details`). `ip_state`: the person's IP is in a covered state (Illinois, Texas, Colorado). `ip_unknown`: the evidence places the person in the US but the state cannot be told (the IP is in the US with no state, or cannot be located while your details or the document place the person in the US). `document_issuer`: the identity document was issued by a covered state. `forced`: the application's mode is Always. `customer_collected`: you sent `biometric_consent` when creating the session. `null` while not required. */ + trigger?: + | "declared_state" + | "ip_state" + | "ip_unknown" + | "document_issuer" + | "forced" + | "customer_collected" + | (string & {}) + | null; + /** `not_required`; `pending` (required and not given: the person has not answered yet, or `reason` says why it was not collected); `given`; `declined` (a decline was recorded and the session ended as `Declined`; no Didit screen offers a decline today). */ + status?: "not_required" | "pending" | "given" | "declined" | (string & {}); + /** When consent was given. For `customer_collected`, the `given_at` you sent. */ + given_at?: Date | null; + /** When a decline was recorded. */ + declined_at?: Date | null; + /** Version id of the consent text the person answered, or the one you sent. */ + text_version?: string | null; + /** SHA-256 (hex) of the exact consent text shown, in the language it was shown in. */ + text_hash?: string | null; + /** Language the consent text was shown in. */ + locale?: string | null; + /** The biometric data policy the consent step linked to. */ + policy_url?: string | null; + /** Workflow step the session was on when the person answered, for example `LIVENESS`. */ + step?: string | null; + /** Workflow graph node the session was on when the person answered. */ + node_id?: string | null; + /** IP the answer came from. */ + ip_address?: string | null; + /** ISO 3166-1 alpha-2 country of the answer's IP. */ + ip_country_code?: string | null; + /** ISO 3166-2 subdivision of the answer's IP, for example `US-IL`. */ + ip_state?: string | null; + /** City of the answer's IP. */ + ip_city?: string | null; + /** User agent of the answer's request. */ + user_agent?: string | null; + /** The person's IP the requirement was last evaluated from. Can differ from `ip_address` when the person answered on another device. */ + detected_ip_address?: string | null; + /** ISO 3166-1 alpha-2 country of the detected IP. */ + detected_country_code?: string | null; + /** ISO 3166-2 subdivision of the detected IP. */ + detected_state?: string | null; + /** City of the detected IP. */ + detected_city?: string | null; + /** ISO 3166-2 subdivision you declared: `user_state`, or the state of a US address in `expected_details`. */ + declared_state?: string | null; + /** ISO 3166-2 subdivision that issued the identity document, read at ID capture. */ + document_state?: string | null; + /** US states (USPS codes, for example `IL`) whose biometric law covers the person. `US` when the evidence places the person in the US but no state is known. Empty when none applies. */ + jurisdictions?: string[]; + /** The shortest biometric retention limit, in months, among `jurisdictions`. `null` when none applies. */ + retention_months?: number | null; + /** `BIOMETRIC_CONSENT_UNSUPPORTED_SDK`: consent was required and a native SDK below 4.10.0, or one that did not report its version, captured a face without the consent step; `status` stays `pending`, or `given` if the person later agreed in the hosted flow. `BIOMETRIC_CONSENT_DECLINED`: a decline was recorded (it replaces the SDK reason). `null` otherwise, and always `null` when the session did not require consent. */ + reason?: + "BIOMETRIC_CONSENT_UNSUPPORTED_SDK" | "BIOMETRIC_CONSENT_DECLINED" | (string & {}) | null; + /** `BIOMETRIC_CONSENT_DECLINED` when a recorded decline ended the session, otherwise `null`. */ + decline_reason?: "BIOMETRIC_CONSENT_DECLINED" | (string & {}) | null; + /** When the decline deleted the session's face images, videos and face templates. `null` unless a decline was recorded. */ + face_data_deleted_at?: Date | null; +} + /** Contact details supplied when the session was created (used for notification emails, prefilled phone/email verification, and communication language). `null` when neither an email nor a phone number was provided. */ export interface SessionDecisionContactDetails { /** Email address of the user (e.g., "john.doe@example.com") that will be used during the Email Verification step. If not provided, the user must provide it during the verification flow. */ @@ -6558,6 +6671,8 @@ export interface SessionDecisionIdVerifications { matches?: SessionDecisionIdVerificationsMatches[]; /** Document-liveness scores per fraud type, reported whether or not a warning fired, so an approved document can still be audited. `null` when the check did not run. */ document_liveness?: SessionDecisionIdVerificationsDocumentLiveness | null; + /** Address Validation of the address printed on this document: whether it exists and its normalised form, as the document read it (a later correction of `address` does not run it again). Present when this ID verification step enabled the Address Validation add-on; `null` otherwise. */ + address_validation?: AddressValidationResult | null; } /** Quality metrics for the back document image. Same structure as front_image_quality_score. */ @@ -7889,6 +8004,8 @@ export interface SessionDecisionPoaVerifications { warnings?: SessionDecisionPoaVerificationsWarnings[]; /** QR codes and barcodes detected across every processed page of the document. Empty when none are found. A located but undecodable QR code has `parsed_payload: null`. */ detected_codes?: DocumentAIDetectedCode[]; + /** Address Validation of the address on this proof of address: whether it exists and its normalised form, as the document read it (a later correction of `poa_address` does not run it again). Present when this Proof of Address step enabled the Address Validation add-on and the upload passed the document checks; `null` otherwise. */ + address_validation?: AddressValidationResult | null; } /** File-forensics metadata extracted from the uploaded POA document. Always contains the keys below (values are `null` when not applicable, e.g. EXIF dates for PDFs). `null` when no metadata could be extracted. */ @@ -8194,7 +8311,7 @@ export interface SessionDecisionRegistryChecksCompany { registry_tier?: string | null; /** The tier the registry delivered and that is billed. Also in `ownership.tier_delivered`. */ registry_tier_delivered?: string | null; - /** `not_requested`, `pending`, `delivered` or `unavailable`. Also in `ownership.status`. */ + /** `not_requested`, `pending`, `delivered`, `not_held` or `unavailable`. Also in `ownership.status`. */ registry_ownership_status?: string | null; } @@ -10036,9 +10153,195 @@ export interface TravelRuleNameMatch { mismatched_names?: string[]; } +/** This application's own participant on one network. */ +export interface TravelRuleNetworkParticipant { + network?: "gtr" | "trust" | "verifyvasp" | "sygna" | (string & {}); + rail?: "GTR" | "TRUST" | "VERIFYVASP" | "SYGNA" | (string & {}); + mode?: "platform" | "own" | (string & {}); + /** not_requested: never requested, or the network refused the first request (see last_error); questionnaire_pending: waiting for the due-diligence questionnaire to be approved; submitted_to_network: handed to the network's onboarding; active: the network gave this application its own participant code; suspended / rejected: Didit suspended or rejected it. */ + status?: + | "not_requested" + | "questionnaire_pending" + | "submitted_to_network" + | "active" + | "suspended" + | "rejected" + | (string & {}); + participant_code?: string; + requested_at?: Date | null; + submitted_at?: Date | null; + activated_at?: Date | null; + suspended_at?: Date | null; + /** The network's answer when it refused the registration; empty otherwise. */ + last_error?: string; + /** Every status change, oldest first. */ + history?: TravelRuleNetworkParticipantHistory[]; +} + +export interface TravelRuleNetworkParticipantHistory { + at?: Date; + from?: string; + to?: string; + /** Review note or the network's answer, when there is one. */ + note?: string; +} + +/** This application's own participant on the network, or null when it has none. */ +export interface TravelRuleNetworkParticipantSummary { + /** platform: carried by Didit's membership; own: onboarded under the application's own membership. */ + mode?: "platform" | "own" | (string & {}); + /** not_requested: never requested, or the network refused the first request (see last_error); questionnaire_pending: waiting for the due-diligence questionnaire to be approved; submitted_to_network: handed to the network's onboarding; active: the network gave this application its own participant code; suspended / rejected: Didit suspended or rejected it. */ + status?: + | "not_requested" + | "questionnaire_pending" + | "submitted_to_network" + | "active" + | "suspended" + | "rejected" + | (string & {}); + /** The code the network gave this application (for GTR, its VASP code). Outbound messages carry this code, never Didit's own. */ + participant_code?: string; + requested_at?: Date | null; + activated_at?: Date | null; + /** The network's answer when it refused the registration; empty otherwise. */ + last_error?: string; +} + +export interface TravelRuleNetworkQuestionnaire { + /** Answers can be saved while draft or changes_requested; submitted, approved and rejected are Didit's to move. */ + status?: "draft" | "submitted" | "changes_requested" | "approved" | "rejected" | (string & {}); + /** Didit's network due-diligence questionnaire. Saves are partial: a save keeps every answer it does not name. Submitting requires every field except lei and data_protection_contact_email, and the three attestations true. */ + answers?: TravelRuleNetworkQuestionnaireAnswers; + submitted_at?: Date | null; + reviewed_at?: Date | null; + /** Didit's note on its last decision, for example what to change. */ + review_note?: string; + /** Every status change, oldest first. */ + history?: TravelRuleNetworkQuestionnaireHistory[]; + updated_at?: Date | null; +} + +/** Didit's network due-diligence questionnaire. Saves are partial: a save keeps every answer it does not name. Submitting requires every field except lei and data_protection_contact_email, and the three attestations true. */ +export interface TravelRuleNetworkQuestionnaireAnswers { + legal_name?: string; + registration_number?: string; + /** ISO 3166-1 alpha-2 country code. */ + incorporation_country?: string; + registered_address?: string; + website?: string; + /** Optional. A valid ISO 17442 LEI when given. */ + lei?: string; + /** Every licence or registration the VASP holds. */ + licences?: TravelRuleNetworkQuestionnaireAnswersLicences[]; + operating_jurisdictions?: string[]; + compliance_officer_name?: string; + compliance_officer_email?: string; + /** Must be true to submit. */ + aml_ctf_policy_attested?: boolean; + /** Must be true to submit. */ + sanctions_screening_attested?: boolean; + /** Must be true to submit. */ + data_protection_attested?: boolean; + /** Optional. */ + data_protection_contact_email?: string; + expected_monthly_transfers?: number; + /** Expected monthly volume in USD, up to 2 decimal places. Returned as a string. */ + expected_monthly_volume_usd?: string; + /** Asset tickers, stored upper-case, sorted and de-duplicated. */ + assets?: string[]; +} + +export interface TravelRuleNetworkQuestionnaireAnswersLicences { + regulator: string; + /** ISO 3166-1 alpha-2 country code. */ + jurisdiction: string; + licence_type: string; + licence_number: string; +} + +export interface TravelRuleNetworkQuestionnaireHistory { + at?: Date; + from?: string; + to?: string; + /** Review note or the network's answer, when there is one. */ + note?: string; +} + +/** How Travel Rule messages on one network would be sent for this application, and why they cannot be. Configuration status, not a live health or delivery check. */ +export interface TravelRuleNetworkState { + rail?: "GTR" | "TRUST" | "VERIFYVASP" | "SYGNA" | (string & {}); + /** own: the application's connected membership is used; didit: Didit's membership carries the application as its own active participant; off: platform routing disabled in disabled_networks with no own membership; unavailable: nothing can send on this network yet (see state and reason). */ + mode?: "own" | "didit" | "off" | "unavailable" | (string & {}); + /** own_membership: your connected membership sends; live_through_didit: your own participant on Didit's membership sends; off: you switched platform routing off; didit_onboarding: Didit's own membership of this network is not live yet, so it cannot carry anyone; questionnaire_needed / questionnaire_in_review: the due-diligence questionnaire must be submitted or is with Didit; request_access: the questionnaire is approved, request the network; request_sent: your participant is being onboarded; unavailable: see reason; error: see reason. */ + state?: + | "own_membership" + | "live_through_didit" + | "off" + | "didit_onboarding" + | "questionnaire_needed" + | "questionnaire_in_review" + | "request_access" + | "request_sent" + | "unavailable" + | "error" + | (string & {}); + /** Machine-readable detail of state. own_membership_only: Didit does not carry applications on this network (TRUST), connect your own membership; sandbox_application: sandbox transfers never reach a network; provisioning_failed: the network refused the registration, the participant's last_error says why and requesting the network again retries. */ + reason?: + | "own_membership_connected" + | "platform_network_switched_off" + | "own_membership_only" + | "sandbox_application" + | "didit_membership_not_live" + | "participant_suspended" + | "participant_rejected" + | "questionnaire_not_submitted" + | "questionnaire_submitted" + | "questionnaire_changes_requested" + | "questionnaire_rejected" + | "participant_not_requested" + | "participant_active" + | "participant_key_missing" + | "participant_key_unresolved" + | "provisioning_failed" + | "questionnaire_pending" + | "submitted_to_network" + | (string & {}); + /** Whether Didit's own membership of this network is configured; not a live health check. */ + platform_available?: boolean; + /** Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange. */ + connected?: boolean; + /** This application's own participant on the network, or null when it has none. */ + participant?: TravelRuleNetworkParticipantSummary | null; + /** Directory VASPs reachable on this network. */ + vasps_reachable?: number; + /** This application's transfers carried on this network in the last 30 days. */ + routed_30d?: number; +} + +export interface TravelRuleNetworksOverview { + /** Whether network access through Didit's membership is open. While false, questionnaire saves, submissions and network requests answer 403. */ + platform_networks_enabled?: boolean; + questionnaire?: TravelRuleNetworkQuestionnaire; + participants?: TravelRuleNetworkParticipant[]; + /** Per network, how this application would send and why it cannot. No network, including GTR, is included by default: a network is reachable through your own connected membership, or through Didit's membership once Didit's membership of that network is live and your own participant on it is active. Not a live health or delivery check. */ + networks?: TravelRuleNetworksOverviewNetworks; +} + +/** Per network, how this application would send and why it cannot. No network, including GTR, is included by default: a network is reachable through your own connected membership, or through Didit's membership once Didit's membership of that network is live and your own participant on it is active. Not a live health or delivery check. */ +export interface TravelRuleNetworksOverviewNetworks { + /** How Travel Rule messages on one network would be sent for this application, and why they cannot be. Configuration status, not a live health or delivery check. */ + gtr?: TravelRuleNetworkState; + /** How Travel Rule messages on one network would be sent for this application, and why they cannot be. Configuration status, not a live health or delivery check. */ + trust?: TravelRuleNetworkState; + /** How Travel Rule messages on one network would be sent for this application, and why they cannot be. Configuration status, not a live health or delivery check. */ + verifyvasp?: TravelRuleNetworkState; + /** How Travel Rule messages on one network would be sent for this application, and why they cannot be. Configuration status, not a live health or delivery check. */ + sygna?: TravelRuleNetworkState; +} + /** One thing that moves a `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE` transfer on. See [Why a transfer is stuck](/transaction-monitoring/travel-rule#why-a-transfer-is-stuck). */ export interface TravelRuleNextAction { - /** `select_counterparty_vasp`: the end user picks the destination VASP in the wallet-ownership widget. `declare_self_hosted`: the end user declares the wallet self-hosted in the widget. `add_vasp_hint`: tell Didit who the counterparty is, with a VASP name, LEI or travel address (for example a `travel_address` on the destination wallet's address-book entry), then resend. `update_compliance_email` and `resend` are `action` values of `PATCH /v3/transactions/{transaction_id}/travel-rule/`: give the counterparty's compliance email again, and re-run routing. */ + /** `select_counterparty_vasp`: the end user picks the destination VASP in the wallet-ownership widget, or you PATCH `action=beneficiary_vasp`. `declare_self_hosted`: the end user declares the wallet self-hosted in the widget. `add_vasp_hint`: tell Didit who the counterparty is, with a VASP name, LEI or travel address (for example a `travel_address` on the destination wallet's address-book entry), then resend. `update_compliance_email` and `resend` are `action` values of `PATCH /v3/transactions/{transaction_id}/travel-rule/`: give the counterparty's compliance email again, and re-run routing. */ action: | "select_counterparty_vasp" | "declare_self_hosted" @@ -10101,7 +10404,7 @@ export interface TravelRuleSettingsDetail { travel_address?: string; /** Networks opted out of platform membership. Does not disable your connected own memberships or provision unavailable networks. */ disabled_networks?: ("GTR" | "TRUST" | "VERIFYVASP" | "SYGNA" | (string & {}))[]; - /** Effective membership configuration for each network. No network, including GTR, is included by default. This is not a live health or delivery check. */ + /** Per network, how this application would send and why it cannot. No network, including GTR, is included by default: a network is reachable through your own connected membership, or through Didit's membership once Didit's membership of that network is live and your own participant on it is active. Not a live health or delivery check. */ networks?: TravelRuleSettingsDetailNetworks; /** Whether the Didit network lists your VASP profile, so other Didit customers find it in the directory and reach it on the INTERNAL rail, and each condition it still misses. */ didit_network?: TravelRuleSettingsDetailDiditNetwork; @@ -10126,52 +10429,16 @@ export interface TravelRuleSettingsDetailDiditNetwork { compliance_email_confirmed?: boolean; } -/** Effective membership configuration for each network. No network, including GTR, is included by default. This is not a live health or delivery check. */ +/** Per network, how this application would send and why it cannot. No network, including GTR, is included by default: a network is reachable through your own connected membership, or through Didit's membership once Didit's membership of that network is live and your own participant on it is active. Not a live health or delivery check. */ export interface TravelRuleSettingsDetailNetworks { - gtr?: TravelRuleSettingsDetailNetworksGtr; - trust?: TravelRuleSettingsDetailNetworksTrust; - verifyvasp?: TravelRuleSettingsDetailNetworksVerifyvasp; - sygna?: TravelRuleSettingsDetailNetworksSygna; -} - -export interface TravelRuleSettingsDetailNetworksGtr { - rail?: "GTR" | (string & {}); - /** own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available. */ - mode?: "own" | "didit" | "off" | "unavailable" | (string & {}); - /** Whether platform membership is configured; not a live health check. */ - platform_available?: boolean; - /** Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange. */ - connected?: boolean; -} - -export interface TravelRuleSettingsDetailNetworksSygna { - rail?: "SYGNA" | (string & {}); - /** own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available. */ - mode?: "own" | "didit" | "off" | "unavailable" | (string & {}); - /** Whether platform membership is configured; not a live health check. */ - platform_available?: boolean; - /** Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange. */ - connected?: boolean; -} - -export interface TravelRuleSettingsDetailNetworksTrust { - rail?: "TRUST" | (string & {}); - /** own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available. */ - mode?: "own" | "didit" | "off" | "unavailable" | (string & {}); - /** Whether platform membership is configured; not a live health check. */ - platform_available?: boolean; - /** Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange. */ - connected?: boolean; -} - -export interface TravelRuleSettingsDetailNetworksVerifyvasp { - rail?: "VERIFYVASP" | (string & {}); - /** own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available. */ - mode?: "own" | "didit" | "off" | "unavailable" | (string & {}); - /** Whether platform membership is configured; not a live health check. */ - platform_available?: boolean; - /** Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange. */ - connected?: boolean; + /** How Travel Rule messages on one network would be sent for this application, and why they cannot be. Configuration status, not a live health or delivery check. */ + gtr?: TravelRuleNetworkState; + /** How Travel Rule messages on one network would be sent for this application, and why they cannot be. Configuration status, not a live health or delivery check. */ + trust?: TravelRuleNetworkState; + /** How Travel Rule messages on one network would be sent for this application, and why they cannot be. Configuration status, not a live health or delivery check. */ + verifyvasp?: TravelRuleNetworkState; + /** How Travel Rule messages on one network would be sent for this application, and why they cannot be. Configuration status, not a live health or delivery check. */ + sygna?: TravelRuleNetworkState; } export interface TravelRuleTestVaspInboundResponse { @@ -10372,6 +10639,55 @@ export interface TravelRuleUtxoEntry { vout?: number | null; } +/** Why a wallet-ownership proof, or its last attempt, fell short of VERIFIED. Empty when nothing failed. A PENDING proof can carry one: the last attempt failed and the end user can try again. DEPOSIT_MISMATCH, DEPOSIT_STALE, TXID_REUSED and REVIEW_REJECTED close the proof as REJECTED. SIGNATURE_INVALID: the signature does not match the wallet and challenge. SIGNATURE_UNSUPPORTED_SCHEME: the wallet signed in a format Didit does not verify. DEPOSIT_NOT_FOUND, DEPOSIT_UNCONFIRMED, DEPOSIT_LOOKUP_FAILED: the test deposit is not readable or not confirmed yet; Didit keeps checking it until the test window closes. DEPOSIT_MISMATCH: the transaction does not send the expected amount from the wallet to the deposit address. DEPOSIT_STALE: the transaction predates the challenge. TXID_REUSED: the transaction already proved another wallet. CHAIN_UNSUPPORTED: the method is not available on this chain. WINDOW_EXPIRED: the test window closed. OWNERSHIP_CHANGED: the wallet's holder changed, or the wallet was deleted from your address book, after the widget session was minted. METHOD_SWITCHED: the end user picked another method before finishing this one. REVIEW_REJECTED: your reviewer rejected the screenshot. ABANDONED: the widget session lapsed before the end user finished. */ +export type TravelRuleWalletProofFailureReason = + | "" + | "SIGNATURE_INVALID" + | "SIGNATURE_UNSUPPORTED_SCHEME" + | "DEPOSIT_NOT_FOUND" + | "DEPOSIT_UNCONFIRMED" + | "DEPOSIT_LOOKUP_FAILED" + | "DEPOSIT_MISMATCH" + | "DEPOSIT_STALE" + | "TXID_REUSED" + | "CHAIN_UNSUPPORTED" + | "WINDOW_EXPIRED" + | "OWNERSHIP_CHANGED" + | "METHOD_SWITCHED" + | "REVIEW_REJECTED" + | "ABANDONED" + | (string & {}); + +export interface TravelRuleWalletProofReview { + proof_id?: string; + method?: "SCREENSHOT" | (string & {}); + status?: "PENDING" | "VERIFIED" | "REJECTED" | "EXPIRED" | (string & {}); + wallet_address?: string; + /** Canonical chain identifier, e.g. ETH, BTC, SOL, TRX. */ + chain?: string; + holder_name?: string; + /** Whether the address book entry is ownership-verified, by this proof or another one. */ + is_ownership_verified?: boolean; + /** The transaction whose Travel Rule transfer the widget session was minted for. Null for a session minted without a transaction_id. */ + transaction_id?: string | null; + file_name?: string | null; + content_type?: string | null; + /** Time-limited link to the uploaded screenshot. List the queue again for a fresh link. */ + screenshot_url?: string | null; + /** How many files the end user uploaded for this proof. */ + attempts?: number; + /** REVIEW_REJECTED after a rejection, OWNERSHIP_CHANGED when the wallet changed holder or was deleted before a decision. Empty otherwise. */ + failure_reason?: TravelRuleWalletProofFailureReason; + /** When the end user picked the screenshot method. */ + method_chosen_at?: Date | null; + decided_at?: Date | null; + /** Who closed the proof: review: for a console reviewer, review for an API-key decision, widget or sweeper when Didit closed it automatically. Empty while PENDING. */ + decided_by?: string; + /** The note sent with the decision. Empty when none. */ + review_note?: string; + created_at?: Date; +} + export interface TravelRuleWidgetSessionResponse { widget_session_id?: string; /** Opaque session token embedded in the widget URL. */ @@ -10954,7 +11270,7 @@ export interface WalletAddressEntry { uuid?: string; /** The wallet address; `""` for an entry imported by its hash only. Unique per application, chain and memo, comparing the address without regard to case. After you delete an entry, you can register the same address again. */ address?: string; - /** Chain token for the address, for example `ETH` or `BTC`. When Didit routes or registers a transfer, it matches address book entries on the wallet address and, when the transfer carries a chain, on this value too, ignoring case. A transfer's chain is `beneficiary_data.chain`, filled from the transaction's crypto chain or currency symbol (for example `ETH`) when you do not send it, or the `chain` of an after-deposit registration, so use the same token everywhere. An entry with an empty chain matches any chain. A Lightning payee (a node id or a Lightning Address) registered on bitcoin is stored lowercased on `LIGHTNING`, where Lightning transfers are matched. */ + /** Chain token for the address, for example `ETH` or `BTC`. When Didit routes or registers a transfer, it matches address book entries on the wallet address and, when the transfer carries a chain, on this value too, ignoring case. A transfer's chain is `beneficiary_data.chain`, filled from the transaction's crypto chain or currency symbol (for example `ETH`) when you do not send it, or the `chain` of an after-deposit registration, so use the same token everywhere. An entry with an empty chain matches any chain. A Lightning payee (a node id or a Lightning Address) registered on bitcoin is stored lowercased on `LIGHTNING`, where Lightning transfers are matched. Known chains are stored under one canonical identifier whatever spelling you send: "ethereum", "Ethereum" and "ERC20" are all stored as ETH, "BNB Smart Chain" and "BEP20" as BNB, "tron" as TRX. An unknown chain name is kept as given. */ chain?: string; /** Your internal identifier for the holder. */ holder_vendor_data?: string; @@ -11207,6 +11523,8 @@ export interface WebhookConfig { face_retention_policy: "delete_with_session" | "retain_until_user_deleted" | (string & {}); /** Retention period, in days, of biometric templates under `retain_until_user_deleted`. Required when that policy is set. */ face_retention_days: number | null; + /** When the hosted flow and the native SDKs ask the person for explicit biometric consent before a face capture. `auto` (default) asks only where a trigger applies: a declared state, the person's IP, or the identity document's issuer is in Illinois, Texas or Colorado, or the person looks to be in the US without a known state. `always` asks every person whose workflow captures a face. `never` never shows the step, and you collect consent yourself before the selfie. Each change is recorded in the audit log. See [Biometric consent](https://docs.didit.me/getting-started/biometric-consent). */ + biometric_consent_mode?: "auto" | "always" | "never" | (string & {}); /** Every event type a destination can subscribe to, with a display label. */ available_events: WebhookConfigAvailableEvents[]; /** Hostnames (exact match, HTTPS only) a standalone Digital ID wallet verification may send the user back to with its `return_url`. Your white-label domain is always allowed as well. See [Register your return destination](https://docs.didit.me/standalone-apis/digital-id-wallets#register-your-return-destination). */ @@ -11384,13 +11702,13 @@ export interface WorkflowDetail { kyb_role_settings?: Record[]; created_at?: Date; updated_at?: Date; - /** Whether a simple KYC or KYB workflow runs Bank verification. It runs after the contact steps and before database validation, AML and IP analysis. Set in the console. */ + /** Whether a simple KYC or KYB workflow runs Bank verification. It runs after the contact steps (and Location verification) and before database validation, AML and IP analysis. Bank verification is not generally available yet: turning it on is refused wherever it is not enabled. */ is_bank_verification_enabled?: boolean; - /** Bank verification settings of a simple workflow, with the same keys a `BANK_VERIFICATION` node takes (for example `bank_countries` and `bank_method_order`). Every key is optional and an absent key keeps its default, so `{}` means all defaults. See [Bank verification settings](/management-api/workflows/feature-configs#bank-verification). */ + /** Bank verification settings of a simple workflow, with the same keys a `BANK_VERIFICATION` node takes (for example `bank_countries` and `bank_method_order`). Unknown keys are dropped. Every key is optional and an absent key keeps its default, so `{}` means all defaults. See [Bank verification settings](/management-api/workflows/feature-configs#bank-verification). */ bank_verification_config?: Record; - /** Whether a simple KYC workflow runs Location verification. It runs after the contact steps and before Bank verification. Never on KYB workflows. Set in the console. */ + /** Whether a simple KYC workflow runs Location verification. It runs after the contact steps and before Bank verification and database validation. Business (KYB) workflows cannot run it. */ is_geolocation_enabled?: boolean; - /** Location verification settings of a simple workflow, with the same keys a `GEOLOCATION` node takes (for example `is_geolocation_geofencing_enabled` and `geolocation_max_attempts`). Every key is optional and an absent key keeps its default, so `{}` means all defaults. */ + /** Location verification settings of a simple workflow, with the same keys a `GEOLOCATION` node takes (for example `is_geolocation_geofencing_enabled` and `geolocation_max_attempts`). Unknown keys are dropped. Every key is optional and an absent key keeps its default, so `{}` means all defaults. */ geolocation_config?: Record; [key: string]: unknown; } diff --git a/src/types/id-verification.ts b/src/types/id-verification.ts index 5b33768..73cbb4b 100644 --- a/src/types/id-verification.ts +++ b/src/types/id-verification.ts @@ -11,7 +11,7 @@ export interface IdVerificationVerifyParams { back_image?: Uploadable; /** Password to decrypt `back_image` when it is an encrypted PDF. */ back_image_password?: string; - /** Screens both images for presentation fraud: screen replays (`SCREEN_CAPTURE_DETECTED`), printed copies (`PRINTED_COPY_DETECTED`), and portrait manipulation (`PORTRAIT_MANIPULATION_DETECTED`). Any of these auto-declines. Enabled by default; set to `false` to skip fraud screening and reduce latency. */ + /** Screens both images for presentation fraud: screen replays (`SCREEN_CAPTURE_DETECTED`), printed copies (`PRINTED_COPY_DETECTED`), and portrait manipulation (`PORTRAIT_MANIPULATION_DETECTED`). Any of these auto-declines. Enabled by default; set to `false` to skip fraud screening and reduce latency. When enabled and the provider returns no usable score, the response is `In Review` with a `DOCUMENT_LIVENESS_NOT_COMPUTED` warning rather than `Approved`. */ perform_document_liveness?: boolean; /** Accepted and validated (1–120) but **currently not applied** by this endpoint - it never produces a `MINIMUM_AGE_NOT_MET` warning from this field. Enforce age rules from the returned `date_of_birth`/`age`, or use a verification-session workflow with age restrictions. */ minimum_age?: number | null; @@ -48,8 +48,8 @@ export interface IdVerificationVerifyResponse { } export interface IdVerificationVerifyResponseIdVerification { - /** `Approved` when no warning resolves to a decline; `Declined` otherwise - the `warnings` list explains why. */ - status?: "Approved" | "Declined" | (string & {}); + /** `Approved` when no warning resolves to a decline and the requested document-liveness check produced a usable result; `In Review` when that requested check produced no usable result (`DOCUMENT_LIVENESS_NOT_COMPUTED`); `Declined` when a warning resolves to a decline - the `warnings` list explains why. */ + status?: "Approved" | "Declined" | "In Review" | (string & {}); /** Detected document type display name: `Identity Card`, `Passport`, `Driver's License`, `Residence Permit`, `Health Insurance Card`, or `Tax Card`. `null` when the document could not be classified. */ document_type?: string | null; /** Finer-grained subtype when available (e.g. `ID_CARD_GENERIC`, `EPASSPORT`). */ @@ -103,7 +103,7 @@ export interface IdVerificationVerifyResponseIdVerification { mrz?: IdVerificationVerifyResponseIdVerificationMrz; /** Structured, geocoded breakdown of `address`. `null` when the document carries no address or it could not be parsed. */ parsed_address?: IdVerificationVerifyResponseIdVerificationParsedAddress | null; - /** Empty on a clean approval. Every entry explains one detected risk; entries with `log_type: "error"` set `status` to `Declined`. */ + /** Empty on a clean approval. Every entry explains one detected risk; entries with `log_type: "error"` set `status` to `Declined`, and `DOCUMENT_LIVENESS_NOT_COMPUTED` (`log_type: "warning"`) sets it to `In Review`. */ warnings?: IdVerificationVerifyResponseIdVerificationWarnings[]; /** Barcodes decoded from the document (PDF417, QR, …). Empty when the document has none or none could be read. */ barcodes?: IdVerificationVerifyResponseIdVerificationBarcodes[]; @@ -224,13 +224,13 @@ export interface IdVerificationVerifyResponseIdVerificationParsedAddressDocument } export interface IdVerificationVerifyResponseIdVerificationWarnings { - /** Machine-readable risk code. Always-decline risks: `DOCUMENT_EXPIRED`, `MINIMUM_AGE_NOT_MET`, `PORTRAIT_IMAGE_NOT_DETECTED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, `PUBLIC_DOCUMENT_IMAGE_DETECTED`, `INVALID_DATE`, `COULD_NOT_RECOGNIZE_DOCUMENT` (400-only, never a warning), `DOCUMENT_NUMBER_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`. Governed by request options: `MRZ_NOT_DETECTED` / `MRZ_VALIDATION_FAILED` (`invalid_mrz_action`), `DATA_INCONSISTENT` / `MRZ_AND_DATA_EXTRACTED_FROM_OCR_NOT_SAME` / `DOCUMENT_NAME_DIFFERENT_FROM_OTHER_APPROVED_DOCUMENTS` (`inconsistent_data_action`), `EXPIRATION_DATE_NOT_DETECTED` (`expiration_date_not_detected_action`). Other risks (e.g. `POSSIBLE_DUPLICATED_USER`) are informational. */ + /** Machine-readable risk code. Always-decline risks: `DOCUMENT_EXPIRED`, `MINIMUM_AGE_NOT_MET`, `PORTRAIT_IMAGE_NOT_DETECTED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, `PUBLIC_DOCUMENT_IMAGE_DETECTED`, `INVALID_DATE`, `COULD_NOT_RECOGNIZE_DOCUMENT` (400-only, never a warning), `DOCUMENT_NUMBER_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`. Governed by request options: `MRZ_NOT_DETECTED` / `MRZ_VALIDATION_FAILED` (`invalid_mrz_action`), `DATA_INCONSISTENT` / `MRZ_AND_DATA_EXTRACTED_FROM_OCR_NOT_SAME` / `DOCUMENT_NAME_DIFFERENT_FROM_OTHER_APPROVED_DOCUMENTS` (`inconsistent_data_action`), `EXPIRATION_DATE_NOT_DETECTED` (`expiration_date_not_detected_action`). Other risks (e.g. `POSSIBLE_DUPLICATED_USER`) are informational. `DOCUMENT_LIVENESS_NOT_COMPUTED` is raised when `perform_document_liveness=true` but the forgery check returned no usable score; it sets `status` to `In Review`. */ risk?: string; /** Feature that raised the warning. Always `ID_VERIFICATION` on this endpoint. */ feature?: "ID_VERIFICATION" | (string & {}); /** Extra context for some risks (e.g. `POSSIBLE_DUPLICATED_USER` includes `duplicated_session_id`); `null` for most. */ additional_data?: Record | null; - /** Severity. `error` warnings drive `status` to `Declined`; `information` entries are advisory and never decline on their own. (`warning` is not produced by this endpoint - its options only accept `DECLINE` or `NO_ACTION`.) */ + /** Severity. `error` warnings drive `status` to `Declined`; `warning` (only `DOCUMENT_LIVENESS_NOT_COMPUTED`, when the requested forgery check produced no usable score) drives `status` to `In Review`; `information` entries are advisory and never change the status. */ log_type?: "error" | "warning" | "information" | (string & {}); /** Human-readable one-line summary of the risk. */ short_description?: string; diff --git a/src/types/index.ts b/src/types/index.ts index f61a93a..df1c533 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,7 @@ // Code generated by codegen from openapi/*.json. DO NOT EDIT. export type * from "./account.js"; +export type * from "./address-validation.js"; export type * from "./age-estimation.js"; export type * from "./aml.js"; export type * from "./aml-monitoring.js"; diff --git a/src/types/kyb.ts b/src/types/kyb.ts index e6ce55b..2b825ec 100644 --- a/src/types/kyb.ts +++ b/src/types/kyb.ts @@ -166,9 +166,9 @@ export interface KybListShareholdersResponse202KybRegistry { is_editable?: boolean; /** Same as the top-level `registry_tier_delivered`. */ registry_tier_delivered?: "basic" | "shareholders" | "ubo" | (string & {}) | null; - /** `delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier. */ + /** `delivered` when the ownership data came back, `not_held` when the ownership registry answered and does not list this company (see `registry_data.ownership_error`), `unavailable` when an ownership tier was requested but the registry had none for this company or could not be asked (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier. */ registry_ownership_status?: - "not_requested" | "delivered" | "unavailable" | "pending" | (string & {}) | null; + "not_requested" | "delivered" | "not_held" | "unavailable" | "pending" | (string & {}) | null; /** The outcome of the ownership (Shareholders / UBO) tier, in the same shape for every country and registry source. */ ownership?: KYBRegistryOwnership; /** The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`. */ @@ -374,9 +374,9 @@ export interface KybListShareholdersResponseKybRegistry { is_editable?: boolean; /** Same as the top-level `registry_tier_delivered`. */ registry_tier_delivered?: "basic" | "shareholders" | "ubo" | (string & {}) | null; - /** `delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier. */ + /** `delivered` when the ownership data came back, `not_held` when the ownership registry answered and does not list this company (see `registry_data.ownership_error`), `unavailable` when an ownership tier was requested but the registry had none for this company or could not be asked (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier. */ registry_ownership_status?: - "not_requested" | "delivered" | "unavailable" | "pending" | (string & {}) | null; + "not_requested" | "delivered" | "not_held" | "unavailable" | "pending" | (string & {}) | null; /** The outcome of the ownership (Shareholders / UBO) tier, in the same shape for every country and registry source. */ ownership?: KYBRegistryOwnership; /** The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`. */ @@ -625,9 +625,9 @@ export interface KybListUbosResponse202KybRegistry { is_editable?: boolean; /** Same as the top-level `registry_tier_delivered`. */ registry_tier_delivered?: "basic" | "shareholders" | "ubo" | (string & {}) | null; - /** `delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier. */ + /** `delivered` when the ownership data came back, `not_held` when the ownership registry answered and does not list this company (see `registry_data.ownership_error`), `unavailable` when an ownership tier was requested but the registry had none for this company or could not be asked (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier. */ registry_ownership_status?: - "not_requested" | "delivered" | "unavailable" | "pending" | (string & {}) | null; + "not_requested" | "delivered" | "not_held" | "unavailable" | "pending" | (string & {}) | null; /** The outcome of the ownership (Shareholders / UBO) tier, in the same shape for every country and registry source. */ ownership?: KYBRegistryOwnership; /** The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`. */ @@ -833,9 +833,9 @@ export interface KybListUbosResponseKybRegistry { is_editable?: boolean; /** Same as the top-level `registry_tier_delivered`. */ registry_tier_delivered?: "basic" | "shareholders" | "ubo" | (string & {}) | null; - /** `delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier. */ + /** `delivered` when the ownership data came back, `not_held` when the ownership registry answered and does not list this company (see `registry_data.ownership_error`), `unavailable` when an ownership tier was requested but the registry had none for this company or could not be asked (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier. */ registry_ownership_status?: - "not_requested" | "delivered" | "unavailable" | "pending" | (string & {}) | null; + "not_requested" | "delivered" | "not_held" | "unavailable" | "pending" | (string & {}) | null; /** The outcome of the ownership (Shareholders / UBO) tier, in the same shape for every country and registry source. */ ownership?: KYBRegistryOwnership; /** The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`. */ @@ -1098,8 +1098,9 @@ export interface KybSelectResponseKybRegistry { is_editable?: boolean; /** Same as the top-level `registry_tier_delivered`. */ registry_tier_delivered?: "basic" | "shareholders" | "ubo" | (string & {}); - /** `delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `not_requested` on the Lite tier. */ - registry_ownership_status?: "not_requested" | "delivered" | "unavailable" | (string & {}) | null; + /** `delivered` when the ownership data came back, `not_held` when the ownership registry answered and does not list this company (see `registry_data.ownership_error`), `unavailable` when an ownership tier was requested but the registry had none for this company or could not be asked (see `registry_data.ownership_error`), `not_requested` on the Lite tier. */ + registry_ownership_status?: + "not_requested" | "delivered" | "not_held" | "unavailable" | (string & {}) | null; /** The outcome of the ownership (Shareholders / UBO) tier, in the same shape for every country and registry source. */ ownership?: KYBRegistryOwnership; /** The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`. */ diff --git a/src/types/networks.ts b/src/types/networks.ts index d96a550..522aa58 100644 --- a/src/types/networks.ts +++ b/src/types/networks.ts @@ -68,9 +68,9 @@ export interface NetworksListResponse { counts?: NetworkListCounts; /** The organization's cross-organization sharing mode. Only `global` compares networks across organizations; `private` and `off` keep them to the organization's own history. */ sharing_mode: "off" | "private" | "global" | (string & {}); - /** Whether cross-organization signals apply to this organization. They need both the Identity Enterprise plan and the `global` sharing mode. */ + /** Whether cross-organization signals apply to this organization. They need both the Enterprise plan and the `global` sharing mode. */ cross_org_available: boolean; - /** Why cross-organization signals do not apply, checked in this order. `plan`: the organization's Identity plan does not include the cross-organization fraud network (it needs Identity Enterprise), whatever the sharing mode. `sharing_mode`: the plan includes it, but the sharing mode is `private` or `off`. Null when `cross_org_available` is `true`. */ + /** Why cross-organization signals do not apply, checked in this order. `plan`: the organization's plan does not include the cross-organization fraud network (it needs the Enterprise plan), whatever the sharing mode. `sharing_mode`: the plan includes it, but the sharing mode is `private` or `off`. Null when `cross_org_available` is `true`. */ cross_org_unavailable_reason: "plan" | "sharing_mode" | (string & {}) | null; } diff --git a/src/types/regulatory-reports.ts b/src/types/regulatory-reports.ts index d3c7c2f..b049041 100644 --- a/src/types/regulatory-reports.ts +++ b/src/types/regulatory-reports.ts @@ -35,6 +35,11 @@ export interface RegulatoryReportsCreateParamsNarrative { how?: string; } +export interface RegulatoryReportsDeleteParams { + /** Set to true to confirm that a finalized report and its generated files are deleted permanently. */ + confirm_delete?: boolean; +} + export interface RegulatoryReportsListParams { /** Page size. */ limit?: number; diff --git a/src/types/sessions.ts b/src/types/sessions.ts index 540ae93..7e81dba 100644 --- a/src/types/sessions.ts +++ b/src/types/sessions.ts @@ -142,6 +142,8 @@ export interface SessionsCreateParams { callback_method?: "initiator" | "completer" | "both" | (string & {}); /** Arbitrary JSON stored with the session and echoed back in the response and webhooks. Any JSON value is accepted (object, string, number, array), but a JSON object of key/value pairs is recommended. Not shown to the end user. Use it to pass your own correlation ids, A/B variants, or business context. */ metadata?: unknown; + /** Tags added to the session when it is created, by name. Up to 10, each up to 50 characters. Names are case-insensitive and stored in upper case. A name that does not exist yet in the application is created as a new tag. The tags show on the session in the console, where you can filter by them. */ + tags?: string[]; /** Language code (ISO 639-1) for the verification process interface. Controls the language displayed to the end user during verification. If not provided, the browser's language will be automatically detected and used. Check all the supported languages [here](/integration/supported-languages). */ language?: | "en" @@ -209,6 +211,18 @@ export interface SessionsCreateParams { sandbox_scenario?: string; /** Electronic Signature input for this session. `documents` are the documents to sign, for a workflow whose Electronic Signature step takes documents from the session (`signature_documents_source` `session` or `both`): up to 20 documents and 50 MB in total. `field_values` fills the fields whose `prefill` is `api.`, on workflow and session documents alike. Creating a session again for the same `workflow_id` and `vendor_data` returns the pending session; if that call sends `sign`, its documents, fields and values must be identical to the first ones, and anything different returns `400` with `{"sign": "signature_document_changed"}`. See [Electronic signature documents](/sessions-api/create-session#electronic-signature-documents). */ sign?: SessionsCreateParamsSign; + /** Where the person lives, when you know it: an ISO 3166-2 subdivision code (`US-IL`, `ES-M`) or a US state code or name (`IL`, `Illinois`). A state whose law requires explicit biometric consent (Illinois, Texas, Colorado) makes the verification flow ask for it before capturing a face, with trigger `declared_state`. An unrecognized value returns `400`. See [Biometric consent](/getting-started/biometric-consent). */ + user_state?: string; + /** Explicit biometric consent you already collected from the person. It satisfies the requirement, so the verification flow does not ask again, and the decision records it as `status: "given"` with trigger `customer_collected`. An answer the person already gave in the flow is never replaced. See [Biometric consent](/getting-started/biometric-consent). */ + biometric_consent?: SessionsCreateParamsBiometricConsent; +} + +/** Explicit biometric consent you already collected from the person. It satisfies the requirement, so the verification flow does not ask again, and the decision records it as `status: "given"` with trigger `customer_collected`. An answer the person already gave in the flow is never replaced. See [Biometric consent](/getting-started/biometric-consent). */ +export interface SessionsCreateParamsBiometricConsent { + /** When the person gave explicit biometric consent (ISO 8601). Cannot be in the future. */ + given_at: string | Date; + /** Version id of the consent text the person accepted, in your own records. */ + text_version: string; } /** User contact information that can be used for notifications, prefilling verification forms, and phone verification. This includes email address, preferred language for communications, and phone number. */ @@ -332,6 +346,8 @@ export interface SessionsCreateResponse { vendor_data: string | null; /** Arbitrary JSON payload you stored with the session at creation time. Echoed back verbatim - whatever JSON value you sent (object, string, number, array) is returned as-is, though a JSON object is recommended. Not shown to the end user. Always present in responses; `null` when not provided at creation time. */ metadata: unknown; + /** The tags on the session, by name. */ + tags?: string[]; /** Current status of the session. Newly created sessions return `Not Started`. If a non-finished session already existed for the same `vendor_data` on the workflow's latest published version, the returned status reflects that existing session. */ status: | "Not Started" @@ -618,7 +634,7 @@ export interface SessionsListParams { status?: string; /** Exact-match filter on the `vendor_data` you sent at session creation (your own user/business identifier). For partial matches use `search` instead. */ vendor_data?: string; - /** Smart free-text search. A full UUID matches `session_id`; numeric input matches session-number prefix, document/personal numbers, phone prefix, or `vendor_data`; input containing `@` matches email prefixes; other text matches names (accent-insensitive) and `vendor_data`. Free text containing digits additionally matches document/personal-number search fields and session-number prefixes. */ + /** A complete displayed reference such as `S-0001` (user) or `B-0001` (business) matches only that session number and kind; leading zeros and letter case do not affect matching. Smart free-text search. A full UUID matches `session_id`; numeric input matches session-number prefix, document/personal numbers, phone prefix, or `vendor_data`; input containing `@` matches email prefixes; other text matches names (accent-insensitive) and `vendor_data`. Free text containing digits additionally matches document/personal-number search fields and session-number prefixes. */ search?: string; /** Comma-separated workflow UUIDs (find them on the Workflows page in the Console). Malformed UUIDs in the list are ignored; if none are valid the result is empty. */ workflow_id?: string; diff --git a/src/types/transactions.ts b/src/types/transactions.ts index 5d1f10e..0ce2c55 100644 --- a/src/types/transactions.ts +++ b/src/types/transactions.ts @@ -14,6 +14,20 @@ import type { TravelRuleUtxo, } from "./components.js"; +export interface TransactionsBatchDeleteParamsAllTransactions { + /** Delete every transaction in the application. `transaction_uuids` is ignored. */ + delete_all: boolean; + /** Ignored when `delete_all` is true. */ + transaction_uuids?: string[]; +} + +export interface TransactionsBatchDeleteParamsSelectedTransactions { + /** Didit transaction UUIDs to delete. Required unless `delete_all` is true. */ + transaction_uuids: string[]; + /** Leave out or send `false` when you select transactions with `transaction_uuids`. */ + delete_all?: boolean; +} + export interface TransactionsCreateParams { /** Your unique identifier for this transaction (max 128 characters). */ transaction_id: string; @@ -565,17 +579,24 @@ export interface TransactionsTravelRuleTriggerTestVaspInboundParams { reference?: string; } -/** Provide exactly one of: payment_txn_id (finish), action=cancel, action=resend, action=update_compliance_email with compliance_email, or action=update_originator_wallet with wallet_address. */ +/** Provide exactly one of: payment_txn_id (finish), action=cancel, action=resend, action=update_compliance_email with compliance_email, action=update_originator_wallet with wallet_address, or action=beneficiary_vasp with beneficiary_vasp. */ export interface TransactionsTravelRuleUpdateTransferParams { /** The on-chain transaction hash. Requires the transfer to be an outbound transfer in COMPLETED status. */ payment_txn_id?: string; - /** "cancel" cancels a non-terminal transfer. "resend" re-runs routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA. "update_compliance_email" replaces the compliance email the request goes to, on an outbound transfer in one of those three statuses; send "resend" afterwards. "update_originator_wallet" records the wallet the funds actually left from (`wallet_address`, `memo`). */ + /** "cancel" cancels a non-terminal transfer. "resend" re-runs routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA. "update_compliance_email" replaces the compliance email the request goes to, on an outbound transfer in one of those three statuses; send "resend" afterwards. "update_originator_wallet" records the wallet the funds actually left from (`wallet_address`, `memo`). "beneficiary_vasp" names the counterparty VASP yourself (requires `beneficiary_vasp`) for an outbound transfer in one of those statuses: it is routed to that VASP and sent again. */ action?: - "cancel" | "resend" | "update_compliance_email" | "update_originator_wallet" | (string & {}); + | "cancel" + | "resend" + | "update_compliance_email" + | "update_originator_wallet" + | "beneficiary_vasp" + | (string & {}); /** Required with `action: update_compliance_email`. Kept as this transfer's `beneficiary_data.counterparty_vasp_email`: it belongs to this transfer only and joins no directory entry and no contact list. */ compliance_email?: string; /** With `action: update_originator_wallet`: the wallet the funds left from. When the transfer's UTXO inputs are known it must be one of them. */ wallet_address?: string; /** With `action: update_originator_wallet`: the wallet's memo or tag on a memo network. Optional. */ memo?: string; + /** Required with action=beneficiary_vasp: the `id` of a directory VASP, from `GET /v3/travel-rule/vasps/`. Your choice outranks every automatic discovery step, which never overrides it. */ + beneficiary_vasp?: string; } diff --git a/src/types/travel-rule.ts b/src/types/travel-rule.ts index 0d8d02f..054d381 100644 --- a/src/types/travel-rule.ts +++ b/src/types/travel-rule.ts @@ -7,6 +7,7 @@ import type { TravelRuleMatchingRules, TravelRuleTransferRow, TravelRuleUtxo, + TravelRuleWalletProofReview, VaspDirectoryEntry, WalletAddressEntry, } from "./components.js"; @@ -38,7 +39,7 @@ export interface TravelRuleCounterpartyContactsListResponse { export interface TravelRuleCreateWidgetSessionParams { /** The wallet address to verify. Optional; when empty, the address must be supplied when a proof is started. */ wallet_address?: string; - /** Chain token for the address, for example `ETH`, `BTC` or `SOL`. Use the same token as in your wallet address book. Message signing and the Satoshi test are offered only for a supported chain. */ + /** Chain token for the address, for example `ETH`, `BTC` or `SOL`. Use the same token as in your wallet address book. Message signing and the Satoshi test are offered only for a supported chain. The address book entry the proof verifies stores it under its canonical identifier (ETH, BTC, SOL, TRX). */ chain?: string; /** Name of the wallet holder, used for the beneficiary name match. */ holder_name?: string; @@ -80,6 +81,44 @@ export interface TravelRuleListVaspsResponse { results?: VaspDirectoryEntry[]; } +/** Didit's network due-diligence questionnaire. Saves are partial: a save keeps every answer it does not name. Submitting requires every field except lei and data_protection_contact_email, and the three attestations true. */ +export interface TravelRuleNetworksUpdateQuestionnaireParams { + legal_name?: string; + registration_number?: string; + /** ISO 3166-1 alpha-2 country code. */ + incorporation_country?: string; + registered_address?: string; + website?: string; + /** Optional. A valid ISO 17442 LEI when given. */ + lei?: string; + /** Every licence or registration the VASP holds. */ + licences?: TravelRuleNetworksUpdateQuestionnaireParamsLicences[]; + operating_jurisdictions?: string[]; + compliance_officer_name?: string; + compliance_officer_email?: string; + /** Must be true to submit. */ + aml_ctf_policy_attested?: boolean; + /** Must be true to submit. */ + sanctions_screening_attested?: boolean; + /** Must be true to submit. */ + data_protection_attested?: boolean; + /** Optional. */ + data_protection_contact_email?: string; + expected_monthly_transfers?: number; + /** Expected monthly volume in USD, up to 2 decimal places. Returned as a string. */ + expected_monthly_volume_usd?: string; + /** Asset tickers, stored upper-case, sorted and de-duplicated. */ + assets?: string[]; +} + +export interface TravelRuleNetworksUpdateQuestionnaireParamsLicences { + regulator: string; + /** ISO 3166-1 alpha-2 country code. */ + jurisdiction: string; + licence_type: string; + licence_number: string; +} + export interface TravelRuleRegisterInboundParams { /** Chain the deposit settled on, for example `ETH`. Stored lowercase and compared case-insensitively with your wallet address book entries, so use the same chain token there. */ chain: string; @@ -210,7 +249,7 @@ export interface TravelRuleWalletAddressesBulkImportParams { export interface TravelRuleWalletAddressesCreateParams { /** The wallet address. An XRP X-address is stored as its classic address and its tag, so it matches transfers that name either form. */ address: string; - /** Chain token for the address, for example `ETH` or `BTC`. When Didit routes or registers a transfer, it matches address book entries on the wallet address and, when the transfer carries a chain, on this value too, ignoring case. A transfer's chain is `beneficiary_data.chain`, filled from the transaction's crypto chain or currency symbol (for example `ETH`) when you do not send it, or the `chain` of an after-deposit registration, so use the same token everywhere. An entry with an empty chain matches any chain. */ + /** Chain token for the address, for example `ETH` or `BTC`. When Didit routes or registers a transfer, it matches address book entries on the wallet address and, when the transfer carries a chain, on this value too, ignoring case. A transfer's chain is `beneficiary_data.chain`, filled from the transaction's crypto chain or currency symbol (for example `ETH`) when you do not send it, or the `chain` of an after-deposit registration, so use the same token everywhere. An entry with an empty chain matches any chain. Known chains are stored under one canonical identifier whatever spelling you send: "ethereum", "Ethereum" and "ERC20" are all stored as ETH, "BNB Smart Chain" and "BEP20" as BNB, "tron" as TRX. An unknown chain name is kept as given. */ chain?: string; holder_vendor_data?: string; holder_name?: string; @@ -243,6 +282,26 @@ export interface TravelRuleWalletAddressesUpdateParams { travel_address?: string; } +export interface TravelRuleWalletProofsDecideParams { + decision: "approve" | "reject" | (string & {}); + /** Optional. Kept with the proof and returned as review_note. */ + note?: string; +} + +export interface TravelRuleWalletProofsListParams { + /** Which proofs to list. Defaults to PENDING, the ones still to decide. */ + status?: "PENDING" | "VERIFIED" | "REJECTED" | "EXPIRED" | (string & {}); + limit?: number; + offset?: number; +} + +export interface TravelRuleWalletProofsListResponse { + count?: number; + next?: string | null; + previous?: string | null; + results?: TravelRuleWalletProofReview[]; +} + export interface TravelRuleWorkspaceClaimParams { /** The directory entry this workspace is. Read ids from `GET /v3/travel-rule/vasps/`. */ vasp_id: string; diff --git a/src/types/webhooks.ts b/src/types/webhooks.ts index cf1656b..d39c398 100644 --- a/src/types/webhooks.ts +++ b/src/types/webhooks.ts @@ -309,6 +309,8 @@ export interface WebhooksUpdateConfigParams { face_retention_policy?: "delete_with_session" | "retain_until_user_deleted" | (string & {}); /** Retention period, in days, of biometric templates under `retain_until_user_deleted`. */ face_retention_days?: number | null; + /** When the hosted flow and the native SDKs ask the person for explicit biometric consent before a face capture. `auto` (default) asks only where a trigger applies: a declared state, the person's IP, or the identity document's issuer is in Illinois, Texas or Colorado, or the person looks to be in the US without a known state. `always` asks every person whose workflow captures a face. `never` never shows the step, and you collect consent yourself before the selfie. Each change is recorded in the audit log. See [Biometric consent](https://docs.didit.me/getting-started/biometric-consent). */ + biometric_consent_mode?: "auto" | "always" | "never" | (string & {}); /** Replaces the whole list of Digital ID wallet return hosts. Bare hostnames only, such as `shop.example.com`: no scheme, port, path or wildcard. Values are lowercased and duplicates are dropped. Send `[]` to clear the list. */ allowed_return_hosts?: string[]; } diff --git a/tests/generated/address-validation.test.ts b/tests/generated/address-validation.test.ts new file mode 100644 index 0000000..69c37c3 --- /dev/null +++ b/tests/generated/address-validation.test.ts @@ -0,0 +1,32 @@ +// Code generated by codegen from openapi/*.json. DO NOT EDIT. + +import { describe, expect, it } from "vitest"; + +import { + API_ORIGIN, + expectAuth, + expectMatchesExample, + mockClient, + responseExample, +} from "../helpers/mock-api.js"; + +describe("address_validation", () => { + it("address_validation.validate: POST /v3/address-validation/", async () => { + const { client, calls } = mockClient({ + status: 200, + json: responseExample("POST /v3/address-validation/"), + }); + const result = await client.addressValidation.validate({ country: "US" }); + expect(calls).toHaveLength(1); + const request = calls[0]!; + expect(request.method).toBe("POST"); + expect(request.origin).toBe(API_ORIGIN); + expect(request.path).toBe("/v3/address-validation/"); + expect(request.query).toEqual({}); + expectAuth(request, "api_key"); + expect(request.headers.get("accept")).toBe("application/json"); + expect(request.contentType).toBe("application/json"); + expect(request.json).toEqual({ country: "US" }); + expectMatchesExample(result, responseExample("POST /v3/address-validation/")); + }); +}); diff --git a/tests/generated/regulatory-reports.test.ts b/tests/generated/regulatory-reports.test.ts index 7c48506..de58047 100644 --- a/tests/generated/regulatory-reports.test.ts +++ b/tests/generated/regulatory-reports.test.ts @@ -56,7 +56,8 @@ describe("regulatory_reports", () => { expect(request.query).toEqual({}); expectAuth(request, "api_key"); expect(request.headers.get("accept")).toBe("application/json"); - expect(request.bodyText).toBe(""); + expect(request.contentType).toBe("application/json"); + expect(request.json).toEqual({}); }); it("regulatory_reports.finalize: POST /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/finalize/", async () => { diff --git a/tests/generated/transactions.test.ts b/tests/generated/transactions.test.ts index ca4445d..2c8f9fc 100644 --- a/tests/generated/transactions.test.ts +++ b/tests/generated/transactions.test.ts @@ -12,6 +12,22 @@ import { } from "../helpers/mock-api.js"; describe("transactions", () => { + it("transactions.batch_delete: DELETE /v3/organization/{organization_id}/application/{application_id}/transactions/delete/", async () => { + const { client, calls } = mockClient({ status: 204 }); + await client.transactions.batchDelete({ + transaction_uuids: ["00000000-0000-4000-8000-000000000001"], + }); + expect(calls).toHaveLength(1); + const request = calls[0]!; + expect(request.method).toBe("DELETE"); + expect(request.origin).toBe(API_ORIGIN); + expect(request.path).toBe("/v3/organization/org-123/application/app-456/transactions/delete/"); + expect(request.query).toEqual({}); + expectAuth(request, "api_key"); + expect(request.headers.get("accept")).toBe("application/json"); + expect(request.json).toEqual({ transaction_uuids: ["00000000-0000-4000-8000-000000000001"] }); + }); + it("transactions.create: POST /v3/transactions/", async () => { const { client, calls } = mockClient({ status: 201, diff --git a/tests/generated/travel-rule.test.ts b/tests/generated/travel-rule.test.ts index 46dc0e6..a793d2e 100644 --- a/tests/generated/travel-rule.test.ts +++ b/tests/generated/travel-rule.test.ts @@ -152,6 +152,103 @@ describe("travel_rule", () => { expect(page.hasNextPage()).toBe(false); }); + it("travel_rule.networks.request_access: POST /v3/travel-rule/networks/{network}/request/", async () => { + const { client, calls } = mockClient({ + status: 200, + json: responseExample("POST /v3/travel-rule/networks/{network}/request/"), + }); + const result = await client.travelRule.networks.requestAccess("network-1"); + expect(calls).toHaveLength(1); + const request = calls[0]!; + expect(request.method).toBe("POST"); + expect(request.origin).toBe(API_ORIGIN); + expect(request.path).toBe("/v3/travel-rule/networks/network-1/request/"); + expect(request.query).toEqual({}); + expectAuth(request, "api_key"); + expect(request.headers.get("accept")).toBe("application/json"); + expect(request.bodyText).toBe(""); + expectMatchesExample( + result, + responseExample("POST /v3/travel-rule/networks/{network}/request/"), + ); + }); + + it("travel_rule.networks.retrieve: GET /v3/travel-rule/networks/", async () => { + const { client, calls } = mockClient({ + status: 200, + json: responseExample("GET /v3/travel-rule/networks/"), + }); + const result = await client.travelRule.networks.retrieve(); + expect(calls).toHaveLength(1); + const request = calls[0]!; + expect(request.method).toBe("GET"); + expect(request.origin).toBe(API_ORIGIN); + expect(request.path).toBe("/v3/travel-rule/networks/"); + expect(request.query).toEqual({}); + expectAuth(request, "api_key"); + expect(request.headers.get("accept")).toBe("application/json"); + expect(request.bodyText).toBe(""); + expectMatchesExample(result, responseExample("GET /v3/travel-rule/networks/")); + }); + + it("travel_rule.networks.retrieve_questionnaire: GET /v3/travel-rule/networks/questionnaire/", async () => { + const { client, calls } = mockClient({ + status: 200, + json: responseExample("GET /v3/travel-rule/networks/questionnaire/"), + }); + const result = await client.travelRule.networks.retrieveQuestionnaire(); + expect(calls).toHaveLength(1); + const request = calls[0]!; + expect(request.method).toBe("GET"); + expect(request.origin).toBe(API_ORIGIN); + expect(request.path).toBe("/v3/travel-rule/networks/questionnaire/"); + expect(request.query).toEqual({}); + expectAuth(request, "api_key"); + expect(request.headers.get("accept")).toBe("application/json"); + expect(request.bodyText).toBe(""); + expectMatchesExample(result, responseExample("GET /v3/travel-rule/networks/questionnaire/")); + }); + + it("travel_rule.networks.submit_questionnaire: POST /v3/travel-rule/networks/questionnaire/submit/", async () => { + const { client, calls } = mockClient({ + status: 200, + json: responseExample("POST /v3/travel-rule/networks/questionnaire/submit/"), + }); + const result = await client.travelRule.networks.submitQuestionnaire(); + expect(calls).toHaveLength(1); + const request = calls[0]!; + expect(request.method).toBe("POST"); + expect(request.origin).toBe(API_ORIGIN); + expect(request.path).toBe("/v3/travel-rule/networks/questionnaire/submit/"); + expect(request.query).toEqual({}); + expectAuth(request, "api_key"); + expect(request.headers.get("accept")).toBe("application/json"); + expect(request.bodyText).toBe(""); + expectMatchesExample( + result, + responseExample("POST /v3/travel-rule/networks/questionnaire/submit/"), + ); + }); + + it("travel_rule.networks.update_questionnaire: PUT /v3/travel-rule/networks/questionnaire/", async () => { + const { client, calls } = mockClient({ + status: 200, + json: responseExample("PUT /v3/travel-rule/networks/questionnaire/"), + }); + const result = await client.travelRule.networks.updateQuestionnaire(); + expect(calls).toHaveLength(1); + const request = calls[0]!; + expect(request.method).toBe("PUT"); + expect(request.origin).toBe(API_ORIGIN); + expect(request.path).toBe("/v3/travel-rule/networks/questionnaire/"); + expect(request.query).toEqual({}); + expectAuth(request, "api_key"); + expect(request.headers.get("accept")).toBe("application/json"); + expect(request.contentType).toBe("application/json"); + expect(request.json).toEqual({}); + expectMatchesExample(result, responseExample("PUT /v3/travel-rule/networks/questionnaire/")); + }); + it("travel_rule.register_inbound: POST /v3/travel-rule/inbound/", async () => { const { client, calls } = mockClient({ status: 200, @@ -475,6 +572,52 @@ describe("travel_rule", () => { ); }); + it("travel_rule.wallet_proofs.decide: POST /v3/travel-rule/wallet-proofs/{proof_uuid}/decision/", async () => { + const { client, calls } = mockClient({ + status: 200, + json: responseExample("POST /v3/travel-rule/wallet-proofs/{proof_uuid}/decision/"), + }); + const result = await client.travelRule.walletProofs.decide("proof-uuid-1", { + decision: "approve", + }); + expect(calls).toHaveLength(1); + const request = calls[0]!; + expect(request.method).toBe("POST"); + expect(request.origin).toBe(API_ORIGIN); + expect(request.path).toBe("/v3/travel-rule/wallet-proofs/proof-uuid-1/decision/"); + expect(request.query).toEqual({}); + expectAuth(request, "api_key"); + expect(request.headers.get("accept")).toBe("application/json"); + expect(request.contentType).toBe("application/json"); + expect(request.json).toEqual({ decision: "approve" }); + expectMatchesExample( + result, + responseExample("POST /v3/travel-rule/wallet-proofs/{proof_uuid}/decision/"), + ); + }); + + it("travel_rule.wallet_proofs.list: GET /v3/travel-rule/wallet-proofs/", async () => { + const { client, calls } = mockClient({ + status: 200, + json: responseExample("GET /v3/travel-rule/wallet-proofs/", "page"), + }); + const page = await client.travelRule.walletProofs.list(); + expect(calls).toHaveLength(1); + const request = calls[0]!; + expect(request.method).toBe("GET"); + expect(request.origin).toBe(API_ORIGIN); + expect(request.path).toBe("/v3/travel-rule/wallet-proofs/"); + expect(request.query).toEqual({}); + expectAuth(request, "api_key"); + expect(request.headers.get("accept")).toBe("application/json"); + expect(request.bodyText).toBe(""); + expectMatchesExample( + { ...page }, + responseExample("GET /v3/travel-rule/wallet-proofs/", "page"), + ); + expect(page.hasNextPage()).toBe(false); + }); + it("travel_rule.workspace.claim: POST /v3/travel-rule/workspace/claim/", async () => { const { client, calls } = mockClient({ status: 200,