diff --git a/openapi/SOURCE.json b/openapi/SOURCE.json index 0bee370..5250c3a 100644 --- a/openapi/SOURCE.json +++ b/openapi/SOURCE.json @@ -1,8 +1,8 @@ { "verification.json": { "url": "https://docs.didit.me/openapi-25.json", - "sha256": "6157d174fff4e0093ca7ac8e0f064ffc777d1cfa4a7eeb2b0e88fbf86f4a060b", - "fetched_at": "2026-10-25T06:41:01Z" + "sha256": "b2658eb22e23f868d2df5fe513c8dcdadce3abf2e35d1bbebce0382c911038db", + "fetched_at": "2026-10-27T06:46:11Z" }, "account.json": { "url": "https://docs.didit.me/openapi-auth.json", diff --git a/openapi/verification.json b/openapi/verification.json index 0236ac8..1125811 100644 --- a/openapi/verification.json +++ b/openapi/verification.json @@ -1509,7 +1509,7 @@ "/v3/session/imports/": { "post": { "summary": "Create a verification import job", - "description": "Create an asynchronous import job for historical User Verification sessions, Business Verification sessions, transaction monitoring records, or workflow custom status rules. Use this to migrate from providers such as Sumsub, MetaMap, Veriff, Onfido, Persona, Trulioo, Jumio, Incode, iDenfy, or any other provider after transforming the export into Didit's canonical CSV/NDJSON format. Imports are disabled by default and must be enabled per organization by Didit.", + "description": "Create an asynchronous import job for historical User Verification sessions, Business Verification sessions, transaction monitoring records, or workflow custom status rules. Set `source=provider` to migrate from another provider: Didit reads Sumsub, Onfido, Persona and ComplyCube with your read-only key, reads Veriff, iDenfy and MetaMap records by the IDs you upload in `file`, and imports an export file in Didit's format for the other providers. Record IDs are namespaced by provider (`sumsub:`) so a re-run skips everything already in Didit. Imports are disabled by default and must be enabled per organization by Didit.", "operationId": "post_v3_session_imports_create", "x-didit-sdk": { "resource": "sessions.imports", @@ -1560,6 +1560,81 @@ "type": "string", "format": "uri", "description": "Alternative to file upload. Must be an HTTPS URL resolving to a public address (private/loopback networks are rejected). Mutually exclusive with `file`." + }, + "source": { + "type": "string", + "enum": [ + "file", + "provider" + ], + "description": "`provider` makes this a migration from another identity verification provider. Omit for a file import." + }, + "provider": { + "type": "string", + "enum": [ + "sumsub", + "jumio", + "veriff", + "onfido", + "persona", + "metamap", + "socure", + "idnow", + "shufti", + "idenfy", + "ondato", + "trulioo", + "au10tix", + "complycube", + "other" + ], + "description": "Migration source, from `GET /v3/session/imports/providers/`. Required when `source` is `provider`." + }, + "credentials": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "The provider's read-only key, with the field names `credential_fields` lists. Encrypted at rest, never returned, and erased when the import ends. Send it as a JSON string in multipart." + }, + "options": { + "type": "object", + "properties": { + "region": { + "type": "string", + "description": "The provider region, one of its `regions` (Onfido: `eu`, `us`, `ca`)." + } + } + }, + "status_mapping": { + "type": "object", + "additionalProperties": { + "type": "string", + "enum": [ + "Approved", + "Declined", + "In Review", + "Expired", + "Not Finished", + "Abandoned" + ] + }, + "description": "Edits to the provider's default status mapping: source status to Didit status. `Not Finished` and `Abandoned` are skipped and counted, never imported." + }, + "dry_run": { + "type": "boolean", + "default": false, + "description": "Count what the migration would import (by status, already in Didit, same person, not finished) and write nothing. The job ends `READY`; start it with `POST /v3/session/imports/{importId}/start/`." + }, + "face_templates": { + "type": "boolean", + "default": false, + "description": "Build face templates from the imported selfies and search them against the application's faces and blocklist. Matches are flagged for review only. Requires `lawful_basis_confirmed`." + }, + "lawful_basis_confirmed": { + "type": "boolean", + "default": false, + "description": "Confirms you have a lawful basis to share these selfies with Didit as processor." } } } @@ -1599,6 +1674,81 @@ "type": "string", "format": "uri", "description": "Alternative to file upload. Must be an HTTPS URL resolving to a public address (private/loopback networks are rejected). Mutually exclusive with `file`." + }, + "source": { + "type": "string", + "enum": [ + "file", + "provider" + ], + "description": "`provider` makes this a migration from another identity verification provider. Omit for a file import." + }, + "provider": { + "type": "string", + "enum": [ + "sumsub", + "jumio", + "veriff", + "onfido", + "persona", + "metamap", + "socure", + "idnow", + "shufti", + "idenfy", + "ondato", + "trulioo", + "au10tix", + "complycube", + "other" + ], + "description": "Migration source, from `GET /v3/session/imports/providers/`. Required when `source` is `provider`." + }, + "credentials": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "The provider's read-only key, with the field names `credential_fields` lists. Encrypted at rest, never returned, and erased when the import ends. Send it as a JSON string in multipart." + }, + "options": { + "type": "object", + "properties": { + "region": { + "type": "string", + "description": "The provider region, one of its `regions` (Onfido: `eu`, `us`, `ca`)." + } + } + }, + "status_mapping": { + "type": "object", + "additionalProperties": { + "type": "string", + "enum": [ + "Approved", + "Declined", + "In Review", + "Expired", + "Not Finished", + "Abandoned" + ] + }, + "description": "Edits to the provider's default status mapping: source status to Didit status. `Not Finished` and `Abandoned` are skipped and counted, never imported." + }, + "dry_run": { + "type": "boolean", + "default": false, + "description": "Count what the migration would import (by status, already in Didit, same person, not finished) and write nothing. The job ends `READY`; start it with `POST /v3/session/imports/{importId}/start/`." + }, + "face_templates": { + "type": "boolean", + "default": false, + "description": "Build face templates from the imported selfies and search them against the application's faces and blocklist. Matches are flagged for review only. Requires `lawful_basis_confirmed`." + }, + "lawful_basis_confirmed": { + "type": "boolean", + "default": false, + "description": "Confirms you have a lawful basis to share these selfies with Didit as processor." } } }, @@ -1650,7 +1800,7 @@ } }, "400": { - "description": "Invalid workflow, file type, file size, source URL, or selector combination.", + "description": "Invalid workflow, file type, file size, source URL, or selector combination. A migration whose key the provider refuses answers `code` (`invalid_credentials`, `missing_permission`, `wrong_region`, `provider_unavailable`, `rate_limited`, `ids_required`, `record_not_found`), `missing_permissions` and `suggested_region`.", "content": { "application/json": { "examples": { @@ -68276,217 +68426,118 @@ } } }, - "/v3/session/{sessionId}/simulate/": { - "post": { - "summary": "Simulate a sandbox session's status", - "description": "Force a terminal status on a sandbox session after the fact, from your backend, without running the flow. Sandbox applications only: a live session returns `404`. Works for User Verification (KYC) and Business Verification (KYB) sessions.\n\nSet `new_status`, or let `scenario` or `risk` supply it. `risk` (or the first risk the scenario's magic values carry) adds that warning to the session. The change is recorded in the session's activity.\n\n`send_webhooks` (default `true`) sends exactly the webhooks a real status change sends. With `false` the status still changes and the console still updates live, but no webhook is logged or sent, the missed-webhook recovery never re-sends that decision, and the next status change sends webhooks as usual. The response echoes the value applied.\n\nRequires the `write:sessions` privilege; a scoped API key needs write access to sessions. An unknown `session_id` returns `404` and another application's session returns `403`, both before the body is read; a live session returns `404` only after the body is validated. To simulate from the hosted flow with the session's own token, use `POST /v3/session/{sessionId}/sandbox/simulate/`. See [Sandbox & Test Data](/integration/sandbox-testing) for the scenario catalog.", - "operationId": "post_v3_session_simulate", + "/v3/organization/bank-verification-pricing/": { + "get": { + "summary": "Get bank verification prices (public, unauthenticated)", + "description": "Returns the list price of each Bank Verification method in every country: bank sign-in with its balance and transactions add-ons, and the account check. Use it to quote a bank step before you build a workflow, or to show bank prices in your own product.\n\nPublic: send no auth header. The response contains no organization data and is the same for every caller. It is cached publicly for one hour and carries an `ETag`, so send `If-None-Match` to get a `304 Not Modified` when nothing changed.\n\nA method is `available` only where Didit serves it in production today, and its `price_usd` is `null` everywhere else, so the feed never shows a price you cannot buy. `available: false` covers a country with no approved price, a method the country does not support, and a route that is not switched on yet.\n\nThese are list prices before your plan discount. Your plan discount applies to the whole bank price, and a negotiated or contract price replaces the list price for your organization.", + "operationId": "get_v3_bank_verification_pricing", "tags": [ - "Sessions" + "Pricing" ], "security": [ - { - "ApiKeyAuth": [] - } + {} ], - "parameters": [ + "parameters": [], + "x-codeSamples": [ { - "in": "path", - "name": "sessionId", - "required": true, - "description": "UUID of the sandbox verification session. Accepts both user (KYC) and business (KYB) session IDs.", - "schema": { - "type": "string", - "format": "uuid", - "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" - } + "lang": "curl", + "label": "curl", + "source": "curl -fsS https://verification.didit.me/v3/organization/bank-verification-pricing/" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.get('https://verification.didit.me/v3/organization/bank-verification-pricing/', timeout=10)\nresp.raise_for_status()\nfeed = resp.json()\nfor iso3, country in feed['countries'].items():\n sign_in = country['methods']['open_banking']\n if sign_in['available']:\n print(iso3, country['name'], sign_in['price_usd'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const feed = await fetch('https://verification.didit.me/v3/organization/bank-verification-pricing/').then((r) => r.json());\nconst gbr = feed.countries.GBR.methods;\nconsole.log(gbr.open_banking.available ? gbr.open_banking.price_usd : 'Not available');\nconsole.log(gbr.account_lookup.available ? gbr.account_lookup.price_usd : 'Not available');" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SandboxSimulateRequest" - }, - "examples": { - "Scenario Without Webhooks": { - "summary": "Decline through a scenario without sending webhooks", - "value": { - "scenario": "decline_aml_hit", - "send_webhooks": false - } - }, - "Status Only": { - "summary": "Approve and send the usual webhooks", - "value": { - "new_status": "Approved" - } - } - } - } - } + "x-didit-sdk": { + "resource": "pricing", + "method": "bank_verification", + "auth": "none", + "server": true }, "responses": { "200": { - "description": "The status changed.", + "description": "Bank Verification list prices by country.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboxSimulateResponse" + "$ref": "#/components/schemas/BankVerificationPricing" }, "examples": { - "Simulated": { - "summary": "Declined through a scenario, webhooks not sent", - "value": { - "session_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "previous_status": "In Progress", - "status": "Declined", - "scenario": "decline_aml_hit", - "risk": "POSSIBLE_MATCH_FOUND", - "send_webhooks": false - } - } - } - } - } - }, - "400": { - "description": "Validation error: an unknown scenario or risk, no `new_status` when neither `scenario` nor `risk` supplies one, a status this endpoint does not set, or the session already has the requested status (a bare JSON array).", - "content": { - "application/json": { - "examples": { - "Unknown Scenario": { - "summary": "scenario is not in the catalog", - "value": { - "scenario": [ - "Unknown sandbox scenario: 'not-a-scenario'." - ] - } - }, - "Unknown Risk": { - "summary": "risk is not in the catalog", - "value": { - "risk": [ - "Unknown sandbox risk case: 'NOT_A_RISK'." - ] - } - }, - "Missing new_status": { - "summary": "No new_status, scenario or risk", - "value": { - "new_status": [ - "This field is required." - ] - } - }, - "Invalid new_status": { - "summary": "A status the simulation does not set", - "value": { - "new_status": [ - "\"Resubmitted\" is not a valid choice." - ] - } - }, - "Same Status": { - "summary": "The session already has this status", - "value": [ - "The new status is the same as the current status." - ] - } - } - } - } - }, - "401": { - "description": "The scoped API key has expired.", - "content": { - "application/json": { - "examples": { - "Expired API Key": { - "summary": "Scoped API key past its expiry date", + "Two countries": { + "summary": "One country with both methods, one with neither", "value": { - "detail": "This API key has expired." - } - } - } - } - } - }, - "403": { - "description": "Missing or invalid credentials, a caller without the `write:sessions` privilege, a scoped API key without the access this endpoint needs, or a `session_id` that belongs to another application.", - "content": { - "application/json": { - "examples": { - "Invalid Credentials": { - "summary": "Missing or invalid x-api-key", - "value": { - "detail": "Authentication credentials were not provided or are invalid." - } - }, - "Missing Privilege": { - "summary": "Caller lacks write:sessions", - "value": { - "detail": "You do not have permission to perform this action." - } - }, - "Scoped Key Without Access": { - "summary": "Scoped API key without this access", - "value": { - "detail": "This API key has no write access to sessions." - } - } - } - } - } - }, - "404": { - "description": "No session with this `session_id` exists, or it is a live (non-sandbox) session.", - "content": { - "application/json": { - "examples": { - "Not Found": { - "summary": "Unknown or live session", - "value": { - "detail": "Not found." + "currency": "USD", + "statement_upload": { + "price_usd": "0.0000", + "included_after_charged_bank_attempt": true, + "standalone_available": false + }, + "own_provider_fee_usd": "0.0200", + "countries": { + "GBR": { + "name": "United Kingdom", + "methods": { + "open_banking": { + "available": true, + "market": "UK and EEA", + "price_usd": "0.7900", + "add_ons": { + "balance": "0.3500", + "transactions": "1.3000" + } + }, + "account_lookup": { + "available": true, + "price_usd": "0.4000" + } + } + }, + "CAN": { + "name": "Canada", + "methods": { + "open_banking": { + "available": false, + "market": null, + "price_usd": null, + "add_ons": {} + }, + "account_lookup": { + "available": false, + "price_usd": null + } + } + } + } } } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST \\\n https://verification.didit.me/v3/session/3fa85f64-5717-4562-b3fc-2c963f66afa6/simulate/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"scenario\": \"decline_aml_hit\", \"send_webhooks\": false}'" }, - { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/session/3fa85f64-5717-4562-b3fc-2c963f66afa6/simulate/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n json={\"scenario\": \"decline_aml_hit\", \"send_webhooks\": False},\n)\nresponse.raise_for_status()\nprint(response.json())" + "304": { + "description": "Not modified: the `ETag` you sent in `If-None-Match` still matches. No body." } - ], - "x-didit-sdk": { - "resource": "sessions", - "method": "simulate", - "auth": "api_key", - "server": true } } }, - "/v3/session/{sessionId}/sandbox/simulate/": { + "/v3/session/{sessionId}/simulate/": { "post": { - "summary": "Simulate a sandbox session's status from the client", - "description": "The same simulation as `POST /v3/session/{sessionId}/simulate/`, authenticated with the session's own `Session-Token` (returned in `session_token` by [Create Session](/sessions-api/create-session)) instead of your API key, so a test harness or the hosted flow can call it from the client. The body, the response and `send_webhooks` behave exactly as on that endpoint.\n\nA `Session-Token` of a live (non-sandbox) application, or one that does not match the `sessionId` in the path, returns `404`. See [Sandbox & Test Data](/integration/sandbox-testing).", - "operationId": "post_v3_session_sandbox_simulate", + "summary": "Simulate a sandbox session's status", + "description": "Force a terminal status on a sandbox session after the fact, from your backend, without running the flow. Sandbox applications only: a live session returns `404`. Works for User Verification (KYC) and Business Verification (KYB) sessions.\n\nSet `new_status`, or let `scenario` or `risk` supply it. `risk` (or the first risk the scenario's magic values carry) adds that warning to the session. The change is recorded in the session's activity.\n\n`send_webhooks` (default `true`) sends exactly the webhooks a real status change sends. With `false` the status still changes and the console still updates live, but no webhook is logged or sent, the missed-webhook recovery never re-sends that decision, and the next status change sends webhooks as usual. The response echoes the value applied.\n\nRequires the `write:sessions` privilege; a scoped API key needs write access to sessions. An unknown `session_id` returns `404` and another application's session returns `403`, both before the body is read; a live session returns `404` only after the body is validated. To simulate from the hosted flow with the session's own token, use `POST /v3/session/{sessionId}/sandbox/simulate/`. See [Sandbox & Test Data](/integration/sandbox-testing) for the scenario catalog.", + "operationId": "post_v3_session_simulate", "tags": [ "Sessions" ], "security": [ { - "SessionTokenAuth": [] + "ApiKeyAuth": [] } ], "parameters": [ @@ -68494,7 +68545,207 @@ "in": "path", "name": "sessionId", "required": true, - "description": "UUID of the sandbox verification session. Must match the session encoded in the Session-Token used to authenticate.", + "description": "UUID of the sandbox verification session. Accepts both user (KYC) and business (KYB) session IDs.", + "schema": { + "type": "string", + "format": "uuid", + "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxSimulateRequest" + }, + "examples": { + "Scenario Without Webhooks": { + "summary": "Decline through a scenario without sending webhooks", + "value": { + "scenario": "decline_aml_hit", + "send_webhooks": false + } + }, + "Status Only": { + "summary": "Approve and send the usual webhooks", + "value": { + "new_status": "Approved" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The status changed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboxSimulateResponse" + }, + "examples": { + "Simulated": { + "summary": "Declined through a scenario, webhooks not sent", + "value": { + "session_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "previous_status": "In Progress", + "status": "Declined", + "scenario": "decline_aml_hit", + "risk": "POSSIBLE_MATCH_FOUND", + "send_webhooks": false + } + } + } + } + } + }, + "400": { + "description": "Validation error: an unknown scenario or risk, no `new_status` when neither `scenario` nor `risk` supplies one, a status this endpoint does not set, or the session already has the requested status (a bare JSON array).", + "content": { + "application/json": { + "examples": { + "Unknown Scenario": { + "summary": "scenario is not in the catalog", + "value": { + "scenario": [ + "Unknown sandbox scenario: 'not-a-scenario'." + ] + } + }, + "Unknown Risk": { + "summary": "risk is not in the catalog", + "value": { + "risk": [ + "Unknown sandbox risk case: 'NOT_A_RISK'." + ] + } + }, + "Missing new_status": { + "summary": "No new_status, scenario or risk", + "value": { + "new_status": [ + "This field is required." + ] + } + }, + "Invalid new_status": { + "summary": "A status the simulation does not set", + "value": { + "new_status": [ + "\"Resubmitted\" is not a valid choice." + ] + } + }, + "Same Status": { + "summary": "The session already has this status", + "value": [ + "The new status is the same as the current status." + ] + } + } + } + } + }, + "401": { + "description": "The scoped API key has expired.", + "content": { + "application/json": { + "examples": { + "Expired API Key": { + "summary": "Scoped API key past its expiry date", + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "Missing or invalid credentials, a caller without the `write:sessions` privilege, a scoped API key without the access this endpoint needs, or a `session_id` that belongs to another application.", + "content": { + "application/json": { + "examples": { + "Invalid Credentials": { + "summary": "Missing or invalid x-api-key", + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + }, + "Missing Privilege": { + "summary": "Caller lacks write:sessions", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped Key Without Access": { + "summary": "Scoped API key without this access", + "value": { + "detail": "This API key has no write access to sessions." + } + } + } + } + } + }, + "404": { + "description": "No session with this `session_id` exists, or it is a live (non-sandbox) session.", + "content": { + "application/json": { + "examples": { + "Not Found": { + "summary": "Unknown or live session", + "value": { + "detail": "Not found." + } + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST \\\n https://verification.didit.me/v3/session/3fa85f64-5717-4562-b3fc-2c963f66afa6/simulate/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"scenario\": \"decline_aml_hit\", \"send_webhooks\": false}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/session/3fa85f64-5717-4562-b3fc-2c963f66afa6/simulate/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n json={\"scenario\": \"decline_aml_hit\", \"send_webhooks\": False},\n)\nresponse.raise_for_status()\nprint(response.json())" + } + ], + "x-didit-sdk": { + "resource": "sessions", + "method": "simulate", + "auth": "api_key", + "server": true + } + } + }, + "/v3/session/{sessionId}/sandbox/simulate/": { + "post": { + "summary": "Simulate a sandbox session's status from the client", + "description": "The same simulation as `POST /v3/session/{sessionId}/simulate/`, authenticated with the session's own `Session-Token` (returned in `session_token` by [Create Session](/sessions-api/create-session)) instead of your API key, so a test harness or the hosted flow can call it from the client. The body, the response and `send_webhooks` behave exactly as on that endpoint.\n\nA `Session-Token` of a live (non-sandbox) application, or one that does not match the `sessionId` in the path, returns `404`. See [Sandbox & Test Data](/integration/sandbox-testing).", + "operationId": "post_v3_session_sandbox_simulate", + "tags": [ + "Sessions" + ], + "security": [ + { + "SessionTokenAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sessionId", + "required": true, + "description": "UUID of the sandbox verification session. Must match the session encoded in the Session-Token used to authenticate.", "schema": { "type": "string", "format": "uuid", @@ -72665,6 +72916,717 @@ } } } + }, + "/v3/session/imports/providers/": { + "get": { + "summary": "List migration providers", + "description": "The 15 providers you can migrate verifications from: what each needs to connect, whether its API can list records, the permissions its key needs and its default status mapping.", + "operationId": "get_v3_session_imports_providers_list", + "x-didit-sdk": { + "resource": "sessions.imports", + "method": "list_providers", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "responses": { + "200": { + "description": "Migration providers.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportProvider" + } + }, + "mapping_targets": { + "type": "array", + "items": { + "type": "string" + } + }, + "skipped_statuses": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the caller cannot import verifications for this application.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/session/imports/providers/check/": { + "post": { + "summary": "Check a provider key", + "description": "Read the provider with your key before creating a migration. Nothing is stored.", + "operationId": "post_v3_session_imports_providers_check", + "x-didit-sdk": { + "resource": "sessions.imports", + "method": "check_provider", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": [ + "sumsub", + "jumio", + "veriff", + "onfido", + "persona", + "metamap", + "socure", + "idnow", + "shufti", + "idenfy", + "ondato", + "trulioo", + "au10tix", + "complycube", + "other" + ], + "description": "Migration source, from `GET /v3/session/imports/providers/`. Required when `source` is `provider`." + }, + "credentials": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "The provider's read-only key, with the field names `credential_fields` lists. Encrypted at rest, never returned, and erased when the import ends. Send it as a JSON string in multipart." + }, + "options": { + "type": "object", + "properties": { + "region": { + "type": "string", + "description": "The provider region, one of its `regions` (Onfido: `eu`, `us`, `ca`)." + } + } + }, + "record_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "For `api_ids` providers: the first record ID of your export." + } + }, + "required": [ + "provider", + "credentials" + ] + } + } + } + }, + "responses": { + "200": { + "description": "The key can read records.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean" + }, + "provider": { + "type": "string" + }, + "credential_hint": { + "type": "string", + "nullable": true + } + } + } + } + } + }, + "400": { + "description": "The provider refused the key: `code`, `detail`, `missing_permissions`, `suggested_region`." + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the caller cannot import verifications for this application.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/session/imports/{importId}/start/": { + "post": { + "summary": "Start a dry run", + "description": "Start a migration whose dry run finished (`READY`): the same job imports for real, with the status mapping and face templates choice as sent now.", + "operationId": "post_v3_session_imports_start", + "x-didit-sdk": { + "resource": "sessions.imports", + "method": "start", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "importId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status_mapping": { + "type": "object", + "additionalProperties": { + "type": "string", + "enum": [ + "Approved", + "Declined", + "In Review", + "Expired", + "Not Finished", + "Abandoned" + ] + }, + "description": "Edits to the provider's default status mapping: source status to Didit status. `Not Finished` and `Abandoned` are skipped and counted, never imported." + }, + "face_templates": { + "type": "boolean", + "default": false, + "description": "Build face templates from the imported selfies and search them against the application's faces and blocklist. Matches are flagged for review only. Requires `lawful_basis_confirmed`." + }, + "lawful_basis_confirmed": { + "type": "boolean", + "default": false, + "description": "Confirms you have a lawful basis to share these selfies with Didit as processor." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The import job.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionImportJob" + } + } + } + }, + "400": { + "description": "The job is not in a state this action applies to." + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the caller cannot import verifications for this application.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/session/imports/{importId}/pause/": { + "post": { + "summary": "Pause an import job", + "description": "Pause a queued or running import.", + "operationId": "post_v3_session_imports_pause", + "x-didit-sdk": { + "resource": "sessions.imports", + "method": "pause", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "importId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "The import job.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionImportJob" + } + } + } + }, + "400": { + "description": "The job is not in a state this action applies to." + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the caller cannot import verifications for this application.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/session/imports/{importId}/resume/": { + "post": { + "summary": "Resume an import job", + "description": "Resume an import you paused. A job paused by the provider's rate limit resumes by itself.", + "operationId": "post_v3_session_imports_resume", + "x-didit-sdk": { + "resource": "sessions.imports", + "method": "resume", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "importId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "The import job.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionImportJob" + } + } + } + }, + "400": { + "description": "The job is not in a state this action applies to." + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the caller cannot import verifications for this application.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/session/imports/{importId}/cancel/": { + "post": { + "summary": "Cancel an import job", + "description": "Cancel an import. What was imported stays; the uploaded file and the key are erased.", + "operationId": "post_v3_session_imports_cancel", + "x-didit-sdk": { + "resource": "sessions.imports", + "method": "cancel", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "importId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "The import job.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionImportJob" + } + } + } + }, + "400": { + "description": "The job is not in a state this action applies to." + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the caller cannot import verifications for this application.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/session/imports/{importId}/retry/": { + "post": { + "summary": "Retry a migration", + "description": "Run a migration that ended again as a new job, with a new key (the old one was erased). It keeps the provider, workflow, region, status mapping and face templates choice; everything already in Didit is skipped.", + "operationId": "post_v3_session_imports_retry", + "x-didit-sdk": { + "resource": "sessions.imports", + "method": "retry", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "importId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": false, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "credentials": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "The provider's read-only key, with the field names `credential_fields` lists. Encrypted at rest, never returned, and erased when the import ends. Send it as a JSON string in multipart." + }, + "file": { + "type": "string", + "format": "binary", + "description": "The record IDs or export file again, for `api_ids` and `export_file` providers." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "The new import job.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionImportJob" + } + } + } + }, + "400": { + "description": "Not a migration, not ended, or the provider refused the key." + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the caller cannot import verifications for this application.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/import-jobs/": { + "get": { + "summary": "List import jobs", + "description": "Every import of the application in one shape: verification imports and migrations, people and businesses.", + "operationId": "get_v3_import_jobs_list", + "x-didit-sdk": { + "resource": "sessions.imports", + "method": "list_jobs", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "scope", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "all", + "sessions", + "users", + "businesses" + ], + "default": "all" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + } + } + ], + "responses": { + "200": { + "description": "Import jobs, newest first.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportJobSummary" + } + }, + "counts": { + "type": "object", + "properties": { + "all": { + "type": "integer" + }, + "running_and_waiting": { + "type": "integer" + }, + "needs_attention": { + "type": "integer" + }, + "done": { + "type": "integer" + } + } + } + } + } + } + } + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the caller cannot import verifications for this application.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/import-jobs/active/": { + "get": { + "summary": "List active import jobs", + "description": "What is running or waiting, plus what just ended, for a progress indicator.", + "operationId": "get_v3_import_jobs_active", + "x-didit-sdk": { + "resource": "sessions.imports", + "method": "list_active_jobs", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "responses": { + "200": { + "description": "Active import jobs.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ImportJobSummary" + } + }, + "running": { + "type": "integer" + } + } + } + } + } + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the caller cannot import verifications for this application.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } } }, "components": { @@ -72774,7 +73736,8 @@ "COMPLETED", "FAILED", "PAUSED", - "CANCELLED" + "CANCELLED", + "READY" ] }, "import_type": { @@ -72849,6 +73812,99 @@ "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 } } }, @@ -92295,7 +93351,7 @@ "bank_verifications": { "type": "array", "nullable": true, - "description": "Bank Verification results, one item per Bank Verification step, for User and Business Verification. `null` until a step produces one. Each item's `method` says how the account was verified: `open_banking` (bank sign-in) or `account_lookup` (account check), or a `microdeposits` or `statement` fallback. See [Bank verification methods](/core-technology/bank-verification/verification-methods).", + "description": "Bank Verification results, one item per Bank Verification step, for User and Business Verification. `null` until a step produces one. Each item's `method` says how the account was verified: `open_banking` (bank sign-in) or `account_lookup` (account check), or a `statement` or `microdeposits` fallback, and `none` when the person continued without a bank before any attempt was evaluated (`continued_without_bank`). See [Bank verification methods](/core-technology/bank-verification/verification-methods).", "items": { "$ref": "#/components/schemas/BankVerificationResult" } @@ -94735,6 +95791,26 @@ "updated_at": { "type": "string", "format": "date-time" + }, + "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." + }, + "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).", + "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." + }, + "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.", + "example": {} } }, "additionalProperties": true @@ -104141,7 +105217,7 @@ }, "BankVerificationResult": { "type": "object", - "description": "One Bank Verification step's result, for User and Business Verification. `method` says how the account was verified. See [Bank verification methods](/core-technology/bank-verification/verification-methods).", + "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", @@ -104179,9 +105255,10 @@ "open_banking", "account_lookup", "microdeposits", - "statement" + "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). `null` until an account is evaluated. Workflow rules read the same value as `bank.method`." + "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`." }, "provider": { "type": "string", @@ -104552,7 +105629,7 @@ "usage_type": { "type": "string", "nullable": true, - "description": "The usage line the charge was booked on." + "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." } } }, @@ -104574,7 +105651,61 @@ "not_covered": { "type": "boolean", "nullable": true, - "description": "Whether the person's bank or country could not be reached on this step." + "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." + }, + "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." + }, + "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." + }, + "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." + }, + "institution_name": { + "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." + }, + "continued_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the person continued." + } + } }, "evidence_purged_at": { "type": "string", @@ -104594,7 +105725,7 @@ "items": { "type": "string" }, - "description": "Why the verdict came out the way it did, for example `OWNER_NAME_PARTIAL`. Several may apply." + "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", @@ -104634,6 +105765,416 @@ "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": { + "type": "string", + "enum": [ + "USD" + ], + "description": "Currency of every price in the response.", + "example": "USD" + }, + "statement_upload": { + "$ref": "#/components/schemas/BankStatementUploadPricing" + }, + "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" + } + } + } + }, + "BankStatementUploadPricing": { + "type": "object", + "description": "How the bank statement upload is priced.", + "required": [ + "price_usd", + "included_after_charged_bank_attempt", + "standalone_available" + ], + "properties": { + "price_usd": { + "type": "string", + "description": "Price of a statement upload after a charged bank attempt. Decimal string with 4 decimals, in USD.", + "example": "0.0000" + }, + "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 + }, + "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", + "description": "Country name in English.", + "example": "United Kingdom" + }, + "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" + }, + "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 + }, + "market": { + "type": "string", + "nullable": true, + "description": "The market whose price applies. `null` when not available.", + "example": "UK and EEA" + }, + "price_usd": { + "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" + }, + "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" + } + } + } + } + }, + "BankAccountCheckPricing": { + "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" + ], + "properties": { + "available": { + "type": "boolean", + "description": "`true` only where Didit serves the account check in this country in production.", + "example": true + }, + "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" + } + } + }, + "ImportProvider": { + "type": "object", + "properties": { + "key": { + "type": "string", + "enum": [ + "sumsub", + "jumio", + "veriff", + "onfido", + "persona", + "metamap", + "socure", + "idnow", + "shufti", + "idenfy", + "ondato", + "trulioo", + "au10tix", + "complycube", + "other" + ] + }, + "name": { + "type": "string" + }, + "mode": { + "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" + } + } + } + }, + "regions": { + "type": "array", + "items": { + "type": "string" + } + }, + "required_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "docs_url": { + "type": "string", + "nullable": true + }, + "status_mapping": { + "type": "object", + "additionalProperties": { + "type": "string", + "enum": [ + "Approved", + "Declined", + "In Review", + "Expired", + "Not Finished", + "Abandoned" + ] + } + } + } + }, + "ImportJobSummary": { + "type": "object", + "description": "One import job of any kind, in the shape the console's Import jobs drawer reads.", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "kind": { + "type": "string", + "enum": [ + "sessions", + "businesses", + "users", + "status_rules", + "transactions" + ] + }, + "source": { + "type": "string", + "enum": [ + "file", + "api", + "provider" + ] + }, + "provider": { + "type": "string", + "nullable": true + }, + "provider_name": { + "type": "string", + "nullable": true + }, + "filename": { + "type": "string", + "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." + }, + "status": { + "type": "string" + }, + "is_dry_run": { + "type": "boolean" + }, + "credential_hint": { + "type": "string", + "nullable": true + }, + "resumes_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "last_error": { + "type": "string", + "nullable": true + }, + "started_by": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "nullable": true + } + } + }, + "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" + } + } + }, + "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" + } + } + }, + "eta_seconds": { + "type": "integer", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "started_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "completed_at": { + "type": "string", + "format": "date-time", + "nullable": true + } + } } }, "securitySchemes": { diff --git a/src/DiditClient.php b/src/DiditClient.php index c557ce6..d514d30 100644 --- a/src/DiditClient.php +++ b/src/DiditClient.php @@ -96,6 +96,8 @@ final class DiditClient extends BaseClient public readonly Resources\Analytics $analytics; + public readonly Resources\Pricing $pricing; + public readonly Resources\Investigator $investigator; public readonly Resources\Account $account; @@ -140,6 +142,7 @@ protected function initResources(): void $this->blocklist = new Resources\Blocklist($this->requester); $this->monitoringEvents = new Resources\MonitoringEvents($this->requester); $this->analytics = new Resources\Analytics($this->requester); + $this->pricing = new Resources\Pricing($this->requester); $this->investigator = new Resources\Investigator($this->requester); $this->account = new Resources\Account($this->requester); } diff --git a/src/Params.php b/src/Params.php index b2bf53c..0151030 100644 --- a/src/Params.php +++ b/src/Params.php @@ -1809,20 +1809,53 @@ * workflow_id: string, * vendor_data?: string|null, * } + * @phpstan-type SessionsImportsCheckProviderParams array{ + * provider: string, + * credentials: array, + * options?: SessionsImportsCheckProviderParamsOptions, + * record_ids?: list, + * } + * @phpstan-type SessionsImportsCheckProviderParamsOptions array{ + * region?: string, + * } * @phpstan-type SessionsImportsCreateParams array{ * import_type?: string, * workflow_id?: string, * source_format?: string, * source_file_url?: string, + * source?: string, + * provider?: string, + * credentials?: array, + * options?: SessionsImportsCreateParamsOptions, + * status_mapping?: array, + * dry_run?: bool, + * face_templates?: bool, + * lawful_basis_confirmed?: bool, * file?: \Didit\File|\Psr\Http\Message\StreamInterface|resource, * } + * @phpstan-type SessionsImportsCreateParamsOptions array{ + * region?: string, + * } * @phpstan-type SessionsImportsListErrorsParams array{ * limit?: int, * offset?: int, * } + * @phpstan-type SessionsImportsListJobsParams array{ + * scope?: string, + * limit?: int, + * } * @phpstan-type SessionsImportsRetrieveTemplateParams array{ * import_type?: string, * } + * @phpstan-type SessionsImportsRetryParams array{ + * credentials?: array, + * file?: \Didit\File|\Psr\Http\Message\StreamInterface|resource, + * } + * @phpstan-type SessionsImportsStartParams array{ + * status_mapping?: array, + * face_templates?: bool, + * lawful_basis_confirmed?: bool, + * } * @phpstan-type SessionsListAmlScreeningHistoryParams array{ * node_id?: string, * limit?: int, diff --git a/src/Resources/Pricing.php b/src/Resources/Pricing.php new file mode 100644 index 0000000..4b1a5a7 --- /dev/null +++ b/src/Resources/Pricing.php @@ -0,0 +1,41 @@ +requester->request(new Operation( + method: 'GET', + path: '/v3/organization/bank-verification-pricing/', + auth: 'none', + ), [], [], $options); + + return Types\BankVerificationPricing::fromResponse($response); + } +} diff --git a/src/Resources/Sessions/Imports.php b/src/Resources/Sessions/Imports.php index 4f36927..15f23b3 100644 --- a/src/Resources/Sessions/Imports.php +++ b/src/Resources/Sessions/Imports.php @@ -19,9 +19,13 @@ /** * The `sessions.imports` resource. * + * @phpstan-import-type SessionsImportsCheckProviderParams from Params * @phpstan-import-type SessionsImportsCreateParams from Params * @phpstan-import-type SessionsImportsListErrorsParams from Params + * @phpstan-import-type SessionsImportsListJobsParams from Params * @phpstan-import-type SessionsImportsRetrieveTemplateParams from Params + * @phpstan-import-type SessionsImportsRetryParams from Params + * @phpstan-import-type SessionsImportsStartParams from Params */ final class Imports extends BaseResource { @@ -29,10 +33,11 @@ final class Imports extends BaseResource * Create a verification import job. * * Create an asynchronous import job for historical User Verification sessions, Business Verification - * sessions, transaction monitoring records, or workflow custom status rules. Use this to migrate from - * providers such as Sumsub, MetaMap, Veriff, Onfido, Persona, Trulioo, Jumio, Incode, iDenfy, or any - * other provider after transforming the export into Didit's canonical CSV/NDJSON format. Imports are - * disabled by default and must be enabled per organization by Didit. + * sessions, transaction monitoring records, or workflow custom status rules. Set `source=provider` to + * migrate from another provider: Didit reads Sumsub, Onfido, Persona and ComplyCube with your + * read-only key, reads Veriff, iDenfy and MetaMap records by the IDs you upload in `file`, and imports + * an export file in Didit's format for the other providers. Record IDs are namespaced by provider + * (`sumsub:`) so a re-run skips everything already in Didit. * * `POST /v3/session/imports/` * @@ -47,6 +52,7 @@ public function create(array $params = [], ?RequestOptions $options = null): Typ method: 'POST', path: '/v3/session/imports/', bodyTypes: ['json', 'multipart'], + jsonHints: ['credentials' => ['o'], 'options' => ['o'], 'status_mapping' => ['o']], ), [], $params, $options); return Types\SessionImportJob::fromResponse($response); @@ -122,4 +128,204 @@ public function listErrors(string $importId, array $params = [], ?RequestOptions query: ['limit' => 'limit', 'offset' => 'offset'], ), ['importId' => $importId], $params, $options, static fn (mixed $v) => Hydrate::model($v, Types\SessionImportError::class)); } + + /** + * List migration providers. + * + * The 15 providers you can migrate verifications from: what each needs to connect, whether its API can + * list records, the permissions its key needs and its default status mapping. + * + * `GET /v3/session/imports/providers/` + * + * @throws DiditException + */ + public function listProviders(?RequestOptions $options = null): Types\SessionsImportsListProvidersResponse + { + $response = $this->requester->request(new Operation( + method: 'GET', + path: '/v3/session/imports/providers/', + ), [], [], $options); + + return Types\SessionsImportsListProvidersResponse::fromResponse($response); + } + + /** + * Check a provider key. + * + * Read the provider with your key before creating a migration. Nothing is stored. + * + * `POST /v3/session/imports/providers/check/` + * + * @param SessionsImportsCheckProviderParams $params + * + * @throws DiditException + * @throws \InvalidArgumentException when a path parameter is empty, "." or "..", or a parameter value cannot be encoded + */ + public function checkProvider( + array $params, + ?RequestOptions $options = null, + ): Types\SessionsImportsCheckProviderResponse { + $response = $this->requester->request(new Operation( + method: 'POST', + path: '/v3/session/imports/providers/check/', + bodyTypes: ['json'], + jsonHints: ['credentials' => ['o'], 'options' => ['o']], + ), [], $params, $options); + + return Types\SessionsImportsCheckProviderResponse::fromResponse($response); + } + + /** + * Start a dry run. + * + * Start a migration whose dry run finished (`READY`): the same job imports for real, with the status + * mapping and face templates choice as sent now. + * + * `POST /v3/session/imports/{importId}/start/` + * + * @param SessionsImportsStartParams $params + * + * @throws DiditException + * @throws \InvalidArgumentException when a path parameter is empty, "." or "..", or a parameter value cannot be encoded + */ + public function start(string $importId, array $params = [], ?RequestOptions $options = null): Types\SessionImportJob + { + $response = $this->requester->request(new Operation( + method: 'POST', + path: '/v3/session/imports/{importId}/start/', + bodyTypes: ['json'], + jsonHints: ['status_mapping' => ['o']], + ), ['importId' => $importId], $params, $options); + + return Types\SessionImportJob::fromResponse($response); + } + + /** + * Pause an import job. + * + * Pause a queued or running import. + * + * `POST /v3/session/imports/{importId}/pause/` + * + * @throws DiditException + * @throws \InvalidArgumentException when a path parameter is empty, "." or "..", or a parameter value cannot be encoded + */ + public function pause(string $importId, ?RequestOptions $options = null): Types\SessionImportJob + { + $response = $this->requester->request(new Operation( + method: 'POST', + path: '/v3/session/imports/{importId}/pause/', + ), ['importId' => $importId], [], $options); + + return Types\SessionImportJob::fromResponse($response); + } + + /** + * Resume an import job. + * + * Resume an import you paused. A job paused by the provider's rate limit resumes by itself. + * + * `POST /v3/session/imports/{importId}/resume/` + * + * @throws DiditException + * @throws \InvalidArgumentException when a path parameter is empty, "." or "..", or a parameter value cannot be encoded + */ + public function resume(string $importId, ?RequestOptions $options = null): Types\SessionImportJob + { + $response = $this->requester->request(new Operation( + method: 'POST', + path: '/v3/session/imports/{importId}/resume/', + ), ['importId' => $importId], [], $options); + + return Types\SessionImportJob::fromResponse($response); + } + + /** + * Cancel an import job. + * + * Cancel an import. What was imported stays; the uploaded file and the key are erased. + * + * `POST /v3/session/imports/{importId}/cancel/` + * + * @throws DiditException + * @throws \InvalidArgumentException when a path parameter is empty, "." or "..", or a parameter value cannot be encoded + */ + public function cancel(string $importId, ?RequestOptions $options = null): Types\SessionImportJob + { + $response = $this->requester->request(new Operation( + method: 'POST', + path: '/v3/session/imports/{importId}/cancel/', + ), ['importId' => $importId], [], $options); + + return Types\SessionImportJob::fromResponse($response); + } + + /** + * Retry a migration. + * + * Run a migration that ended again as a new job, with a new key (the old one was erased). It keeps the + * provider, workflow, region, status mapping and face templates choice; everything already in Didit is + * skipped. + * + * `POST /v3/session/imports/{importId}/retry/` + * + * @param SessionsImportsRetryParams $params + * + * @throws DiditException + * @throws \InvalidArgumentException when a path parameter is empty, "." or "..", or a parameter value cannot be encoded + */ + public function retry(string $importId, array $params = [], ?RequestOptions $options = null): Types\SessionImportJob + { + $response = $this->requester->request(new Operation( + method: 'POST', + path: '/v3/session/imports/{importId}/retry/', + bodyTypes: ['multipart'], + jsonHints: ['credentials' => ['o']], + ), ['importId' => $importId], $params, $options); + + return Types\SessionImportJob::fromResponse($response); + } + + /** + * List import jobs. + * + * Every import of the application in one shape: verification imports and migrations, people and + * businesses. + * + * `GET /v3/import-jobs/` + * + * @param SessionsImportsListJobsParams $params + * + * @throws DiditException + * @throws \InvalidArgumentException when a path parameter is empty, "." or "..", or a parameter value cannot be encoded + */ + public function listJobs(array $params = [], ?RequestOptions $options = null): Types\SessionsImportsListJobsResponse + { + $response = $this->requester->request(new Operation( + method: 'GET', + path: '/v3/import-jobs/', + query: ['scope' => 'scope', 'limit' => 'limit'], + ), [], $params, $options); + + return Types\SessionsImportsListJobsResponse::fromResponse($response); + } + + /** + * List active import jobs. + * + * What is running or waiting, plus what just ended, for a progress indicator. + * + * `GET /v3/import-jobs/active/` + * + * @throws DiditException + */ + public function listActiveJobs(?RequestOptions $options = null): Types\SessionsImportsListActiveJobsResponse + { + $response = $this->requester->request(new Operation( + method: 'GET', + path: '/v3/import-jobs/active/', + ), [], [], $options); + + return Types\SessionsImportsListActiveJobsResponse::fromResponse($response); + } } diff --git a/src/Types/BankAccountCheckPricing.php b/src/Types/BankAccountCheckPricing.php new file mode 100644 index 0000000..7362a19 --- /dev/null +++ b/src/Types/BankAccountCheckPricing.php @@ -0,0 +1,36 @@ + $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->available = Hydrate::bool($data['available'] ?? null); + $this->priceUsd = Hydrate::string($data['price_usd'] ?? null); + } +} diff --git a/src/Types/BankSignInPricing.php b/src/Types/BankSignInPricing.php new file mode 100644 index 0000000..d84dd3d --- /dev/null +++ b/src/Types/BankSignInPricing.php @@ -0,0 +1,44 @@ + $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->available = Hydrate::bool($data['available'] ?? null); + $this->market = Hydrate::string($data['market'] ?? null); + $this->priceUsd = Hydrate::string($data['price_usd'] ?? null); + $this->addOns = Hydrate::model($data['add_ons'] ?? null, BankSignInPricingAddOns::class); + } +} diff --git a/src/Types/BankSignInPricingAddOns.php b/src/Types/BankSignInPricingAddOns.php new file mode 100644 index 0000000..8d301df --- /dev/null +++ b/src/Types/BankSignInPricingAddOns.php @@ -0,0 +1,34 @@ + $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->balance = Hydrate::string($data['balance'] ?? null); + $this->transactions = Hydrate::string($data['transactions'] ?? null); + } +} diff --git a/src/Types/BankStatementUploadPricing.php b/src/Types/BankStatementUploadPricing.php new file mode 100644 index 0000000..aad2164 --- /dev/null +++ b/src/Types/BankStatementUploadPricing.php @@ -0,0 +1,42 @@ + $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->priceUsd = Hydrate::string($data['price_usd'] ?? null); + $this->includedAfterChargedBankAttempt = Hydrate::bool($data['included_after_charged_bank_attempt'] ?? null); + $this->standaloneAvailable = Hydrate::bool($data['standalone_available'] ?? null); + } +} diff --git a/src/Types/BankVerificationPricing.php b/src/Types/BankVerificationPricing.php new file mode 100644 index 0000000..d397bfb --- /dev/null +++ b/src/Types/BankVerificationPricing.php @@ -0,0 +1,53 @@ +|null + */ + public readonly ?array $countries; + + /** + * @param array $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->currency = Hydrate::string($data['currency'] ?? null); + $this->statementUpload = Hydrate::model($data['statement_upload'] ?? null, BankStatementUploadPricing::class); + $this->ownProviderFeeUsd = Hydrate::string($data['own_provider_fee_usd'] ?? null); + $this->countries = Hydrate::map($data['countries'] ?? null, static fn (mixed $v) => Hydrate::model($v, BankVerificationPricingCountry::class)); + } +} diff --git a/src/Types/BankVerificationPricingCountry.php b/src/Types/BankVerificationPricingCountry.php new file mode 100644 index 0000000..fecdc55 --- /dev/null +++ b/src/Types/BankVerificationPricingCountry.php @@ -0,0 +1,32 @@ + $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->name = Hydrate::string($data['name'] ?? null); + $this->methods = Hydrate::model($data['methods'] ?? null, BankVerificationPricingMethods::class); + } +} diff --git a/src/Types/BankVerificationPricingCurrency.php b/src/Types/BankVerificationPricingCurrency.php new file mode 100644 index 0000000..24e66ce --- /dev/null +++ b/src/Types/BankVerificationPricingCurrency.php @@ -0,0 +1,21 @@ + $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->openBanking = Hydrate::model($data['open_banking'] ?? null, BankSignInPricing::class); + $this->accountLookup = Hydrate::model($data['account_lookup'] ?? null, BankAccountCheckPricing::class); + } +} diff --git a/src/Types/BankVerificationResult.php b/src/Types/BankVerificationResult.php index 9866465..4f390fb 100644 --- a/src/Types/BankVerificationResult.php +++ b/src/Types/BankVerificationResult.php @@ -11,8 +11,11 @@ /** * One Bank Verification step's result, for User and Business Verification. `method` says how the - * account was verified. See [Bank verification - * methods](/core-technology/bank-verification/verification-methods). + * 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). */ final class BankVerificationResult extends BaseModel { @@ -38,8 +41,10 @@ final class BankVerificationResult extends BaseModel /** * 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). `null` until an account is evaluated. - * Workflow rules read the same value as `bank.method`. + * 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`. * * Known values: {@see BankVerificationResultMethod}. */ @@ -94,9 +99,26 @@ final class BankVerificationResult extends BaseModel /** Whether the step ended without a match after the person left at their bank. */ public readonly ?bool $connectionAbandoned; - /** Whether the person's bank or country could not be reached on this step. */ + /** + * 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. + */ public readonly ?bool $notCovered; + /** + * 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. + */ + public readonly ?bool $continuedWithoutBank; + + /** + * 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`. + */ + public readonly ?BankVerificationResultContinuation $continuation; + /** When the bank evidence was deleted under `bank_evidence_retention_days`. `null` otherwise. */ public readonly ?\DateTimeImmutable $evidencePurgedAt; @@ -109,6 +131,9 @@ final class BankVerificationResult extends BaseModel /** * 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. * * @var list|null */ @@ -144,6 +169,8 @@ public function __construct(array $data) $this->signals = Hydrate::assoc($data['signals'] ?? null); $this->connectionAbandoned = Hydrate::bool($data['connection_abandoned'] ?? null); $this->notCovered = Hydrate::bool($data['not_covered'] ?? null); + $this->continuedWithoutBank = Hydrate::bool($data['continued_without_bank'] ?? null); + $this->continuation = Hydrate::model($data['continuation'] ?? null, BankVerificationResultContinuation::class); $this->evidencePurgedAt = Hydrate::dateTime($data['evidence_purged_at'] ?? null); $this->warnings = Hydrate::list($data['warnings'] ?? null, Hydrate::string(...)); $this->reasonCodes = Hydrate::list($data['reason_codes'] ?? null, Hydrate::string(...)); diff --git a/src/Types/BankVerificationResultBilling.php b/src/Types/BankVerificationResultBilling.php index e6f2bd3..0010afc 100644 --- a/src/Types/BankVerificationResultBilling.php +++ b/src/Types/BankVerificationResultBilling.php @@ -23,7 +23,11 @@ final class BankVerificationResultBilling extends BaseModel /** The marketplace platform fee charged, for `byok`. */ public readonly ?string $marketplaceFee; - /** The usage line the charge was booked on. */ + /** + * 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. + */ public readonly ?string $usageType; /** diff --git a/src/Types/BankVerificationResultContinuation.php b/src/Types/BankVerificationResultContinuation.php new file mode 100644 index 0000000..4c1641f --- /dev/null +++ b/src/Types/BankVerificationResultContinuation.php @@ -0,0 +1,72 @@ +|null + */ + public readonly ?array $offered; + + /** + * 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. + */ + public readonly ?string $institutionId; + + /** + * 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. + */ + public readonly ?string $institutionName; + + /** When the person continued. */ + public readonly ?\DateTimeImmutable $continuedAt; + + /** + * @param array $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->reason = Hydrate::string($data['reason'] ?? null); + $this->country = Hydrate::string($data['country'] ?? null); + $this->offered = Hydrate::list($data['offered'] ?? null, Hydrate::string(...)); + $this->institutionId = Hydrate::string($data['institution_id'] ?? null); + $this->institutionName = Hydrate::string($data['institution_name'] ?? null); + $this->continuedAt = Hydrate::dateTime($data['continued_at'] ?? null); + } +} diff --git a/src/Types/BankVerificationResultContinuationOffered.php b/src/Types/BankVerificationResultContinuationOffered.php new file mode 100644 index 0000000..cc17947 --- /dev/null +++ b/src/Types/BankVerificationResultContinuationOffered.php @@ -0,0 +1,21 @@ + $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->id = Hydrate::string($data['id'] ?? null); + $this->kind = Hydrate::string($data['kind'] ?? null); + $this->source = Hydrate::string($data['source'] ?? null); + $this->provider = Hydrate::string($data['provider'] ?? null); + $this->providerName = Hydrate::string($data['provider_name'] ?? null); + $this->filename = Hydrate::string($data['filename'] ?? null); + $this->state = Hydrate::string($data['state'] ?? null); + $this->status = Hydrate::string($data['status'] ?? null); + $this->isDryRun = Hydrate::bool($data['is_dry_run'] ?? null); + $this->credentialHint = Hydrate::string($data['credential_hint'] ?? null); + $this->resumesAt = Hydrate::dateTime($data['resumes_at'] ?? null); + $this->lastError = Hydrate::string($data['last_error'] ?? null); + $this->startedBy = Hydrate::model($data['started_by'] ?? null, ImportJobSummaryStartedBy::class); + $this->progress = Hydrate::model($data['progress'] ?? null, ImportJobSummaryProgress::class); + $this->faceTemplates = Hydrate::model($data['face_templates'] ?? null, ImportJobSummaryFaceTemplates::class); + $this->etaSeconds = Hydrate::int($data['eta_seconds'] ?? null); + $this->createdAt = Hydrate::dateTime($data['created_at'] ?? null); + $this->startedAt = Hydrate::dateTime($data['started_at'] ?? null); + $this->completedAt = Hydrate::dateTime($data['completed_at'] ?? null); + } +} diff --git a/src/Types/ImportJobSummaryFaceTemplates.php b/src/Types/ImportJobSummaryFaceTemplates.php new file mode 100644 index 0000000..0d3d738 --- /dev/null +++ b/src/Types/ImportJobSummaryFaceTemplates.php @@ -0,0 +1,40 @@ + $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->enabled = Hydrate::bool($data['enabled'] ?? null); + $this->built = Hydrate::int($data['built'] ?? null); + $this->duplicates = Hydrate::int($data['duplicates'] ?? null); + $this->blocklisted = Hydrate::int($data['blocklisted'] ?? null); + } +} diff --git a/src/Types/ImportJobSummaryKind.php b/src/Types/ImportJobSummaryKind.php new file mode 100644 index 0000000..3e80fdd --- /dev/null +++ b/src/Types/ImportJobSummaryKind.php @@ -0,0 +1,27 @@ + $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->total = Hydrate::int($data['total'] ?? null); + $this->processed = Hydrate::int($data['processed'] ?? null); + $this->imported = Hydrate::int($data['imported'] ?? null); + $this->updated = Hydrate::int($data['updated'] ?? null); + $this->alreadyInDidit = Hydrate::int($data['already_in_didit'] ?? null); + $this->failed = Hydrate::int($data['failed'] ?? null); + $this->notFinished = Hydrate::int($data['not_finished'] ?? null); + $this->samePerson = Hydrate::int($data['same_person'] ?? null); + } +} diff --git a/src/Types/ImportJobSummarySource.php b/src/Types/ImportJobSummarySource.php new file mode 100644 index 0000000..aa494e6 --- /dev/null +++ b/src/Types/ImportJobSummarySource.php @@ -0,0 +1,23 @@ + $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->name = Hydrate::string($data['name'] ?? null); + $this->email = Hydrate::string($data['email'] ?? null); + } +} diff --git a/src/Types/ImportJobSummaryState.php b/src/Types/ImportJobSummaryState.php new file mode 100644 index 0000000..2462aaf --- /dev/null +++ b/src/Types/ImportJobSummaryState.php @@ -0,0 +1,35 @@ +|null */ + public readonly ?array $credentialFields; + + /** @var list|null */ + public readonly ?array $regions; + + /** @var list|null */ + public readonly ?array $requiredPermissions; + + /** The `docs_url` field. */ + public readonly ?string $docsUrl; + + /** @var array|null */ + public readonly ?array $statusMapping; + + /** + * @param array $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->key = Hydrate::string($data['key'] ?? null); + $this->name = Hydrate::string($data['name'] ?? null); + $this->mode = Hydrate::string($data['mode'] ?? null); + $this->recordNoun = Hydrate::string($data['record_noun'] ?? null); + $this->credentialFields = Hydrate::list($data['credential_fields'] ?? null, static fn (mixed $v) => Hydrate::model($v, ImportProviderCredentialFields::class)); + $this->regions = Hydrate::list($data['regions'] ?? null, Hydrate::string(...)); + $this->requiredPermissions = Hydrate::list($data['required_permissions'] ?? null, Hydrate::string(...)); + $this->docsUrl = Hydrate::string($data['docs_url'] ?? null); + $this->statusMapping = Hydrate::map($data['status_mapping'] ?? null, Hydrate::string(...)); + } +} diff --git a/src/Types/ImportProviderCredentialFields.php b/src/Types/ImportProviderCredentialFields.php new file mode 100644 index 0000000..85cb917 --- /dev/null +++ b/src/Types/ImportProviderCredentialFields.php @@ -0,0 +1,36 @@ + $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->name = Hydrate::string($data['name'] ?? null); + $this->label = Hydrate::string($data['label'] ?? null); + $this->secret = Hydrate::bool($data['secret'] ?? null); + } +} diff --git a/src/Types/ImportProviderKey.php b/src/Types/ImportProviderKey.php new file mode 100644 index 0000000..d96b5d8 --- /dev/null +++ b/src/Types/ImportProviderKey.php @@ -0,0 +1,47 @@ +|null diff --git a/src/Types/SessionImportJob.php b/src/Types/SessionImportJob.php index d824d86..3c750fa 100644 --- a/src/Types/SessionImportJob.php +++ b/src/Types/SessionImportJob.php @@ -68,6 +68,72 @@ final class SessionImportJob extends BaseModel /** The `completed_at` field. */ public readonly ?\DateTimeImmutable $completedAt; + /** + * The job's state as the console shows it; `errors` is a completed job with rejected rows. + * + * Known values: {@see SessionImportJobState}. + */ + public readonly ?string $state; + + /** Known values: {@see SessionImportJobSource}. */ + public readonly ?string $source; + + /** Migration source; empty for a file or API import. */ + public readonly ?string $provider; + + /** @var array|null */ + public readonly ?array $providerOptions; + + /** The first characters of the key the migration holds. The key itself is never returned. */ + public readonly ?string $credentialHint; + + /** The `credentials_expires_at` field. */ + public readonly ?\DateTimeImmutable $credentialsExpiresAt; + + /** When a job paused by the provider's rate limit resumes by itself. */ + public readonly ?\DateTimeImmutable $resumesAt; + + /** @var array|null */ + public readonly ?array $statusMapping; + + /** The `is_dry_run` field. */ + public readonly ?bool $isDryRun; + + /** + * What a dry run found: `total`, `by_status`, `already_in_didit`, `same_person`, `not_finished`, + * `failed`, `unmapped_statuses`. + * + * @var array|null + */ + public readonly ?array $dryRunSummary; + + /** The `face_templates_enabled` field. */ + public readonly ?bool $faceTemplatesEnabled; + + /** The `lawful_basis_confirmed_at` field. */ + public readonly ?\DateTimeImmutable $lawfulBasisConfirmedAt; + + /** The `not_finished_count` field. */ + public readonly ?int $notFinishedCount; + + /** The `same_person_count` field. */ + public readonly ?int $samePersonCount; + + /** The `face_templates_count` field. */ + public readonly ?int $faceTemplatesCount; + + /** The `face_duplicates_count` field. */ + public readonly ?int $faceDuplicatesCount; + + /** The `face_blocklist_hits_count` field. */ + public readonly ?int $faceBlocklistHitsCount; + + /** The `created_by_name` field. */ + public readonly ?string $createdByName; + + /** The `created_by_email` field. */ + public readonly ?string $createdByEmail; + /** * @param array $data the decoded JSON object */ @@ -92,5 +158,24 @@ public function __construct(array $data) $this->updatedAt = Hydrate::dateTime($data['updated_at'] ?? null); $this->startedAt = Hydrate::dateTime($data['started_at'] ?? null); $this->completedAt = Hydrate::dateTime($data['completed_at'] ?? null); + $this->state = Hydrate::string($data['state'] ?? null); + $this->source = Hydrate::string($data['source'] ?? null); + $this->provider = Hydrate::string($data['provider'] ?? null); + $this->providerOptions = Hydrate::assoc($data['provider_options'] ?? null); + $this->credentialHint = Hydrate::string($data['credential_hint'] ?? null); + $this->credentialsExpiresAt = Hydrate::dateTime($data['credentials_expires_at'] ?? null); + $this->resumesAt = Hydrate::dateTime($data['resumes_at'] ?? null); + $this->statusMapping = Hydrate::map($data['status_mapping'] ?? null, Hydrate::string(...)); + $this->isDryRun = Hydrate::bool($data['is_dry_run'] ?? null); + $this->dryRunSummary = Hydrate::assoc($data['dry_run_summary'] ?? null); + $this->faceTemplatesEnabled = Hydrate::bool($data['face_templates_enabled'] ?? null); + $this->lawfulBasisConfirmedAt = Hydrate::dateTime($data['lawful_basis_confirmed_at'] ?? null); + $this->notFinishedCount = Hydrate::int($data['not_finished_count'] ?? null); + $this->samePersonCount = Hydrate::int($data['same_person_count'] ?? null); + $this->faceTemplatesCount = Hydrate::int($data['face_templates_count'] ?? null); + $this->faceDuplicatesCount = Hydrate::int($data['face_duplicates_count'] ?? null); + $this->faceBlocklistHitsCount = Hydrate::int($data['face_blocklist_hits_count'] ?? null); + $this->createdByName = Hydrate::string($data['created_by_name'] ?? null); + $this->createdByEmail = Hydrate::string($data['created_by_email'] ?? null); } } diff --git a/src/Types/SessionImportJobSource.php b/src/Types/SessionImportJobSource.php new file mode 100644 index 0000000..14bc586 --- /dev/null +++ b/src/Types/SessionImportJobSource.php @@ -0,0 +1,23 @@ + $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->ok = Hydrate::bool($data['ok'] ?? null); + $this->provider = Hydrate::string($data['provider'] ?? null); + $this->credentialHint = Hydrate::string($data['credential_hint'] ?? null); + } +} diff --git a/src/Types/SessionsImportsCreateParamsProvider.php b/src/Types/SessionsImportsCreateParamsProvider.php new file mode 100644 index 0000000..b757473 --- /dev/null +++ b/src/Types/SessionsImportsCreateParamsProvider.php @@ -0,0 +1,49 @@ +|null */ + public readonly ?array $results; + + /** The `running` field. */ + public readonly ?int $running; + + /** + * @param array $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->results = Hydrate::list($data['results'] ?? null, static fn (mixed $v) => Hydrate::model($v, ImportJobSummary::class)); + $this->running = Hydrate::int($data['running'] ?? null); + } +} diff --git a/src/Types/SessionsImportsListJobsParamsScope.php b/src/Types/SessionsImportsListJobsParamsScope.php new file mode 100644 index 0000000..ff2075c --- /dev/null +++ b/src/Types/SessionsImportsListJobsParamsScope.php @@ -0,0 +1,25 @@ +|null */ + public readonly ?array $results; + + /** The `counts` field. */ + public readonly ?SessionsImportsListJobsResponseCounts $counts; + + /** + * @param array $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->results = Hydrate::list($data['results'] ?? null, static fn (mixed $v) => Hydrate::model($v, ImportJobSummary::class)); + $this->counts = Hydrate::model($data['counts'] ?? null, SessionsImportsListJobsResponseCounts::class); + } +} diff --git a/src/Types/SessionsImportsListJobsResponseCounts.php b/src/Types/SessionsImportsListJobsResponseCounts.php new file mode 100644 index 0000000..e0acf0f --- /dev/null +++ b/src/Types/SessionsImportsListJobsResponseCounts.php @@ -0,0 +1,40 @@ + $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->all = Hydrate::int($data['all'] ?? null); + $this->runningAndWaiting = Hydrate::int($data['running_and_waiting'] ?? null); + $this->needsAttention = Hydrate::int($data['needs_attention'] ?? null); + $this->done = Hydrate::int($data['done'] ?? null); + } +} diff --git a/src/Types/SessionsImportsListProvidersResponse.php b/src/Types/SessionsImportsListProvidersResponse.php new file mode 100644 index 0000000..a0f7016 --- /dev/null +++ b/src/Types/SessionsImportsListProvidersResponse.php @@ -0,0 +1,36 @@ +|null */ + public readonly ?array $providers; + + /** @var list|null */ + public readonly ?array $mappingTargets; + + /** @var list|null */ + public readonly ?array $skippedStatuses; + + /** + * @param array $data the decoded JSON object + */ + public function __construct(array $data) + { + parent::__construct($data); + $this->providers = Hydrate::list($data['providers'] ?? null, static fn (mixed $v) => Hydrate::model($v, ImportProvider::class)); + $this->mappingTargets = Hydrate::list($data['mapping_targets'] ?? null, Hydrate::string(...)); + $this->skippedStatuses = Hydrate::list($data['skipped_statuses'] ?? null, Hydrate::string(...)); + } +} diff --git a/src/Types/SessionsImportsStartParamsStatusMappingValue.php b/src/Types/SessionsImportsStartParamsStatusMappingValue.php new file mode 100644 index 0000000..094a3b2 --- /dev/null +++ b/src/Types/SessionsImportsStartParamsStatusMappingValue.php @@ -0,0 +1,29 @@ +|null + */ + public readonly ?array $bankVerificationConfig; + + /** + * 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. + */ + public readonly ?bool $isGeolocationEnabled; + + /** + * 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. + * + * @var array|null + */ + public readonly ?array $geolocationConfig; + /** * @param array $data the decoded JSON object */ @@ -149,5 +180,9 @@ public function __construct(array $data) $this->kybRoleSettings = Hydrate::list($data['kyb_role_settings'] ?? null, Hydrate::assoc(...)); $this->createdAt = Hydrate::dateTime($data['created_at'] ?? null); $this->updatedAt = Hydrate::dateTime($data['updated_at'] ?? null); + $this->isBankVerificationEnabled = Hydrate::bool($data['is_bank_verification_enabled'] ?? null); + $this->bankVerificationConfig = Hydrate::assoc($data['bank_verification_config'] ?? null); + $this->isGeolocationEnabled = Hydrate::bool($data['is_geolocation_enabled'] ?? null); + $this->geolocationConfig = Hydrate::assoc($data['geolocation_config'] ?? null); } } diff --git a/tests/Generated/PricingTest.php b/tests/Generated/PricingTest.php new file mode 100644 index 0000000..14c2238 --- /dev/null +++ b/tests/Generated/PricingTest.php @@ -0,0 +1,36 @@ +queueJson(200, '{"currency":"USD","statement_upload":{"price_usd":"0.0000","included_after_charged_bank_attempt":true,"standalone_available":false},"own_provider_fee_usd":"0.0200","countries":{"GBR":{"name":"United Kingdom","methods":{"open_banking":{"available":true,"market":"UK and EEA","price_usd":"0.7900","add_ons":{"balance":"0.3500","transactions":"1.3000"}},"account_lookup":{"available":true,"price_usd":"0.4000"}}},"CAN":{"name":"Canada","methods":{"open_banking":{"available":false,"market":null,"price_usd":null,"add_ons":{}},"account_lookup":{"available":false,"price_usd":null}}}}}'); + + $result = $this->client->pricing->bankVerification(); + + $request = $this->lastRequest(); + self::assertSame('GET', $request->getMethod()); + self::assertSame('verification.didit.me', $request->getUri()->getHost()); + self::assertSame('/v3/organization/bank-verification-pricing/', $request->getUri()->getPath()); + self::assertSame([], $this->queryParams($request)); + self::assertSame('', $request->getHeaderLine('x-api-key')); + self::assertSame('', $request->getHeaderLine('Authorization')); + self::assertSame('', (string) $request->getBody()); + self::assertSame(200, $result->rawResponse()?->status); + self::assertSame($this->decoded('{"currency":"USD","statement_upload":{"price_usd":"0.0000","included_after_charged_bank_attempt":true,"standalone_available":false},"own_provider_fee_usd":"0.0200","countries":{"GBR":{"name":"United Kingdom","methods":{"open_banking":{"available":true,"market":"UK and EEA","price_usd":"0.7900","add_ons":{"balance":"0.3500","transactions":"1.3000"}},"account_lookup":{"available":true,"price_usd":"0.4000"}}},"CAN":{"name":"Canada","methods":{"open_banking":{"available":false,"market":null,"price_usd":null,"add_ons":{}},"account_lookup":{"available":false,"price_usd":null}}}}}'), $result->toArray()); + self::assertSame('USD', $result->currency); + } +} diff --git a/tests/Generated/Sessions/ImportsTest.php b/tests/Generated/Sessions/ImportsTest.php index ab5c554..4ada4c7 100644 --- a/tests/Generated/Sessions/ImportsTest.php +++ b/tests/Generated/Sessions/ImportsTest.php @@ -26,6 +26,20 @@ public function testCreate(): void 'workflow_id' => '3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f', 'source_format' => 'csv', 'source_file_url' => 'https://example.com/callback', + 'source' => 'file', + 'provider' => 'sumsub', + 'credentials' => [ + 'key' => 'credentials-value', + ], + 'options' => [ + 'region' => 'region-value', + ], + 'status_mapping' => [ + 'key' => 'Approved', + ], + 'dry_run' => true, + 'face_templates' => true, + 'lawful_basis_confirmed' => true, 'file' => File::fromBytes('fake-file-bytes', 'file.jpg', 'image/jpeg'), ]); @@ -49,6 +63,30 @@ public function testCreate(): void 'source_file_url' => [ 'https://example.com/callback', ], + 'source' => [ + 'file', + ], + 'provider' => [ + 'sumsub', + ], + 'credentials' => [ + '{"key":"credentials-value"}', + ], + 'options' => [ + '{"region":"region-value"}', + ], + 'status_mapping' => [ + '{"key":"Approved"}', + ], + 'dry_run' => [ + 'true', + ], + 'face_templates' => [ + 'true', + ], + 'lawful_basis_confirmed' => [ + 'true', + ], 'file' => [ 'file:file.jpg:image/jpeg:fake-file-bytes', ], @@ -82,7 +120,7 @@ public function testRetrieveTemplate(): void public function testRetrieve(): void { - $this->queueJson(200, '{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","status":"PENDING","import_type":"user_verification","source_format":"csv","source_filename":"source_filename-value","total_rows":1,"processed_rows":1,"checkpoint_row":1,"created_count":1,"updated_count":1,"skipped_count":1,"failed_count":1,"total_media_bytes":1,"last_error":"last_error-value","created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z"}'); + $this->queueJson(200, '{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","status":"PENDING","import_type":"user_verification","source_format":"csv","source_filename":"source_filename-value","total_rows":1,"processed_rows":1,"checkpoint_row":1,"created_count":1,"updated_count":1,"skipped_count":1,"failed_count":1,"total_media_bytes":1,"last_error":"last_error-value","created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z","state":"ready","source":"file","provider":"provider-value","provider_options":{"key":"value"},"credential_hint":"credential_hint-value","credentials_expires_at":"2026-01-15T10:30:00Z","resumes_at":"2026-01-15T10:30:00Z","status_mapping":{"key":"status_mapping-value"},"is_dry_run":true,"dry_run_summary":{"key":"value"},"face_templates_enabled":true,"lawful_basis_confirmed_at":"2026-01-15T10:30:00Z","not_finished_count":1,"same_person_count":1,"face_templates_count":1,"face_duplicates_count":1,"face_blocklist_hits_count":1,"created_by_name":"created_by_name-value","created_by_email":"created_by_email-value"}'); $result = $this->client->sessions->imports->retrieve('importId-1'); @@ -94,7 +132,7 @@ public function testRetrieve(): void self::assertSame(self::API_KEY, $request->getHeaderLine('x-api-key')); self::assertSame('', (string) $request->getBody()); self::assertSame(200, $result->rawResponse()?->status); - self::assertSame($this->decoded('{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","status":"PENDING","import_type":"user_verification","source_format":"csv","source_filename":"source_filename-value","total_rows":1,"processed_rows":1,"checkpoint_row":1,"created_count":1,"updated_count":1,"skipped_count":1,"failed_count":1,"total_media_bytes":1,"last_error":"last_error-value","created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z"}'), $result->toArray()); + self::assertSame($this->decoded('{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","status":"PENDING","import_type":"user_verification","source_format":"csv","source_filename":"source_filename-value","total_rows":1,"processed_rows":1,"checkpoint_row":1,"created_count":1,"updated_count":1,"skipped_count":1,"failed_count":1,"total_media_bytes":1,"last_error":"last_error-value","created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z","state":"ready","source":"file","provider":"provider-value","provider_options":{"key":"value"},"credential_hint":"credential_hint-value","credentials_expires_at":"2026-01-15T10:30:00Z","resumes_at":"2026-01-15T10:30:00Z","status_mapping":{"key":"status_mapping-value"},"is_dry_run":true,"dry_run_summary":{"key":"value"},"face_templates_enabled":true,"lawful_basis_confirmed_at":"2026-01-15T10:30:00Z","not_finished_count":1,"same_person_count":1,"face_templates_count":1,"face_duplicates_count":1,"face_blocklist_hits_count":1,"created_by_name":"created_by_name-value","created_by_email":"created_by_email-value"}'), $result->toArray()); self::assertSame('3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f', $result->uuid); } @@ -122,4 +160,202 @@ public function testListErrors(): void self::assertFalse($result->hasNextPage()); self::assertSame($this->decoded('{"count":1,"next":null,"previous":null,"results":[{"uuid":"5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d","row_number":17,"external_id":"ext-000017","error":"status: invalid value \'unknown_status\'","raw_row":{"external_id":"ext-000017","status":"unknown_status","first_name":"Jane"},"created_at":"2026-05-17T08:44:02Z"}]}'), $result->toArray()); } + + public function testListProviders(): void + { + $this->queueJson(200, '{"providers":[{"key":"sumsub","name":"name-value","mode":"api_list","record_noun":"record_noun-value","credential_fields":[{"name":"name-value","label":"label-value","secret":true}],"regions":["regions-value"],"required_permissions":["required_permissions-value"],"docs_url":"docs_url-value","status_mapping":{"key":"Approved"}}],"mapping_targets":["mapping_targets-value"],"skipped_statuses":["skipped_statuses-value"]}'); + + $result = $this->client->sessions->imports->listProviders(); + + $request = $this->lastRequest(); + self::assertSame('GET', $request->getMethod()); + self::assertSame('verification.didit.me', $request->getUri()->getHost()); + self::assertSame('/v3/session/imports/providers/', $request->getUri()->getPath()); + self::assertSame([], $this->queryParams($request)); + self::assertSame(self::API_KEY, $request->getHeaderLine('x-api-key')); + self::assertSame('', (string) $request->getBody()); + self::assertSame(200, $result->rawResponse()?->status); + self::assertSame($this->decoded('{"providers":[{"key":"sumsub","name":"name-value","mode":"api_list","record_noun":"record_noun-value","credential_fields":[{"name":"name-value","label":"label-value","secret":true}],"regions":["regions-value"],"required_permissions":["required_permissions-value"],"docs_url":"docs_url-value","status_mapping":{"key":"Approved"}}],"mapping_targets":["mapping_targets-value"],"skipped_statuses":["skipped_statuses-value"]}'), $result->toArray()); + } + + public function testCheckProvider(): void + { + $this->queueJson(200, '{"ok":true,"provider":"provider-value","credential_hint":"credential_hint-value"}'); + + $result = $this->client->sessions->imports->checkProvider([ + 'provider' => 'sumsub', + 'credentials' => [ + 'key' => 'credentials-value', + ], + 'options' => [ + 'region' => 'region-value', + ], + 'record_ids' => [ + 'record_ids-value', + ], + ]); + + $request = $this->lastRequest(); + self::assertSame('POST', $request->getMethod()); + self::assertSame('verification.didit.me', $request->getUri()->getHost()); + self::assertSame('/v3/session/imports/providers/check/', $request->getUri()->getPath()); + self::assertSame([], $this->queryParams($request)); + self::assertSame(self::API_KEY, $request->getHeaderLine('x-api-key')); + self::assertSame('application/json', $request->getHeaderLine('Content-Type')); + self::assertSame('{"provider":"sumsub","credentials":{"key":"credentials-value"},"options":{"region":"region-value"},"record_ids":["record_ids-value"]}', $this->canonicalBody($request)); + self::assertSame(200, $result->rawResponse()?->status); + self::assertSame($this->decoded('{"ok":true,"provider":"provider-value","credential_hint":"credential_hint-value"}'), $result->toArray()); + self::assertTrue($result->ok); + } + + public function testStart(): void + { + $this->queueJson(200, '{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","status":"PENDING","import_type":"user_verification","source_format":"csv","source_filename":"source_filename-value","total_rows":1,"processed_rows":1,"checkpoint_row":1,"created_count":1,"updated_count":1,"skipped_count":1,"failed_count":1,"total_media_bytes":1,"last_error":"last_error-value","created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z","state":"ready","source":"file","provider":"provider-value","provider_options":{"key":"value"},"credential_hint":"credential_hint-value","credentials_expires_at":"2026-01-15T10:30:00Z","resumes_at":"2026-01-15T10:30:00Z","status_mapping":{"key":"status_mapping-value"},"is_dry_run":true,"dry_run_summary":{"key":"value"},"face_templates_enabled":true,"lawful_basis_confirmed_at":"2026-01-15T10:30:00Z","not_finished_count":1,"same_person_count":1,"face_templates_count":1,"face_duplicates_count":1,"face_blocklist_hits_count":1,"created_by_name":"created_by_name-value","created_by_email":"created_by_email-value"}'); + + $result = $this->client->sessions->imports->start('importId-1', [ + 'status_mapping' => [ + 'key' => 'Approved', + ], + 'face_templates' => true, + 'lawful_basis_confirmed' => true, + ]); + + $request = $this->lastRequest(); + self::assertSame('POST', $request->getMethod()); + self::assertSame('verification.didit.me', $request->getUri()->getHost()); + self::assertSame('/v3/session/imports/importId-1/start/', $request->getUri()->getPath()); + self::assertSame([], $this->queryParams($request)); + self::assertSame(self::API_KEY, $request->getHeaderLine('x-api-key')); + self::assertSame('application/json', $request->getHeaderLine('Content-Type')); + self::assertSame('{"status_mapping":{"key":"Approved"},"face_templates":true,"lawful_basis_confirmed":true}', $this->canonicalBody($request)); + self::assertSame(200, $result->rawResponse()?->status); + self::assertSame($this->decoded('{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","status":"PENDING","import_type":"user_verification","source_format":"csv","source_filename":"source_filename-value","total_rows":1,"processed_rows":1,"checkpoint_row":1,"created_count":1,"updated_count":1,"skipped_count":1,"failed_count":1,"total_media_bytes":1,"last_error":"last_error-value","created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z","state":"ready","source":"file","provider":"provider-value","provider_options":{"key":"value"},"credential_hint":"credential_hint-value","credentials_expires_at":"2026-01-15T10:30:00Z","resumes_at":"2026-01-15T10:30:00Z","status_mapping":{"key":"status_mapping-value"},"is_dry_run":true,"dry_run_summary":{"key":"value"},"face_templates_enabled":true,"lawful_basis_confirmed_at":"2026-01-15T10:30:00Z","not_finished_count":1,"same_person_count":1,"face_templates_count":1,"face_duplicates_count":1,"face_blocklist_hits_count":1,"created_by_name":"created_by_name-value","created_by_email":"created_by_email-value"}'), $result->toArray()); + self::assertSame('3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f', $result->uuid); + } + + public function testPause(): void + { + $this->queueJson(200, '{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","status":"PENDING","import_type":"user_verification","source_format":"csv","source_filename":"source_filename-value","total_rows":1,"processed_rows":1,"checkpoint_row":1,"created_count":1,"updated_count":1,"skipped_count":1,"failed_count":1,"total_media_bytes":1,"last_error":"last_error-value","created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z","state":"ready","source":"file","provider":"provider-value","provider_options":{"key":"value"},"credential_hint":"credential_hint-value","credentials_expires_at":"2026-01-15T10:30:00Z","resumes_at":"2026-01-15T10:30:00Z","status_mapping":{"key":"status_mapping-value"},"is_dry_run":true,"dry_run_summary":{"key":"value"},"face_templates_enabled":true,"lawful_basis_confirmed_at":"2026-01-15T10:30:00Z","not_finished_count":1,"same_person_count":1,"face_templates_count":1,"face_duplicates_count":1,"face_blocklist_hits_count":1,"created_by_name":"created_by_name-value","created_by_email":"created_by_email-value"}'); + + $result = $this->client->sessions->imports->pause('importId-1'); + + $request = $this->lastRequest(); + self::assertSame('POST', $request->getMethod()); + self::assertSame('verification.didit.me', $request->getUri()->getHost()); + self::assertSame('/v3/session/imports/importId-1/pause/', $request->getUri()->getPath()); + self::assertSame([], $this->queryParams($request)); + self::assertSame(self::API_KEY, $request->getHeaderLine('x-api-key')); + self::assertSame('', (string) $request->getBody()); + self::assertSame(200, $result->rawResponse()?->status); + self::assertSame($this->decoded('{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","status":"PENDING","import_type":"user_verification","source_format":"csv","source_filename":"source_filename-value","total_rows":1,"processed_rows":1,"checkpoint_row":1,"created_count":1,"updated_count":1,"skipped_count":1,"failed_count":1,"total_media_bytes":1,"last_error":"last_error-value","created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z","state":"ready","source":"file","provider":"provider-value","provider_options":{"key":"value"},"credential_hint":"credential_hint-value","credentials_expires_at":"2026-01-15T10:30:00Z","resumes_at":"2026-01-15T10:30:00Z","status_mapping":{"key":"status_mapping-value"},"is_dry_run":true,"dry_run_summary":{"key":"value"},"face_templates_enabled":true,"lawful_basis_confirmed_at":"2026-01-15T10:30:00Z","not_finished_count":1,"same_person_count":1,"face_templates_count":1,"face_duplicates_count":1,"face_blocklist_hits_count":1,"created_by_name":"created_by_name-value","created_by_email":"created_by_email-value"}'), $result->toArray()); + self::assertSame('3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f', $result->uuid); + } + + public function testResume(): void + { + $this->queueJson(200, '{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","status":"PENDING","import_type":"user_verification","source_format":"csv","source_filename":"source_filename-value","total_rows":1,"processed_rows":1,"checkpoint_row":1,"created_count":1,"updated_count":1,"skipped_count":1,"failed_count":1,"total_media_bytes":1,"last_error":"last_error-value","created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z","state":"ready","source":"file","provider":"provider-value","provider_options":{"key":"value"},"credential_hint":"credential_hint-value","credentials_expires_at":"2026-01-15T10:30:00Z","resumes_at":"2026-01-15T10:30:00Z","status_mapping":{"key":"status_mapping-value"},"is_dry_run":true,"dry_run_summary":{"key":"value"},"face_templates_enabled":true,"lawful_basis_confirmed_at":"2026-01-15T10:30:00Z","not_finished_count":1,"same_person_count":1,"face_templates_count":1,"face_duplicates_count":1,"face_blocklist_hits_count":1,"created_by_name":"created_by_name-value","created_by_email":"created_by_email-value"}'); + + $result = $this->client->sessions->imports->resume('importId-1'); + + $request = $this->lastRequest(); + self::assertSame('POST', $request->getMethod()); + self::assertSame('verification.didit.me', $request->getUri()->getHost()); + self::assertSame('/v3/session/imports/importId-1/resume/', $request->getUri()->getPath()); + self::assertSame([], $this->queryParams($request)); + self::assertSame(self::API_KEY, $request->getHeaderLine('x-api-key')); + self::assertSame('', (string) $request->getBody()); + self::assertSame(200, $result->rawResponse()?->status); + self::assertSame($this->decoded('{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","status":"PENDING","import_type":"user_verification","source_format":"csv","source_filename":"source_filename-value","total_rows":1,"processed_rows":1,"checkpoint_row":1,"created_count":1,"updated_count":1,"skipped_count":1,"failed_count":1,"total_media_bytes":1,"last_error":"last_error-value","created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z","state":"ready","source":"file","provider":"provider-value","provider_options":{"key":"value"},"credential_hint":"credential_hint-value","credentials_expires_at":"2026-01-15T10:30:00Z","resumes_at":"2026-01-15T10:30:00Z","status_mapping":{"key":"status_mapping-value"},"is_dry_run":true,"dry_run_summary":{"key":"value"},"face_templates_enabled":true,"lawful_basis_confirmed_at":"2026-01-15T10:30:00Z","not_finished_count":1,"same_person_count":1,"face_templates_count":1,"face_duplicates_count":1,"face_blocklist_hits_count":1,"created_by_name":"created_by_name-value","created_by_email":"created_by_email-value"}'), $result->toArray()); + self::assertSame('3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f', $result->uuid); + } + + public function testCancel(): void + { + $this->queueJson(200, '{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","status":"PENDING","import_type":"user_verification","source_format":"csv","source_filename":"source_filename-value","total_rows":1,"processed_rows":1,"checkpoint_row":1,"created_count":1,"updated_count":1,"skipped_count":1,"failed_count":1,"total_media_bytes":1,"last_error":"last_error-value","created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z","state":"ready","source":"file","provider":"provider-value","provider_options":{"key":"value"},"credential_hint":"credential_hint-value","credentials_expires_at":"2026-01-15T10:30:00Z","resumes_at":"2026-01-15T10:30:00Z","status_mapping":{"key":"status_mapping-value"},"is_dry_run":true,"dry_run_summary":{"key":"value"},"face_templates_enabled":true,"lawful_basis_confirmed_at":"2026-01-15T10:30:00Z","not_finished_count":1,"same_person_count":1,"face_templates_count":1,"face_duplicates_count":1,"face_blocklist_hits_count":1,"created_by_name":"created_by_name-value","created_by_email":"created_by_email-value"}'); + + $result = $this->client->sessions->imports->cancel('importId-1'); + + $request = $this->lastRequest(); + self::assertSame('POST', $request->getMethod()); + self::assertSame('verification.didit.me', $request->getUri()->getHost()); + self::assertSame('/v3/session/imports/importId-1/cancel/', $request->getUri()->getPath()); + self::assertSame([], $this->queryParams($request)); + self::assertSame(self::API_KEY, $request->getHeaderLine('x-api-key')); + self::assertSame('', (string) $request->getBody()); + self::assertSame(200, $result->rawResponse()?->status); + self::assertSame($this->decoded('{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","status":"PENDING","import_type":"user_verification","source_format":"csv","source_filename":"source_filename-value","total_rows":1,"processed_rows":1,"checkpoint_row":1,"created_count":1,"updated_count":1,"skipped_count":1,"failed_count":1,"total_media_bytes":1,"last_error":"last_error-value","created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z","state":"ready","source":"file","provider":"provider-value","provider_options":{"key":"value"},"credential_hint":"credential_hint-value","credentials_expires_at":"2026-01-15T10:30:00Z","resumes_at":"2026-01-15T10:30:00Z","status_mapping":{"key":"status_mapping-value"},"is_dry_run":true,"dry_run_summary":{"key":"value"},"face_templates_enabled":true,"lawful_basis_confirmed_at":"2026-01-15T10:30:00Z","not_finished_count":1,"same_person_count":1,"face_templates_count":1,"face_duplicates_count":1,"face_blocklist_hits_count":1,"created_by_name":"created_by_name-value","created_by_email":"created_by_email-value"}'), $result->toArray()); + self::assertSame('3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f', $result->uuid); + } + + public function testRetry(): void + { + $this->queueJson(201, '{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","status":"PENDING","import_type":"user_verification","source_format":"csv","source_filename":"source_filename-value","total_rows":1,"processed_rows":1,"checkpoint_row":1,"created_count":1,"updated_count":1,"skipped_count":1,"failed_count":1,"total_media_bytes":1,"last_error":"last_error-value","created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z","state":"ready","source":"file","provider":"provider-value","provider_options":{"key":"value"},"credential_hint":"credential_hint-value","credentials_expires_at":"2026-01-15T10:30:00Z","resumes_at":"2026-01-15T10:30:00Z","status_mapping":{"key":"status_mapping-value"},"is_dry_run":true,"dry_run_summary":{"key":"value"},"face_templates_enabled":true,"lawful_basis_confirmed_at":"2026-01-15T10:30:00Z","not_finished_count":1,"same_person_count":1,"face_templates_count":1,"face_duplicates_count":1,"face_blocklist_hits_count":1,"created_by_name":"created_by_name-value","created_by_email":"created_by_email-value"}'); + + $result = $this->client->sessions->imports->retry('importId-1', [ + 'credentials' => [ + 'key' => 'credentials-value', + ], + 'file' => File::fromBytes('fake-file-bytes', 'file.jpg', 'image/jpeg'), + ]); + + $request = $this->lastRequest(); + self::assertSame('POST', $request->getMethod()); + self::assertSame('verification.didit.me', $request->getUri()->getHost()); + self::assertSame('/v3/session/imports/importId-1/retry/', $request->getUri()->getPath()); + self::assertSame([], $this->queryParams($request)); + self::assertSame(self::API_KEY, $request->getHeaderLine('x-api-key')); + self::assertStringStartsWith('multipart/form-data; boundary=', $request->getHeaderLine('Content-Type')); + self::assertSame([ + 'credentials' => [ + '{"key":"credentials-value"}', + ], + 'file' => [ + 'file:file.jpg:image/jpeg:fake-file-bytes', + ], + ], $this->multipartFields($request)); + self::assertSame(201, $result->rawResponse()?->status); + self::assertSame($this->decoded('{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","status":"PENDING","import_type":"user_verification","source_format":"csv","source_filename":"source_filename-value","total_rows":1,"processed_rows":1,"checkpoint_row":1,"created_count":1,"updated_count":1,"skipped_count":1,"failed_count":1,"total_media_bytes":1,"last_error":"last_error-value","created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z","state":"ready","source":"file","provider":"provider-value","provider_options":{"key":"value"},"credential_hint":"credential_hint-value","credentials_expires_at":"2026-01-15T10:30:00Z","resumes_at":"2026-01-15T10:30:00Z","status_mapping":{"key":"status_mapping-value"},"is_dry_run":true,"dry_run_summary":{"key":"value"},"face_templates_enabled":true,"lawful_basis_confirmed_at":"2026-01-15T10:30:00Z","not_finished_count":1,"same_person_count":1,"face_templates_count":1,"face_duplicates_count":1,"face_blocklist_hits_count":1,"created_by_name":"created_by_name-value","created_by_email":"created_by_email-value"}'), $result->toArray()); + self::assertSame('3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f', $result->uuid); + } + + public function testListJobs(): void + { + $this->queueJson(200, '{"results":[{"id":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","kind":"sessions","source":"file","provider":"provider-value","provider_name":"provider_name-value","filename":"filename-value","state":"ready","status":"status-value","is_dry_run":true,"credential_hint":"credential_hint-value","resumes_at":"2026-01-15T10:30:00Z","last_error":"last_error-value","started_by":{"name":"name-value","email":"email-value"},"progress":{"total":1,"processed":1,"imported":1,"updated":1,"already_in_didit":1,"failed":1,"not_finished":1,"same_person":1},"face_templates":{"enabled":true,"built":1,"duplicates":1,"blocklisted":1},"eta_seconds":1,"created_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z"}],"counts":{"all":1,"running_and_waiting":1,"needs_attention":1,"done":1}}'); + + $result = $this->client->sessions->imports->listJobs([ + 'scope' => 'all', + 'limit' => 1, + ]); + + $request = $this->lastRequest(); + self::assertSame('GET', $request->getMethod()); + self::assertSame('verification.didit.me', $request->getUri()->getHost()); + self::assertSame('/v3/import-jobs/', $request->getUri()->getPath()); + self::assertSame([ + 'scope' => 'all', + 'limit' => '1', + ], $this->queryParams($request)); + self::assertSame(self::API_KEY, $request->getHeaderLine('x-api-key')); + self::assertSame('', (string) $request->getBody()); + self::assertSame(200, $result->rawResponse()?->status); + self::assertSame($this->decoded('{"results":[{"id":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","kind":"sessions","source":"file","provider":"provider-value","provider_name":"provider_name-value","filename":"filename-value","state":"ready","status":"status-value","is_dry_run":true,"credential_hint":"credential_hint-value","resumes_at":"2026-01-15T10:30:00Z","last_error":"last_error-value","started_by":{"name":"name-value","email":"email-value"},"progress":{"total":1,"processed":1,"imported":1,"updated":1,"already_in_didit":1,"failed":1,"not_finished":1,"same_person":1},"face_templates":{"enabled":true,"built":1,"duplicates":1,"blocklisted":1},"eta_seconds":1,"created_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z"}],"counts":{"all":1,"running_and_waiting":1,"needs_attention":1,"done":1}}'), $result->toArray()); + } + + public function testListActiveJobs(): void + { + $this->queueJson(200, '{"results":[{"id":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","kind":"sessions","source":"file","provider":"provider-value","provider_name":"provider_name-value","filename":"filename-value","state":"ready","status":"status-value","is_dry_run":true,"credential_hint":"credential_hint-value","resumes_at":"2026-01-15T10:30:00Z","last_error":"last_error-value","started_by":{"name":"name-value","email":"email-value"},"progress":{"total":1,"processed":1,"imported":1,"updated":1,"already_in_didit":1,"failed":1,"not_finished":1,"same_person":1},"face_templates":{"enabled":true,"built":1,"duplicates":1,"blocklisted":1},"eta_seconds":1,"created_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z"}],"running":1}'); + + $result = $this->client->sessions->imports->listActiveJobs(); + + $request = $this->lastRequest(); + self::assertSame('GET', $request->getMethod()); + self::assertSame('verification.didit.me', $request->getUri()->getHost()); + self::assertSame('/v3/import-jobs/active/', $request->getUri()->getPath()); + self::assertSame([], $this->queryParams($request)); + self::assertSame(self::API_KEY, $request->getHeaderLine('x-api-key')); + self::assertSame('', (string) $request->getBody()); + self::assertSame(200, $result->rawResponse()?->status); + self::assertSame($this->decoded('{"results":[{"id":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","kind":"sessions","source":"file","provider":"provider-value","provider_name":"provider_name-value","filename":"filename-value","state":"ready","status":"status-value","is_dry_run":true,"credential_hint":"credential_hint-value","resumes_at":"2026-01-15T10:30:00Z","last_error":"last_error-value","started_by":{"name":"name-value","email":"email-value"},"progress":{"total":1,"processed":1,"imported":1,"updated":1,"already_in_didit":1,"failed":1,"not_finished":1,"same_person":1},"face_templates":{"enabled":true,"built":1,"duplicates":1,"blocklisted":1},"eta_seconds":1,"created_at":"2026-01-15T10:30:00Z","started_at":"2026-01-15T10:30:00Z","completed_at":"2026-01-15T10:30:00Z"}],"running":1}'), $result->toArray()); + self::assertSame(1, $result->running); + } } diff --git a/tests/Generated/WorkflowsTest.php b/tests/Generated/WorkflowsTest.php index a5a9d0b..01c6753 100644 --- a/tests/Generated/WorkflowsTest.php +++ b/tests/Generated/WorkflowsTest.php @@ -96,7 +96,7 @@ public function testRetrieve(): void public function testUpdate(): void { - $this->queueJson(200, '{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","workflow_id":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","workflow_label":"workflow_label-value","workflow_type":"kyc","is_default":true,"is_archived":true,"version":1,"status":"draft","published_at":"2026-01-15T10:30:00Z","is_simple_workflow":true,"is_editable":true,"workflow_url":"https://example.com/callback","workflow_graph":{"key":"value"},"features":"features-value","total_price":1.5,"min_price":1.5,"max_price":1.5,"special_add_on_prices":{"key":"value"},"max_retry_attempts":1,"retry_window_days":1,"session_expiration_time":1,"questionnaire":{"key":"value"},"kyb_role_settings":[{"key":"value"}],"created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z"}'); + $this->queueJson(200, '{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","workflow_id":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","workflow_label":"workflow_label-value","workflow_type":"kyc","is_default":true,"is_archived":true,"version":1,"status":"draft","published_at":"2026-01-15T10:30:00Z","is_simple_workflow":true,"is_editable":true,"workflow_url":"https://example.com/callback","workflow_graph":{"key":"value"},"features":"features-value","total_price":1.5,"min_price":1.5,"max_price":1.5,"special_add_on_prices":{"key":"value"},"max_retry_attempts":1,"retry_window_days":1,"session_expiration_time":1,"questionnaire":{"key":"value"},"kyb_role_settings":[{"key":"value"}],"created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z","is_bank_verification_enabled":true,"bank_verification_config":{"key":"value"},"is_geolocation_enabled":true,"geolocation_config":{"key":"value"}}'); $result = $this->client->workflows->update('settingsUuid-1', [ 'workflow_label' => 'workflow_label-value', @@ -125,7 +125,7 @@ public function testUpdate(): void self::assertSame('application/json', $request->getHeaderLine('Content-Type')); self::assertSame('{"workflow_label":"workflow_label-value","is_default":true,"status":"draft","features":[{"feature":"OCR"}],"is_white_label_enabled":true,"is_desktop_allowed":true,"max_retry_attempts":1,"retry_window_days":1,"face_liveness_max_attempts":1,"face_match_max_attempts":1,"session_expiration_time":1}', $this->canonicalBody($request)); self::assertSame(200, $result->rawResponse()?->status); - self::assertSame($this->decoded('{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","workflow_id":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","workflow_label":"workflow_label-value","workflow_type":"kyc","is_default":true,"is_archived":true,"version":1,"status":"draft","published_at":"2026-01-15T10:30:00Z","is_simple_workflow":true,"is_editable":true,"workflow_url":"https://example.com/callback","workflow_graph":{"key":"value"},"features":"features-value","total_price":1.5,"min_price":1.5,"max_price":1.5,"special_add_on_prices":{"key":"value"},"max_retry_attempts":1,"retry_window_days":1,"session_expiration_time":1,"questionnaire":{"key":"value"},"kyb_role_settings":[{"key":"value"}],"created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z"}'), $result->toArray()); + self::assertSame($this->decoded('{"uuid":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","workflow_id":"3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f","workflow_label":"workflow_label-value","workflow_type":"kyc","is_default":true,"is_archived":true,"version":1,"status":"draft","published_at":"2026-01-15T10:30:00Z","is_simple_workflow":true,"is_editable":true,"workflow_url":"https://example.com/callback","workflow_graph":{"key":"value"},"features":"features-value","total_price":1.5,"min_price":1.5,"max_price":1.5,"special_add_on_prices":{"key":"value"},"max_retry_attempts":1,"retry_window_days":1,"session_expiration_time":1,"questionnaire":{"key":"value"},"kyb_role_settings":[{"key":"value"}],"created_at":"2026-01-15T10:30:00Z","updated_at":"2026-01-15T10:30:00Z","is_bank_verification_enabled":true,"bank_verification_config":{"key":"value"},"is_geolocation_enabled":true,"geolocation_config":{"key":"value"}}'), $result->toArray()); self::assertSame('3f2c8a7e-5b1d-4c9a-8e6f-2a1b3c4d5e6f', $result->uuid); }