From efad1e450d42e2314548a1897ee3ae9ab6f8fbb4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 06:48:45 +0000 Subject: [PATCH 1/2] chore(spec): sync API spec 2026-10-23 --- openapi/SOURCE.json | 8 +- openapi/account.json | 783 +- openapi/overlay.json | 1705 +- openapi/verification.json | 144233 +++++++++------ src/didit/resources/__init__.py | 65 + src/didit/resources/account/__init__.py | 13 +- src/didit/resources/account/applications.py | 40 +- src/didit/resources/account/organizations.py | 33 +- src/didit/resources/aml.py | 93 +- src/didit/resources/analytics.py | 232 + src/didit/resources/billing.py | 12 +- src/didit/resources/blocklist.py | 386 + src/didit/resources/business_sessions.py | 16 +- src/didit/resources/businesses.py | 265 +- src/didit/resources/cases/__init__.py | 134 +- src/didit/resources/cases/blueprints.py | 18 +- .../resources/cases/regulatory_reports.py | 6 +- src/didit/resources/cases/requests.py | 89 +- src/didit/resources/customization.py | 22 +- src/didit/resources/investigator.py | 1404 + src/didit/resources/kyb.py | 22 +- src/didit/resources/lists/__init__.py | 645 + src/didit/resources/lists/entries.py | 501 + src/didit/resources/monitoring_events.py | 1226 + src/didit/resources/networks/__init__.py | 317 +- src/didit/resources/phone.py | 4 +- src/didit/resources/regulatory_reports.py | 6 +- src/didit/resources/sessions/__init__.py | 3300 +- src/didit/resources/sessions/signatures.py | 450 + src/didit/resources/sessions/webhooks.py | 325 + src/didit/resources/transactions/__init__.py | 287 +- src/didit/resources/transactions/notes.py | 278 + src/didit/resources/transactions/rules.py | 12 +- .../resources/transactions/travel_rule.py | 389 +- src/didit/resources/travel_rule/__init__.py | 1507 +- .../travel_rule/counterparty_contacts.py | 338 + src/didit/resources/travel_rule/transfers.py | 223 + .../resources/travel_rule/wallet_addresses.py | 119 +- src/didit/resources/travel_rule/workspace.py | 535 + src/didit/resources/users.py | 210 +- src/didit/resources/wallet_screening.py | 10 +- src/didit/resources/webhooks/__init__.py | 216 +- src/didit/resources/webhooks/destinations.py | 400 +- src/didit/resources/workflows.py | 38 +- src/didit/types/__init__.py | 3572 +- .../account/applications/update_params.py | 2 +- .../types/account/resend_otp_response.py | 14 + .../retrieve_dataset_freshness_response.py | 17 + .../billing/retrieve_balance_response.py | 2 +- src/didit/types/billing/top_up_params.py | 4 +- src/didit/types/billing/top_up_response.py | 4 +- .../__init__.py | 0 src/didit/types/blocklist/add_params.py | 25 + src/didit/types/blocklist/add_response.py | 11 + src/didit/types/blocklist/list_response.py | 20 + src/didit/types/blocklist/remove_params.py | 25 + src/didit/types/blocklist/remove_response.py | 11 + .../list_networks_response.py | 11 - .../businesses/list_networks_response.py | 11 - ...etrieve_ongoing_monitoring_job_response.py | 88 - ...session_ongoing_monitoring_job_response.py | 88 - .../businesses/set_aml_monitoring_response.py | 299 - .../set_ongoing_monitoring_response.py | 193 - ...set_session_ongoing_monitoring_response.py | 193 - src/didit/types/businesses/update_params.py | 3 + .../types/businesses/update_status_params.py | 3 + .../types/cases/bulk_update_status_params.py | 11 +- .../cases/bulk_update_status_response.py | 18 - .../cases/regulatory_reports/list_response.py | 18 - .../types/cases/requests/remind_response.py | 23 + src/didit/types/cases/resolve_params.py | 14 +- src/didit/types/cases/update_params.py | 6 + .../types/components/aml_dataset_freshness.py | 41 + .../aml_dataset_source_freshness.py | 24 + .../types/components/aml_monitoring_job.py | 137 + .../aml_monitoring_toggle_result.py | 180 + src/didit/types/components/aml_screening.py | 526 + .../types/components/aml_screening_run.py | 46 + src/didit/types/components/application.py | 6 +- .../application_analytics_country_count.py | 17 + .../application_analytics_data_point.py | 51 + ...application_analytics_demographic_point.py | 15 + ...application_analytics_demographic_stats.py | 35 + .../application_analytics_device_details.py | 42 + .../application_analytics_device_point.py | 17 + .../application_analytics_location_stats.py | 51 + ...ication_analytics_original_status_point.py | 17 + ...ication_analytics_original_status_stats.py | 16 + ...application_analytics_request_breakdown.py | 43 + ...pplication_analytics_resubmission_point.py | 19 + ...pplication_analytics_resubmission_stats.py | 20 + .../application_analytics_summary.py | 104 + ...ation_analytics_verification_time_point.py | 17 + ...ation_analytics_verification_time_stats.py | 44 + .../application_analytics_warning_stats.py | 30 + .../application_analytics_widget.py | 166 + .../application_analytics_widget_response.py | 25 + .../application_analytics_workflow_funnel.py | 26 + .../application_credentials_summary.py | 2 +- .../types/components/application_product.py | 21 + .../types/components/blocklisted_item.py | 46 + .../components/branding_customization.py | 3 + .../components/business_session_list_item.py | 63 + .../case_blueprint_preset_install_result.py} | 4 +- .../case_bulk_update_result.py} | 8 +- src/didit/types/components/case_checker.py | 18 + src/didit/types/components/case_detail.py | 36 +- src/didit/types/components/case_event.py | 7 + src/didit/types/components/case_list_item.py | 55 +- src/didit/types/components/case_subject.py | 6 + .../types/components/case_team_summary.py | 14 + src/didit/types/components/compliance_list.py | 59 + .../types/components/compliance_list_entry.py | 93 + .../entity_network_membership_list.py} | 4 +- .../components/feature_status_override.py | 72 + .../investigator_batch_accept_response.py | 34 + .../types/components/investigator_block.py | 43 + .../types/components/investigator_decision.py | 92 + .../components/investigator_queue_row.py | 43 + .../components/investigator_run_detail.py | 33 + .../types/components/investigator_settings.py | 40 + .../types/components/kyb_key_people_party.py | 195 + .../components/kyb_registry_search_results.py | 60 + .../types/components/monitoring_event.py | 110 + .../monitoring_event_create_request.py | 152 + .../components/monitoring_event_detail.py | 168 + .../monitoring_event_ingest_result.py | 62 + .../types/components/monitoring_event_page.py | 22 + .../components/monitoring_event_rule_run.py | 44 + .../components/network_detail_response.py | 106 + .../types/components/network_list_counts.py | 171 + .../types/components/network_outcomes.py | 22 + .../types/components/network_pattern_score.py | 28 + src/didit/types/components/network_tag.py | 29 + .../network_timeline.py} | 6 +- .../components/network_timeline_account.py | 30 + .../components/network_timeline_amount.py | 15 + .../components/network_timeline_attempt.py | 32 + .../network_timeline_case_linked.py | 20 + .../network_timeline_decline_reason.py | 16 + .../components/network_timeline_device.py | 13 + .../types/components/network_timeline_ip.py | 21 + .../components/network_timeline_login.py | 32 + .../components/network_timeline_note_added.py | 14 + .../types/components/network_timeline_page.py | 16 + .../components/network_timeline_route.py | 17 + .../components/network_timeline_rule_hit.py | 26 + .../components/network_timeline_source_ref.py | 27 + .../network_timeline_transaction.py | 31 + .../network_typed_timeline_event.py | 79 + .../ongoing_monitoring_job.py} | 16 +- .../ongoing_monitoring_toggle_result.py | 39 + .../components/paginated_application_list.py | 20 + .../components/paginated_organization_list.py | 20 + .../regulatory_report_list.py} | 4 +- src/didit/types/components/review_item.py | 5 +- .../components/sandbox_simulate_response.py | 41 + .../session_decision.py} | 1464 +- .../session_deletion_job.py} | 8 +- src/didit/types/components/session_event.py | 93 + .../types/components/session_sign_document.py | 24 + .../components/session_webhook_delivery.py | 40 + src/didit/types/components/signature_field.py | 71 + .../components/signature_field_anchor.py | 30 + .../components/signature_field_result.py | 68 + .../components/signature_field_validation.py | 26 + .../types/components/signature_result.py | 343 + .../components/simple_workflow_feature.py | 3 +- .../types/components/transaction_detail.py | 69 +- .../types/components/transaction_note.py | 61 + .../components/transaction_rule_action.py | 209 +- .../transaction_rule_action_add_note.py | 28 + .../transaction_rule_action_add_score.py | 28 + .../transaction_rule_action_add_tags.py | 40 + .../transaction_rule_action_add_to_list.py | 28 + .../transaction_rule_action_change_status.py | 34 + .../transaction_rule_action_open_case.py | 40 + .../components/transaction_rule_condition.py | 4 +- .../types/components/transaction_summary.py | 132 + .../components/transaction_travel_rule.py | 113 + ...transaction_verification_session_action.py | 24 + .../transaction_wallet_ownership_action.py | 22 + .../components/travel_rule_chain_details.py | 70 + .../components/travel_rule_chain_side.py | 33 + .../travel_rule_counterparty_contact.py | 51 + .../travel_rule_counterparty_decision.py | 35 + ...vel_rule_counterparty_decision_response.py | 12 + .../travel_rule_counterparty_questionnaire.py | 39 + ...el_rule_counterparty_questionnaire_list.py | 14 + .../travel_rule_counterparty_screening.py | 32 + ...avel_rule_counterparty_screening_result.py | 19 + ...avel_rule_counterparty_screening_status.py | 23 + .../components/travel_rule_data_match.py | 30 + .../travel_rule_data_sharing_fields.py | 63 + .../components/travel_rule_data_unmasked.py | 33 + .../types/components/travel_rule_delivery.py | 36 + .../components/travel_rule_discovery_step.py | 40 + .../travel_rule_inbound_response.py | 6 +- .../components/travel_rule_matching_rules.py | 95 + .../types/components/travel_rule_message.py | 37 + .../travel_rule_message_attachment.py | 36 + .../components/travel_rule_name_match.py | 42 + .../components/travel_rule_next_action.py | 27 + .../components/travel_rule_settings_detail.py | 79 +- .../travel_rule_test_vasp_inbound_response.py | 30 + .../components/travel_rule_transfer_detail.py | 78 +- .../components/travel_rule_transfer_row.py | 92 + .../types/components/travel_rule_utxo.py | 19 + .../components/travel_rule_utxo_entry.py | 38 + .../types/components/travel_rule_workspace.py | 65 + ...vel_rule_workspace_domain_code_response.py | 18 + .../travel_rule_workspace_txt_record.py | 14 + .../components/user_session_list_item.py | 263 + .../types/components/vasp_directory_entry.py | 250 +- .../types/components/wallet_address_entry.py | 18 +- .../components/wallet_address_entry_update.py | 2 +- .../wallet_address_import_report.py | 41 + .../components/wallet_address_import_row.py | 39 + src/didit/types/components/webhook_config.py | 52 + .../components/webhook_config_destination.py | 39 + .../webhook_destination_delivery.py | 37 + .../webhook_destination_delivery_detail.py | 43 + src/didit/types/components/webhook_event.py | 21 + src/didit/types/components/workflow_detail.py | 99 + .../types/customization/update_params.py | 10 +- src/didit/types/investigator/__init__.py | 1 + .../types/investigator/accept_run_params.py | 19 + .../investigator/batch_accept_runs_params.py | 12 + .../investigator/list_activity_response.py | 20 + .../investigator/list_guardrails_response.py | 32 + .../types/investigator/list_queue_response.py | 19 + .../types/investigator/override_run_params.py | 29 + .../retrieve_dashboard_response.py | 22 + .../retrieve_performance_response.py | 18 + .../investigator/retrieve_summary_response.py | 26 + .../retrieve_transaction_response.py | 14 + .../investigator/update_settings_params.py | 15 + src/didit/types/kyb/search_params.py | 7 +- src/didit/types/kyb/search_response.py | 56 +- src/didit/types/lists/__init__.py | 1 + src/didit/types/lists/create_params.py | 38 + src/didit/types/lists/entries/__init__.py | 1 + .../types/lists/entries/create_params.py | 48 + .../types/lists/entries/list_response.py | 20 + .../types/lists/entries/upload_face_params.py | 14 + src/didit/types/lists/list_response.py | 20 + src/didit/types/lists/update_params.py | 14 + src/didit/types/lists/update_response.py | 14 + src/didit/types/monitoring_events/__init__.py | 1 + .../monitoring_events/create_batch_params.py | 15 + .../create_batch_response.py | 89 + .../types/monitoring_events/create_params.py | 152 + .../create_sdk_token_params.py | 44 + .../create_sdk_token_response.py | 40 + .../retrieve_enrichment_metrics_response.py | 41 + src/didit/types/networks/add_tag_params.py | 11 + src/didit/types/networks/list_response.py | 27 +- src/didit/types/networks/remove_tag_params.py | 11 + src/didit/types/sessions/add_images_params.py | 21 + .../types/sessions/add_images_response.py | 11 + .../types/sessions/batch_delete_params.py | 59 +- .../types/sessions/batch_delete_response.py | 60 +- .../bulk_update_aml_hit_status_response.py | 14 + src/didit/types/sessions/create_params.py | 19 +- src/didit/types/sessions/create_response.py | 2 +- .../list_aml_screening_history_response.py | 20 + .../types/sessions/list_events_response.py | 20 + .../list_feature_overrides_response.py | 22 + .../types/sessions/list_networks_response.py | 11 - src/didit/types/sessions/list_response.py | 348 +- .../sessions/preview_decision_email_params.py | 16 + .../preview_decision_email_response.py | 50 + .../sessions/preview_resubmission_params.py | 4 +- .../sessions/preview_resubmission_response.py | 28 +- .../types/sessions/signatures/__init__.py | 1 + .../sessions/signatures/validate_params.py | 11 + .../sessions/signatures/validate_response.py | 11 + src/didit/types/sessions/simulate_params.py | 30 + .../update_aml_hit_status_response.py | 19 + .../sessions/update_aml_status_params.py | 18 + .../sessions/update_aml_status_response.py | 21 + .../update_document_ai_data_params.py | 16 + .../update_document_ai_data_response.py | 28 + .../sessions/update_feature_status_params.py | 37 + .../update_feature_status_response.py | 14 +- .../types/sessions/update_status_params.py | 2 +- src/didit/types/transactions/create_params.py | 33 +- .../transactions/list_networks_response.py | 11 - .../list_party_networks_response.py | 11 - .../transactions/list_related_response.py | 20 + src/didit/types/transactions/list_response.py | 7 + .../types/transactions/notes/__init__.py | 1 + .../types/transactions/notes/create_params.py | 16 + .../types/transactions/notes/list_response.py | 20 + .../travel_rule/confirm_ownership_params.py | 4 + .../travel_rule/create_message_params.py | 28 + .../trigger_test_vasp_inbound_params.py | 28 + .../travel_rule/update_transfer_params.py | 15 +- .../counterparty_contacts/__init__.py | 1 + .../counterparty_contacts/create_params.py | 16 + .../counterparty_contacts/list_response.py | 17 + .../create_widget_session_params.py | 14 +- .../travel_rule/register_inbound_params.py | 25 +- .../types/travel_rule/screen_vasp_params.py | 11 + ..._compliance_email_confirmation_response.py | 10 + .../send_vasp_questionnaire_params.py | 11 + .../types/travel_rule/transfers/__init__.py | 1 + .../travel_rule/transfers/list_response.py | 17 + .../travel_rule/update_settings_params.py | 31 +- .../update_vasp_decision_params.py | 15 + .../wallet_addresses/bulk_import_params.py | 14 + .../wallet_addresses/create_params.py | 9 +- .../wallet_addresses/update_params.py | 2 +- .../types/travel_rule/workspace/__init__.py | 1 + .../travel_rule/workspace/claim_params.py | 11 + .../workspace/confirm_domain_params.py | 10 + .../workspace/enable_billing_params.py | 11 + src/didit/types/users/create_params.py | 26 +- src/didit/types/users/create_response.py | 192 + .../users/set_aml_monitoring_response.py | 299 - .../users/set_ongoing_monitoring_response.py | 193 - src/didit/types/users/update_params.py | 5 +- src/didit/types/users/update_status_params.py | 3 + .../types/wallet_screening/screen_params.py | 5 +- .../webhooks/destinations/create_params.py | 2 +- .../webhooks/destinations/create_response.py | 34 +- .../destinations/list_deliveries_response.py | 20 + .../webhooks/destinations/list_response.py | 54 +- .../retrieve_analytics_response.py | 62 + .../destinations/retrieve_response.py | 34 +- .../webhooks/destinations/update_response.py | 33 + .../types/webhooks/update_config_params.py | 36 + tests/generated/test_account.py | 145 +- tests/generated/test_aml.py | 40 + tests/generated/test_analytics.py | 311 + tests/generated/test_billing.py | 2 +- tests/generated/test_blocklist.py | 127 + tests/generated/test_business_sessions.py | 18 +- tests/generated/test_businesses.py | 129 +- tests/generated/test_cases.py | 419 +- tests/generated/test_customization.py | 2 + tests/generated/test_investigator.py | 662 + tests/generated/test_lists.py | 366 + tests/generated/test_monitoring_events.py | 746 + tests/generated/test_networks.py | 410 +- tests/generated/test_regulatory_reports.py | 6 +- tests/generated/test_sessions.py | 990 +- tests/generated/test_transactions.py | 668 +- tests/generated/test_travel_rule.py | 1631 +- tests/generated/test_users.py | 86 +- tests/generated/test_webhooks.py | 443 + tests/generated/test_workflows.py | 43 +- 352 files changed, 122299 insertions(+), 63111 deletions(-) create mode 100644 src/didit/resources/analytics.py create mode 100644 src/didit/resources/blocklist.py create mode 100644 src/didit/resources/investigator.py create mode 100644 src/didit/resources/lists/__init__.py create mode 100644 src/didit/resources/lists/entries.py create mode 100644 src/didit/resources/monitoring_events.py create mode 100644 src/didit/resources/sessions/signatures.py create mode 100644 src/didit/resources/sessions/webhooks.py create mode 100644 src/didit/resources/transactions/notes.py create mode 100644 src/didit/resources/travel_rule/counterparty_contacts.py create mode 100644 src/didit/resources/travel_rule/transfers.py create mode 100644 src/didit/resources/travel_rule/workspace.py create mode 100644 src/didit/types/account/resend_otp_response.py create mode 100644 src/didit/types/aml/retrieve_dataset_freshness_response.py rename src/didit/types/{business_sessions => blocklist}/__init__.py (100%) create mode 100644 src/didit/types/blocklist/add_params.py create mode 100644 src/didit/types/blocklist/add_response.py create mode 100644 src/didit/types/blocklist/list_response.py create mode 100644 src/didit/types/blocklist/remove_params.py create mode 100644 src/didit/types/blocklist/remove_response.py delete mode 100644 src/didit/types/business_sessions/list_networks_response.py delete mode 100644 src/didit/types/businesses/list_networks_response.py delete mode 100644 src/didit/types/businesses/retrieve_ongoing_monitoring_job_response.py delete mode 100644 src/didit/types/businesses/retrieve_session_ongoing_monitoring_job_response.py delete mode 100644 src/didit/types/businesses/set_aml_monitoring_response.py delete mode 100644 src/didit/types/businesses/set_ongoing_monitoring_response.py delete mode 100644 src/didit/types/businesses/set_session_ongoing_monitoring_response.py delete mode 100644 src/didit/types/cases/bulk_update_status_response.py delete mode 100644 src/didit/types/cases/regulatory_reports/list_response.py create mode 100644 src/didit/types/cases/requests/remind_response.py create mode 100644 src/didit/types/components/aml_dataset_freshness.py create mode 100644 src/didit/types/components/aml_dataset_source_freshness.py create mode 100644 src/didit/types/components/aml_monitoring_job.py create mode 100644 src/didit/types/components/aml_monitoring_toggle_result.py create mode 100644 src/didit/types/components/aml_screening.py create mode 100644 src/didit/types/components/aml_screening_run.py create mode 100644 src/didit/types/components/application_analytics_country_count.py create mode 100644 src/didit/types/components/application_analytics_data_point.py create mode 100644 src/didit/types/components/application_analytics_demographic_point.py create mode 100644 src/didit/types/components/application_analytics_demographic_stats.py create mode 100644 src/didit/types/components/application_analytics_device_details.py create mode 100644 src/didit/types/components/application_analytics_device_point.py create mode 100644 src/didit/types/components/application_analytics_location_stats.py create mode 100644 src/didit/types/components/application_analytics_original_status_point.py create mode 100644 src/didit/types/components/application_analytics_original_status_stats.py create mode 100644 src/didit/types/components/application_analytics_request_breakdown.py create mode 100644 src/didit/types/components/application_analytics_resubmission_point.py create mode 100644 src/didit/types/components/application_analytics_resubmission_stats.py create mode 100644 src/didit/types/components/application_analytics_summary.py create mode 100644 src/didit/types/components/application_analytics_verification_time_point.py create mode 100644 src/didit/types/components/application_analytics_verification_time_stats.py create mode 100644 src/didit/types/components/application_analytics_warning_stats.py create mode 100644 src/didit/types/components/application_analytics_widget.py create mode 100644 src/didit/types/components/application_analytics_widget_response.py create mode 100644 src/didit/types/components/application_analytics_workflow_funnel.py create mode 100644 src/didit/types/components/application_product.py create mode 100644 src/didit/types/components/blocklisted_item.py create mode 100644 src/didit/types/components/business_session_list_item.py rename src/didit/types/{cases/blueprints/install_presets_response.py => components/case_blueprint_preset_install_result.py} (61%) rename src/didit/types/{cases/bulk_assign_response.py => components/case_bulk_update_result.py} (50%) create mode 100644 src/didit/types/components/case_checker.py create mode 100644 src/didit/types/components/case_team_summary.py create mode 100644 src/didit/types/components/compliance_list.py create mode 100644 src/didit/types/components/compliance_list_entry.py rename src/didit/types/{users/list_networks_response.py => components/entity_network_membership_list.py} (76%) create mode 100644 src/didit/types/components/feature_status_override.py create mode 100644 src/didit/types/components/investigator_batch_accept_response.py create mode 100644 src/didit/types/components/investigator_block.py create mode 100644 src/didit/types/components/investigator_decision.py create mode 100644 src/didit/types/components/investigator_queue_row.py create mode 100644 src/didit/types/components/investigator_run_detail.py create mode 100644 src/didit/types/components/investigator_settings.py create mode 100644 src/didit/types/components/kyb_key_people_party.py create mode 100644 src/didit/types/components/kyb_registry_search_results.py create mode 100644 src/didit/types/components/monitoring_event.py create mode 100644 src/didit/types/components/monitoring_event_create_request.py create mode 100644 src/didit/types/components/monitoring_event_detail.py create mode 100644 src/didit/types/components/monitoring_event_ingest_result.py create mode 100644 src/didit/types/components/monitoring_event_page.py create mode 100644 src/didit/types/components/monitoring_event_rule_run.py create mode 100644 src/didit/types/components/network_detail_response.py create mode 100644 src/didit/types/components/network_list_counts.py create mode 100644 src/didit/types/components/network_outcomes.py create mode 100644 src/didit/types/components/network_pattern_score.py create mode 100644 src/didit/types/components/network_tag.py rename src/didit/types/{networks/retrieve_timeline_response.py => components/network_timeline.py} (64%) create mode 100644 src/didit/types/components/network_timeline_account.py create mode 100644 src/didit/types/components/network_timeline_amount.py create mode 100644 src/didit/types/components/network_timeline_attempt.py create mode 100644 src/didit/types/components/network_timeline_case_linked.py create mode 100644 src/didit/types/components/network_timeline_decline_reason.py create mode 100644 src/didit/types/components/network_timeline_device.py create mode 100644 src/didit/types/components/network_timeline_ip.py create mode 100644 src/didit/types/components/network_timeline_login.py create mode 100644 src/didit/types/components/network_timeline_note_added.py create mode 100644 src/didit/types/components/network_timeline_page.py create mode 100644 src/didit/types/components/network_timeline_route.py create mode 100644 src/didit/types/components/network_timeline_rule_hit.py create mode 100644 src/didit/types/components/network_timeline_source_ref.py create mode 100644 src/didit/types/components/network_timeline_transaction.py create mode 100644 src/didit/types/components/network_typed_timeline_event.py rename src/didit/types/{users/retrieve_ongoing_monitoring_job_response.py => components/ongoing_monitoring_job.py} (81%) create mode 100644 src/didit/types/components/ongoing_monitoring_toggle_result.py create mode 100644 src/didit/types/components/paginated_application_list.py create mode 100644 src/didit/types/components/paginated_organization_list.py rename src/didit/types/{regulatory_reports/list_response.py => components/regulatory_report_list.py} (82%) create mode 100644 src/didit/types/components/sandbox_simulate_response.py rename src/didit/types/{sessions/retrieve_decision_response.py => components/session_decision.py} (73%) rename src/didit/types/{sessions/retrieve_deletion_job_response.py => components/session_deletion_job.py} (89%) create mode 100644 src/didit/types/components/session_event.py create mode 100644 src/didit/types/components/session_sign_document.py create mode 100644 src/didit/types/components/session_webhook_delivery.py create mode 100644 src/didit/types/components/signature_field.py create mode 100644 src/didit/types/components/signature_field_anchor.py create mode 100644 src/didit/types/components/signature_field_result.py create mode 100644 src/didit/types/components/signature_field_validation.py create mode 100644 src/didit/types/components/signature_result.py create mode 100644 src/didit/types/components/transaction_note.py create mode 100644 src/didit/types/components/transaction_rule_action_add_note.py create mode 100644 src/didit/types/components/transaction_rule_action_add_score.py create mode 100644 src/didit/types/components/transaction_rule_action_add_tags.py create mode 100644 src/didit/types/components/transaction_rule_action_add_to_list.py create mode 100644 src/didit/types/components/transaction_rule_action_change_status.py create mode 100644 src/didit/types/components/transaction_rule_action_open_case.py create mode 100644 src/didit/types/components/transaction_summary.py create mode 100644 src/didit/types/components/transaction_travel_rule.py create mode 100644 src/didit/types/components/transaction_verification_session_action.py create mode 100644 src/didit/types/components/transaction_wallet_ownership_action.py create mode 100644 src/didit/types/components/travel_rule_chain_details.py create mode 100644 src/didit/types/components/travel_rule_chain_side.py create mode 100644 src/didit/types/components/travel_rule_counterparty_contact.py create mode 100644 src/didit/types/components/travel_rule_counterparty_decision.py create mode 100644 src/didit/types/components/travel_rule_counterparty_decision_response.py create mode 100644 src/didit/types/components/travel_rule_counterparty_questionnaire.py create mode 100644 src/didit/types/components/travel_rule_counterparty_questionnaire_list.py create mode 100644 src/didit/types/components/travel_rule_counterparty_screening.py create mode 100644 src/didit/types/components/travel_rule_counterparty_screening_result.py create mode 100644 src/didit/types/components/travel_rule_counterparty_screening_status.py create mode 100644 src/didit/types/components/travel_rule_data_match.py create mode 100644 src/didit/types/components/travel_rule_data_sharing_fields.py create mode 100644 src/didit/types/components/travel_rule_data_unmasked.py create mode 100644 src/didit/types/components/travel_rule_delivery.py create mode 100644 src/didit/types/components/travel_rule_discovery_step.py create mode 100644 src/didit/types/components/travel_rule_matching_rules.py create mode 100644 src/didit/types/components/travel_rule_message.py create mode 100644 src/didit/types/components/travel_rule_message_attachment.py create mode 100644 src/didit/types/components/travel_rule_name_match.py create mode 100644 src/didit/types/components/travel_rule_next_action.py create mode 100644 src/didit/types/components/travel_rule_test_vasp_inbound_response.py create mode 100644 src/didit/types/components/travel_rule_transfer_row.py create mode 100644 src/didit/types/components/travel_rule_utxo.py create mode 100644 src/didit/types/components/travel_rule_utxo_entry.py create mode 100644 src/didit/types/components/travel_rule_workspace.py create mode 100644 src/didit/types/components/travel_rule_workspace_domain_code_response.py create mode 100644 src/didit/types/components/travel_rule_workspace_txt_record.py create mode 100644 src/didit/types/components/user_session_list_item.py create mode 100644 src/didit/types/components/wallet_address_import_report.py create mode 100644 src/didit/types/components/wallet_address_import_row.py create mode 100644 src/didit/types/components/webhook_config.py create mode 100644 src/didit/types/components/webhook_config_destination.py create mode 100644 src/didit/types/components/webhook_destination_delivery.py create mode 100644 src/didit/types/components/webhook_destination_delivery_detail.py create mode 100644 src/didit/types/components/workflow_detail.py create mode 100644 src/didit/types/investigator/__init__.py create mode 100644 src/didit/types/investigator/accept_run_params.py create mode 100644 src/didit/types/investigator/batch_accept_runs_params.py create mode 100644 src/didit/types/investigator/list_activity_response.py create mode 100644 src/didit/types/investigator/list_guardrails_response.py create mode 100644 src/didit/types/investigator/list_queue_response.py create mode 100644 src/didit/types/investigator/override_run_params.py create mode 100644 src/didit/types/investigator/retrieve_dashboard_response.py create mode 100644 src/didit/types/investigator/retrieve_performance_response.py create mode 100644 src/didit/types/investigator/retrieve_summary_response.py create mode 100644 src/didit/types/investigator/retrieve_transaction_response.py create mode 100644 src/didit/types/investigator/update_settings_params.py create mode 100644 src/didit/types/lists/__init__.py create mode 100644 src/didit/types/lists/create_params.py create mode 100644 src/didit/types/lists/entries/__init__.py create mode 100644 src/didit/types/lists/entries/create_params.py create mode 100644 src/didit/types/lists/entries/list_response.py create mode 100644 src/didit/types/lists/entries/upload_face_params.py create mode 100644 src/didit/types/lists/list_response.py create mode 100644 src/didit/types/lists/update_params.py create mode 100644 src/didit/types/lists/update_response.py create mode 100644 src/didit/types/monitoring_events/__init__.py create mode 100644 src/didit/types/monitoring_events/create_batch_params.py create mode 100644 src/didit/types/monitoring_events/create_batch_response.py create mode 100644 src/didit/types/monitoring_events/create_params.py create mode 100644 src/didit/types/monitoring_events/create_sdk_token_params.py create mode 100644 src/didit/types/monitoring_events/create_sdk_token_response.py create mode 100644 src/didit/types/monitoring_events/retrieve_enrichment_metrics_response.py create mode 100644 src/didit/types/networks/add_tag_params.py create mode 100644 src/didit/types/networks/remove_tag_params.py create mode 100644 src/didit/types/sessions/add_images_params.py create mode 100644 src/didit/types/sessions/add_images_response.py create mode 100644 src/didit/types/sessions/bulk_update_aml_hit_status_response.py create mode 100644 src/didit/types/sessions/list_aml_screening_history_response.py create mode 100644 src/didit/types/sessions/list_events_response.py create mode 100644 src/didit/types/sessions/list_feature_overrides_response.py delete mode 100644 src/didit/types/sessions/list_networks_response.py create mode 100644 src/didit/types/sessions/preview_decision_email_params.py create mode 100644 src/didit/types/sessions/preview_decision_email_response.py create mode 100644 src/didit/types/sessions/signatures/__init__.py create mode 100644 src/didit/types/sessions/signatures/validate_params.py create mode 100644 src/didit/types/sessions/signatures/validate_response.py create mode 100644 src/didit/types/sessions/simulate_params.py create mode 100644 src/didit/types/sessions/update_aml_hit_status_response.py create mode 100644 src/didit/types/sessions/update_aml_status_params.py create mode 100644 src/didit/types/sessions/update_aml_status_response.py create mode 100644 src/didit/types/sessions/update_document_ai_data_params.py create mode 100644 src/didit/types/sessions/update_document_ai_data_response.py delete mode 100644 src/didit/types/transactions/list_networks_response.py delete mode 100644 src/didit/types/transactions/list_party_networks_response.py create mode 100644 src/didit/types/transactions/list_related_response.py create mode 100644 src/didit/types/transactions/notes/__init__.py create mode 100644 src/didit/types/transactions/notes/create_params.py create mode 100644 src/didit/types/transactions/notes/list_response.py create mode 100644 src/didit/types/transactions/travel_rule/create_message_params.py create mode 100644 src/didit/types/transactions/travel_rule/trigger_test_vasp_inbound_params.py create mode 100644 src/didit/types/travel_rule/counterparty_contacts/__init__.py create mode 100644 src/didit/types/travel_rule/counterparty_contacts/create_params.py create mode 100644 src/didit/types/travel_rule/counterparty_contacts/list_response.py create mode 100644 src/didit/types/travel_rule/screen_vasp_params.py create mode 100644 src/didit/types/travel_rule/send_compliance_email_confirmation_response.py create mode 100644 src/didit/types/travel_rule/send_vasp_questionnaire_params.py create mode 100644 src/didit/types/travel_rule/transfers/__init__.py create mode 100644 src/didit/types/travel_rule/transfers/list_response.py create mode 100644 src/didit/types/travel_rule/update_vasp_decision_params.py create mode 100644 src/didit/types/travel_rule/wallet_addresses/bulk_import_params.py create mode 100644 src/didit/types/travel_rule/workspace/__init__.py create mode 100644 src/didit/types/travel_rule/workspace/claim_params.py create mode 100644 src/didit/types/travel_rule/workspace/confirm_domain_params.py create mode 100644 src/didit/types/travel_rule/workspace/enable_billing_params.py create mode 100644 src/didit/types/users/create_response.py delete mode 100644 src/didit/types/users/set_aml_monitoring_response.py delete mode 100644 src/didit/types/users/set_ongoing_monitoring_response.py create mode 100644 src/didit/types/webhooks/destinations/list_deliveries_response.py create mode 100644 src/didit/types/webhooks/destinations/retrieve_analytics_response.py create mode 100644 src/didit/types/webhooks/update_config_params.py create mode 100644 tests/generated/test_analytics.py create mode 100644 tests/generated/test_blocklist.py create mode 100644 tests/generated/test_investigator.py create mode 100644 tests/generated/test_lists.py create mode 100644 tests/generated/test_monitoring_events.py diff --git a/openapi/SOURCE.json b/openapi/SOURCE.json index a1faa6a..4a506cc 100644 --- a/openapi/SOURCE.json +++ b/openapi/SOURCE.json @@ -1,13 +1,13 @@ { "verification.json": { "url": "https://docs.didit.me/openapi-25.json", - "sha256": "d0fe3a250c30a0b07d17276463c04ecd26be50b07ac2896995a77563cfde201b", - "fetched_at": "2026-10-17T10:00:00Z" + "sha256": "9ada6192b24a5d85e983e57e87fccb6e762d74060b6f0b892928356dd432137c", + "fetched_at": "2026-10-23T06:48:12Z" }, "account.json": { "url": "https://docs.didit.me/openapi-auth.json", - "sha256": "171f5294c6d4a53d96f03b8820afc04a025f344ce2be137b5378399f6fc44ee4", - "fetched_at": "2026-10-17T10:00:00Z" + "sha256": "5fafb88b306ab60c7c105b0bf00fa2a6a49b5d2ca8115eb003b332f073844b39", + "fetched_at": "2026-10-23T06:48:12Z" }, "webhook-vectors.json": { "generator": "product-automations scripts/server-sdks/make_vectors.py (seeded 2026-10-17)", diff --git a/openapi/account.json b/openapi/account.json index 7e0d535..ce6a041 100644 --- a/openapi/account.json +++ b/openapi/account.json @@ -27,6 +27,12 @@ "summary": "Register Account", "description": "Step 1 of programmatic onboarding. Creates a pending account and emails a 6-char code (10 min TTL). Rate limit: 5/IP/hour.", "operationId": "programmatic_register", + "x-didit-sdk": { + "resource": "account", + "method": "register", + "auth": "none", + "server": true + }, "tags": [ "Programmatic Auth" ], @@ -145,7 +151,7 @@ } }, "429": { - "description": "Rate limited. More than 5 registration attempts from the same IP in the last hour.", + "description": "Rate limited. More than 5 registration attempts from the same IP in the last hour. Also returned, with a `Retry-After: 5` header, when the service is momentarily checking as many passwords or codes as it can at once; retry after the indicated number of seconds.", "content": { "application/json": { "schema": { @@ -156,6 +162,12 @@ "value": { "detail": "Too many registration attempts. Please try again later." } + }, + "PasswordChecksBusy": { + "summary": "Too many password checks in progress", + "value": { + "detail": "Too many sign-in attempts are being processed right now. Please try again in a few seconds." + } } } } @@ -178,6 +190,9 @@ "label": "JavaScript", "source": "const resp = await fetch(\n \"https://apx.didit.me/auth/v2/programmatic/register/\",\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n email: \"you@yourdomain.com\",\n password: \"MyStr0ng!Pass\",\n }),\n },\n);\nif (!resp.ok) throw new Error(`Register failed: ${resp.status}`);\nconst data = await resp.json();\nconsole.log(data);" } + ], + "security": [ + {} ] } }, @@ -186,6 +201,12 @@ "summary": "Verify Email and Issue Credentials", "description": "Step 2 of onboarding. Exchanges the 6-char code for a verified account, a default org+app, and a JWT pair. Persist the returned `api_key`.", "operationId": "programmatic_verify_email", + "x-didit-sdk": { + "resource": "account", + "method": "verify_email", + "auth": "none", + "server": true + }, "tags": [ "Programmatic Auth" ], @@ -230,7 +251,7 @@ "uuid": "b2c3d4e5-6789-01bc-defg-222222222222", "name": "developer", "client_id": "S9LIYGSoWNuGMLHsvEt9dQ", - "api_key": "05mHcOWL8GathLZlz8oIDawYj9qFAcoSHtz-75PAkuo" + "api_key": "sk_live_example_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } } @@ -288,6 +309,24 @@ } } } + }, + "429": { + "description": "The service is momentarily checking as many passwords or codes as it can at once. Sent with a `Retry-After: 5` header; retry after the indicated number of seconds.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RateLimitError" + }, + "examples": { + "PasswordChecksBusy": { + "summary": "Too many password checks in progress", + "value": { + "detail": "Too many sign-in attempts are being processed right now. Please try again in a few seconds." + } + } + } + } + } } }, "x-codeSamples": [ @@ -306,6 +345,9 @@ "label": "JavaScript", "source": "const resp = await fetch(\n \"https://apx.didit.me/auth/v2/programmatic/verify-email/\",\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n email: \"you@yourdomain.com\",\n code: \"A3K9F2\",\n }),\n },\n);\nif (!resp.ok) throw new Error(`Verify failed: ${resp.status}`);\nconst { application, access_token } = await resp.json();\n// application.api_key -> x-api-key for verification.didit.me/v3\n// access_token -> Authorization: Bearer ... for this auth API" } + ], + "security": [ + {} ] } }, @@ -314,6 +356,12 @@ "summary": "Programmatic Login", "description": "Authenticate any verified email/password Didit account and receive an RS256 JWT pair (default lifetime 86400s). Console-created email/password accounts can use this endpoint; OAuth-only console accounts must set a password first.", "operationId": "programmatic_login", + "x-didit-sdk": { + "resource": "account", + "method": "login", + "auth": "none", + "server": true + }, "tags": [ "Programmatic Auth" ], @@ -425,7 +473,7 @@ } }, "429": { - "description": "Account locked (too many recent failures on this email) or IP rate-limited (too many attempts from this address).", + "description": "Account locked (too many recent failures on this email) or IP rate-limited (too many attempts from this address). Also returned, with a `Retry-After: 5` header, when the service is momentarily checking as many passwords or codes as it can at once; retry after the indicated number of seconds.", "content": { "application/json": { "schema": { @@ -443,6 +491,12 @@ "value": { "detail": "Too many login attempts from this IP address. Please wait before trying again." } + }, + "PasswordChecksBusy": { + "summary": "Too many password checks in progress", + "value": { + "detail": "Too many sign-in attempts are being processed right now. Please try again in a few seconds." + } } } } @@ -465,6 +519,9 @@ "label": "JavaScript", "source": "const resp = await fetch(\n \"https://apx.didit.me/auth/v2/programmatic/login/\",\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n email: \"you@yourdomain.com\",\n password: \"MyStr0ng!Pass\",\n }),\n },\n);\nif (!resp.ok) throw new Error(`Login failed: ${resp.status}`);\nconst { access_token } = await resp.json();\n\n// Use against the Account Management endpoints on this host\nconst orgs = await fetch(\n \"https://apx.didit.me/auth/v2/organizations/me/\",\n { headers: { Authorization: `Bearer ${access_token}` } },\n).then((r) => r.json());" } + ], + "security": [ + {} ] } }, @@ -473,6 +530,12 @@ "summary": "List My Organizations", "description": "List organizations the authenticated user belongs to. Each entry's `uuid` is the `org_id` used on the application endpoints.", "operationId": "list_my_organizations", + "x-didit-sdk": { + "resource": "account.organizations", + "method": "list", + "auth": "account_token", + "server": true + }, "tags": [ "Account Management" ], @@ -483,43 +546,50 @@ ], "responses": { "200": { - "description": "List of organizations the authenticated user belongs to. Use the `uuid` of an entry as `{org_id}` on the application endpoints.", + "description": "Page of organizations the authenticated user belongs to (`limit`/`offset`, 50 per page by default). Use the `uuid` of an entry as `{org_id}` on the application endpoints.", "content": { "application/json": { "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Organization" - } + "$ref": "#/components/schemas/PaginatedOrganizationList" }, "examples": { "Success": { "summary": "A freshly-registered programmatic account", - "value": [ - { - "uuid": "a1b2c3d4-5678-90ab-cdef-111111111111", - "name": "developer", - "contact_email": "developer@gmail.com", - "created_at": "2025-06-01T10:00:00Z" - } - ] + "value": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "uuid": "a1b2c3d4-5678-90ab-cdef-111111111111", + "name": "developer", + "contact_email": "developer@gmail.com", + "created_at": "2025-06-01T10:00:00Z" + } + ] + } }, "Multi": { "summary": "A reseller account that manages several customer organizations", - "value": [ - { - "uuid": "a1b2c3d4-5678-90ab-cdef-111111111111", - "name": "Acme HQ", - "contact_email": "ops@acme.example", - "created_at": "2025-06-01T10:00:00Z" - }, - { - "uuid": "f9e8d7c6-5432-10ab-cdef-444444444444", - "name": "Acme Customer A", - "contact_email": "ops@acme.example", - "created_at": "2025-06-12T08:11:00Z" - } - ] + "value": { + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "uuid": "a1b2c3d4-5678-90ab-cdef-111111111111", + "name": "Acme HQ", + "contact_email": "ops@acme.example", + "created_at": "2025-06-01T10:00:00Z" + }, + { + "uuid": "f9e8d7c6-5432-10ab-cdef-444444444444", + "name": "Acme Customer A", + "contact_email": "ops@acme.example", + "created_at": "2025-06-12T08:11:00Z" + } + ] + } } } } @@ -569,12 +639,36 @@ { "lang": "python", "label": "Python", - "source": "import requests\n\nresp = requests.get(\n \"https://apx.didit.me/auth/v2/organizations/me/\",\n headers={\"Authorization\": f\"Bearer {access_token}\"},\n timeout=10,\n)\nresp.raise_for_status()\norgs = resp.json()\nfirst_org_id = orgs[0][\"uuid\"]" + "source": "import requests\n\nresp = requests.get(\n \"https://apx.didit.me/auth/v2/organizations/me/\",\n headers={\"Authorization\": f\"Bearer {access_token}\"},\n timeout=10,\n)\nresp.raise_for_status()\norgs = resp.json()[\"results\"] # paginated: count, next, previous, results\nfirst_org_id = orgs[0][\"uuid\"]" }, { "lang": "javascript", "label": "JavaScript", - "source": "const resp = await fetch(\n \"https://apx.didit.me/auth/v2/organizations/me/\",\n { headers: { Authorization: `Bearer ${accessToken}` } },\n);\nif (!resp.ok) throw new Error(`List orgs failed: ${resp.status}`);\nconst orgs = await resp.json();\nconst firstOrgId = orgs[0].uuid;" + "source": "const resp = await fetch(\n \"https://apx.didit.me/auth/v2/organizations/me/\",\n { headers: { Authorization: `Bearer ${accessToken}` } },\n);\nif (!resp.ok) throw new Error(`List orgs failed: ${resp.status}`);\nconst { results: orgs } = await resp.json(); // paginated: count, next, previous, results\nconst firstOrgId = orgs[0].uuid;" + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "default": 50 + }, + "description": "Page size. Defaults to 50." + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "description": "Zero-based offset of the first result." } ] } @@ -584,6 +678,12 @@ "summary": "Create Application", "description": "Create an application inside an organization. The response includes `api_key`; persist it now (recoverable via GET). Requires owner/admin JWT.", "operationId": "create_organization_application", + "x-didit-sdk": { + "resource": "account.applications", + "method": "create", + "auth": "account_token", + "server": true + }, "tags": [ "Account Management" ], @@ -654,7 +754,7 @@ "uuid": "b2c3d4e5-6789-01bc-defg-222222222222", "name": "Acme Customer App", "client_id": "S9LIYGSoWNuGMLHsvEt9dQ", - "api_key": "05mHcOWL8GathLZlz8oIDawYj9qFAcoSHtz-75PAkuo", + "api_key": "sk_live_example_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "website_url": "https://acme.example", "redirect_uris": [ "https://acme.example/callback" @@ -662,7 +762,31 @@ "terms_url": "https://acme.example/terms", "privacy_url": "https://acme.example/privacy", "description": null, - "created_at": "2025-06-01T10:00:00Z" + "created_at": "2025-06-01T10:00:00Z", + "products": [ + { + "product": "identity", + "status": "enabled", + "modules": { + "user_verification": true, + "business_verification": true, + "case_management": true + } + }, + { + "product": "transactions", + "status": "disabled", + "modules": { + "travel_rule": true, + "case_management": true + } + }, + { + "product": "authentication", + "status": "disabled", + "modules": {} + } + ] } } } @@ -689,7 +813,7 @@ } }, "401": { - "description": "Access token missing, malformed, or expired.", + "description": "Access token malformed, invalid, or expired. Get a new one with `POST /programmatic/login/`.", "content": { "application/json": { "schema": { @@ -700,20 +824,38 @@ "value": { "detail": "Invalid access token" } + }, + "ExpiredToken": { + "value": { + "detail": "Expired access token" + } } } } } }, "403": { - "description": "The authenticated user is a member of the organization but is not an owner or admin. Only owners and admins can create applications.", + "description": "You cannot create applications in this organization. A member of the organization who is not an owner or admin gets `Only organization owners and admins can manage applications.` An owner or admin whose access to the organization is suspended gets `Your access to this organization is suspended.` A request without an `Authorization` header gets `You do not have permission to perform this action.`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthError" }, "examples": { - "Forbidden": { + "NotOwnerOrAdmin": { + "summary": "Member who is not an owner or admin", + "value": { + "detail": "Only organization owners and admins can manage applications." + } + }, + "Suspended": { + "summary": "Membership suspended", + "value": { + "detail": "Your access to this organization is suspended." + } + }, + "MissingToken": { + "summary": "No Authorization header", "value": { "detail": "You do not have permission to perform this action." } @@ -723,16 +865,17 @@ } }, "404": { - "description": "Organization not found, or the authenticated user is not a member of it.", + "description": "The organization does not exist or you are not a member of it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthError" }, "examples": { - "NotFound": { + "OrganizationNotFound": { + "summary": "Organization not found, or you are not a member", "value": { - "detail": "Not found." + "detail": "Organization not found." } } } @@ -761,10 +904,16 @@ "get": { "summary": "List the organization's applications", "operationId": "get_org_applications_list", + "x-didit-sdk": { + "resource": "account.applications", + "method": "list", + "auth": "account_token", + "server": true + }, "tags": [ "Account Management" ], - "description": "List every application in the organization. Use this to discover `app_id` values when recovering credentials from a new machine — follow with `GET /organizations/me/{org_id}/applications/{app_id}/` for the API key. Requires an owner or admin Bearer token.", + "description": "List the organization's applications, 50 per page by default. Use this to discover `app_id` values when recovering credentials from a new machine, then call `GET /organizations/me/{org_id}/applications/{app_id}/` for the API key. Requires an owner or admin Bearer token.", "parameters": [ { "name": "org_id", @@ -776,17 +925,218 @@ "format": "uuid", "example": "a1b2c3d4-5678-90ab-cdef-111111111111" } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "default": 50 + }, + "description": "Page size. Defaults to 50." + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "description": "Zero-based offset of the first result." + }, + { + "name": "limit", + "in": "query", + "required": false, + "description": "Number of applications per page. Defaults to 50.", + "schema": { + "type": "integer", + "minimum": 1, + "default": 50, + "example": 50 + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "description": "Number of applications to skip before the page starts. Defaults to 0.", + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "example": 0 + } } ], "responses": { "200": { - "description": "Array of applications (same item shape as the application detail response)." + "description": "Page of the organization's applications (`limit`/`offset`, 50 per page by default), each with the same shape as the application detail response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedApplicationList", + "type": "object", + "properties": { + "count": { + "type": "integer", + "description": "Total number of applications in the organization.", + "example": 1 + }, + "next": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL of the next page, or `null` on the last page.", + "example": null + }, + "previous": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL of the previous page, or `null` on the first page.", + "example": null + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Application" + } + } + }, + "required": [ + "count", + "next", + "previous", + "results" + ] + }, + "examples": { + "Success": { + "summary": "An organization with one application", + "value": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "uuid": "b2c3d4e5-6789-01bc-defg-222222222222", + "name": "My App", + "client_id": "S9LIYGSoWNuGMLHsvEt9dQ", + "api_key": "sk_live_example_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "website_url": "https://example.com", + "redirect_uris": [ + "https://example.com/callback" + ], + "terms_url": "https://example.com/terms", + "privacy_url": "https://example.com/privacy", + "description": "Production application", + "created_at": "2025-06-01T10:00:00Z", + "products": [ + { + "product": "identity", + "status": "enabled", + "modules": { + "user_verification": true, + "business_verification": true, + "case_management": true + } + }, + { + "product": "transactions", + "status": "disabled", + "modules": { + "travel_rule": true, + "case_management": true + } + }, + { + "product": "authentication", + "status": "disabled", + "modules": {} + } + ] + } + ] + } + } + } + } + } }, "401": { - "description": "Missing/invalid Bearer token." + "description": "Access token malformed, invalid, or expired. Get a new one with `POST /programmatic/login/`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthError" + }, + "examples": { + "InvalidToken": { + "value": { + "detail": "Invalid access token" + } + }, + "ExpiredToken": { + "value": { + "detail": "Expired access token" + } + } + } + } + } }, "403": { - "description": "Token holder is not an owner or admin of this organization." + "description": "You cannot list this organization's applications. A member of the organization who is not an owner or admin gets `Only organization owners and admins can manage applications.` An owner or admin whose access to the organization is suspended gets `Your access to this organization is suspended.` A request without an `Authorization` header gets `You do not have permission to perform this action.`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthError" + }, + "examples": { + "NotOwnerOrAdmin": { + "summary": "Member who is not an owner or admin", + "value": { + "detail": "Only organization owners and admins can manage applications." + } + }, + "Suspended": { + "summary": "Membership suspended", + "value": { + "detail": "Your access to this organization is suspended." + } + }, + "MissingToken": { + "summary": "No Authorization header", + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "The organization does not exist or you are not a member of it.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthError" + }, + "examples": { + "OrganizationNotFound": { + "summary": "Organization not found, or you are not a member", + "value": { + "detail": "Organization not found." + } + } + } + } + } } }, "security": [ @@ -801,6 +1151,12 @@ "summary": "Get Application Credentials", "description": "Read one application's full record including `client_id` and `api_key`. Use this to recover a lost `api_key`.", "operationId": "get_application_credentials", + "x-didit-sdk": { + "resource": "account.applications", + "method": "retrieve", + "auth": "account_token", + "server": true + }, "tags": [ "Account Management" ], @@ -847,7 +1203,7 @@ "uuid": "b2c3d4e5-6789-01bc-defg-222222222222", "name": "My App", "client_id": "S9LIYGSoWNuGMLHsvEt9dQ", - "api_key": "05mHcOWL8GathLZlz8oIDawYj9qFAcoSHtz-75PAkuo", + "api_key": "sk_live_example_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "website_url": "https://example.com", "redirect_uris": [ "https://example.com/callback" @@ -855,7 +1211,31 @@ "terms_url": "https://example.com/terms", "privacy_url": "https://example.com/privacy", "description": "Production application", - "created_at": "2025-06-01T10:00:00Z" + "created_at": "2025-06-01T10:00:00Z", + "products": [ + { + "product": "identity", + "status": "enabled", + "modules": { + "user_verification": true, + "business_verification": true, + "case_management": true + } + }, + { + "product": "transactions", + "status": "disabled", + "modules": { + "travel_rule": true, + "case_management": true + } + }, + { + "product": "authentication", + "status": "disabled", + "modules": {} + } + ] } } } @@ -863,7 +1243,7 @@ } }, "401": { - "description": "Access token missing, malformed, or expired.", + "description": "Access token malformed, invalid, or expired. Get a new one with `POST /programmatic/login/`.", "content": { "application/json": { "schema": { @@ -874,20 +1254,38 @@ "value": { "detail": "Invalid access token" } + }, + "ExpiredToken": { + "value": { + "detail": "Expired access token" + } } } } } }, "403": { - "description": "Bearer token is valid but the user does not have permission to read credentials for this application.", + "description": "You cannot read this application. A member of the organization who is not an owner or admin gets `Only organization owners and admins can manage applications.` An owner or admin whose access to the organization is suspended gets `Your access to this organization is suspended.` A request without an `Authorization` header gets `You do not have permission to perform this action.`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthError" }, "examples": { - "Forbidden": { + "NotOwnerOrAdmin": { + "summary": "Member who is not an owner or admin", + "value": { + "detail": "Only organization owners and admins can manage applications." + } + }, + "Suspended": { + "summary": "Membership suspended", + "value": { + "detail": "Your access to this organization is suspended." + } + }, + "MissingToken": { + "summary": "No Authorization header", "value": { "detail": "You do not have permission to perform this action." } @@ -897,16 +1295,23 @@ } }, "404": { - "description": "Organization not found, application not found in that organization, or the authenticated user is not a member of the organization.", + "description": "The organization does not exist or you are not a member of it, or the application does not exist in that organization.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthError" }, "examples": { - "NotFound": { + "OrganizationNotFound": { + "summary": "Organization not found, or you are not a member", + "value": { + "detail": "Organization not found." + } + }, + "ApplicationNotFound": { + "summary": "No such application in this organization", "value": { - "detail": "Not found." + "detail": "Application not found." } } } @@ -934,8 +1339,14 @@ }, "patch": { "summary": "Update Application", - "description": "Update application metadata (name, URLs, redirect URIs). `api_key` and `client_id` are never rotated. Requires owner/admin JWT.", + "description": "Update application metadata (name, URLs, redirect URIs). `api_key` and `client_id` are never rotated. `products` is read-only: a `products` key in the body is ignored. Requires owner/admin JWT.", "operationId": "update_organization_application", + "x-didit-sdk": { + "resource": "account.applications", + "method": "update", + "auth": "account_token", + "server": true + }, "tags": [ "Account Management" ], @@ -1006,7 +1417,7 @@ }, "responses": { "200": { - "description": "Application updated. The response is the full application record after the change; `uuid`, `client_id`, and `api_key` are unchanged.", + "description": "Application updated. The response is the full application record after the change, including `products`; `uuid`, `client_id`, and `api_key` are unchanged.", "content": { "application/json": { "schema": { @@ -1018,7 +1429,7 @@ "uuid": "b2c3d4e5-6789-01bc-defg-222222222222", "name": "Acme Production App", "client_id": "S9LIYGSoWNuGMLHsvEt9dQ", - "api_key": "05mHcOWL8GathLZlz8oIDawYj9qFAcoSHtz-75PAkuo", + "api_key": "sk_live_example_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "website_url": "https://app.acme.example", "redirect_uris": [ "https://acme.example/callback" @@ -1026,7 +1437,31 @@ "terms_url": "https://acme.example/terms", "privacy_url": "https://acme.example/privacy", "description": null, - "created_at": "2025-06-01T10:00:00Z" + "created_at": "2025-06-01T10:00:00Z", + "products": [ + { + "product": "identity", + "status": "enabled", + "modules": { + "user_verification": true, + "business_verification": true, + "case_management": true + } + }, + { + "product": "transactions", + "status": "disabled", + "modules": { + "travel_rule": true, + "case_management": true + } + }, + { + "product": "authentication", + "status": "disabled", + "modules": {} + } + ] } } } @@ -1053,7 +1488,7 @@ } }, "401": { - "description": "Access token missing, malformed, or expired.", + "description": "Access token malformed, invalid, or expired. Get a new one with `POST /programmatic/login/`.", "content": { "application/json": { "schema": { @@ -1064,20 +1499,38 @@ "value": { "detail": "Invalid access token" } + }, + "ExpiredToken": { + "value": { + "detail": "Expired access token" + } } } } } }, "403": { - "description": "The user is a member of the organization but is not an owner or admin. Only owners and admins can update applications.", + "description": "You cannot update this application. A member of the organization who is not an owner or admin gets `Only organization owners and admins can manage applications.` An owner or admin whose access to the organization is suspended gets `Your access to this organization is suspended.` A request without an `Authorization` header gets `You do not have permission to perform this action.`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthError" }, "examples": { - "Forbidden": { + "NotOwnerOrAdmin": { + "summary": "Member who is not an owner or admin", + "value": { + "detail": "Only organization owners and admins can manage applications." + } + }, + "Suspended": { + "summary": "Membership suspended", + "value": { + "detail": "Your access to this organization is suspended." + } + }, + "MissingToken": { + "summary": "No Authorization header", "value": { "detail": "You do not have permission to perform this action." } @@ -1087,16 +1540,23 @@ } }, "404": { - "description": "Organization not found, application not found in that organization, or user is not a member of the organization.", + "description": "The organization does not exist or you are not a member of it, or the application does not exist in that organization.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AuthError" }, "examples": { - "NotFound": { + "OrganizationNotFound": { + "summary": "Organization not found, or you are not a member", + "value": { + "detail": "Organization not found." + } + }, + "ApplicationNotFound": { + "summary": "No such application in this organization", "value": { - "detail": "Not found." + "detail": "Application not found." } } } @@ -1127,6 +1587,12 @@ "post": { "summary": "Resend the email verification code", "operationId": "post_programmatic_resend_otp", + "x-didit-sdk": { + "resource": "account", + "method": "resend_otp", + "auth": "none", + "server": true + }, "tags": [ "Programmatic Auth" ], @@ -1167,8 +1633,27 @@ "description": "A new code was emailed.", "content": { "application/json": { + "schema": { + "type": "object", + "required": [ + "message", + "email" + ], + "properties": { + "message": { + "type": "string", + "description": "Always `A new verification code has been sent to your email.`" + }, + "email": { + "type": "string", + "format": "email", + "description": "The address the code was sent to, normalized: lowercased, with any `+tag` removed from the local part (`Dev+test@Example.com` becomes `dev@example.com`)." + } + } + }, "example": { - "detail": "A new verification code has been sent to your email." + "message": "A new verification code has been sent to your email.", + "email": "you@yourdomain.com" } } } @@ -1193,17 +1678,30 @@ } }, "429": { - "description": "Cooldown active (60s between resends) or IP budget exhausted (5/hour, shared with registration).", + "description": "Cooldown active (60s between resends) or IP budget exhausted (5/hour, shared with registration). Also returned, with a `Retry-After: 5` header, when the service is momentarily checking as many passwords or codes as it can at once; retry after the indicated number of seconds.", "content": { "application/json": { - "example": { - "detail": "Please wait before requesting a new code." + "examples": { + "CooldownActive": { + "summary": "Cooldown or IP budget", + "value": { + "detail": "Please wait before requesting a new code." + } + }, + "PasswordChecksBusy": { + "summary": "Too many password checks in progress", + "value": { + "detail": "Too many sign-in attempts are being processed right now. Please try again in a few seconds." + } + } } } } } }, - "security": [] + "security": [ + {} + ] } } }, @@ -1400,8 +1898,8 @@ }, "api_key": { "type": "string", - "description": "Use this as `x-api-key` on every verification API call.", - "example": "05mHcOWL8GathLZlz8oIDawYj9qFAcoSHtz-75PAkuo" + "description": "Use this as `x-api-key` on every verification API call. New keys start with `sk_live_` (live application) or `sk_test_` (sandbox application), followed by 43 URL-safe characters (letters, digits, `-` and `_`). Keys created before this format have no prefix and keep working unchanged.", + "example": "sk_live_example_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } } }, @@ -1427,8 +1925,8 @@ }, "api_key": { "type": "string", - "description": "Long-lived secret (also called `client_secret`). Use as the `x-api-key` header for every call to `https://verification.didit.me/v3/...` (sessions, workflows, AML, etc.). Treat as a credential; never expose client-side.", - "example": "05mHcOWL8GathLZlz8oIDawYj9qFAcoSHtz-75PAkuo" + "description": "Long-lived secret (also called `client_secret`). Use as the `x-api-key` header for every call to `https://verification.didit.me/v3/...` (sessions, workflows, AML, etc.). Treat as a credential; never expose client-side. New keys start with `sk_live_` (live application) or `sk_test_` (sandbox application), followed by 43 URL-safe characters (letters, digits, `-` and `_`). Keys created before this format have no prefix and keep working unchanged.", + "example": "sk_live_example_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, "website_url": { "type": "string", @@ -1467,6 +1965,38 @@ "type": "string", "format": "date-time", "example": "2025-06-01T10:00:00Z" + }, + "products": { + "type": "array", + "readOnly": true, + "description": "The application's products. Always all three, in this order: `identity`, `transactions`, `authentication`. A new application has `identity` enabled with every module on, and `transactions` and `authentication` disabled. Read-only: a `products` key in a create or update body is ignored.", + "items": { + "$ref": "#/components/schemas/ApplicationProduct" + }, + "example": [ + { + "product": "identity", + "status": "enabled", + "modules": { + "user_verification": true, + "business_verification": true, + "case_management": true + } + }, + { + "product": "transactions", + "status": "disabled", + "modules": { + "travel_rule": true, + "case_management": true + } + }, + { + "product": "authentication", + "status": "disabled", + "modules": {} + } + ] } } }, @@ -1512,7 +2042,7 @@ }, "ApplicationUpdateRequest": { "type": "object", - "description": "PATCH body for application metadata. Send only the fields you want to change; omitted fields are preserved. `uuid`, `client_id`, and `api_key` cannot be updated.", + "description": "PATCH body for application metadata. Send only the fields you want to change; omitted fields are preserved. `uuid`, `client_id`, and `api_key` cannot be updated. `products` is read-only: a `products` key in the body is ignored.", "properties": { "name": { "type": "string", @@ -1606,6 +2136,115 @@ "example": "Too many login attempts from this IP address. Please wait before trying again." } } + }, + "PaginatedOrganizationList": { + "type": "object", + "required": [ + "count", + "next", + "previous", + "results" + ], + "properties": { + "count": { + "type": "integer", + "description": "Total number of organizations." + }, + "next": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL of the next page, or `null` on the last page." + }, + "previous": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL of the previous page, or `null` on the first page." + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Organization" + } + } + } + }, + "PaginatedApplicationList": { + "type": "object", + "required": [ + "count", + "next", + "previous", + "results" + ], + "properties": { + "count": { + "type": "integer", + "description": "Total number of applications." + }, + "next": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL of the next page, or `null` on the last page." + }, + "previous": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL of the previous page, or `null` on the first page." + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Application" + } + } + } + }, + "ApplicationProduct": { + "type": "object", + "description": "One product of an application and its state. Read-only: the application endpoints return it and ignore it in a request body.", + "properties": { + "product": { + "type": "string", + "enum": [ + "identity", + "transactions", + "authentication" + ], + "description": "`identity` (Identity verification), `transactions` (Transaction monitoring) or `authentication` (Authentication).", + "example": "identity" + }, + "status": { + "type": "string", + "enum": [ + "enabled", + "early_access", + "disabled" + ], + "description": "Whether the product is on for this application. `early_access` applies only to `authentication`, which Didit turns on for you.", + "example": "enabled" + }, + "modules": { + "type": "object", + "additionalProperties": { + "type": "boolean" + }, + "description": "Module switches of the product, each `true` or `false`. `identity` has `user_verification`, `business_verification` and `case_management`. `transactions` has `travel_rule` and `case_management`. `authentication` has none (`{}`). `case_management` is one setting shown under both `identity` and `transactions`, so it has the same value on both.", + "example": { + "user_verification": true, + "business_verification": true, + "case_management": true + } + } + }, + "required": [ + "product", + "status", + "modules" + ] } } } diff --git a/openapi/overlay.json b/openapi/overlay.json index 327aa4d..d7edbdd 100644 --- a/openapi/overlay.json +++ b/openapi/overlay.json @@ -1,1708 +1,5 @@ { "description": "SDK naming (x-didit-sdk) for operations whose upstream OpenAPI definition does not carry it yet. Keys: \" \" for verification.json, \"account \" for account.json. Upstream x-didit-sdk wins; delete an entry once upstream carries the same value. See docs/DESIGN.md section 4.", "version": 1, - "operations": { - "DELETE /v3/biometric-templates/{template_uuid}/": { - "auth": "api_key", - "method": "delete", - "resource": "biometric_templates", - "server": true - }, - "DELETE /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/": { - "auth": "api_key", - "method": "delete", - "resource": "cases.blueprints", - "scope": "application", - "server": true - }, - "DELETE /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/": { - "auth": "api_key", - "method": "discard_draft", - "resource": "cases.blueprints", - "scope": "application", - "server": true - }, - "DELETE /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/": { - "auth": "api_key", - "method": "discard_draft", - "resource": "cases.routing_rules", - "scope": "application", - "server": true - }, - "DELETE /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/": { - "auth": "api_key", - "method": "delete", - "resource": "cases", - "scope": "application", - "server": true - }, - "DELETE /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/{link_uuid}/": { - "auth": "api_key", - "method": "delete", - "resource": "cases.links", - "scope": "application", - "server": true - }, - "DELETE /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/{note_uuid}/": { - "auth": "api_key", - "method": "delete", - "resource": "cases.notes", - "scope": "application", - "server": true - }, - "DELETE /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/{note_uuid}/": { - "auth": "api_key", - "method": "delete", - "resource": "networks.notes", - "scope": "application", - "server": true - }, - "DELETE /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/": { - "auth": "api_key", - "method": "delete", - "resource": "regulatory_reports", - "scope": "application", - "server": true - }, - "DELETE /v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/": { - "auth": "api_key", - "method": "delete", - "resource": "report_templates", - "scope": "application", - "server": true - }, - "DELETE /v3/questionnaires/{questionnaire_uuid}/": { - "auth": "api_key", - "method": "delete", - "resource": "questionnaires", - "server": true - }, - "DELETE /v3/session/{sessionId}/delete/": { - "auth": "api_key", - "method": "delete", - "resource": "sessions", - "server": true - }, - "DELETE /v3/transactions/rules/install/": { - "auth": "api_key", - "method": "uninstall_library", - "resource": "transactions.rules", - "server": true - }, - "DELETE /v3/transactions/rules/{rule_uuid}/": { - "auth": "api_key", - "method": "delete", - "resource": "transactions.rules", - "server": true - }, - "DELETE /v3/travel-rule/wallet-addresses/{entry_uuid}/": { - "auth": "api_key", - "method": "delete", - "resource": "travel_rule.wallet_addresses", - "server": true - }, - "DELETE /v3/webhook/destinations/{destination_uuid}/": { - "auth": "api_key", - "method": "delete", - "resource": "webhooks.destinations", - "server": true - }, - "DELETE /v3/workflows/{settings_uuid}/": { - "auth": "api_key", - "method": "delete", - "resource": "workflows", - "server": true - }, - "GET /system/healthcheck": { - "auth": "public", - "method": "healthcheck", - "resource": "system", - "server": false - }, - "GET /v1/transactions/{transaction_id}/": { - "auth": "device_token", - "method": "retrieve_from_device", - "resource": "transactions", - "server": false - }, - "GET /v3/aml-monitoring/jobs/{job_id}/": { - "auth": "api_key", - "method": "retrieve_job", - "resource": "aml_monitoring", - "server": true - }, - "GET /v3/billing/balance/": { - "auth": "api_key", - "method": "retrieve_balance", - "resource": "billing", - "server": true - }, - "GET /v3/biometric-templates/": { - "auth": "api_key", - "method": "list", - "resource": "biometric_templates", - "server": true - }, - "GET /v3/biometric-templates/count/": { - "auth": "api_key", - "method": "count", - "resource": "biometric_templates", - "server": true - }, - "GET /v3/biometric-templates/{template_uuid}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "biometric_templates", - "server": true - }, - "GET /v3/businesses/": { - "auth": "api_key", - "method": "list", - "resource": "businesses", - "server": true - }, - "GET /v3/businesses/{vendor_data}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "businesses", - "server": true - }, - "GET /v3/case-requests/{request_uuid}/": { - "auth": "device_token", - "method": "retrieve", - "resource": "case_requests", - "server": false - }, - "GET /v3/customization/": { - "auth": "api_key", - "method": "retrieve", - "resource": "customization", - "server": true - }, - "GET /v3/fraud/checks/{check_id}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "fraud_checks", - "server": true - }, - "GET /v3/id-verification/wallets/": { - "auth": "api_key", - "method": "list", - "resource": "id_wallets", - "server": true - }, - "GET /v3/id-verification/wallets/verifications/{request_id}/": { - "auth": "api_key", - "method": "retrieve_verification", - "resource": "id_wallets", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/business-sessions/{business_session_id}/networks/": { - "auth": "api_key", - "method": "list_networks", - "resource": "business_sessions", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/{job_id}/": { - "auth": "api_key", - "method": "retrieve_session_ongoing_monitoring_job", - "resource": "businesses", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/businesses/{vendor_business_uuid}/networks/": { - "auth": "api_key", - "method": "list_networks", - "resource": "businesses", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/": { - "auth": "api_key", - "method": "list", - "resource": "cases", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/": { - "auth": "api_key", - "method": "list", - "resource": "cases.blueprints", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/": { - "auth": "api_key", - "method": "list_presets", - "resource": "cases.blueprints", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "cases.blueprints", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/": { - "auth": "api_key", - "method": "retrieve_draft", - "resource": "cases.blueprints", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/history/": { - "auth": "api_key", - "method": "list_history", - "resource": "cases.blueprints", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/reviewers/": { - "auth": "api_key", - "method": "list_reviewers", - "resource": "cases", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/": { - "auth": "api_key", - "method": "list", - "resource": "cases.routing_rules", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "cases.routing_rules", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/": { - "auth": "api_key", - "method": "retrieve_draft", - "resource": "cases.routing_rules", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/history/": { - "auth": "api_key", - "method": "list_history", - "resource": "cases.routing_rules", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/statistics/": { - "auth": "api_key", - "method": "retrieve_statistics", - "resource": "cases", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/teams/": { - "auth": "api_key", - "method": "list", - "resource": "cases.teams", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/teams/{record_uuid}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "cases.teams", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "cases", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/action-receipts/": { - "auth": "api_key", - "method": "list_action_receipts", - "resource": "cases", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/aml/": { - "auth": "api_key", - "method": "list_aml_hits", - "resource": "cases", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/checklist/": { - "auth": "api_key", - "method": "list", - "resource": "cases.checklist", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/events/": { - "auth": "api_key", - "method": "list_events", - "resource": "cases", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/": { - "auth": "api_key", - "method": "list", - "resource": "cases.links", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/": { - "auth": "api_key", - "method": "list", - "resource": "cases.notes", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/presign/": { - "auth": "api_key", - "method": "retrieve_attachment_url", - "resource": "cases.notes", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/print/": { - "auth": "api_key", - "method": "print", - "resource": "cases", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/regulatory-reports/": { - "auth": "api_key", - "method": "list", - "resource": "cases.regulatory_reports", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/": { - "auth": "api_key", - "method": "list", - "resource": "cases.requests", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/options/": { - "auth": "api_key", - "method": "retrieve_options", - "resource": "cases.requests", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/attachment/": { - "auth": "api_key", - "method": "retrieve_attachment", - "resource": "cases.requests", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/networks/": { - "auth": "api_key", - "method": "list", - "resource": "networks", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "networks", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/graph/": { - "auth": "api_key", - "method": "retrieve_graph", - "resource": "networks", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/map/": { - "auth": "api_key", - "method": "retrieve_map", - "resource": "networks", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/members/": { - "auth": "api_key", - "method": "list_members", - "resource": "networks", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/": { - "auth": "api_key", - "method": "list", - "resource": "networks.notes", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/report/": { - "auth": "api_key", - "method": "download_report", - "resource": "networks", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/signals/": { - "auth": "api_key", - "method": "list_signals", - "resource": "networks", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/timeline/": { - "auth": "api_key", - "method": "retrieve_timeline", - "resource": "networks", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/": { - "auth": "api_key", - "method": "list", - "resource": "regulatory_reports", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "regulatory_reports", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/download-url/": { - "auth": "api_key", - "method": "retrieve_download_url", - "resource": "regulatory_reports", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/report-templates/": { - "auth": "api_key", - "method": "list", - "resource": "report_templates", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "report_templates", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/sessions/{session_id}/networks/": { - "auth": "api_key", - "method": "list_networks", - "resource": "sessions", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/networks/": { - "auth": "api_key", - "method": "list_networks", - "resource": "transactions", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/parties/{transaction_party_uuid}/networks/": { - "auth": "api_key", - "method": "list_party_networks", - "resource": "transactions", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/users/{vendor_user_uuid}/networks/": { - "auth": "api_key", - "method": "list_networks", - "resource": "users", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/{job_id}/": { - "auth": "api_key", - "method": "retrieve_ongoing_monitoring_job", - "resource": "businesses", - "scope": "application", - "server": true - }, - "GET /v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/{job_id}/": { - "auth": "api_key", - "method": "retrieve_ongoing_monitoring_job", - "resource": "users", - "scope": "application", - "server": true - }, - "GET /v3/questionnaires/": { - "auth": "api_key", - "method": "list", - "resource": "questionnaires", - "server": true - }, - "GET /v3/questionnaires/{questionnaire_uuid}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "questionnaires", - "server": true - }, - "GET /v3/session/imports/template/": { - "auth": "api_key", - "method": "retrieve_template", - "resource": "sessions.imports", - "server": true - }, - "GET /v3/session/imports/{importId}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "sessions.imports", - "server": true - }, - "GET /v3/session/imports/{importId}/errors/": { - "auth": "api_key", - "method": "list_errors", - "resource": "sessions.imports", - "server": true - }, - "GET /v3/session/{sessionId}/decision/": { - "auth": "api_key", - "method": "retrieve_decision", - "resource": "sessions", - "server": true - }, - "GET /v3/session/{sessionId}/generate-pdf/": { - "auth": "api_key", - "method": "generate_pdf", - "resource": "sessions", - "server": true - }, - "GET /v3/sessions/": { - "auth": "api_key", - "method": "list", - "resource": "sessions", - "server": true - }, - "GET /v3/sessions/delete/{job_id}/": { - "auth": "api_key", - "method": "retrieve_deletion_job", - "resource": "sessions", - "server": true - }, - "GET /v3/sessions/{session_id}/reviews/": { - "auth": "api_key", - "method": "list", - "resource": "sessions.reviews", - "server": true - }, - "GET /v3/transactions/": { - "auth": "api_key", - "method": "list", - "resource": "transactions", - "server": true - }, - "GET /v3/transactions/rules/": { - "auth": "api_key", - "method": "list", - "resource": "transactions.rules", - "server": true - }, - "GET /v3/transactions/rules/library/": { - "auth": "api_key", - "method": "list_library", - "resource": "transactions.rules", - "server": true - }, - "GET /v3/transactions/rules/{rule_uuid}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "transactions.rules", - "server": true - }, - "GET /v3/transactions/{transaction_id}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "transactions", - "server": true - }, - "GET /v3/travel-rule/pickup/{token}/": { - "auth": "public", - "method": "retrieve_pickup", - "resource": "travel_rule", - "server": false - }, - "GET /v3/travel-rule/settings/": { - "auth": "api_key", - "method": "retrieve_settings", - "resource": "travel_rule", - "server": true - }, - "GET /v3/travel-rule/vasps/": { - "auth": "api_key", - "method": "list_vasps", - "resource": "travel_rule", - "server": true - }, - "GET /v3/travel-rule/wallet-addresses/": { - "auth": "api_key", - "method": "list", - "resource": "travel_rule.wallet_addresses", - "server": true - }, - "GET /v3/travel-rule/widget/{token}/": { - "auth": "public", - "method": "retrieve_widget_context", - "resource": "travel_rule", - "server": false - }, - "GET /v3/users/": { - "auth": "api_key", - "method": "list", - "resource": "users", - "server": true - }, - "GET /v3/users/{vendor_data}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "users", - "server": true - }, - "GET /v3/users/{vendor_data}/generate-pdf/": { - "auth": "api_key", - "method": "generate_pdf", - "resource": "users", - "server": true - }, - "GET /v3/webhook/destinations/": { - "auth": "api_key", - "method": "list", - "resource": "webhooks.destinations", - "server": true - }, - "GET /v3/webhook/destinations/{destination_uuid}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "webhooks.destinations", - "server": true - }, - "GET /v3/workflows/": { - "auth": "api_key", - "method": "list", - "resource": "workflows", - "server": true - }, - "GET /v3/workflows/{settings_uuid}/": { - "auth": "api_key", - "method": "retrieve", - "resource": "workflows", - "server": true - }, - "PATCH /v3/businesses/{vendor_data}/": { - "auth": "api_key", - "method": "update", - "resource": "businesses", - "server": true - }, - "PATCH /v3/businesses/{vendor_data}/update-status/": { - "auth": "api_key", - "method": "update_status", - "resource": "businesses", - "server": true - }, - "PATCH /v3/customization/": { - "auth": "api_key", - "method": "update", - "resource": "customization", - "server": true - }, - "PATCH /v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/": { - "auth": "api_key", - "method": "set_session_ongoing_monitoring", - "resource": "businesses", - "scope": "application", - "server": true - }, - "PATCH /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/": { - "auth": "api_key", - "method": "update", - "resource": "cases.blueprints", - "scope": "application", - "server": true - }, - "PATCH /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/": { - "auth": "api_key", - "method": "update", - "resource": "cases.routing_rules", - "scope": "application", - "server": true - }, - "PATCH /v3/organization/{organization_id}/application/{application_id}/cases/teams/{record_uuid}/": { - "auth": "api_key", - "method": "update", - "resource": "cases.teams", - "scope": "application", - "server": true - }, - "PATCH /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/": { - "auth": "api_key", - "method": "update", - "resource": "cases", - "scope": "application", - "server": true - }, - "PATCH /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/checklist/{item_uuid}/": { - "auth": "api_key", - "method": "toggle_item", - "resource": "cases.checklist", - "scope": "application", - "server": true - }, - "PATCH /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/{note_uuid}/": { - "auth": "api_key", - "method": "update", - "resource": "cases.notes", - "scope": "application", - "server": true - }, - "PATCH /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/": { - "auth": "api_key", - "method": "update", - "resource": "regulatory_reports", - "scope": "application", - "server": true - }, - "PATCH /v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/": { - "auth": "api_key", - "method": "update", - "resource": "report_templates", - "scope": "application", - "server": true - }, - "PATCH /v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/": { - "auth": "api_key", - "method": "set_ongoing_monitoring", - "resource": "businesses", - "scope": "application", - "server": true - }, - "PATCH /v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/": { - "auth": "api_key", - "method": "set_ongoing_monitoring", - "resource": "users", - "scope": "application", - "server": true - }, - "PATCH /v3/questionnaires/{questionnaire_uuid}/": { - "auth": "api_key", - "method": "update", - "resource": "questionnaires", - "server": true - }, - "PATCH /v3/session/{sessionId}/bulk-update-aml-hit-status/": { - "auth": "api_key", - "method": "bulk_update_aml_hit_status", - "resource": "sessions", - "server": true - }, - "PATCH /v3/session/{sessionId}/features/{nodeId}/update-status/": { - "auth": "api_key", - "method": "update_feature_status", - "resource": "sessions", - "server": true - }, - "PATCH /v3/session/{sessionId}/kyb/parties/{partyUuid}/": { - "auth": "api_key", - "method": "update_kyb_party", - "resource": "sessions", - "server": true - }, - "PATCH /v3/session/{sessionId}/kyb/{companyUuid}/update-data/": { - "auth": "api_key", - "method": "update_kyb_company_data", - "resource": "sessions", - "server": true - }, - "PATCH /v3/session/{sessionId}/update-aml-hit-status/": { - "auth": "api_key", - "method": "update_aml_hit_status", - "resource": "sessions", - "server": true - }, - "PATCH /v3/session/{sessionId}/update-data/": { - "auth": "api_key", - "method": "update_data", - "resource": "sessions", - "server": true - }, - "PATCH /v3/session/{sessionId}/update-poa-data/": { - "auth": "api_key", - "method": "update_poa_data", - "resource": "sessions", - "server": true - }, - "PATCH /v3/session/{sessionId}/update-status/": { - "auth": "api_key", - "method": "update_status", - "resource": "sessions", - "server": true - }, - "PATCH /v3/transactions/rules/{rule_uuid}/": { - "auth": "api_key", - "method": "update", - "resource": "transactions.rules", - "server": true - }, - "PATCH /v3/transactions/{transaction_id}/travel-rule/": { - "auth": "api_key", - "method": "update_transfer", - "resource": "transactions.travel_rule", - "server": true - }, - "PATCH /v3/travel-rule/wallet-addresses/{entry_uuid}/": { - "auth": "api_key", - "method": "update", - "resource": "travel_rule.wallet_addresses", - "server": true - }, - "PATCH /v3/users/{vendor_data}/": { - "auth": "api_key", - "method": "update", - "resource": "users", - "server": true - }, - "PATCH /v3/users/{vendor_data}/update-status/": { - "auth": "api_key", - "method": "update_status", - "resource": "users", - "server": true - }, - "PATCH /v3/webhook/destinations/{destination_uuid}/": { - "auth": "api_key", - "method": "update", - "resource": "webhooks.destinations", - "server": true - }, - "PATCH /v3/workflows/{settings_uuid}/": { - "auth": "api_key", - "method": "update", - "resource": "workflows", - "server": true - }, - "POST /v1/transactions/": { - "auth": "device_token", - "method": "submit_from_device", - "resource": "transactions", - "server": false - }, - "POST /v3/age-estimation/": { - "auth": "api_key", - "method": "estimate", - "resource": "age_estimation", - "server": true - }, - "POST /v3/aml/": { - "auth": "api_key", - "method": "screen", - "resource": "aml", - "server": true - }, - "POST /v3/billing/top-up/": { - "auth": "api_key", - "method": "top_up", - "resource": "billing", - "server": true - }, - "POST /v3/biometric-templates/delete/": { - "auth": "api_key", - "method": "batch_delete", - "resource": "biometric_templates", - "server": true - }, - "POST /v3/businesses/aml-monitoring/": { - "auth": "api_key", - "method": "set_aml_monitoring", - "resource": "businesses", - "server": true - }, - "POST /v3/businesses/create/": { - "auth": "api_key", - "method": "create", - "resource": "businesses", - "server": true - }, - "POST /v3/businesses/delete/": { - "auth": "api_key", - "method": "batch_delete", - "resource": "businesses", - "server": true - }, - "POST /v3/case-requests/{request_uuid}/": { - "auth": "device_token", - "method": "respond", - "resource": "case_requests", - "server": false - }, - "POST /v3/database-validation/": { - "auth": "api_key", - "method": "validate", - "resource": "database_validation", - "server": true - }, - "POST /v3/document-ai/": { - "auth": "api_key", - "method": "analyze", - "resource": "document_ai", - "server": true - }, - "POST /v3/email/check/": { - "auth": "api_key", - "method": "check_code", - "resource": "email", - "server": true - }, - "POST /v3/email/risk/": { - "auth": "api_key", - "method": "assess_risk", - "resource": "email", - "server": true - }, - "POST /v3/email/send/": { - "auth": "api_key", - "method": "send_code", - "resource": "email", - "server": true - }, - "POST /v3/face-match/": { - "auth": "api_key", - "method": "compare", - "resource": "face_match", - "server": true - }, - "POST /v3/face-search/": { - "auth": "api_key", - "method": "search", - "resource": "face_search", - "server": true - }, - "POST /v3/fraud/checks/": { - "auth": "api_key", - "method": "create", - "resource": "fraud_checks", - "server": true - }, - "POST /v3/fraud/checks/plan/": { - "auth": "api_key", - "method": "plan", - "resource": "fraud_checks", - "server": true - }, - "POST /v3/id-verification/": { - "auth": "api_key", - "method": "verify", - "resource": "id_verification", - "server": true - }, - "POST /v3/id-verification/wallets/audkenni/": { - "auth": "api_key", - "method": "start_audkenni", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/bankid-cz/": { - "auth": "api_key", - "method": "start_bankid_cz", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/bankid-no/": { - "auth": "api_key", - "method": "start_bankid_no", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/bankid-se/": { - "auth": "api_key", - "method": "start_bankid_se", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/buypass/": { - "auth": "api_key", - "method": "start_buypass", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/connectid/": { - "auth": "api_key", - "method": "start_connectid", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/diia/": { - "auth": "api_key", - "method": "start_diia", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/eparaksts-mobile/": { - "auth": "api_key", - "method": "start_eparaksts_mobile", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/eudi/": { - "auth": "api_key", - "method": "start_eudi", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/franceconnect/": { - "auth": "api_key", - "method": "start_franceconnect", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/frejaid/": { - "auth": "api_key", - "method": "start_frejaid", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/ftn/": { - "auth": "api_key", - "method": "start_ftn", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/govbr/": { - "auth": "api_key", - "method": "start_govbr", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/govuk-wallet/": { - "auth": "api_key", - "method": "start_govuk_wallet", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/idcard-ee/": { - "auth": "api_key", - "method": "start_idcard_ee", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/idin/": { - "auth": "api_key", - "method": "start_idin", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/itsme/": { - "auth": "api_key", - "method": "start_itsme", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/mitid/": { - "auth": "api_key", - "method": "start_mitid", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/mobile-id/": { - "auth": "api_key", - "method": "start_mobile_id", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/mojeid/": { - "auth": "api_key", - "method": "start_mojeid", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/oneid/": { - "auth": "api_key", - "method": "start_oneid", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/personalausweis/": { - "auth": "api_key", - "method": "start_personalausweis", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/smart-id/": { - "auth": "api_key", - "method": "start_smart_id", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/uae-pass/": { - "auth": "api_key", - "method": "start_uae_pass", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/verifications/{request_id}/cancel/": { - "auth": "api_key", - "method": "cancel_verification", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/verifications/{request_id}/poll/": { - "auth": "api_key", - "method": "poll_verification", - "resource": "id_wallets", - "server": true - }, - "POST /v3/id-verification/wallets/vipps/": { - "auth": "api_key", - "method": "start_vipps", - "resource": "id_wallets", - "server": true - }, - "POST /v3/ip/risk/": { - "auth": "api_key", - "method": "assess_risk", - "resource": "ip", - "server": true - }, - "POST /v3/kyb/search/": { - "auth": "api_key", - "method": "search", - "resource": "kyb", - "server": true - }, - "POST /v3/kyb/select/": { - "auth": "api_key", - "method": "select", - "resource": "kyb", - "server": true - }, - "POST /v3/kyb/shareholders/": { - "auth": "api_key", - "method": "list_shareholders", - "resource": "kyb", - "server": true - }, - "POST /v3/kyb/ubo/": { - "auth": "api_key", - "method": "list_ubos", - "resource": "kyb", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/": { - "auth": "api_key", - "method": "create", - "resource": "cases", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/": { - "auth": "api_key", - "method": "create", - "resource": "cases.blueprints", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/install/": { - "auth": "api_key", - "method": "install_presets", - "resource": "cases.blueprints", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/publish/": { - "auth": "api_key", - "method": "publish", - "resource": "cases.blueprints", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/bulk/assign/": { - "auth": "api_key", - "method": "bulk_assign", - "resource": "cases", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/bulk/status/": { - "auth": "api_key", - "method": "bulk_update_status", - "resource": "cases", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/duplicates/": { - "auth": "api_key", - "method": "find_duplicates", - "resource": "cases", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/": { - "auth": "api_key", - "method": "create", - "resource": "cases.routing_rules", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/preview/": { - "auth": "api_key", - "method": "preview", - "resource": "cases.routing_rules", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/publish/": { - "auth": "api_key", - "method": "publish", - "resource": "cases.routing_rules", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/teams/": { - "auth": "api_key", - "method": "create", - "resource": "cases.teams", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/action-receipts/{receipt_uuid}/retry/": { - "auth": "api_key", - "method": "retry_action_receipt", - "resource": "cases", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/approve/": { - "auth": "api_key", - "method": "approve_resolution", - "resource": "cases", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/assign/": { - "auth": "api_key", - "method": "assign", - "resource": "cases", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/entity-actions/": { - "auth": "api_key", - "method": "apply_entity_action", - "resource": "cases", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/escalate/": { - "auth": "api_key", - "method": "escalate", - "resource": "cases", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/": { - "auth": "api_key", - "method": "create", - "resource": "cases.links", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/": { - "auth": "api_key", - "method": "create", - "resource": "cases.notes", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/presign/": { - "auth": "api_key", - "method": "create_attachment_upload", - "resource": "cases.notes", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/regulatory-reports/": { - "auth": "api_key", - "method": "create", - "resource": "cases.regulatory_reports", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reject-approval/": { - "auth": "api_key", - "method": "reject_resolution", - "resource": "cases", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reopen/": { - "auth": "api_key", - "method": "reopen", - "resource": "cases", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/request-info/": { - "auth": "api_key", - "method": "request_info", - "resource": "cases", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/": { - "auth": "api_key", - "method": "create", - "resource": "cases.requests", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/preview/": { - "auth": "api_key", - "method": "preview", - "resource": "cases.requests", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/cancel/": { - "auth": "api_key", - "method": "cancel", - "resource": "cases.requests", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/resolve/": { - "auth": "api_key", - "method": "resolve", - "resource": "cases", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/transfer/": { - "auth": "api_key", - "method": "transfer", - "resource": "cases", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/": { - "auth": "api_key", - "method": "create", - "resource": "networks.notes", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/": { - "auth": "api_key", - "method": "create", - "resource": "regulatory_reports", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/finalize/": { - "auth": "api_key", - "method": "finalize", - "resource": "regulatory_reports", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/validate/": { - "auth": "api_key", - "method": "validate", - "resource": "regulatory_reports", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/report-templates/": { - "auth": "api_key", - "method": "create", - "resource": "report_templates", - "scope": "application", - "server": true - }, - "POST /v3/organization/{organization_id}/application/{application_id}/vendor-users/by-id/{didit_internal_id}/faces/upload/": { - "auth": "api_key", - "method": "upload_face", - "resource": "users", - "scope": "application", - "server": true - }, - "POST /v3/passive-liveness/": { - "auth": "api_key", - "method": "check", - "resource": "passive_liveness", - "server": true - }, - "POST /v3/phone/check/": { - "auth": "api_key", - "method": "check_code", - "resource": "phone", - "server": true - }, - "POST /v3/phone/risk/": { - "auth": "api_key", - "method": "assess_risk", - "resource": "phone", - "server": true - }, - "POST /v3/phone/send/": { - "auth": "api_key", - "method": "send_code", - "resource": "phone", - "server": true - }, - "POST /v3/poa/": { - "auth": "api_key", - "method": "verify", - "resource": "proof_of_address", - "server": true - }, - "POST /v3/questionnaires/": { - "auth": "api_key", - "method": "create", - "resource": "questionnaires", - "server": true - }, - "POST /v3/session/": { - "auth": "api_key", - "method": "create", - "resource": "sessions", - "server": true - }, - "POST /v3/session/import-shared/": { - "auth": "api_key", - "method": "import_shared", - "resource": "sessions", - "server": true - }, - "POST /v3/session/imports/": { - "auth": "api_key", - "method": "create", - "resource": "sessions.imports", - "server": true - }, - "POST /v3/session/{sessionId}/resubmission-preview/": { - "auth": "api_key", - "method": "preview_resubmission", - "resource": "sessions", - "server": true - }, - "POST /v3/session/{sessionId}/sandbox/arm/": { - "auth": "device_token", - "method": "arm_sandbox", - "resource": "sessions", - "server": false - }, - "POST /v3/session/{sessionId}/share/": { - "auth": "api_key", - "method": "share", - "resource": "sessions", - "server": true - }, - "POST /v3/sessions/delete/": { - "auth": "api_key", - "method": "batch_delete", - "resource": "sessions", - "server": true - }, - "POST /v3/sessions/{session_id}/reviews/": { - "auth": "api_key", - "method": "create", - "resource": "sessions.reviews", - "server": true - }, - "POST /v3/transactions/": { - "auth": "api_key", - "method": "create", - "resource": "transactions", - "server": true - }, - "POST /v3/transactions/rules/": { - "auth": "api_key", - "method": "create", - "resource": "transactions.rules", - "server": true - }, - "POST /v3/transactions/rules/backtest/": { - "auth": "api_key", - "method": "backtest", - "resource": "transactions.rules", - "server": true - }, - "POST /v3/transactions/rules/install/": { - "auth": "api_key", - "method": "install_library", - "resource": "transactions.rules", - "server": true - }, - "POST /v3/transactions/sdk-token/": { - "auth": "api_key", - "method": "create_sdk_token", - "resource": "transactions", - "server": true - }, - "POST /v3/transactions/{transaction_id}/travel-rule/ownership/": { - "auth": "api_key", - "method": "confirm_ownership", - "resource": "transactions.travel_rule", - "server": true - }, - "POST /v3/travel-rule/inbound/": { - "auth": "api_key", - "method": "register_inbound", - "resource": "travel_rule", - "server": true - }, - "POST /v3/travel-rule/pickup/{token}/respond/": { - "auth": "public", - "method": "respond_pickup", - "resource": "travel_rule", - "server": false - }, - "POST /v3/travel-rule/wallet-addresses/": { - "auth": "api_key", - "method": "create", - "resource": "travel_rule.wallet_addresses", - "server": true - }, - "POST /v3/travel-rule/widget-session/": { - "auth": "api_key", - "method": "create_widget_session", - "resource": "travel_rule", - "server": true - }, - "POST /v3/users/aml-monitoring/": { - "auth": "api_key", - "method": "set_aml_monitoring", - "resource": "users", - "server": true - }, - "POST /v3/users/create/": { - "auth": "api_key", - "method": "create", - "resource": "users", - "server": true - }, - "POST /v3/users/delete/": { - "auth": "api_key", - "method": "batch_delete", - "resource": "users", - "server": true - }, - "POST /v3/wallet-screening/": { - "auth": "api_key", - "method": "screen", - "resource": "wallet_screening", - "server": true - }, - "POST /v3/wallet-screening/pdf/": { - "auth": "api_key", - "method": "screen_pdf", - "resource": "wallet_screening", - "server": true - }, - "POST /v3/webhook/destinations/": { - "auth": "api_key", - "method": "create", - "resource": "webhooks.destinations", - "server": true - }, - "POST /v3/workflows/": { - "auth": "api_key", - "method": "create", - "resource": "workflows", - "server": true - }, - "PUT /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/": { - "auth": "api_key", - "method": "save_draft", - "resource": "cases.blueprints", - "scope": "application", - "server": true - }, - "PUT /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/": { - "auth": "api_key", - "method": "save_draft", - "resource": "cases.routing_rules", - "scope": "application", - "server": true - }, - "PUT /v3/travel-rule/settings/": { - "auth": "api_key", - "method": "update_settings", - "resource": "travel_rule", - "server": true - }, - "account GET /organizations/me/": { - "resource": "account.organizations", - "method": "list", - "auth": "account_token", - "server": true - }, - "account GET /organizations/me/{org_id}/applications/": { - "resource": "account.applications", - "method": "list", - "auth": "account_token", - "server": true - }, - "account GET /organizations/me/{org_id}/applications/{app_id}/": { - "resource": "account.applications", - "method": "retrieve", - "auth": "account_token", - "server": true - }, - "account PATCH /organizations/me/{org_id}/applications/{app_id}/": { - "resource": "account.applications", - "method": "update", - "auth": "account_token", - "server": true - }, - "account POST /organizations/me/{org_id}/applications/": { - "resource": "account.applications", - "method": "create", - "auth": "account_token", - "server": true - }, - "account POST /programmatic/login/": { - "resource": "account", - "method": "login", - "auth": "none", - "server": true - }, - "account POST /programmatic/register/": { - "resource": "account", - "method": "register", - "auth": "none", - "server": true - }, - "account POST /programmatic/resend-otp/": { - "resource": "account", - "method": "resend_otp", - "auth": "none", - "server": true - }, - "account POST /programmatic/verify-email/": { - "resource": "account", - "method": "verify_email", - "auth": "none", - "server": true - } - } + "operations": {} } \ No newline at end of file diff --git a/openapi/verification.json b/openapi/verification.json index c721f93..73528cc 100644 --- a/openapi/verification.json +++ b/openapi/verification.json @@ -17,10 +17,18 @@ "summary": "Service healthcheck (public, unauthenticated)", "description": "Public unauthenticated liveness probe. Returns `HTTP 200` with `{status: \"ok\", timestamp}` when service, DB, cache, and broker are healthy.", "operationId": "get_system_healthcheck", + "x-didit-sdk": { + "resource": "system", + "method": "healthcheck", + "auth": "public", + "server": false + }, "tags": [ "System" ], - "security": [], + "security": [ + {} + ], "parameters": [], "responses": { "200": { @@ -93,6 +101,12 @@ "summary": "Risk-score an email address", "description": "Return deliverability, disposable-address, breach, cross-session match, and normalized risk-intelligence signals without sending an OTP. The endpoint creates an API session and returns its id as `request_id`.\n\n**Decision.** The top-level `status` mirrors `email.status`. It is normally `Approved`; the risk evaluation can produce `In Review` or `Declined`.\n\n**Compatibility aliases.** Live responses expose the normalized intelligence object as both `email.email_intelligence` and `email.enrichment`. The values are identical. New integrations can read `email.enrichment`. Both keys are absent when no usable intelligence data was returned.\n\n**Billing.** Live calls use Email Risk API pricing. The request is billable once the lookup starts, even when some best-effort fields are unavailable.\n\n**Sandbox.** Sandbox keys validate the request and return deterministic approved data without sending an email, consuming credits, creating a session, or contributing to Networks.\n\n**Authentication.** Send your application's API key in `x-api-key`. Missing or invalid credentials return `403` - this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests per minute per API key across all POST/PATCH/DELETE endpoints.\n\n**Social footprint.** Set `enable_social` to `true` to add a `email_social` block: the online platforms this address is registered on, with per-category counts and any profile details available. `profiles_registered: 0` means the platforms were checked and the address was found on none of them - a valid, billable answer. The block is absent when the add-on was not requested or the check could not be completed, and it is not billed in that case. Billing adds one `email_social` unit per request on top of this endpoint's own price.", "operationId": "post_v3email_risk", + "x-didit-sdk": { + "resource": "email", + "method": "assess_risk", + "auth": "api_key", + "server": true + }, "tags": [ "Standalone APIs" ], @@ -305,6 +319,12 @@ "summary": "Risk-score an IP address", "description": "Return geolocation, network ownership, VPN/Tor and data-center flags, threat data, and `country_mismatch` for a supplied IP address. The endpoint creates an API session and returns its id as `request_id`.\n\n**Decision.** `status` is `In Review` when the resolved country conflicts with `claimed_country`, or when the address is associated with VPN, Tor, or data-center infrastructure. Otherwise it is `Approved`.\n\n**Best effort.** IP enrichment is best effort. If no enrichment source returns usable data, the endpoint returns an approved response with `ip.ip_address: null`, `ip.country_mismatch: null`, and an empty `ip.threat` object.\n\n**Billing.** Live calls use IP Risk API pricing. The request is billable once the lookup starts.\n\n**Sandbox.** Sandbox keys validate the request and return deterministic approved data without consuming credits, creating a session, or contributing to Networks.\n\n**Authentication.** Send your application's API key in `x-api-key`. Missing or invalid credentials return `403` - this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests per minute per API key across all POST/PATCH/DELETE endpoints.", "operationId": "post_v3ip_risk", + "x-didit-sdk": { + "resource": "ip", + "method": "assess_risk", + "auth": "api_key", + "server": true + }, "tags": [ "Standalone APIs" ], @@ -459,6 +479,12 @@ "summary": "Risk-score a phone number", "description": "Resolve a phone number and return carrier, line-type, porting, disposable-number, cross-session match, and normalized risk-intelligence signals without sending an OTP. The endpoint creates an API session and returns its id as `request_id`.\n\n**Decision.** The top-level `status` mirrors `phone.status`. It is normally `Approved`; the risk evaluation can produce `In Review` or `Declined`.\n\n**Compatibility aliases.** Live responses expose the normalized intelligence object as both `phone.phone_intelligence` and `phone.enrichment`. The values are identical. New integrations can read `phone.enrichment`. Both keys are absent when no usable intelligence data was returned.\n\n**Billing.** Live calls use Phone Risk API pricing. The request is billable once the lookup starts, even when some best-effort fields are unavailable.\n\n**Sandbox.** Sandbox keys validate the request and return deterministic approved data without sending an OTP, consuming credits, creating a session, or contributing to Networks.\n\n**Authentication.** Send your application's API key in `x-api-key`. Missing or invalid credentials return `403` - this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests per minute per API key across all POST/PATCH/DELETE endpoints.\n\n**Social footprint.** Set `enable_social` to `true` to add a `phone_social` block: the online platforms this number is registered on, with per-category counts and any profile details available. `profiles_registered: 0` means the platforms were checked and the number was found on none of them - a valid, billable answer. The block is absent when the add-on was not requested or the check could not be completed, and it is not billed in that case. Billing adds one `phone_social` unit per request on top of this endpoint's own price.", "operationId": "post_v3phone_risk", + "x-didit-sdk": { + "resource": "phone", + "method": "assess_risk", + "auth": "api_key", + "server": true + }, "tags": [ "Standalone APIs" ], @@ -664,6 +690,12 @@ "summary": "Create a verification session", "description": "Create a User Verification (KYC) or Business Verification (KYB) session and receive a hosted verification `url` plus a `session_token` to redirect or embed for your end user. The `workflow_id` selects which verification steps run and whether the session is KYC or KYB.\n\nCall this from your backend (never the browser — the API key is a secret) whenever a user or business needs to be verified: at signup, before a sensitive action, or when re-verification is required. Prerequisites: a published workflow (created in the Console [Workflows](https://docs.didit.me/console/workflows) page) and an application API key with available credits. Sandbox applications bypass the credit check.\n\n**Idempotency:** when `vendor_data` is provided and an unfinished session (`Not Started`, `In Progress`, `Resubmitted`, or `Awaiting User`) with the same `vendor_data` already exists on the workflow's **current latest published version** (and that session already has a hosted verification URL), the existing session is returned (still `201`) instead of creating a duplicate — with its `callback` and `metadata` updated to the newly provided values. An unfinished session created on an older version of the workflow (one that has since been republished) is **not** reused; a new session pinned to the latest published version is created instead. Sessions in `Approved`, `Declined`, `In Review`, `Expired`, `Abandoned`, or `Kyc Expired` are never reused.\n\n**Side effects:** the hosted verification URL is generated and stored; if `contact_details.send_notification_emails` is `true` and an email is provided, a verification invite email is sent. The session expires after the workflow's configured session expiration time. For KYB workflows, `contact_details.phone` and `expected_details` are ignored.", "operationId": "post_v3_session_create", + "x-didit-sdk": { + "resource": "sessions", + "method": "create", + "auth": "api_key", + "server": true + }, "tags": [ "Sessions" ], @@ -743,7 +775,7 @@ "type": "string", "format": "uri", "nullable": true, - "description": "Final redirect URL the user is sent to after completing the flow. Didit appends `?verificationSessionId={session_id}&status={status}` to this URL. Falls back to the workflow's configured callback URL when not provided in the request; `null` when neither is set.", + "description": "Final redirect URL the user is sent to after completing the flow. Didit appends `?verificationSessionId={session_id}&status={status}` to this URL. Falls back to the workflow's configured callback URL when not provided in the request; `null` when neither is set. When the application has allowed domains and this URL is a web URL outside them (for example a workflow default set before the list changed), the hosted flow does not redirect the user to it.", "example": "https://example.com/verification/callback" }, "workflow_id": { @@ -800,7 +832,7 @@ } }, "400": { - "description": "Bad request — the payload failed validation (missing or unknown `workflow_id`, invalid `language`, blocklisted `vendor_data`, malformed `portrait_image`, `address` without `country`/`poa_country`, invalid `sandbox_scenario`, etc.) or your organization does not have enough credits to start the session.\n\nThe body is an object keyed by the offending field name (or `detail` for non-field errors). Values are an **array of strings** for input-validation failures (e.g. missing required field, invalid language, oversized portrait image) and a **plain string** for failures detected while creating the session (unknown `workflow_id`, blocklisted `vendor_data`, no reference face available for a Face Match workflow). Handle both shapes.", + "description": "Bad request — the payload failed validation (missing or unknown `workflow_id`, invalid `language`, blocklisted `vendor_data`, malformed `portrait_image`, `address` without `country`/`poa_country`, invalid `sandbox_scenario`, `sign` documents, fields or values the workflow's Electronic Signature step refuses, such as `signature_document_not_supported`, `signature_document_conversion_failed`, `field_anchor_not_found` or `field_value_missing`, etc.) or your organization does not have enough credits to start the session.\n\nThe body is an object keyed by the offending field name (or `detail` for non-field errors). Values are an **array of strings** for input-validation failures (e.g. missing required field, invalid language, oversized portrait image) and a **plain string** for failures detected while creating the session (unknown `workflow_id`, blocklisted `vendor_data`, no reference face available for a Face Match workflow). Handle both shapes.", "content": { "application/json": { "schema": { @@ -866,7 +898,7 @@ } }, "403": { - "description": "Authentication or authorization failed. The endpoint returns `403` whenever the `x-api-key` header is missing, malformed, the API key has expired, or the API key is valid but the client does not have permission to create sessions in this application (for example the API key belongs to a different organization). Re-fetch the application API key via the [Auth API](/auth-api/get-credentials) and retry.\n\n**Note:** All four failure modes return the same response body — `{\"detail\": \"You do not have permission to perform this action.\"}` — and there is no machine-readable discriminator (no error code field, no `WWW-Authenticate` challenge) that lets you tell them apart. If you need to differentiate (for example to surface a more specific error to the end user), check the `x-api-key` header is present and belongs to the intended application before calling this endpoint.", + "description": "Authentication or authorization failed. The endpoint returns `403` whenever the `x-api-key` header is missing, malformed, the API key has expired, or the API key is valid but the client does not have permission to create sessions in this application (for example the API key belongs to a different organization). These failures share the body `{\"detail\": \"You do not have permission to perform this action.\"}`. Check that the `x-api-key` header is present and belongs to the intended application, re-fetch the application API key via the [Auth API](/auth-api/get-credentials) if needed, and retry.\n\n**Deleted application:** for a few minutes after an application is deleted, a request made with one of its API keys can answer `{\"detail\": \"This application was deleted.\", \"code\": \"application_deleted\"}`. Do not retry: the application and its API keys no longer exist. After those few minutes, the key is refused like any other invalid key.", "content": { "application/json": { "schema": { @@ -875,8 +907,16 @@ "detail": { "type": "string", "example": "You do not have permission to perform this action." + }, + "code": { + "type": "string", + "description": "Machine-readable reason, present only on some failures. `application_deleted`: the API key belongs to an application that was deleted.", + "example": "application_deleted" } - } + }, + "required": [ + "detail" + ] }, "examples": { "Missing or invalid API key": { @@ -890,6 +930,13 @@ "value": { "detail": "You do not have permission to perform this action." } + }, + "Application deleted": { + "summary": "API key of a deleted application", + "value": { + "detail": "This application was deleted.", + "code": "application_deleted" + } } } } @@ -1007,7 +1054,7 @@ }, "callback": { "type": "string", - "description": "URL to redirect the user to after verification completes. Didit automatically appends `verificationSessionId` and `status` (Approved, Declined, In Review) as query parameters. Custom URL schemes (e.g. `myapp://`) are supported for mobile callbacks. If omitted, the workflow's default `callback_url` is used.", + "description": "URL to redirect the user to after verification completes. Didit automatically appends `verificationSessionId` and `status` (Approved, Declined, In Review) as query parameters. Custom URL schemes (e.g. `myapp://`) are supported for mobile callbacks. If omitted, the workflow's default `callback_url` is used. When the application has allowed domains (`allowed_domains` on `PATCH /v3/webhook/`), an `http` or `https` callback must be on one of those origins (same scheme, host and port), otherwise the request fails with 400 on `callback`: `callback must be on one of the application's allowed domains: https://app.example.com`. Custom URL schemes are always accepted.", "example": "https://example.com/verification/callback", "x-readme-id": "0.2" }, @@ -1332,6 +1379,39 @@ "type": "string", "description": "Sandbox-only scenario slug. Only accepted when the API key belongs to a sandbox-mode application; rejected on live applications. When set, the create flow expands the scenario's bundled magic values onto the session's input fields (contact_details, expected_details) so the mocked providers reproduce the scenario's outcome through the real workflow pipeline. See the scenario catalog at GET /v1/sandbox/scenarios/ or [Sandbox & Test Data](/integration/sandbox-testing). You can also arm (or re-arm) a scenario after create via POST /v3/session/{sessionId}/sandbox/arm/, as long as the session is still `Not Started`. For non-doc lookup, use `lookup_partial_match`, `lookup_no_match`, or `lookup_provider_error` to test the configured attempt budget and fallback. For identity wallets, use `wallet_cancelled`, `wallet_timeout`, or `wallet_provider_error`; `approve` simulates a verified wallet. All catalog wallets can be tested in sandbox, including schemes unavailable on live applications. Simulated attempts are not charged.", "example": "decline_document_expired" + }, + "sign": { + "type": "object", + "description": "Electronic Signature input for this session. `documents` are the documents to sign, for a workflow whose Electronic Signature step takes documents from the session (`signature_documents_source` `session` or `both`): up to 20 documents and 50 MB in total. `field_values` fills the fields whose `prefill` is `api.`, on workflow and session documents alike. Creating a session again for the same `workflow_id` and `vendor_data` returns the pending session; if that call sends `sign`, its documents, fields and values must be identical to the first ones, and anything different returns `400` with `{\"sign\": \"signature_document_changed\"}`. See [Electronic signature documents](/sessions-api/create-session#electronic-signature-documents).", + "properties": { + "documents": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "items": { + "$ref": "#/components/schemas/SessionSignDocument" + } + }, + "field_values": { + "type": "object", + "description": "Values keyed by the `` of each `api.` pre-fill source: strings of 1 to 500 characters for `text` and `data` fields, booleans for a `checkbox`. `400` with `field_value_unknown` for a key no field uses, `field_value_missing` for a required field without its value, and `field_value_invalid` for a value that fails its field's validation.", + "additionalProperties": { + "oneOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + { + "type": "boolean" + } + ] + }, + "example": { + "credit_limit": "3,000 EUR" + } + } + } } }, "example": { @@ -1431,6 +1511,12 @@ "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.", "operationId": "post_v3_session_imports_create", + "x-didit-sdk": { + "resource": "sessions.imports", + "method": "create", + "auth": "api_key", + "server": true + }, "tags": [ "Sessions" ], @@ -1665,6 +1751,12 @@ "summary": "Download verification import template", "description": "Download the canonical CSV header for verification imports. The column set is determined by `import_type`. The first column of session templates is the idempotency key `external_id`.", "operationId": "get_v3_session_imports_template", + "x-didit-sdk": { + "resource": "sessions.imports", + "method": "retrieve_template", + "auth": "api_key", + "server": true + }, "tags": [ "Sessions" ], @@ -1725,6 +1817,12 @@ "summary": "Retrieve a verification import job", "description": "Retrieve import progress counters and terminal status for one job. Poll until `status` is `COMPLETED` or `FAILED`; `failed_count` > 0 means some rows were rejected — list them via the errors endpoint.", "operationId": "get_v3_session_imports_retrieve", + "x-didit-sdk": { + "resource": "sessions.imports", + "method": "retrieve", + "auth": "api_key", + "server": true + }, "tags": [ "Sessions" ], @@ -1792,6 +1890,12 @@ "summary": "List verification import row errors", "description": "List row-level errors for a verification import job, ordered by `row_number`. Use this endpoint to produce a corrected retry file containing only failed rows.", "operationId": "get_v3_session_imports_errors", + "x-didit-sdk": { + "resource": "sessions.imports", + "method": "list_errors", + "auth": "api_key", + "server": true + }, "tags": [ "Sessions" ], @@ -1923,8 +2027,17 @@ "/v3/session/{sessionId}/decision/": { "get": { "summary": "Retrieve the full decision for a verification session", - "description": "Retrieve the complete decision report for a verification session — the rolled-up session `status` plus one report per verification feature the workflow executed. This is the canonical read endpoint for both User Verification (KYC) and Business Verification (KYB) sessions: the same path serves both, and the top-level `session_kind` discriminator tells you which shape you received (`\"user\"` or `\"business\"`).\n\n**Every feature report is a plural array.** V3 workflows are graphs that can run the same feature more than once, so each block (`id_verifications`, `nfc_verifications`, `liveness_checks`, `face_matches`, `poa_verifications`, `document_ai_documents`, `phone_verifications`, `email_verifications`, `aml_screenings`, `ip_analyses`, `database_validations`, `questionnaire_responses`) is a JSON array whose items each carry a `node_id` identifying the workflow graph node that produced them. Read NFC as `response.nfc_verifications[0]` — never as a singular `nfc` field. A block is `null` until the workflow has run that feature at least once; features the workflow does not include never appear with data.\n\n**When to call.** The recommended pattern is event-driven: wait for the `status.updated` webhook that fires when the session reaches a decision (`Approved`, `Declined`, `In Review`), then call this endpoint once to fetch the full report. Polling also works — the endpoint can be called at any point in the session lifecycle and returns the data produced so far — but webhook-then-fetch is cheaper and faster. Media URLs in the response (document images, videos, PDFs) are short-lived presigned links: fetch them promptly or re-request the decision to get fresh ones, and do not persist them as long-term references.\n\n**Statuses.** The top-level `status` is the session lifecycle status (`Not Started`, `In Progress`, `Awaiting User`, `In Review`, `Approved`, `Declined`, `Resubmitted`, `Expired`, `Kyc Expired`, `Abandoned`). Each feature item additionally carries its own feature-level `status` (`Not Finished`, `Approved`, `Declined`, `In Review`, ...). A session can be `Approved` overall while an individual feature is `In Review`, depending on the workflow's decision rules.\n\n**Authentication and visibility.** Authenticate with the application API key in the `x-api-key` header (a console user Bearer token with the `read:sessions` permission also works). Only sessions owned by the authenticated application are visible. If your workflow restricts response attributes, non-allowed fields inside feature items are returned as `null` while `status`, `warnings`, and `node_id` are always preserved.\n\n**Rate limits.** GET requests are limited to 600 per minute per API key. Exceeding the limit returns `429` with `Retry-After` and `X-RateLimit-*` headers.", + "description": "Retrieve the complete decision report for a verification session — the rolled-up session `status` plus one report per verification feature the workflow executed. This is the canonical read endpoint for both User Verification (KYC) and Business Verification (KYB) sessions: the same path serves both, and the top-level `session_kind` discriminator tells you which shape you received (`\"user\"` or `\"business\"`).\n\n**Every feature report is a plural array.** V3 workflows are graphs that can run the same feature more than once, so each block (`id_verifications`, `nfc_verifications`, `liveness_checks`, `face_matches`, `poa_verifications`, `document_ai_documents`, `phone_verifications`, `email_verifications`, `aml_screenings`, `ip_analyses`, `database_validations`, `questionnaire_responses`) is a JSON array whose items each carry a `node_id` identifying the workflow graph node that produced them. Read NFC as `response.nfc_verifications[0]` — never as a singular `nfc` field. A block is `null` until the workflow has run that feature at least once; features the workflow does not include never appear with data. Electronic Signature results are in `signatures`, which is present only when the workflow has an Electronic Signature step.\n\n**When to call.** The recommended pattern is event-driven: wait for the `status.updated` webhook that fires when the session reaches a decision (`Approved`, `Declined`, `In Review`), then call this endpoint once to fetch the full report. Polling also works — the endpoint can be called at any point in the session lifecycle and returns the data produced so far — but webhook-then-fetch is cheaper and faster. Media URLs in the response (document images, videos, PDFs) are short-lived presigned links: fetch them promptly or re-request the decision to get fresh ones, and do not persist them as long-term references.\n\n**Statuses.** The top-level `status` is the session lifecycle status (`Not Started`, `In Progress`, `Awaiting User`, `In Review`, `Approved`, `Declined`, `Resubmitted`, `Expired`, `Kyc Expired`, `Abandoned`). Each feature item additionally carries its own feature-level `status` (`Not Finished`, `Approved`, `Declined`, `In Review`, ...). A session can be `Approved` overall while an individual feature is `In Review`, depending on the workflow's decision rules.\n\n**Authentication and visibility.** Authenticate with the application API key in the `x-api-key` header (a console user Bearer token with the `read:sessions` permission also works). Only sessions owned by the authenticated application are visible. If your workflow restricts response attributes, non-allowed fields inside feature items are returned as `null` while `status`, `warnings`, and `node_id` are always preserved.\n\n**Rate limits.** GET requests are limited to 600 per minute per API key. Exceeding the limit returns `429` with `Retry-After` and `X-RateLimit-*` headers.", "operationId": "get_v3_session_decision", + "x-didit-sdk": { + "resource": "sessions", + "method": "retrieve_decision", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], "responses": { "200": { "description": "Full decision report. `session_kind` discriminates the two shapes: `user` sessions carry the KYC feature arrays shown below; `business` sessions instead carry `registry_checks`, `document_verifications` and `key_people_checks` alongside the shared arrays. Feature arrays are `null` until the workflow has run that feature; media URLs are short-lived presigned links.", @@ -2322,6 +2435,8 @@ "status": "In Review", "is_nfc_skipped": false, "skip_reason": null, + "chip_protocol": "icao", + "jpki": null, "portrait_image": "https:///portrait.jpg", "signature_image": "https:///signature.jpg", "chip_data": { @@ -4085,6123 +4200,24344 @@ } } }, + "schema": { + "$ref": "#/components/schemas/SessionDecision" + } + } + } + }, + "403": { + "description": "Authentication or authorization failed. **This endpoint never returns `401`** — Didit's API-key authentication does not emit a `WWW-Authenticate` challenge, so missing or invalid credentials also surface as `403`. Two `detail` strings distinguish the failure modes:\n\n- `\"Authentication credentials were not provided or are invalid.\"` — no `x-api-key` header (and no `Authorization: Bearer` token), or the supplied credential failed introspection (revoked, expired, or malformed).\n- `\"You do not have permission to perform this action.\"` — the credential is valid but not allowed to read this session: a console-user Bearer token lacks the `read:sessions` permission or cannot access the owning application, or an API key belongs to a different organization than the session.\n\nNote that for a completely unknown `sessionId` the endpoint resolves the owning application before validating credentials, so a missing session returns `404` even when the credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "example": "Authentication credentials were not provided or are invalid." + } + } + }, + "examples": { + "Missing or invalid credentials": { + "summary": "No x-api-key header, or the key failed introspection", + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + }, + "Insufficient permissions": { + "summary": "Valid credential lacking read:sessions or access to the owning application", + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "Session not found. Returned when no live session matches the supplied `sessionId` as either a User Verification (KYC) or a Business Verification (KYB) session — a session that has been deleted also lands here. Body is the standard `{\"detail\": ...}` envelope.\n\n**Two distinct `detail` strings.** If the `sessionId` is unknown as both a KYC and a KYB session, the pre-auth owner lookup fails and you receive `{\"detail\": \"Not found.\"}` — this happens before credential validation, so it is returned even for unauthenticated calls. If the session exists but cannot be retrieved under the authenticated application (for example a stale or just-deleted record), the view returns `{\"detail\": \"Session not found.\"}`. Both indicate the same outcome — the session is not accessible — so match on the `404` status code rather than the string.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "example": "Not found." + } + } + }, + "examples": { + "Unknown session id": { + "summary": "sessionId is unknown to this application", + "value": { + "detail": "Not found." + } + }, + "Session not retrievable": { + "summary": "Session exists but is not retrievable for this application", + "value": { + "detail": "Session not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. GET requests are limited to **600 per minute per credential** (API key, Bearer token, or source IP when unauthenticated). Inspect `Retry-After` / `X-RateLimit-Reset` and back off before retrying. If you hit this limit while polling for decisions, switch to the `status.updated` webhook and fetch the decision once per status change instead.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying the request.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + }, + "X-RateLimit-Limit": { + "description": "Maximum number of requests allowed in the current window (600 for GET requests).", + "schema": { + "type": "integer", + "example": 600 + } + }, + "X-RateLimit-Remaining": { + "description": "Requests remaining in the current window.", + "schema": { + "type": "integer", + "example": 0 + } + }, + "X-RateLimit-Reset": { + "description": "Unix timestamp (seconds) at which the current window resets.", + "schema": { + "type": "integer", + "example": 1750000000 + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable explanation of the rate-limit breach.", + "example": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } + }, + "examples": { + "Throttled": { + "summary": "More than 600 GET requests in one minute from the same credential", + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "UUID of the verification session whose decision you want to retrieve. This is the `session_id` returned by `POST /v3/session/`. The same path works for both User Verification (KYC) and Business Verification (KYB) sessions — the server resolves the id against both session types.\n\n**Must be a valid UUID.** The route only matches well-formed UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{\"detail\": ...}` envelope shown below. Validate the UUID format client-side so you can distinguish an input error from a genuine missing-session `404`.", + "schema": { + "type": "string", + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } + }, + { + "name": "include", + "in": "query", + "required": false, + "description": "Comma-separated list of extra payload sections. Supported values: `events` and `resubmissions`, alone or together (`events,resubmissions`). When `include=events` is passed, the response additionally contains the session event timeline (`events`, including the `WORKFLOW_BRANCH_EVALUATED` / `WORKFLOW_STATUS_NODE_REACHED` routing events described on the Retrieve Session page), the full activity log (`activity_reviews`), blocklist flags (`blocklisted`), a per-session `cost_breakdown`, workflow metadata (`workflow_type`, `workflow_version`, `workflow_version_id`, and `workflow_terminal_node_id` - the last recorded workflow status node that set a session status, or `null` if none has been recorded; historical provenance that is not cleared by manual approvals, declines or resubmissions, and is replaced by a later workflow status node), and session metadata (`api_service` — string or `null`, set only for standalone-API sessions; `session_type` — `API`, `HOSTED`, or `MIGRATED`; `has_device_nfc_support` — boolean). Two fields change relative to the plain shape: the top-level `session_kind` discriminator is **omitted** (the events shape only exists for user sessions), and the `features` array switches from plain strings to `{feature, node_id}` objects — where the email feature is reported as `EMAIL` instead of `EMAIL_VERIFICATION`. This expanded shape is primarily used by the Didit Console; most API integrations should omit the parameter. **KYB limitation:** `include=events` only applies to user (KYC) sessions. Business sessions (`session_kind = \"business\"`) always return the standard business decision shape — `events`, `activity_reviews`, `blocklisted`, `cost_breakdown` and the extra `workflow_*`/session metadata are never returned for business sessions, and their `features` array stays a plain string array. With `include=resubmissions` (user and business sessions alike) the response also contains `resubmissions`: one entry per resubmission request, oldest first, each with `number`, `created_at`, `previous_status`, `nodes`, `reasons`, `requested_by` and `results` (what the resubmitted steps returned before the request, under the same keys and in the same shape as the current results). Each `cost_breakdown` item carries `usage_type`, `price`, `details` and `created_at`: a resubmitted step that runs again is billed as a new item.", + "schema": { + "type": "string", + "enum": [ + "events", + "resubmissions", + "events,resubmissions" + ], + "example": "events" + } + } + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X GET 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/decision/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Accept: application/json'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nsession_id = \"11111111-2222-3333-4444-555555555555\"\nurl = f\"https://verification.didit.me/v3/session/{session_id}/decision/\"\nheaders = {\n 'x-api-key': 'YOUR_API_KEY',\n \"Accept\": \"application/json\",\n}\n\nresponse = requests.get(url, headers=headers, timeout=15)\nresponse.raise_for_status()\ndecision = response.json()\n\nprint(\"session_kind:\", decision[\"session_kind\"])\nprint(\"top-level status:\", decision[\"status\"])\n\n# nfc_verifications is ALWAYS an array — never a singular `nfc` field.\nfor nfc in decision.get(\"nfc_verifications\") or []:\n print(\"nfc node_id:\", nfc[\"node_id\"], \"status:\", nfc[\"status\"])\n\nfor id_check in decision.get(\"id_verifications\") or []:\n print(\"id node_id:\", id_check[\"node_id\"], \"status:\", id_check[\"status\"])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const sessionId = '11111111-2222-3333-4444-555555555555';\nconst response = await fetch(\n `https://verification.didit.me/v3/session/${sessionId}/decision/`,\n {\n method: 'GET',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n Accept: 'application/json',\n },\n }\n);\n\nif (!response.ok) {\n throw new Error(`Decision fetch failed: ${response.status}`);\n}\nconst decision = await response.json();\n\nconsole.log('session_kind:', decision.session_kind);\nconsole.log('top-level status:', decision.status);\n\n// nfc_verifications is ALWAYS an array — never a singular `nfc` field.\nfor (const nfc of decision.nfc_verifications ?? []) {\n console.log('nfc node_id:', nfc.node_id, 'status:', nfc.status);\n}\n\nfor (const idCheck of decision.id_verifications ?? []) {\n console.log('id node_id:', idCheck.node_id, 'status:', idCheck.status);\n}" + } + ] + } + }, + "/v3/session/{sessionId}/delete/": { + "delete": { + "summary": "Delete a verification session (KYC or KYB)", + "description": "Delete a single verification session, User Verification (KYC) or Business Verification (KYB), by its `session_id`. The id is resolved against KYC sessions first, then KYB sessions, so both kinds are deleted through this one URL.\n\n**What is deleted:**\n- The session and every verification attempt it holds, including results that a resubmission replaced: the decision, extracted data, all related feature records (ID verification, face, liveness, face match, AML, registry checks, IP analysis, and so on), its reviews and its activity log.\n- All stored files owned by the session: document front/back photos (full, cropped, and privacy-blurred variants), document-capture videos, portrait crops, NFC chip portrait and signature images, face reference images, liveness videos, face-match source/target images, Proof of Address documents, and any extra uploaded files (KYC); uploaded company documents and extra files (KYB). Electronic Signature documents, signed PDFs, evidence PDFs and signature images (KYC and KYB): their download paths stop working. Previously issued media URLs (`https:///...`) stop resolving.\n- The session's face data. Its face embedding is deleted too, unless you choose to keep an image-free template (see **Face biometric data**).\n- Webhook deliveries still queued for the session, and their retries. Webhooks already delivered are not affected.\n- For a Business Verification (KYB) session: the verifications it created for its key people (KYC) and its corporate owners (KYB), recursively, reported as `deleted_child_verifications`. A verification it only links to, such as an existing approved KYC it reused, is not deleted.\n\nThe session disappears from every read endpoint immediately: `GET /v3/session/{sessionId}/decision/` returns `404` and `GET /v3/sessions/` stops listing it. Ongoing AML monitoring and KYB registry monitoring started by the deleted verifications stop.\n\n**What is not affected:** blocklist entries created from this session (face or document) are **not** removed; manage those with the blocklist endpoints. Share tokens issued for the session are not revoked, but there is nothing left for them to import: an import with one returns `400` with `{\"share_token\": [\"Original session does not exist.\"]}`. Credits already consumed are **not** refunded. The parent User or Business entity is **not** deleted; use `POST /v3/users/delete/` or `POST /v3/businesses/delete/` for those. If the User's profile portrait came from this session, the portrait is removed with it.\n\n**Face biometric data:** by default the session's face embedding is deleted with the session (`face_retention_outcome: deleted`). An application that opted in to biometric-template retention (`face_retention_policy: retain_until_user_deleted` on `PATCH /v3/webhook/`), or a request with `retain_face_embeddings: true`, keeps one separately managed, image-free face biometric template anchored to the session's User (`retained_with_user`) so that duplicate detection, Face Search, and biometric authentication keep working. The template never contains the face image, media, identity fields, the decision, or the deleted session's id; it is biometric data deleted at `biometric_template_expires_at`, or earlier on User deletion, privacy erasure, or an explicit purge through `/v3/biometric-templates/`. A session without a linked User cannot retain a template (`ineligible_no_vendor_user`). Send `deletion_instruction: privacy_erasure` for data-subject requests: it purges every retained template for that User and never retains a new one.\n\n**Irreversible:** deletion is permanent and there is no restore endpoint. Export anything you need (for example a decision PDF) before you call it.\n\n**Side effects:** no webhook is emitted for deletions.\n\n**Response:** `200 OK` once the deletion has completed, with a JSON body reporting the `face_retention_outcome`, the retained template's expiry and, for a KYB session, `deleted_child_verifications`. Earlier versions of this endpoint returned `204 No Content`; update clients that assert on `204`. Keep your `instruction_id` in your own erasure log.\n\n**Idempotency:** not idempotent at the HTTP level: the first call returns `200`; repeating it returns `404` because the session no longer resolves. After a `503` the session is still in place; repeat the same request to finish deleting it.\n\n**Authentication:** send your application's API key in the `x-api-key` header; the session must belong to that application. Console user access tokens (`Authorization: Bearer ...`) may also call this endpoint when they carry the `delete:sessions` permission. Authentication and permission failures both return `403`; this API never returns `401`.\n\n**Rate limit:** shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.\n\nTo bulk-delete KYC sessions by their numeric `session_number`, use `POST /v3/sessions/delete/` instead.", + "operationId": "delete_v3_session_by_id", + "x-didit-sdk": { + "resource": "sessions", + "method": "delete", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "parameters": [ + { + "in": "path", + "name": "sessionId", + "required": true, + "description": "UUID (`session_id`) of the User Verification (KYC) or Business Verification (KYB) session to delete, as returned when the session was created. Must be a canonical hyphenated UUID — a non-UUID value does not match the route and returns `404`.", + "schema": { + "type": "string", + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } + } + ], + "responses": { + "200": { + "description": "Session deleted. The body reports what happened to the session's face biometric data and, for a KYB session, how many verifications it created were deleted with it.", + "content": { + "application/json": { "schema": { "type": "object", "properties": { "session_id": { "type": "string", "format": "uuid", - "description": "The unique identifier of the session for which to retrieve verification results." - }, - "session_kind": { - "type": "string", - "enum": [ - "user", - "business" - ], - "description": "Discriminator indicating whether this is a User Verification (KYC) or Business Verification (KYB) session. When `user`, the KYC feature arrays (`id_verifications`, `nfc_verifications`, `liveness_checks`, `face_matches`, `poa_verifications`, `document_ai_documents`, `database_validations`, ...) apply and the business arrays are absent. When `business`, the payload instead carries `registry_checks`, `document_verifications`, `key_people_checks` plus the shared arrays (`aml_screenings`, `phone_verifications`, `email_verifications`, `ip_analyses`, `questionnaire_responses`). **Omitted when `include=events` is passed** — the events shape only exists for user sessions, so the discriminator is dropped from that variant." + "description": "The deleted session." }, "session_number": { "type": "integer", - "description": "The number of the session.", - "nullable": true - }, - "session_url": { - "type": "string", - "format": "uri", - "description": "The URL of the session.", - "nullable": true + "nullable": true, + "description": "The deleted session's number." }, - "status": { + "face_retention_outcome": { "type": "string", "enum": [ - "Not Started", - "In Progress", - "Awaiting User", - "In Review", - "Approved", - "Declined", - "Resubmitted", - "Expired", - "Kyc Expired", - "Abandoned" + "retained_with_user", + "deleted", + "none", + "ineligible_no_vendor_user" ], - "description": "Overall lifecycle status of the session — the rolled-up workflow result. Terminal values are `Approved`, `Declined`, `In Review`, `Expired`, `Kyc Expired`, and `Abandoned`; `Not Started`, `In Progress`, `Awaiting User`, and `Resubmitted` indicate the session is still moving through the workflow." + "description": "What happened to the session's face biometric data. `deleted`: the face embedding was deleted with the session (default). `retained_with_user`: the session is deleted and one image-free biometric template stays anchored to the User; see `biometric_template_uuid`. `none`: the session had no face embedding (for example a KYB session, or a KYC session without a liveness selfie). `ineligible_no_vendor_user`: retention was requested but the session has no linked User, so the embedding was deleted with the session. A biometric-store failure is never reported through this field on this endpoint: it returns `503` and the session is not deleted." }, - "workflow_id": { + "biometric_template_uuid": { "type": "string", "format": "uuid", - "description": "Stable identifier of the workflow group that produced this session. Workflows are versioned — this value identifies the workflow itself, not the specific version.", - "nullable": true - }, - "features": { - "type": "array", - "items": { - "type": "string", - "description": "The feature of the session." - }, - "description": "List of verification features configured for this session, as display names: `ID_VERIFICATION`, `NFC`, `LIVENESS`, `FACE_MATCH`, `POA`, `PHONE`, `EMAIL_VERIFICATION`, `AML`, `IP_ANALYSIS`, `DATABASE_VALIDATION`, `QUESTIONNAIRE`, `AGE_ESTIMATION`, `FACE_SEARCH` (Business Verification sessions use the KYB feature set: `KYB_REGISTRY`, `KYB_DOCUMENTS`, `KYB_KEY_PEOPLE`, `AML`, `PHONE`, `EMAIL_VERIFICATION`, `IP_ANALYSIS`, `QUESTIONNAIRE`). With `include=events` each entry becomes an object `{feature, node_id}` instead of a plain string, and the email feature is renamed: it appears as `EMAIL` in events mode instead of the `EMAIL_VERIFICATION` used here." + "nullable": true, + "description": "Id of the retained biometric template when `face_retention_outcome` is `retained_with_user`; `null` otherwise. Manage it through `/v3/biometric-templates/{template_uuid}/`." }, - "vendor_data": { + "biometric_template_expires_at": { "type": "string", - "description": "The vendor data of the session.", - "nullable": true - }, - "metadata": { - "type": "object", - "description": "The metadata of the session.", - "nullable": true - }, - "expected_details": { - "type": "object", - "description": "Expected details supplied at session creation — used for cross-validation against extracted data (KYC) or to pre-fill the company registry search (KYB). Echoes back exactly the keys that were provided; `null` when none were supplied. KYC sessions carry the person fields; Business (KYB) sessions carry `company_name`, `registry_country` and `registration_number`.", - "properties": { - "first_name": { - "type": "string", - "description": "User's first name. For example, `John`." - }, - "last_name": { - "type": "string", - "description": "User's last name. For example, `Doe`." - }, - "date_of_birth": { - "type": "string", - "format": "date", - "description": "User's date of birth with format: YYYY-MM-DD. For example, `1990-05-15`." - }, - "gender": { - "type": "string", - "nullable": true, - "enum": [ - "M", - "F" - ], - "default": null, - "description": "User's gender. Must be either 'M', 'F', or null." - }, - "nationality": { - "type": "string", - "description": "ISO 3166-1 alpha-3 country code representing the applicant's country of origin. For example, `USA`." - }, - "country": { - "type": "string", - "description": "ISO 3166-1 alpha-3 country code representing the country of the Proof of Address document, or the country of the applicant's ID document, which may differ from nationality. For example, `GBR`." - }, - "address": { - "type": "string", - "description": "The address in a human readable format, including as much information as possible. For example, `123 Main St, San Francisco, CA 94105, USA`." - }, - "identification_number": { - "type": "string", - "description": "The user's document number, personal number, or tax number. For example, `123456789`." - }, - "ip_address": { - "type": "string", - "format": "ipv4", - "description": "Expected IP address for the session. If the actual IP address differs from this value, a warning will be logged. For example, `192.168.1.100`." - }, - "expected_document_types": { - "type": "array", - "description": "Document types the ID verification step was restricted to at session creation. Empty/unset means no restriction. Codes: `P` (passport), `ID` (national ID), `DL` (driver's license), `RP` (residence permit), `HIC` (health insurance card), `TC` (tax card), `SSC` (social security card).", - "items": { - "type": "string", - "enum": [ - "P", - "ID", - "DL", - "RP", - "HIC", - "TC", - "SSC" - ] - } - }, - "id_country": { - "type": "string", - "nullable": true, - "description": "Expected ID-document issuing country (ISO 3166-1 alpha-3), when restricted at creation time.", - "example": "ESP" - }, - "poa_country": { - "type": "string", - "nullable": true, - "description": "Expected Proof of Address document country (ISO 3166-1 alpha-3), when restricted at creation time.", - "example": "ESP" - }, - "company_name": { - "type": "string", - "description": "Expected legal name of the business. Business (KYB) sessions only." - }, - "registry_country": { - "type": "string", - "description": "Expected registry country as `XX` or `XX-YY` (ISO 3166-1 alpha-2, optionally with an ISO 3166-2 subdivision for state-level registries, e.g. `US-CA`). Business (KYB) sessions only." - }, - "registration_number": { - "type": "string", - "description": "Expected company registration number. Business (KYB) sessions only." - } - }, - "nullable": true - }, - "contact_details": { - "type": "object", - "description": "Contact details supplied when the session was created (used for notification emails, prefilled phone/email verification, and communication language). `null` when neither an email nor a phone number was provided.", - "properties": { - "email": { - "type": "string", - "description": "Email address of the user (e.g., \"john.doe@example.com\") that will be used during the Email Verification step. If not provided, the user must provide it during the verification flow." - }, - "email_lang": { - "type": "string", - "description": "Language code (ISO 639-1) for email notifications. Controls the language of all email communications (e.g., \"en\", \"es\", \"fr\")." - }, - "send_notification_emails": { - "type": "boolean", - "default": false, - "description": "If true and an email is provided, Didit sends the initial \"Verify your identity\" email asynchronously when the User Verification (KYC) or Business Verification (KYB) session is created. Didit also sends verification status notifications for sessions requiring manual review to the provided email address." - }, - "phone": { - "type": "string", - "nullable": true, - "description": "Phone number of the user in E.164 format if available (e.g., +14155552671). `null` when only an email was supplied at session creation. Note: This field returns the original phone number provided during session creation. If the number was invalid, it may not be in E.164 format." - } - }, - "nullable": true + "format": "date-time", + "nullable": true, + "description": "When the retained biometric template is deleted, when `face_retention_outcome` is `retained_with_user`; `null` when no template is retained." }, - "callback": { - "type": "string", - "format": "uri", - "description": "The callback URL of the session.", - "nullable": true + "deleted_child_verifications": { + "type": "integer", + "description": "For a Business Verification (KYB) session: how many verifications it created (its key people's KYC sessions and its corporate owners' KYB sessions, recursively) were deleted with it. Always `0` for a KYC session." + } + } + }, + "examples": { + "Deleted (default)": { + "value": { + "session_id": "4c5c7f3a-3b1d-4b7e-9c39-2b4f7e6a1d02", + "session_number": 1024, + "face_retention_outcome": "deleted", + "biometric_template_uuid": null, + "biometric_template_expires_at": null, + "deleted_child_verifications": 0 + } + }, + "Retained with User": { + "value": { + "session_id": "4c5c7f3a-3b1d-4b7e-9c39-2b4f7e6a1d02", + "session_number": 1024, + "face_retention_outcome": "retained_with_user", + "biometric_template_uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", + "biometric_template_expires_at": "2027-10-09T10:15:00Z", + "deleted_child_verifications": 0 + } + }, + "Ineligible: no User": { + "summary": "Retention requested on a session created without vendor_data", + "value": { + "session_id": "7a1e9c2d-5f4b-4c3a-8e2d-1b0a9f8e7d6c", + "session_number": 1031, + "face_retention_outcome": "ineligible_no_vendor_user", + "biometric_template_uuid": null, + "biometric_template_expires_at": null, + "deleted_child_verifications": 0 + } + }, + "KYB session": { + "value": { + "session_id": "0f3c9b7e-2d1a-4e5f-8c6b-7a9d8e1f2c3b", + "session_number": 88, + "face_retention_outcome": "none", + "biometric_template_uuid": null, + "biometric_template_expires_at": null, + "deleted_child_verifications": 3 }, - "id_verifications": { - "type": "array", - "nullable": true, - "description": "**Array of ID verification (OCR) reports** produced by ID-document steps in the workflow graph. Always a JSON array — read `id_verifications[0]` and iterate if your workflow can run more than one ID check (for example a primary check plus a step-up). `null` until at least one ID step has produced data. Each item is the full report, including `node_id`, feature-level `status`, extracted document fields, image URLs, quality scores, MRZ, address parsing, cross-session matches, and `warnings[]`.", - "items": { - "type": "object", - "description": "One ID verification report - the full report for a single ID Verification step. `verification_method` says which of the three methods produced it (document capture, non-doc lookup or wallet sign-in) and the method-specific evidence block is populated accordingly.", - "properties": { - "node_id": { - "type": "string", - "description": "Workflow graph node id of the OCR step that produced this report." - }, - "verification_method": { - "type": "string", - "enum": [ - "document", - "id_lookup", - "wallet" - ], - "description": "Which ID Verification method produced this report. `document` - the user photographed a physical ID (the default, and the value on every session created before the other methods existed). `id_lookup` - the user typed their national ID number and it was checked against a government register or another authoritative source for that country (`id_lookup.source` names which). `wallet` - the user signed in with a government or bank digital identity." - }, - "assurance": { - "type": "string", - "enum": [ - "documentary", - "data_match", - "cryptographic" - ], - "description": "Assurance tier derived from `verification_method`: `documentary` for a document that was captured and checked, `data_match` for details that matched a record held by the source queried, `cryptographic` for attributes an identity provider authenticated and signed." - }, - "wallet_provider": { - "type": "string", - "nullable": true, - "description": "Catalog wallet id when `verification_method` is `wallet` (`mitid`, `bankid_se`, `itsme`, ...). `null` otherwise." - }, - "fallback_from": { - "type": "object", - "nullable": true, - "description": "Why a non-document method ended the ID step. Set on the declined report when a lookup or a wallet sign-in failed and the country's fallback switch was `decline`. `null` otherwise.", - "properties": { - "method": { - "type": "string", - "enum": [ - "id_lookup", - "wallet" - ], - "description": "The method that failed." - }, - "reason": { - "type": "string", - "description": "The outcome that ended it, e.g. `no_match`, `provider_error`, `cancelled`, `timeout`." - }, - "action": { - "type": "string", - "description": "The configured action that was taken, e.g. `decline`." - } - } - }, - "id_lookup": { - "type": "object", - "nullable": true, - "description": "Registry evidence. Present only when `verification_method` is `id_lookup`; `null` otherwise.", - "properties": { - "source": { - "type": "string", - "description": "Customer-facing name of the source queried, e.g. `Department of Home Affairs` (a government register) or `UK credit bureau and financial services records` (not a government register). This is the only statement of provenance for the lookup; do not assume a government source." - }, - "checked_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the register answered." - }, - "attempts": { - "type": "integer", - "description": "1-based attempt number this report came from." - }, - "max_attempts": { - "type": "integer", - "description": "Attempt budget configured for the country (1 to 5, default 1)." - }, - "outcome": { - "type": "string", - "enum": [ - "match", - "partial_match", - "no_match", - "provider_error", - "format_rejected" - ], - "nullable": true, - "description": "What the register answered. Only `match`, `partial_match` and `no_match` count against the attempt budget and bill." - }, - "comparison": { - "type": "array", - "description": "One row per comparable field the source answered on, plus one row per source error. Comparison rows use the normalised field names below rather than the country's request field keys, because several request fields (first and last name, or any of the identifier aliases) collapse into a single comparison. A source that failed appends a row with `field: \"provider\"` and `result: \"error\"`, so read `result` before treating a row as a field verdict.", - "items": { - "type": "object", - "required": [ - "field", - "label", - "provided", - "record", - "result" - ], - "properties": { - "field": { - "type": "string", - "enum": [ - "identification_number", - "full_name", - "date_of_birth", - "address", - "provider" - ], - "description": "Normalised comparison field. `provider` is not a user field: it marks a source error row." - }, - "label": { - "type": "string", - "description": "Display label for the row, e.g. `ID number`, `Full name`, `Provider`." - }, - "provided": { - "type": "string", - "nullable": true, - "description": "What the user typed. `null` on a `provider` error row." - }, - "record": { - "type": "string", - "nullable": true, - "description": "What the source held. On a `provider` error row this carries the error message instead." - }, - "result": { - "type": "string", - "enum": [ - "match", - "partial", - "no_match", - "error" - ], - "description": "Per-field comparison result. `error` appears only on a `provider` row and means that source did not answer; treat it as evidence about the query, never as a field mismatch." - } - } - } - }, - "registry_portrait": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "Short-lived presigned URL to the portrait the source returned. Only the four government registers that return one (South Africa, Nigeria, Argentina, Panama)." - }, - "face_match_score": { - "type": "number", - "nullable": true, - "description": "Selfie against the registry portrait, 0-100. Only where the register returns a portrait." - } - } - }, - "wallet_verification": { - "type": "object", - "nullable": true, - "description": "Credential evidence. Present only when `verification_method` is `wallet`; `null` otherwise.", - "properties": { - "provider": { - "type": "string", - "description": "Catalog wallet id, e.g. `mitid`." - }, - "provider_name": { - "type": "string", - "nullable": true, - "description": "Wallet name as the end user knows it, e.g. `MitID`." + "summary": "A business verification deleted with the three verifications it created for its key people" + } + } + } + } + }, + "400": { + "description": "Validation error — nothing is deleted.", + "content": { + "application/json": { + "examples": { + "Privacy erasure cannot retain": { + "value": { + "retain_face_embeddings": [ + "Privacy erasure cannot retain biometric templates." + ] + } + }, + "No finite retention duration": { + "summary": "Retention requested without face_retention_days on the request or the application", + "value": { + "detail": "A finite biometric-template retention duration is required." + } + } + } + } + } + }, + "403": { + "description": "Authentication or permission failure. Missing/invalid credentials also return `403` — this API never returns `401`. Exception: when the `sessionId` does not resolve to a live session, the pre-auth owner lookup returns `404` first, even with missing or invalid credentials.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Missing or invalid API key": { + "summary": "No x-api-key header, or the key is invalid", + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + }, + "Insufficient permission": { + "summary": "User token without delete:sessions, or credentials for an application that does not own the session", + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No live KYC or KYB session with this `session_id` exists — the id is unknown or the session was already deleted (repeating a successful delete lands here).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Not Found": { + "summary": "Unknown or already-deleted session", + "value": { + "detail": "Not found." + } + } + } + } + } + }, + "429": { + "description": "Shared write rate limit exceeded (300 POST/PATCH/DELETE requests per minute per API key). Inspect `Retry-After` and the `X-RateLimit-*` response headers before retrying.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Rate limited": { + "summary": "Write budget exhausted", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + } + } + } + }, + "503": { + "description": "The deletion could not finish because a storage service, a biometric store, or a monitoring provider was unavailable. The session is still in place; repeat the same request until it returns `200`. Some of its stored files or its face embedding may already be deleted, so do not rely on them after a `503`. Retries converge on a single template.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Session deletion unavailable": { + "summary": "A storage service or a monitoring provider was unavailable", + "value": { + "detail": "The session could not be deleted because a storage service or a monitoring provider is unavailable. The session is still in place; repeat the request to finish deleting it." + } + }, + "Biometric store unavailable": { + "summary": "A biometric store was unavailable while retaining or purging a template", + "value": { + "detail": "The biometric-template operation could not finish because an external store is unavailable. The operation is retryable." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (default policy)", + "source": "curl -X DELETE \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/delete/ \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "curl", + "label": "curl (retain template)", + "source": "curl -X DELETE \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/delete/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"retain_face_embeddings\": true, \"face_retention_days\": 365}'" + }, + { + "lang": "curl", + "label": "curl (privacy erasure)", + "source": "curl -X DELETE \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/delete/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"deletion_instruction\": \"privacy_erasure\", \"instruction_id\": \"dsar-2026-0142\"}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresponse = requests.delete(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/delete/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n json={\"retain_face_embeddings\": True, \"face_retention_days\": 365},\n)\nresponse.raise_for_status()\nprint(response.json()[\"face_retention_outcome\"])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch(\n 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/delete/',\n {\n method: 'DELETE',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ deletion_instruction: 'privacy_erasure', instruction_id: 'dsar-2026-0142' }),\n },\n);\nconst body = await resp.json();\nconsole.log(body.face_retention_outcome);" + } + ], + "requestBody": { + "required": false, + "description": "Optional. Omit the body entirely to follow the application's retention policy with an operational deletion.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "retain_face_embeddings": { + "type": "boolean", + "nullable": true, + "description": "Override the application's `face_retention_policy` for this deletion. `true` keeps one image-free face biometric template anchored to the session's User after the session is deleted; `false` deletes the face embedding with the session; omit or `null` to follow the application policy. Ignored for KYB sessions and for sessions without a face embedding.", + "example": true + }, + "face_retention_days": { + "type": "integer", + "minimum": 1, + "maximum": 3650, + "description": "Finite retention duration, in days, for a retained template. Required when a template is retained unless the application already sets `face_retention_days`. The template's `expires_at` is the earliest of this duration, the application's general data-retention window, and `face_retention_deadline`.", + "example": 365 + }, + "face_retention_deadline": { + "type": "string", + "format": "date-time", + "description": "Optional hard expiry for a retained template. Must be in the future.", + "example": "2027-08-28T00:00:00Z" + }, + "deletion_instruction": { + "type": "string", + "enum": [ + "operational_session_delete", + "privacy_erasure" + ], + "default": "operational_session_delete", + "description": "The class of instruction you are giving. `operational_session_delete` deletes the session and retains a template only when the policy or `retain_face_embeddings` says so. `privacy_erasure` purges every retained biometric template anchored to the session's User before deleting the session, cannot be overridden by the application policy, and cannot be combined with `retain_face_embeddings: true` (`400`).", + "example": "operational_session_delete" + }, + "instruction_id": { + "type": "string", + "maxLength": 255, + "description": "Your durable reference for this deletion instruction (for example an erasure-ticket id). Recorded on the audit trail and on any retained template. Generated by Didit when omitted.", + "example": "dsar-2026-0142" + } + } + }, + "examples": { + "Retain a biometric template": { + "summary": "Delete the session, keep an image-free template for 365 days", + "value": { + "retain_face_embeddings": true, + "face_retention_days": 365, + "instruction_id": "post-approval-cleanup-2026-08" + } + }, + "Force deletion on an opted-in application": { + "summary": "Delete the face embedding regardless of the application policy", + "value": { + "retain_face_embeddings": false + } + }, + "Privacy erasure": { + "summary": "Data-subject request: delete the session and purge every retained template for that person", + "value": { + "deletion_instruction": "privacy_erasure", + "instruction_id": "dsar-2026-0142" + } + } + } + } + } + } + } + }, + "/v3/session/{sessionId}/generate-pdf/": { + "get": { + "summary": "Download a PDF report for a verification session", + "description": "Render and download a compliance-ready PDF report for a finished verification session. The same path serves both User Verification (KYC) and Business Verification (KYB) sessions — the server resolves the `sessionId` against both session types and picks the matching report template.\n\n**Eligible statuses.** The session must already carry a final or reviewable status:\n\n- KYC sessions: `Approved`, `Declined`, `In Review`, or `Kyc Expired`.\n- KYB sessions: `Approved`, `Declined`, or `In Review`.\n\nAny other status (`Not Started`, `In Progress`, `Expired`, `Abandoned`, …) returns `403` with an explanatory `detail` string — see the `403` response below.\n\n**What the report contains.** The PDF mirrors the console session view at the moment of the request:\n\n- **KYC** — rolled-up session status, the warnings list, and one section per executed feature: ID Verification (document images plus extracted fields), NFC, Liveness, Face Match, Email Verification, Phone Verification, Proof of Address, Questionnaire answers, Database Validation, AML Screening, and IP Analysis — followed by session tags, console review activity (comments and status changes), and the session event timeline.\n- **KYB** — registry checks (company data), AML screenings, business document verifications, key-people checks, questionnaire responses, phone/email verifications, IP analyses, and the session event timeline, followed by tags and review activity.\n\n**Branding.** If your application has white-label customization, the report is rendered with your logo and links to your privacy-policy URL; otherwise it carries Didit branding. Reports are rendered in English — there is no language parameter.\n\n**No caching.** Every call re-renders the PDF from the current session data, so a report generated after a manual review reflects the reviewer’s decision. Two calls for the same session can produce byte-different files — archive the downloaded file if you need an immutable copy.\n\n**Latency.** Rendering downloads every stored image (document sides, selfies, extra files) before composing the PDF, so media-heavy sessions can take several seconds. Use a generous client read timeout (60 s recommended) and stream the body to disk.\n\n**Trailing slash.** The canonical route ends with a trailing slash (`…/generate-pdf/`). A request without it is served directly with the same response — there is no `301` redirect, so no client has to follow redirects and `curl` does not need `-L`. Use the slashed URL as in the samples.\n\nIf you need the underlying data as JSON instead of a rendered report, use `GET /v3/session/{sessionId}/decision/`. For every reportable session of one user in a single document, use `GET /v3/users/{vendor_data}/generate-pdf/`.", + "operationId": "get_v3_session_generate_pdf", + "x-didit-sdk": { + "resource": "sessions", + "method": "generate_pdf", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "UUID of the User Verification (KYC) or Business Verification (KYB) session to render — the `session_id` returned by `POST /v3/session/`. The same path works for both session kinds; use the `Content-Disposition` filename on the response to tell which report type you received.\n\n**Must be a well-formed, lowercase UUID.** The route only matches valid UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{\"detail\": ...}` envelope shown below. The route's UUID converter only matches the canonical lowercase form — uppercase-hex UUIDs are rejected with the HTML 404.", + "schema": { + "type": "string", + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } + } + ], + "responses": { + "200": { + "description": "The rendered PDF document, returned directly as binary `application/pdf` — there is no JSON wrapper and no download-URL indirection. The body starts with the `%PDF` magic bytes (PDF 1.7). Save it to a `.pdf` file or stream it through to your caller.", + "headers": { + "Content-Disposition": { + "description": "`attachment; filename=session_{sessionId}.pdf` for KYC sessions, `attachment; filename=business_{sessionId}.pdf` for KYB sessions — the prefix tells you which report template was rendered.", + "schema": { + "type": "string", + "example": "attachment; filename=session_11111111-2222-3333-4444-555555555555.pdf" + } + }, + "Content-Length": { + "description": "Total size of the PDF in bytes; the body is not chunked.", + "schema": { + "type": "integer", + "example": 25191 + } + } + }, + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "403": { + "description": "Authentication, authorization, or session-status failure. **This endpoint never returns `401`** — Didit's API-key authentication does not emit a `WWW-Authenticate` challenge, so missing or invalid credentials also surface as `403`. Distinguish the failure modes by the `detail` string:\n\n- `\"Authentication credentials were not provided or are invalid.\"` — no `x-api-key` header (and no `Authorization: Bearer` token), or the supplied credential failed introspection (revoked, expired, or malformed).\n- `\"You do not have permission to perform this action.\"` — the credential is valid but cannot read this session: a console-user Bearer token lacks the `read:sessions` permission or cannot access the owning application, or an API key belongs to a different organization than the session.\n- `\"You can only generate a PDF for sessions in review, declined, approved or kyc expired\"` — the KYC session exists but is not in an eligible status yet (for example `Not Started` or `In Progress`). Wait for the `status.updated` webhook before requesting the report.\n- `\"You can only generate a PDF for sessions in review, declined or approved\"` — same condition for KYB sessions.\n\nNote that for a completely unknown `sessionId` the endpoint resolves the owning application before validating credentials, so a missing session returns `404` even when the credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "example": "You can only generate a PDF for sessions in review, declined, approved or kyc expired" + } + } + }, + "examples": { + "Missing or invalid credentials": { + "summary": "No x-api-key header, or the key failed introspection", + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + }, + "Insufficient permissions": { + "summary": "Valid credential lacking read:sessions or access to the owning application", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "KYC session not finished": { + "summary": "KYC session still Not Started / In Progress", + "value": { + "detail": "You can only generate a PDF for sessions in review, declined, approved or kyc expired" + } + }, + "KYB session not finished": { + "summary": "KYB session still Not Started / In Progress", + "value": { + "detail": "You can only generate a PDF for sessions in review, declined or approved" + } + } + } + } + } + }, + "404": { + "description": "No active session with this `sessionId` exists — it was never created, or it has been deleted. Owner resolution happens before credential validation, so this `404` is returned even for unauthenticated requests. Depending on which lookup misses, the `detail` string is `\"Not found.\"` (the common case) or `\"Session not found.\"`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "example": "Not found." + } + } + }, + "examples": { + "Unknown session": { + "summary": "sessionId does not match any KYC or KYB session", + "value": { + "detail": "Not found." + } + }, + "Recently deleted session": { + "summary": "Session was deleted between lookups", + "value": { + "detail": "Session not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. PDF generation has a dedicated limit of **50 requests per minute per credential** (API key, Bearer token, or source IP when unauthenticated), in addition to the global 600/min GET limit. Wait `Retry-After` seconds (also reflected in the `X-RateLimit-*` headers), then retry. Reports are not cached server-side, so download each report once and store the file instead of re-fetching it.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying the request.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + }, + "X-RateLimit-Limit": { + "description": "Maximum number of PDF generations allowed in the current window (50).", + "schema": { + "type": "integer", + "example": 50 + } + }, + "X-RateLimit-Remaining": { + "description": "Requests remaining in the current window.", + "schema": { + "type": "integer", + "example": 0 + } + }, + "X-RateLimit-Reset": { + "description": "Unix timestamp (seconds) at which the current window resets.", + "schema": { + "type": "integer", + "example": 1750000000 + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable explanation of the rate-limit breach.", + "example": "Session PDF generation rate limit exceeded. You can make up to 50 requests per minute." + } + } + }, + "examples": { + "Throttled": { + "summary": "More than 50 PDF generations in one minute from the same credential", + "value": { + "detail": "Session PDF generation rate limit exceeded. You can make up to 50 requests per minute." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl --fail \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/generate-pdf/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n --output report.pdf" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresponse = requests.get(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/generate-pdf/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n stream=True,\n timeout=60,\n)\nresponse.raise_for_status()\nwith open(\"report.pdf\", \"wb\") as fh:\n for chunk in response.iter_content(chunk_size=8192):\n fh.write(chunk)" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "import { writeFile } from 'node:fs/promises';\n\nconst response = await fetch(\n 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/generate-pdf/',\n { headers: { 'x-api-key': 'YOUR_API_KEY' } },\n);\nif (!response.ok) throw new Error(`PDF generation failed: HTTP ${response.status}`);\nawait writeFile('report.pdf', Buffer.from(await response.arrayBuffer()));" + } + ] + } + }, + "/v3/session/{sessionId}/sandbox/arm/": { + "post": { + "summary": "Arm a sandbox scenario on a session", + "description": "Persist a sandbox scenario on a `Not Started` session and expand its bundled magic values (email, phone, or `expected_details` fields) onto the session, so the mocked providers reproduce the scenario's outcome once the flow runs. This is the endpoint the pre-flow scenario picker in Didit's hosted verification UI calls when a tester chooses an outcome before starting capture. Build your own picker against it, or call it directly from a sandbox test harness.\n\nAuthenticate with the session's own `Session-Token` (returned in `session_token` by [Create Session](/sessions-api/create-session)), not your application `x-api-key`: this endpoint is meant to be called from the client, not your backend. A scenario is only accepted while `status` is `Not Started`. Calling it again while still `Not Started` re-arms the session: it swaps in the new scenario's magic values and clears only the values the *previous* scenario wrote, so any value the user already typed by hand (email, first name, etc.) survives the re-arm. Arming a session that has moved past `Not Started`, or that belongs to a live (non-sandbox) application, is rejected.\n\nSee [Sandbox & Test Data](/integration/sandbox-testing) for the full scenario catalog and the other two ways to drive an outcome: `sandbox_scenario` at session create, and typing magic values directly.", + "operationId": "post_v3_session_sandbox_arm", + "x-didit-sdk": { + "resource": "sessions", + "method": "arm_sandbox", + "auth": "device_token", + "server": false + }, + "tags": [ + "Sessions" + ], + "security": [ + { + "SessionTokenAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sessionId", + "required": true, + "description": "UUID of the sandbox verification session to arm. Must match the session encoded in the Session-Token used to authenticate.", + "schema": { + "type": "string", + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "scenario" + ], + "properties": { + "scenario": { + "type": "string", + "description": "Slug of the scenario to arm. See the catalog at GET /v1/sandbox/scenarios/ or the scenario table in [Sandbox & Test Data](/integration/sandbox-testing).", + "example": "decline_document_expired" + } + } + }, + "examples": { + "Arm": { + "summary": "Arm the AML PEP-hit scenario", + "value": { + "scenario": "decline_aml_hit" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Scenario armed. Its magic values are now on the session. `status` reflects the session's current status, which this call never changes: it is always `Not Started` on success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "session_id": { + "type": "string", + "format": "uuid" + }, + "scenario": { + "type": "string", + "description": "The armed scenario slug (echoes the request)." + }, + "status": { + "type": "string", + "example": "Not Started" + } + } + }, + "examples": { + "Armed": { + "summary": "Scenario armed on a Not Started session", + "value": { + "session_id": "11111111-2222-3333-4444-555555555555", + "scenario": "decline_aml_hit", + "status": "Not Started" + } + } + } + } + } + }, + "400": { + "description": "Validation error: an unknown scenario slug, or the session has already progressed past `Not Started`.", + "content": { + "application/json": { + "examples": { + "Unknown Scenario": { + "summary": "scenario is not in the catalog", + "value": { + "scenario": [ + "Unknown sandbox scenario: 'not-a-real-scenario'." + ] + } + }, + "Already Started": { + "summary": "Session moved past Not Started", + "value": [ + "Scenario can only be armed before the verification starts." + ] + } + } + } + } + }, + "404": { + "description": "The Session-Token does not resolve to this `session_id`, or the session belongs to a live (non-sandbox) application.", + "content": { + "application/json": { + "examples": { + "Not Found": { + "summary": "Live session, or session_id / token mismatch", + "value": { + "detail": "Not found." + } + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/sandbox/arm/ \\\n -H 'Session-Token: YOUR_SESSION_TOKEN' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"scenario\": \"decline_aml_hit\"\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/sandbox/arm/\",\n headers={\n \"Session-Token\": \"YOUR_SESSION_TOKEN\",\n \"Content-Type\": \"application/json\",\n },\n json={\"scenario\": \"decline_aml_hit\"},\n)\nresponse.raise_for_status()\nprint(response.json()) # {'session_id': '...', 'scenario': 'decline_aml_hit', 'status': 'Not Started'}" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const response = await fetch(\n 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/sandbox/arm/',\n {\n method: 'POST',\n headers: {\n 'Session-Token': sessionToken,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ scenario: 'decline_aml_hit' }),\n },\n);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst { session_id, scenario, status } = await response.json();" + } + ] + } + }, + "/v3/session/{sessionId}/update-status/": { + "patch": { + "summary": "Approve, decline, or request resubmission for a verification session", + "description": "Manually override the final decision of a KYC or KYB verification session. Set `new_status` to `Approved` or `Declined` to record a manual decision, or to `Resubmitted` to clear specific verification steps and let the user redo them through the same verification URL. The endpoint accepts both user (KYC) and business (KYB) session IDs.\n\nCall it once a session has reached a reviewable state: the current status must be `Approved`, `Declined`, `In Review`, `Kyc Expired`, `Abandoned`, or `Resubmitted`. Sessions that are still `Not Started`, `In Progress`, or `Awaiting User`, or that ended as `Expired`, return `400`. Your API key needs the `write:sessions` permission (a scoped API key needs write access to decisions). The eligible-state check runs before body validation, so an ineligible session returns the `Wrong Current Status` error even when the payload is also invalid.\n\nFor `Resubmitted`, pass `nodes_to_resubmit` to choose which workflow steps the user must redo, or omit it to auto-select existing feature attempts in a resubmittable state (`Declined`, `In Review`, `Not Finished`, `Expired`). Never-attempted features are not auto-selected: list them explicitly in `nodes_to_resubmit`, or the request returns `400` when no recorded attempt needs resubmission. The selected steps' previous results move to the resubmission's history (returned by Get Decision with `include=resubmissions`), the steps are reordered to match the workflow graph, the session's expiration window restarts, and the original verification URL becomes usable again. Backend-only steps (`AML`, `DATABASE_VALIDATION`, `IP_ANALYSIS`) at the head of the list run immediately without user interaction; if every resubmitted step is backend-only, the session re-finalizes within the same request. `KYB_REGISTRY` and `KYB_KEY_PEOPLE` cannot be resubmitted directly: resubmit the relevant child KYC sessions instead and the parent business session recomputes automatically.\n\nEvery call that changes the status fires the `status.updated` [webhook](/integration/webhooks) once the change commits and appends an activity entry (with actor attribution and your `comment`) to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and in the console activity timeline. The user-profile aggregates linked to the session's `vendor_data` update as well. Declining disables ongoing AML monitoring for the session; re-approving re-enables it (from `Resubmitted`, `Declined`, or `Kyc Expired`) when the workflow has monitoring turned on. Approving or declining a child KYC that belongs to a KYB key-people check recomputes the parent business session's status. Set `send_email: true` (with `email_address`) to notify the user: a status notice for `Approved`/`Declined`, or a resubmission email containing the verification link and per-step reasons for `Resubmitted`. To check the email first, preview it with [Preview Decision Email](/sessions-api/preview-decision-email) (`Approved`, `Declined`) or [Preview Resubmission Email](/sessions-api/preview-resubmission) (`Resubmitted`) and pass its `preview_token` as `email_preview_token`: the update then sends exactly the previewed email, or returns `409` before changing anything when the email or the session changed since the preview or the receipt is older than 15 minutes.\n\nRepeating `Approved` or `Declined` on a session that already has that status changes nothing and returns `200`: no webhook, no activity entry and no email. Repeating `Resubmitted` returns `400` (`The new status is the same as the current status.`). Missing or invalid credentials and missing permissions return `403`; an expired scoped API key returns `401`.", + "operationId": "patch_v3_session_update_status", + "x-didit-sdk": { + "resource": "sessions", + "method": "update_status", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sessionId", + "required": true, + "description": "UUID of the verification session to update. Accepts both user (KYC) and business (KYB) session IDs — the service resolves the ID across both session types.", + "schema": { + "type": "string", + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "new_status" + ], + "properties": { + "new_status": { + "type": "string", + "description": "Target status. `Approved` and `Declined` record a final manual decision (each can also overturn the other). `Resubmitted` clears the selected steps and sends the session back to the user. Any other value returns `400`.", + "enum": [ + "Approved", + "Declined", + "Resubmitted" + ], + "example": "Approved" + }, + "comment": { + "type": "string", + "description": "Free-text reason for the change, stored on the session's review trail and returned in the `reviews` array of [Get Decision](#get-/v3/session/-sessionId-/decision/). For example `Duplicated user`.", + "example": "All checks passed manual review" + }, + "nodes_to_resubmit": { + "type": "array", + "description": "Workflow steps the user must redo. Only acted on when `new_status` is `Resubmitted`. For `Approved`/`Declined` the entries are still schema-validated (an invalid `feature` value returns `400`) but schema-valid entries are semantically ignored. If omitted, the server auto-selects existing OCR, Liveness, Face Match, POA, Phone, Email, AML, Database Validation, and Questionnaire attempts whose status is `Declined`, `In Review`, `Not Finished`, or `Expired` (NFC, IP analysis, age estimation, face search, and KYB document attempts are never auto-selected; non-face-match face attempts are selected as `LIVENESS`) — features the user never attempted (no recorded attempt) are NOT selected, so a session with zero attempts returns `400` (\"No features found that need resubmission\") even though nothing was approved; pass `nodes_to_resubmit` explicitly in that case. Steps are executed in workflow-graph order regardless of the order you send them. `KYB_REGISTRY`, `KYB_KEY_PEOPLE`, and the `KYB` alias are rejected with `400` — those parent checks recompute from their child KYC sessions.", + "items": { + "type": "object", + "required": [ + "node_id", + "feature" + ], + "properties": { + "node_id": { + "type": "string", + "description": "Node identifier as it appears in the session's workflow definition (for example `feature_ocr`, `feature_liveness`).", + "example": "feature_ocr" + }, + "feature": { + "type": "string", + "description": "Feature type of the node. The aliases `ID_VERIFICATION`, `POA`, `PHONE`, and `EMAIL` are normalized server-side to `OCR`, `PROOF_OF_ADDRESS`, `PHONE_VERIFICATION`, and `EMAIL_VERIFICATION`. `KYB_REGISTRY`, `KYB_KEY_PEOPLE`, and `KYB` pass schema validation but are always rejected with the business-rule `400` shown in the examples.", + "enum": [ + "OCR", + "OCR_BACK", + "NFC", + "AML", + "FACE", + "LIVENESS", + "FACE_MATCH", + "IP_ANALYSIS", + "AGE_ESTIMATION", + "PROOF_OF_ADDRESS", + "PHONE_VERIFICATION", + "EMAIL_VERIFICATION", + "FACE_SEARCH", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_DOCUMENTS", + "ID_VERIFICATION", + "POA", + "PHONE", + "EMAIL", + "KYB_REGISTRY", + "KYB_KEY_PEOPLE", + "KYB" + ], + "example": "OCR" + } + } + } + }, + "send_email": { + "type": "boolean", + "description": "Whether to email the user about the change. Requires `email_address`. For `Approved`/`Declined` the user receives a status notice; for `Resubmitted` the email includes the verification link and the per-step resubmission reasons.", + "default": false, + "example": false + }, + "email_address": { + "type": "string", + "format": "email", + "description": "Recipient for the notification email. **Required when `send_email` is `true`** — omitting it returns `400`.", + "example": "user@example.com" + }, + "email_language": { + "type": "string", + "description": "Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`).", + "default": "en", + "example": "en" + }, + "email_preview_token": { + "type": "string", + "writeOnly": true, + "minLength": 1, + "maxLength": 512, + "description": "Optional receipt from [Preview Decision Email](/sessions-api/preview-decision-email) (for `Approved` and `Declined`) or [Preview Resubmission Email](/sessions-api/preview-resubmission) (for `Resubmitted`): its `preview_token`. Valid only with `send_email: true`; without it the request returns `400`. The recipient, language, session status, sender and email (and, for a resubmission, the selected steps) must still match the preview, and the receipt expires after 15 minutes. Otherwise the request returns `409` before the session changes or an email is sent. Existing integrations may omit this field." + } + } + }, + "examples": { + "Approve Session": { + "summary": "Approve a session after manual review", + "value": { + "new_status": "Approved", + "comment": "All checks passed manual review" + } + }, + "Decline Session": { + "summary": "Decline a session", + "value": { + "new_status": "Declined", + "comment": "Suspected fraud" + } + }, + "Resubmit Specific Steps": { + "summary": "Request resubmission of chosen steps, with email", + "value": { + "new_status": "Resubmitted", + "nodes_to_resubmit": [ + { + "node_id": "feature_ocr", + "feature": "OCR" + }, + { + "node_id": "feature_liveness", + "feature": "LIVENESS" + } + ], + "send_email": true, + "email_address": "user@example.com", + "email_language": "en" + } + }, + "Resubmit Auto-select": { + "summary": "Request resubmission (auto-select non-approved steps)", + "value": { + "new_status": "Resubmitted" + } + }, + "Approve With Previewed Email": { + "summary": "Approve and send the email returned by Preview Decision Email", + "value": { + "new_status": "Approved", + "send_email": true, + "email_address": "person@example.com", + "email_language": "en", + "email_preview_token": "eyJkaWdlc3QiOiI0ZjJhIn0:1uExAm:Pl3x4mpleS1gnatureValue000000000000000" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Status updated. The response contains only the `session_id`; fetch the updated session via [Get Decision](#get-/v3/session/-sessionId-/decision/). The `status.updated` webhook fires once the change commits. Repeating `Approved` or `Declined` returns the same body and changes nothing.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "session_id": { + "type": "string", + "format": "uuid", + "description": "UUID of the updated session." + } + } + }, + "examples": { + "Updated": { + "summary": "Returned for Approved, Declined, and Resubmitted alike", + "value": { + "session_id": "3472fb7c-8f7c-4d1a-9cf4-cf3d74ce1a60" + } + } + } + } + } + }, + "400": { + "description": "Validation error. Field errors arrive as `{\"\": [\"\"]}` (some business-rule rejections use a string value instead: `{\"nodes_to_resubmit\": \"\"}`), cross-state errors as `{\"detail\": \"\"}`, and the same-status error as a bare JSON array.", + "content": { + "application/json": { + "examples": { + "Wrong Current Status": { + "summary": "Session is not in an updatable state", + "value": { + "detail": "Only sessions with status Approved, Declined, In Review, Kyc Expired, Abandoned, Resubmitted can be updated. Current status: In Progress" + } + }, + "Invalid Status Value": { + "summary": "new_status is a real session status but not one of Approved/Declined/Resubmitted", + "value": { + "new_status": [ + "You can only update the status to 'Approved', 'Declined', or 'Resubmitted'." + ] + } + }, + "Missing new_status": { + "summary": "Required field omitted", + "value": { + "new_status": [ + "This field is required." + ] + } + }, + "Same Status": { + "summary": "Resubmitted requested on a session that is already Resubmitted", + "value": [ + "The new status is the same as the current status." + ] + }, + "Email Address Required": { + "summary": "send_email true without email_address", + "value": { + "email_address": [ + "Email address is required when send_email is true." + ] + } + }, + "Non-resubmittable Feature": { + "summary": "Resubmit targets a KYB parent check", + "value": { + "nodes_to_resubmit": "The following features cannot be resubmitted directly: KYB_REGISTRY. Resubmit the relevant child KYC sessions instead; the parent will recompute automatically." + } + }, + "Nothing To Resubmit": { + "summary": "Auto-select found no non-approved features", + "value": { + "nodes_to_resubmit": "No features found that need resubmission. All features are already approved." + } + }, + "Invalid Feature Choice": { + "summary": "Unknown feature in nodes_to_resubmit", + "value": { + "nodes_to_resubmit": [ + { + "feature": [ + "\"BOGUS\" is not a valid choice. Valid choices are: OCR, OCR_BACK, NFC, AML, FACE, LIVENESS, FACE_MATCH, IP_ANALYSIS, AGE_ESTIMATION, PROOF_OF_ADDRESS, PHONE_VERIFICATION, EMAIL_VERIFICATION, FACE_SEARCH, DATABASE_VALIDATION, QUESTIONNAIRE, KYB_REGISTRY, KYB_DOCUMENTS, KYB_KEY_PEOPLE, ID_VERIFICATION, POA, PHONE, EMAIL, KYB" + ] + } + ] + } + }, + "Invalid Status Value (not a status at all)": { + "summary": "new_status is not a valid StatusChoices value", + "value": { + "new_status": [ + "\"Bogus\" is not a valid choice." + ] + } + }, + "Preview Receipt Without Email": { + "summary": "email_preview_token sent without send_email: true", + "value": { + "email_preview_token": [ + "A preview receipt is only valid when the email is sent." + ] + } + } + } + } + } + }, + "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/invalid credentials or insufficient permissions, 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": "API key lacks write:sessions", + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No KYC or KYB session with this `session_id` exists (deleted sessions also return `404`).", + "content": { + "application/json": { + "examples": { + "Not Found": { + "summary": "Unknown session_id", + "value": { + "detail": "Not found." + } + } + } + } + } + }, + "429": { + "description": "A resubmit targeting only backend-only features (AML, DATABASE_VALIDATION, IP_ANALYSIS) was repeated within the 30-second cooldown window; an immediate repeat cannot change the outcome. Back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Backend-only resubmit throttled": { + "value": { + "detail": "This session was already resubmitted for backend-only features within the last 30 seconds; repeating it cannot change the outcome. Wait before resubmitting again." + } + } + } + } + } + }, + "409": { + "description": "The email preview changed or expired. Generate a new preview before confirming; no session changes or email delivery occur.", + "content": { + "application/json": { + "examples": { + "Preview Changed": { + "summary": "The receipt no longer matches, or is older than 15 minutes", + "value": { + "detail": "This email preview changed or expired. Preview the message again before confirming." + } + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-status/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"new_status\": \"Approved\",\n \"comment\": \"All checks passed manual review\"\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresponse = requests.patch(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-status/\",\n headers={\n \"x-api-key\": \"YOUR_API_KEY\",\n \"Content-Type\": \"application/json\",\n },\n json={\n \"new_status\": \"Resubmitted\",\n \"nodes_to_resubmit\": [\n {\"node_id\": \"feature_ocr\", \"feature\": \"OCR\"},\n {\"node_id\": \"feature_liveness\", \"feature\": \"LIVENESS\"},\n ],\n \"send_email\": True,\n \"email_address\": \"user@example.com\",\n \"email_language\": \"en\",\n },\n)\nresponse.raise_for_status()\nprint(response.json()) # {'session_id': '11111111-2222-3333-4444-555555555555'}" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const response = await fetch(\n 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-status/',\n {\n method: 'PATCH',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n new_status: 'Declined',\n comment: 'Suspected fraud',\n }),\n },\n);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst { session_id } = await response.json();" + } + ] + } + }, + "/v3/id-verification/": { + "post": { + "summary": "ID Verification (document OCR + fraud checks)", + "description": "OCR an identity document and run document fraud checks in one call — returns the extracted holder fields, the parsed MRZ, an `Approved`/`Declined` `status`, and a `warnings` list explaining every issue found. Supports 14,000+ document types from 220+ countries and territories.\n\n**How it works.** Send the document's `front_image` (required) and, for two-sided documents, the `back_image`. Upload uncropped photos with all four corners of the document visible — the service detects, aligns, and crops the document itself, so do not pre-crop. The document is classified automatically (you never declare the country or document type), the visual zone is read with OCR, and the MRZ and any barcodes are decoded. Cross-checks then validate dates, number formats, and MRZ check digits, and compare the visual zone against the MRZ. With `perform_document_liveness=true`, the images are additionally screened for screen replays, printed copies, and portrait manipulation.\n\n**Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Fraud and hard-failure risks always decline: `DOCUMENT_EXPIRED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, `PUBLIC_DOCUMENT_IMAGE_DETECTED`, plus extraction failures (`NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`, `DOCUMENT_NUMBER_NOT_DETECTED`). (`PORTRAIT_IMAGE_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `INVALID_DATE`, and `MRZ_NOT_DETECTED` exist in workflow sessions but are never produced by this standalone endpoint.) Three risk groups are configurable per request via `invalid_mrz_action`, `inconsistent_data_action`, and `expiration_date_not_detected_action` (`DECLINE` or `NO_ACTION`). All other warnings (e.g. `POSSIBLE_DUPLICATED_USER`) are informational and never decline on their own. A document that cannot be processed at all returns `400` with `{\"error\": \"COULD_NOT_RECOGNIZE_DOCUMENT\"}`; a readable but problematic document returns `200` with `status: \"Declined\"` — always inspect `id_verification.status` and `id_verification.warnings`, not just the HTTP code.\n\n**Billing.** Each `200` response consumes one ID Verification API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing.\n\n**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the cropped document/portrait images are stored and returned as short-lived media URLs (`https:///ocr/...`), and a `status.updated` webhook is emitted to your configured webhook endpoints. When `false`, nothing is stored, `request_id` is a one-off correlation UUID, and `portrait_image`/`front_image`/`back_image` are returned inline as base64-encoded JPEG strings.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (a fictional \"Sandbox User\" USA identity card), no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", + "operationId": "post_v3id-verification", + "x-didit-sdk": { + "resource": "id_verification", + "method": "verify", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" + ], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "front_image" + ], + "properties": { + "front_image": { + "type": "string", + "format": "binary", + "description": "Front side of the identity document. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`, `pdf`. Maximum upload size: **10 MB** (larger files are rejected with `400`). PDFs are rendered to an image before OCR; encrypted PDFs require `front_image_password`. Images are automatically compressed to ~0.5 MB and EXIF orientation is applied. Upload the full, uncropped capture with all four corners of the document visible." + }, + "front_image_password": { + "type": "string", + "description": "Password to decrypt `front_image` when it is an encrypted PDF. Sending an encrypted PDF without (or with a wrong) password returns `400`.", + "writeOnly": true + }, + "back_image": { + "type": "string", + "format": "binary", + "description": "Back side of the document — send it whenever the document has one (many ID cards carry the MRZ or a barcode on the back; omitting it simply leaves the MRZ and back-side fields empty in the response — this endpoint does not raise a missing-MRZ warning). Same format and size limits as `front_image`. When omitted, `back_image` and back-side fields are `null` in the response." + }, + "back_image_password": { + "type": "string", + "description": "Password to decrypt `back_image` when it is an encrypted PDF.", + "writeOnly": true + }, + "perform_document_liveness": { + "type": "boolean", + "default": true, + "description": "Screens both images for presentation fraud: screen replays (`SCREEN_CAPTURE_DETECTED`), printed copies (`PRINTED_COPY_DETECTED`), and portrait manipulation (`PORTRAIT_MANIPULATION_DETECTED`). Any of these auto-declines. Enabled by default; set to `false` to skip fraud screening and reduce latency.", + "example": true + }, + "minimum_age": { + "type": "integer", + "nullable": true, + "default": null, + "minimum": 1, + "maximum": 120, + "description": "Accepted and validated (1–120) but **currently not applied** by this endpoint — it never produces a `MINIMUM_AGE_NOT_MET` warning from this field. Enforce age rules from the returned `date_of_birth`/`age`, or use a verification-session workflow with age restrictions." + }, + "expiration_date_not_detected_action": { + "type": "string", + "enum": [ + "NO_ACTION", + "DECLINE" + ], + "default": "NO_ACTION", + "description": "What to do when no expiration date can be read (`EXPIRATION_DATE_NOT_DETECTED`). Default `NO_ACTION` because many identity documents print no expiry date. Note: a *detected and past* expiry date always declines (`DOCUMENT_EXPIRED`), regardless of this option." + }, + "invalid_mrz_action": { + "type": "string", + "enum": [ + "NO_ACTION", + "DECLINE" + ], + "default": "DECLINE", + "description": "What to do when the extracted MRZ fails check-digit validation (`MRZ_VALIDATION_FAILED`). A missing MRZ raises no warning on this endpoint. Only relevant for documents that carry an MRZ. `REVIEW` is not supported on this endpoint and returns `400`." + }, + "inconsistent_data_action": { + "type": "string", + "enum": [ + "NO_ACTION", + "DECLINE" + ], + "default": "DECLINE", + "description": "What to do when extracted data is internally inconsistent: `DATA_INCONSISTENT`, `MRZ_AND_DATA_EXTRACTED_FROM_OCR_NOT_SAME` (visual zone disagrees with the MRZ), or `DOCUMENT_NAME_DIFFERENT_FROM_OTHER_APPROVED_DOCUMENTS`." + }, + "critical_field_occlusion_action": { + "type": "string", + "enum": [ + "NO_ACTION", + "DECLINE" + ], + "default": "DECLINE", + "description": "What to do when a critical printed field was physically covered on the capture (`CRITICAL_FIELD_OCCLUSION_DETECTED`) - an identity number, a name, the date of birth, or the expiry date. Declines by default, like this endpoint's other document-integrity risks: covering the numbers a report is reconciled against is what a redacted document looks like. The value read for a covered field is dropped from the extraction either way, so the matching `*_NOT_DETECTED` warning usually accompanies it. `REVIEW` is not supported on this endpoint and returns `400`." + }, + "document_or_personal_number_format_mismatch_action": { + "type": "string", + "enum": [ + "NO_ACTION", + "DECLINE" + ], + "default": "NO_ACTION", + "description": "What to do when a number does not match the format its issuing authority uses for that document type (`DOCUMENT_NUMBER_FORMAT_MISMATCH`, `PERSONAL_NUMBER_FORMAT_MISMATCH`). Default `NO_ACTION`, which reports the warning without changing the status. `REVIEW` is not supported on this endpoint and returns `400`." + }, + "preferred_characters": { + "type": "string", + "enum": [ + "latin", + "non_latin" + ], + "default": "latin", + "description": "Preferred script for name/address fields on documents that print both Latin and non-Latin text (Arabic, Cyrillic, CJK, …). `latin` returns transliterated/Latin values; `non_latin` prefers the native script." + }, + "save_api_request": { + "type": "boolean", + "default": true, + "description": "When `true` (default), persists the call as an API-type session — visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with document images returned as media URLs. When `false`, nothing is stored, `request_id` is a transient UUID, and images are returned inline as base64 JPEG strings.", + "example": true + }, + "vendor_data": { + "type": "string", + "description": "Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later.", + "example": "user-123" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={\"flow\":\"onboarding\"}`) — it is parsed into an object.", + "example": { + "flow": "onboarding" + } + } + } + }, + "example": { + "front_image": "(binary JPEG/PNG/PDF of the document front)", + "back_image": "(binary JPEG/PNG/PDF of the document back)", + "perform_document_liveness": true, + "save_api_request": true, + "vendor_data": "user-123", + "metadata": { + "flow": "onboarding" + } + } + } + } + }, + "responses": { + "200": { + "description": "Document processed. `id_verification.status` is `Approved` or `Declined`; every detected issue is itemized in `id_verification.warnings`. A problematic document still returns `200` with `status: \"Declined\"` — inspect the body, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id and image fields are short-lived media URLs; with `save_api_request=false` they are inline base64 JPEG strings.", + "content": { + "application/json": { + "examples": { + "Approved": { + "summary": "Two-sided ID card read cleanly (saved — images as media URLs)", + "value": { + "request_id": "11d219ed-d59c-4b1d-8d65-9b933f12d5b8", + "id_verification": { + "status": "Approved", + "document_type": "Identity Card", + "document_subtype": "ID_CARD_GENERIC", + "document_number": "CBX164224", + "personal_number": "20446581H", + "portrait_image": "https:///ocr/11d219ed-d59c-4b1d-8d65-9b933f12d5b8-portrait_image-8c2f.jpg?signature=...", + "front_image": "https:///ocr/11d219ed-d59c-4b1d-8d65-9b933f12d5b8-front_image-4a1e.jpg?signature=...", + "back_image": "https:///ocr/11d219ed-d59c-4b1d-8d65-9b933f12d5b8-back_image-77b0.jpg?signature=...", + "front_image_camera_front": null, + "back_image_camera_front": null, + "front_image_camera_front_face_match_score": null, + "back_image_camera_front_face_match_score": null, + "front_image_quality_score": { + "focus_score": 78.0, + "brightness_score": 83.2, + "brightness_issue": "ok", + "is_document_fully_visible": true, + "resolution_score": 40.7, + "overall_score": 70.2 + }, + "back_image_quality_score": { + "focus_score": 100.0, + "brightness_score": 95.6, + "brightness_issue": "ok", + "is_document_fully_visible": true, + "resolution_score": 43.0, + "overall_score": 84.4 + }, + "date_of_birth": "1980-01-13", + "age": 46, + "expiration_date": "2032-03-31", + "date_of_issue": "2022-03-31", + "issuing_state": "ESP", + "issuing_state_name": "Spain", + "first_name": "Julio Francisco", + "last_name": "Fores Sena", + "full_name": "Julio Francisco Fores Sena", + "gender": "M", + "address": "Brda. Urb. El Cardonal 0052 52 Po3 B,Taco,San Cristobal De La Laguna,Santa Cruz De Tenerife", + "formatted_address": "Av. el Cardonal, 52, b, 38108 La Laguna, Santa Cruz de Tenerife, Spain", + "place_of_birth": "Valencia, Valencia", + "marital_status": "UNKNOWN", + "nationality": "ESP", + "extra_fields": { + "first_surname": "Fores", + "second_surname": "Sena" + }, + "mrz": { + "surname": "FORES SENA", + "name": "JULIO FRANCISCO", + "country": "ESP", + "nationality": "ESP", + "birth_date": "800113", + "expiry_date": "320331", + "sex": "M", + "document_type": "ID", + "document_number": "CBX164224", + "optional_data": "20446581H", + "optional_data_2": "", + "birth_date_hash": "9", + "expiry_date_hash": "8", + "document_number_hash": "3", + "final_hash": "3", + "personal_number": "20446581H", + "warnings": [], + "errors": [], + "mrz_type": "TD1", + "mrz_string": "IDESPCBX164224320446581H<<<<<<\n8001139M3203318ESP<<<<<<<<<<<3\nFORES/ocr/f61681b8-9422-4378-ac3e-04c6a247fb45-portrait_image-1d9a.jpg?signature=...", + "front_image": "https:///ocr/f61681b8-9422-4378-ac3e-04c6a247fb45-front_image-5c2b.jpg?signature=...", + "back_image": null, + "front_image_camera_front": null, + "back_image_camera_front": null, + "front_image_camera_front_face_match_score": null, + "back_image_camera_front_face_match_score": null, + "front_image_quality_score": { + "focus_score": 100.0, + "brightness_score": 95.7, + "brightness_issue": "ok", + "is_document_fully_visible": true, + "resolution_score": 22.2, + "overall_score": 79.3 + }, + "back_image_quality_score": null, + "date_of_birth": "1997-01-30", + "age": 29, + "expiration_date": "2025-11-26", + "date_of_issue": null, + "issuing_state": "ESP", + "issuing_state_name": "Spain", + "first_name": "Lucia", + "last_name": "Marin Castro", + "full_name": "Lucia Marin Castro", + "gender": "F", + "address": null, + "formatted_address": null, + "place_of_birth": null, + "marital_status": "UNKNOWN", + "nationality": "ESP", + "extra_fields": { + "first_surname": "Marin", + "second_surname": "Castro" + }, + "mrz": {}, + "parsed_address": null, + "warnings": [ + { + "risk": "DOCUMENT_EXPIRED", + "feature": "ID_VERIFICATION", + "additional_data": null, + "log_type": "error", + "short_description": "Document expired", + "long_description": "The document's expiration date has passed, rendering it no longer valid for use." + } + ], + "barcodes": [] + }, + "vendor_data": "user-123", + "metadata": null, + "created_at": "2026-06-12T02:25:48.117204+00:00" + } + } + }, + "schema": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." + }, + "id_verification": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "Approved", + "Declined" + ], + "description": "`Approved` when no warning resolves to a decline; `Declined` otherwise — the `warnings` list explains why." + }, + "document_type": { + "type": "string", + "nullable": true, + "description": "Detected document type display name: `Identity Card`, `Passport`, `Driver's License`, `Residence Permit`, `Health Insurance Card`, or `Tax Card`. `null` when the document could not be classified.", + "example": "Identity Card" + }, + "document_subtype": { + "type": "string", + "nullable": true, + "description": "Finer-grained subtype when available (e.g. `ID_CARD_GENERIC`, `EPASSPORT`).", + "example": "ID_CARD_GENERIC" + }, + "document_number": { + "type": "string", + "nullable": true, + "example": "CBX164224" + }, + "personal_number": { + "type": "string", + "nullable": true, + "description": "Secondary personal/national number printed on some documents.", + "example": "20446581H" + }, + "portrait_image": { + "type": "string", + "nullable": true, + "description": "Cropped portrait photo from the document. A short-lived media URL (`https:///ocr/...`) when `save_api_request=true` (default); an inline base64-encoded JPEG string when `save_api_request=false`. `null` when no portrait was detected." + }, + "front_image": { + "type": "string", + "nullable": true, + "description": "Aligned crop of the document front. Media URL when saved, inline base64 JPEG otherwise." + }, + "back_image": { + "type": "string", + "nullable": true, + "description": "Aligned crop of the document back. `null` when no `back_image` was uploaded." + }, + "front_image_camera_front": { + "type": "string", + "nullable": true, + "description": "Always `null` on this endpoint (populated only in camera-capture verification sessions)." + }, + "back_image_camera_front": { + "type": "string", + "nullable": true, + "description": "Always `null` on this endpoint." + }, + "front_image_camera_front_face_match_score": { + "type": "number", + "nullable": true, + "description": "Always `null` on this endpoint." + }, + "back_image_camera_front_face_match_score": { + "type": "number", + "nullable": true, + "description": "Always `null` on this endpoint." + }, + "front_image_quality_score": { + "type": "object", + "nullable": true, + "description": "Capture-quality diagnostics for the uploaded image (`null` when the side was not provided or scoring failed). All scores are 0–100.", + "properties": { + "focus_score": { + "type": "number", + "format": "float", + "description": "Sharpness of the document crop (higher is sharper)." + }, + "brightness_score": { + "type": "number", + "format": "float", + "description": "Exposure quality (higher is better)." + }, + "brightness_issue": { + "type": "string", + "nullable": true, + "description": "`ok` when exposure is fine, otherwise the detected issue (e.g. too dark / too bright)." + }, + "is_document_fully_visible": { + "type": "boolean", + "nullable": true, + "description": "Whether all four corners of the document are inside the frame." + }, + "resolution_score": { + "type": "number", + "format": "float", + "description": "Effective resolution of the document area." + }, + "overall_score": { + "type": "number", + "format": "float", + "description": "Aggregate quality score." + } + } + }, + "back_image_quality_score": { + "type": "object", + "nullable": true, + "description": "Capture-quality diagnostics for the uploaded image (`null` when the side was not provided or scoring failed). All scores are 0–100.", + "properties": { + "focus_score": { + "type": "number", + "format": "float", + "description": "Sharpness of the document crop (higher is sharper)." + }, + "brightness_score": { + "type": "number", + "format": "float", + "description": "Exposure quality (higher is better)." + }, + "brightness_issue": { + "type": "string", + "nullable": true, + "description": "`ok` when exposure is fine, otherwise the detected issue (e.g. too dark / too bright)." + }, + "is_document_fully_visible": { + "type": "boolean", + "nullable": true, + "description": "Whether all four corners of the document are inside the frame." + }, + "resolution_score": { + "type": "number", + "format": "float", + "description": "Effective resolution of the document area." + }, + "overall_score": { + "type": "number", + "format": "float", + "description": "Aggregate quality score." + } + } + }, + "date_of_birth": { + "type": "string", + "format": "date", + "nullable": true, + "example": "1980-01-13" + }, + "age": { + "type": "integer", + "nullable": true, + "description": "Holder age in years, computed from `date_of_birth`.", + "example": 46 + }, + "expiration_date": { + "type": "string", + "format": "date", + "nullable": true, + "example": "2032-03-31" + }, + "date_of_issue": { + "type": "string", + "format": "date", + "nullable": true, + "example": "2022-03-31" + }, + "issuing_state": { + "type": "string", + "nullable": true, + "description": "Issuing country (ISO 3166-1 alpha-3).", + "example": "ESP" + }, + "issuing_state_name": { + "type": "string", + "nullable": true, + "example": "Spain" + }, + "first_name": { + "type": "string", + "nullable": true, + "example": "Julio Francisco" + }, + "last_name": { + "type": "string", + "nullable": true, + "example": "Fores Sena" + }, + "full_name": { + "type": "string", + "nullable": true, + "example": "Julio Francisco Fores Sena" + }, + "gender": { + "type": "string", + "nullable": true, + "description": "`M`, `F`, or `null` when not printed/detected.", + "example": "M" + }, + "address": { + "type": "string", + "nullable": true, + "description": "Address exactly as printed on the document." + }, + "formatted_address": { + "type": "string", + "nullable": true, + "description": "Geocoded, normalized version of `address`." + }, + "place_of_birth": { + "type": "string", + "nullable": true, + "example": "Valencia, Valencia" + }, + "marital_status": { + "type": "string", + "nullable": true, + "description": "`UNKNOWN` unless the document prints marital status.", + "example": "UNKNOWN" + }, + "nationality": { + "type": "string", + "nullable": true, + "description": "Holder nationality (ISO 3166-1 alpha-3).", + "example": "ESP" + }, + "extra_fields": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Document-specific extras that have no dedicated column (e.g. `first_surname`/`second_surname` on Spanish IDs, license classes on driving licenses)." + }, + "mrz": { + "type": "object", + "description": "Parsed machine-readable zone. An **empty object** (`{}`) when the document has no MRZ or it could not be read — also check the `warnings` list in that case. Dates inside the MRZ use the raw `YYMMDD` format.", + "properties": { + "surname": { + "type": "string", + "example": "FORES SENA" + }, + "name": { + "type": "string", + "example": "JULIO FRANCISCO" + }, + "country": { + "type": "string", + "description": "Issuing country (ISO 3166-1 alpha-3).", + "example": "ESP" + }, + "nationality": { + "type": "string", + "description": "Holder nationality (ISO 3166-1 alpha-3).", + "example": "ESP" + }, + "birth_date": { + "type": "string", + "description": "Raw MRZ date of birth (`YYMMDD`).", + "example": "800113" + }, + "expiry_date": { + "type": "string", + "description": "Raw MRZ expiry date (`YYMMDD`).", + "example": "320331" + }, + "sex": { + "type": "string", + "example": "M" + }, + "document_type": { + "type": "string", + "description": "MRZ document-type code (e.g. `ID`, `P`).", + "example": "ID" + }, + "document_number": { + "type": "string", + "example": "CBX164224" + }, + "optional_data": { + "type": "string", + "nullable": true, + "example": "20446581H" + }, + "optional_data_2": { + "type": "string", + "nullable": true, + "example": "" + }, + "birth_date_hash": { + "type": "string", + "description": "MRZ check digit for the birth date.", + "example": "9" + }, + "expiry_date_hash": { + "type": "string", + "description": "MRZ check digit for the expiry date.", + "example": "8" + }, + "document_number_hash": { + "type": "string", + "description": "MRZ check digit for the document number.", + "example": "3" + }, + "final_hash": { + "type": "string", + "description": "Composite MRZ check digit.", + "example": "3" + }, + "personal_number": { + "type": "string", + "nullable": true, + "example": "20446581H" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Non-fatal MRZ parsing warnings." + }, + "errors": { + "type": "array", + "items": { + "type": "string" + }, + "description": "MRZ check-digit/parsing errors. Non-empty errors surface as an `MRZ_VALIDATION_FAILED` warning." + }, + "mrz_type": { + "type": "string", + "description": "MRZ layout: `TD1` (ID cards, 3 lines), `TD2`, or `TD3` (passports, 2 lines).", + "example": "TD1" + }, + "mrz_string": { + "type": "string", + "description": "Raw MRZ text as read, lines separated by `\\n`." + }, + "mrz_key": { + "type": "string", + "description": "Composite key (document number + check digits) used for cross-checks." + } + } + }, + "parsed_address": { + "type": "object", + "nullable": true, + "description": "Structured, geocoded breakdown of `address`. `null` when the document carries no address or it could not be parsed.", + "properties": { + "street_1": { + "type": "string", + "nullable": true, + "example": "Avenida el Cardonal 52" + }, + "street_2": { + "type": "string", + "nullable": true, + "example": "b" + }, + "city": { + "type": "string", + "nullable": true, + "example": "La Laguna" + }, + "region": { + "type": "string", + "nullable": true, + "example": "Canarias" + }, + "country": { + "type": "string", + "nullable": true, + "description": "ISO 3166-1 alpha-2 country code.", + "example": "ES" + }, + "postal_code": { + "type": "string", + "nullable": true, + "example": "38108" + }, + "address_type": { + "type": "string", + "nullable": true, + "example": "Avenida" + }, + "formatted_address": { + "type": "string", + "nullable": true, + "description": "Canonical geocoded address string.", + "example": "Av. el Cardonal, 52, b, 38108 La Laguna, Santa Cruz de Tenerife, Spain" + }, + "raw_results": { + "type": "object", + "additionalProperties": true, + "description": "Raw geocoding payload (`address_components`, `geometry.location`, `place_id`, …) for advanced consumers." + }, + "document_location": { + "type": "object", + "nullable": true, + "properties": { + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + } + }, + "description": "Geocoded coordinates of the document address." + }, + "label": { + "type": "string", + "nullable": true, + "description": "Which document field the address was read from.", + "example": "Spain Identity Card Address" + }, + "is_verified": { + "type": "boolean", + "description": "Whether geocoding confirmed the address exists." + }, + "category": { + "type": "string", + "nullable": true, + "description": "Address category (e.g. `Residential`, `Commercial`).", + "example": "Residential" + } + } + }, + "warnings": { + "type": "array", + "description": "Empty on a clean approval. Every entry explains one detected risk; entries with `log_type: \"error\"` set `status` to `Declined`.", + "items": { + "type": "object", + "properties": { + "risk": { + "type": "string", + "description": "Machine-readable risk code. Always-decline risks: `DOCUMENT_EXPIRED`, `MINIMUM_AGE_NOT_MET`, `PORTRAIT_IMAGE_NOT_DETECTED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, `PUBLIC_DOCUMENT_IMAGE_DETECTED`, `INVALID_DATE`, `COULD_NOT_RECOGNIZE_DOCUMENT` (400-only, never a warning), `DOCUMENT_NUMBER_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`. Governed by request options: `MRZ_NOT_DETECTED` / `MRZ_VALIDATION_FAILED` (`invalid_mrz_action`), `DATA_INCONSISTENT` / `MRZ_AND_DATA_EXTRACTED_FROM_OCR_NOT_SAME` / `DOCUMENT_NAME_DIFFERENT_FROM_OTHER_APPROVED_DOCUMENTS` (`inconsistent_data_action`), `EXPIRATION_DATE_NOT_DETECTED` (`expiration_date_not_detected_action`). Other risks (e.g. `POSSIBLE_DUPLICATED_USER`) are informational.", + "example": "DOCUMENT_EXPIRED" + }, + "feature": { + "type": "string", + "enum": [ + "ID_VERIFICATION" + ], + "description": "Feature that raised the warning. Always `ID_VERIFICATION` on this endpoint." + }, + "additional_data": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Extra context for some risks (e.g. `POSSIBLE_DUPLICATED_USER` includes `duplicated_session_id`); `null` for most." + }, + "log_type": { + "type": "string", + "enum": [ + "error", + "warning", + "information" + ], + "description": "Severity. `error` warnings drive `status` to `Declined`; `information` entries are advisory and never decline on their own. (`warning` is not produced by this endpoint — its options only accept `DECLINE` or `NO_ACTION`.)" + }, + "short_description": { + "type": "string", + "description": "Human-readable one-line summary of the risk." + }, + "long_description": { + "type": "string", + "description": "Human-readable explanation of the risk." + } + } + } + }, + "barcodes": { + "type": "array", + "description": "Barcodes decoded from the document (PDF417, QR, …). Empty when the document has none or none could be read.", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Barcode type (e.g. `PDF417`, `QR_CODE`, `UNKNOWN`)." + }, + "position": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Location of the barcode in the image, when available." + }, + "data": { + "type": "string", + "description": "Decoded payload." + }, + "data_raw": { + "type": "string", + "description": "Raw (undecoded) payload, when different." + }, + "side": { + "type": "string", + "nullable": true, + "description": "`front` or `back`." + } + } + } + } + } + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Echo of the `vendor_data` you sent, or `null`." + }, + "metadata": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Echo of the `metadata` object you sent, or `null`." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp (UTC) of when the response was generated." + } + } + } + } + } + }, + "400": { + "description": "Validation or processing error. Field-level problems return DRF's standard envelope (one array of messages per offending field). Encrypted-PDF problems return `{\"detail\": ...}`. A document image in which no document can be recognized at all returns `{\"error\": \"COULD_NOT_RECOGNIZE_DOCUMENT\"}`. PDF uploads can additionally fail with \"The submitted PDF file is corrupted or invalid.\" or a no-pages error.", + "content": { + "application/json": { + "examples": { + "Missing front image": { + "summary": "`front_image` not included in the form data", + "value": { + "front_image": [ + "No file was submitted." + ] + } + }, + "Unsupported file extension": { + "summary": "File extension outside tiff/jpg/jpeg/png/webp/pdf", + "value": { + "front_image": [ + "File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, webp, pdf." + ] + } + }, + "File too large": { + "summary": "Upload exceeds the 10 MB limit", + "value": { + "front_image": [ + "File size should not exceed 10 MB" + ] + } + }, + "Invalid option value": { + "summary": "Options only accept DECLINE or NO_ACTION", + "value": { + "invalid_mrz_action": [ + "\"REVIEW\" is not a valid choice." + ] + } + }, + "Encrypted PDF": { + "summary": "PDF uploaded without the required password", + "value": { + "detail": "The PDF is encrypted. Please upload a decrypted PDF or a photo instead." + } + }, + "Wrong PDF password": { + "summary": "`front_image_password` / `back_image_password` does not decrypt the PDF", + "value": { + "detail": "The PDF password is incorrect. Please provide the correct password." + } + }, + "Document not recognized": { + "summary": "No identity document could be detected in the image", + "value": { + "error": "COULD_NOT_RECOGNIZE_DOCUMENT" + } + }, + "PDF conversion failure": { + "summary": "Uploaded PDF could not be converted to an image", + "value": { + "error": "Failed to convert front image PDF." + } + } + } + } + } + }, + "403": { + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any image processing happens.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Not enough credits": { + "summary": "Organization balance cannot cover the call", + "value": { + "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "content": { + "application/json": { + "examples": { + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X POST 'https://verification.didit.me/v3/id-verification/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'front_image=@./id_front.jpg' \\\n -F 'back_image=@./id_back.jpg' \\\n -F 'perform_document_liveness=true' \\\n -F 'save_api_request=true' \\\n -F 'vendor_data=user-123'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nurl = 'https://verification.didit.me/v3/id-verification/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('id_front.jpg', 'rb') as front_f, open('id_back.jpg', 'rb') as back_f:\n files = {\n 'front_image': ('id_front.jpg', front_f, 'image/jpeg'),\n 'back_image': ('id_back.jpg', back_f, 'image/jpeg'),\n }\n data = {\n 'perform_document_liveness': 'true',\n 'save_api_request': 'true',\n 'vendor_data': 'user-123',\n }\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=120)\n\nresp.raise_for_status()\nid_v = resp.json()['id_verification']\nprint('status:', id_v['status'])\nprint('name:', id_v.get('full_name'))\nprint('document:', id_v.get('document_type'), id_v.get('document_number'))\nfor w in id_v.get('warnings', []):\n print('warning:', w['risk'], '-', w['log_type'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('front_image', new Blob([fs.readFileSync('./id_front.jpg')]), 'id_front.jpg');\nform.append('back_image', new Blob([fs.readFileSync('./id_back.jpg')]), 'id_back.jpg');\nform.append('perform_document_liveness', 'true');\nform.append('save_api_request', 'true');\nform.append('vendor_data', 'user-123');\n\nconst response = await fetch('https://verification.didit.me/v3/id-verification/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n});\n\nif (!response.ok) throw new Error(`ID verification failed: ${response.status}`);\nconst body = await response.json();\nconsole.log('status:', body.id_verification.status);\nconsole.log('warnings:', body.id_verification.warnings.map((w) => w.risk));" + } + ] + } + }, + "/v3/face-match/": { + "post": { + "summary": "Face Match", + "description": "Compare two face images (1:1) and get back a similarity `score` (0–100) plus an `Approved`/`Declined` `status`.\n\n**How it works.** Both images are decoded and analyzed by a biometric model (set `rotate_image=true` if captures may be sideways or upside down), then a similarity score between the two faces is computed. A score **strictly above** `face_match_score_decline_threshold` (default `30`) returns `status: \"Approved\"`; a score at or below it returns `Declined` with a `LOW_FACE_MATCH_SIMILARITY` warning. If no face pair can be scored at all, the call still returns `200` with `status: \"Declined\"` and a `NO_REFERENCE_IMAGE` warning — `score` is `null` on the default save path, or `0` with `save_api_request=false`. Submit one clear, front-facing face per image for best results.\n\n**Billing.** Each `200` response consumes one Face Match API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing.\n\n**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, both images are stored as the session's face-match source/target images, and a `status.updated` webhook is emitted to your configured webhook endpoints. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later.\n\n**Face search index.** This endpoint never enrolls faces into your face search index and never runs blocklist or duplicate screening — use `POST /v3/passive-liveness/` or `POST /v3/face-search/` for those.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", + "operationId": "post_v3face-match", + "x-didit-sdk": { + "resource": "face_match", + "method": "compare", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" + ], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "user_image", + "ref_image" + ], + "properties": { + "user_image": { + "type": "string", + "format": "binary", + "description": "Live or candidate face image to verify (e.g. a selfie). Submit a single front-facing photo with the subject's face clearly visible and well lit. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy." + }, + "ref_image": { + "type": "string", + "format": "binary", + "description": "Reference face image to compare against (e.g. an ID document portrait or a previously enrolled photo). Same format and size limits as `user_image`." + }, + "face_match_score_decline_threshold": { + "type": "number", + "format": "float", + "default": 30, + "minimum": 0, + "maximum": 100, + "description": "Similarity threshold (0–100) that drives the response `status`. A computed score **at or below** this value sets `status` to `Declined` and adds a `LOW_FACE_MATCH_SIMILARITY` warning; a score strictly above it returns `Approved`. Default `30` is permissive — raise it (e.g. `50`–`60`) for stricter verification. Values outside 0–100 return `400`.", + "example": 50 + }, + "rotate_image": { + "type": "boolean", + "default": false, + "description": "When `true`, the service tries the input images in 90-degree increments (0, 90, 180, 270) and uses the orientation that yields the best face detection — useful for mobile captures with missing EXIF orientation. Adds latency, so only enable it if you cannot guarantee upright images.", + "example": false + }, + "save_api_request": { + "type": "boolean", + "default": true, + "description": "When `true` (default), persists the call as an API-type session — visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, and announced through a `status.updated` webhook. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only.", + "example": true + }, + "vendor_data": { + "type": "string", + "description": "Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later.", + "example": "user-123" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={\"flow\":\"login\"}`) — it is parsed into an object.", + "example": { + "flow": "login", + "device_id": "abc123" + } + } + } + }, + "example": { + "user_image": "(binary JPEG/PNG of the live selfie)", + "ref_image": "(binary JPEG/PNG of the reference photo)", + "face_match_score_decline_threshold": 50, + "rotate_image": false, + "save_api_request": true, + "vendor_data": "user-123", + "metadata": { + "flow": "login" + } + } + } + } + }, + "responses": { + "200": { + "description": "Face match computed. `face_match.score` is a 0–100 float (two decimals); `status` is `Approved` when the score is strictly above `face_match_score_decline_threshold`, otherwise `Declined` with an explanatory warning. A low similarity or unscorable pair still returns `200` — inspect `face_match.status` and `face_match.warnings`, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id.", + "content": { + "application/json": { + "examples": { + "Approved": { + "summary": "Same person — score above the threshold", + "value": { + "request_id": "f9f5a777-8002-44c6-baaf-128bcfd6e226", + "face_match": { + "status": "Approved", + "score": 99.41, + "user_image": { + "entities": [ + { + "bbox": [ + 661, + 728, + 1688, + 2188 + ], + "confidence": 0.732973, + "age": 26.91, + "gender": "male", + "race": null + } + ], + "best_angle": 0 + }, + "ref_image": { + "entities": [ + { + "bbox": [ + 653, + 745, + 1691, + 2185 + ], + "confidence": 0.722082, + "age": 27.0, + "gender": "male", + "race": null + } + ], + "best_angle": 0 + }, + "warnings": [] + }, + "vendor_data": "user-123", + "metadata": { + "flow": "login" + }, + "created_at": "2026-06-12T01:04:42.763237+00:00" + } + }, + "Declined - low similarity": { + "summary": "Score at or below the decline threshold", + "value": { + "request_id": "7cdda24e-311a-46d3-a3bb-e5d9b564ce73", + "face_match": { + "status": "Declined", + "score": 24.87, + "user_image": { + "entities": [ + { + "bbox": [ + 661, + 728, + 1688, + 2188 + ], + "confidence": 0.732973, + "age": 26.91, + "gender": "male", + "race": null + } + ], + "best_angle": 0 + }, + "ref_image": { + "entities": [ + { + "bbox": [ + 156, + 234, + 679, + 898 + ], + "confidence": 0.717775, + "age": 41.2, + "gender": "male", + "race": null + } + ], + "best_angle": 0 + }, + "warnings": [ + { + "risk": "LOW_FACE_MATCH_SIMILARITY", + "feature": "FACEMATCH", + "additional_data": null, + "log_type": "error", + "short_description": "Low face match similarity", + "long_description": "The facial features of the provided image don't closely match the reference image, suggesting a potential identity mismatch." + } + ] + }, + "vendor_data": "user-123", + "metadata": null, + "created_at": "2026-06-12T01:08:19.859116+00:00" + } + } + }, + "schema": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." + }, + "face_match": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "Approved", + "Declined" + ], + "description": "`Approved` when the similarity score is strictly above `face_match_score_decline_threshold` and a score could be computed; `Declined` otherwise (a warning explains why)." + }, + "score": { + "type": "number", + "format": "float", + "nullable": true, + "minimum": 0, + "maximum": 100, + "description": "Similarity score from 0 to 100 — the model's confidence that both images depict the same person. Rounded to two decimals only when `save_api_request` is true; with `save_api_request=false` the raw float is returned. `null` when no face pair could be scored (with the default `save_api_request=true`); with `save_api_request=false` the same condition can surface as `score: 0`. Either way a `NO_REFERENCE_IMAGE` warning is present and `status` is `Declined`.", + "example": 99.41 + }, + "user_image": { + "type": "object", + "description": "Face-detection results for the analyzed images.", + "properties": { + "entities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "bbox": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 4, + "maxItems": 4, + "description": "Bounding box of the detected face as `[x_min, y_min, x_max, y_max]` pixel coordinates in the processed image.", + "example": [ + 661, + 728, + 1688, + 2188 + ] + }, + "confidence": { + "type": "number", + "format": "float", + "minimum": 0, + "maximum": 1, + "description": "Face-detection confidence (0–1).", + "example": 0.732973 + }, + "age": { + "type": "number", + "format": "float", + "description": "Model-estimated age of the detected face, in years. Informational only — it does not affect the face-match decision.", + "example": 26.91 + }, + "gender": { + "type": "string", + "description": "Model-predicted gender of the detected face (`male` or `female`). Informational only.", + "example": "male" + }, + "race": { + "type": "string", + "nullable": true, + "description": "Reserved field — always `null` in responses from this endpoint.", + "example": null + } + } + }, + "description": "One entry per detected face." + }, + "best_angle": { + "type": "integer", + "description": "Rotation (degrees: 0, 90, 180, or 270) that produced the best face detection. Only non-zero when `rotate_image=true` corrected the orientation.", + "example": 0 + } + } + }, + "ref_image": { + "type": "object", + "description": "Face-detection results for the reference image, same shape as `user_image`.", + "properties": { + "entities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "bbox": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 4, + "maxItems": 4, + "description": "Bounding box of the detected face as `[x_min, y_min, x_max, y_max]` pixel coordinates in the processed image.", + "example": [ + 661, + 728, + 1688, + 2188 + ] + }, + "confidence": { + "type": "number", + "format": "float", + "minimum": 0, + "maximum": 1, + "description": "Face-detection confidence (0–1).", + "example": 0.732973 + }, + "age": { + "type": "number", + "format": "float", + "description": "Model-estimated age of the detected face, in years. Informational only — it does not affect the face-match decision.", + "example": 26.91 + }, + "gender": { + "type": "string", + "description": "Model-predicted gender of the detected face (`male` or `female`). Informational only.", + "example": "male" + }, + "race": { + "type": "string", + "nullable": true, + "description": "Reserved field — always `null` in responses from this endpoint.", + "example": null + } + } + }, + "description": "One entry per detected face." + }, + "best_angle": { + "type": "integer", + "example": 0 + } + } + }, + "warnings": { + "type": "array", + "description": "Empty on a clean approval. `NO_REFERENCE_IMAGE` (`error`) — no face pair could be scored; `LOW_FACE_MATCH_SIMILARITY` (`error`) — score at or below the decline threshold. Any of these sets `status` to `Declined`.", + "items": { + "type": "object", + "properties": { + "risk": { + "type": "string", + "enum": [ + "NO_REFERENCE_IMAGE", + "LOW_FACE_MATCH_SIMILARITY" + ], + "description": "Machine-readable risk code." + }, + "feature": { + "type": "string", + "enum": [ + "FACEMATCH" + ], + "description": "Feature that raised the warning. Always `FACEMATCH` on this endpoint." + }, + "additional_data": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Always `null` for face-match warnings." + }, + "log_type": { + "type": "string", + "enum": [ + "error", + "warning", + "information" + ], + "description": "Severity. `error` warnings drive `status` to `Declined`; `warning` and `information` entries are advisory and never decline on their own." + }, + "short_description": { + "type": "string", + "description": "Human-readable one-line summary of the risk." + }, + "long_description": { + "type": "string", + "description": "Human-readable explanation of the risk." + } + } + } + } + } + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Echo of the `vendor_data` you sent, or `null`." + }, + "metadata": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Echo of the `metadata` object you sent, or `null`." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp (UTC) of when the response was generated, e.g. `2026-06-12T01:04:42.763237+00:00`." + } + } + } + } + } + }, + "400": { + "description": "Validation error. Returned when a required image is missing, exceeds 5 MB, has an unsupported extension, or when an option is out of range. The body is DRF's standard field-error envelope: one array of messages per offending field.", + "content": { + "application/json": { + "examples": { + "Missing required image": { + "summary": "`ref_image` (or `user_image`) not included in the form data", + "value": { + "ref_image": [ + "No file was submitted." + ] + } + }, + "Unsupported file extension": { + "summary": "File extension outside tiff/jpg/jpeg/png/webp", + "value": { + "user_image": [ + "File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, webp." + ] + } + }, + "File too large": { + "summary": "Upload exceeds the 5 MB limit", + "value": { + "user_image": [ + "File size should not exceed 5 MB" + ] + } + }, + "Threshold out of range": { + "summary": "`face_match_score_decline_threshold` outside 0–100", + "value": { + "face_match_score_decline_threshold": [ + "Ensure this value is less than or equal to 100." + ] + } + } + } + } + } + }, + "403": { + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any image processing happens.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Not enough credits": { + "summary": "Organization balance cannot cover the call", + "value": { + "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "content": { + "application/json": { + "examples": { + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X POST 'https://verification.didit.me/v3/face-match/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'user_image=@./selfie.jpg' \\\n -F 'ref_image=@./id_portrait.jpg' \\\n -F 'face_match_score_decline_threshold=50' \\\n -F 'save_api_request=true' \\\n -F 'vendor_data=user-123'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nurl = 'https://verification.didit.me/v3/face-match/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('selfie.jpg', 'rb') as user_f, open('id_portrait.jpg', 'rb') as ref_f:\n files = {\n 'user_image': ('selfie.jpg', user_f, 'image/jpeg'),\n 'ref_image': ('id_portrait.jpg', ref_f, 'image/jpeg'),\n }\n data = {\n 'face_match_score_decline_threshold': 50,\n 'save_api_request': 'true',\n 'vendor_data': 'user-123',\n }\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=60)\n\nresp.raise_for_status()\nbody = resp.json()\nprint('status:', body['face_match']['status'])\nprint('score:', body['face_match']['score'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('user_image', new Blob([fs.readFileSync('./selfie.jpg')]), 'selfie.jpg');\nform.append('ref_image', new Blob([fs.readFileSync('./id_portrait.jpg')]), 'id_portrait.jpg');\nform.append('face_match_score_decline_threshold', '50');\nform.append('save_api_request', 'true');\nform.append('vendor_data', 'user-123');\n\nconst response = await fetch('https://verification.didit.me/v3/face-match/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n});\n\nif (!response.ok) throw new Error(`Face match failed: ${response.status}`);\nconst body = await response.json();\nconsole.log('status:', body.face_match.status, 'score:', body.face_match.score);" + } + ] + } + }, + "/v3/age-estimation/": { + "post": { + "summary": "Age Estimation (face age + passive liveness)", + "description": "Estimate a person's age from a single face photo and run a passive liveness check in the same call — returns the model-predicted `age_estimation` (in years), a liveness `score` (0–100), and an `Approved`/`Declined` `status`. The age is **model-predicted from the face, not verified against a document** — use it for age gates and low-friction checks, not as legal proof of age.\n\n**How it works.** The image is analyzed for faces (set `rotate_image=true` if captures may be sideways or upside down); each detected face gets an estimated age, and the largest face in the frame drives `age_estimation`. In parallel, a passive liveness model scores whether the photo shows a live person (no user challenge required) and detects presentation attacks (photos of photos, screens, masks). Submit one clear, front-facing, well-lit face per image for best results.\n\n**Decision logic.** Any warning declines: `status` is `Approved` only when the `warnings` array is empty. A liveness `score` at or below `face_liveness_score_decline_threshold` (default `30`) adds `LOW_LIVENESS_SCORE`; a detected attack adds `LIVENESS_FACE_ATTACK`; no face found by the liveness model adds `NO_FACE_DETECTED`; no estimable face age adds `AGE_NOT_DETECTED` (`age_estimation` is `null`); an estimated age below `age_estimation_decline_threshold` (default `18`) adds `AGE_BELOW_MINIMUM`. Borderline ages near your threshold deserve a fallback to document-based verification — the model's error grows for faces close to the limit.\n\n**Billing.** Each `200` response consumes one Age Estimation API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing.\n\n**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the result is exposed there in `liveness_checks[]`, with `features: [\"AGE_ESTIMATION\"]`), the face crop and reference image are stored, the face embedding is added to your application's face index (used by face search and duplicate detection), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (`age_estimation: 30`, no warnings), no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", + "operationId": "post_v3age-estimation", + "x-didit-sdk": { + "resource": "age_estimation", + "method": "estimate", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" + ], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "user_image" + ], + "properties": { + "user_image": { + "type": "string", + "format": "binary", + "description": "Face photo to analyze (e.g. a selfie). Submit a single front-facing photo with the subject's face clearly visible and well lit. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp` (no PDF). Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.3 MB before processing, so very high resolutions do not improve accuracy." + }, + "face_liveness_score_decline_threshold": { + "type": "number", + "format": "float", + "default": 30, + "minimum": 0, + "maximum": 100, + "description": "Liveness threshold (0–100). A liveness `score` **at or below** this value adds a `LOW_LIVENESS_SCORE` warning and declines. Raise it for stricter anti-spoofing; values outside 0–100 return `400`.", + "example": 30 + }, + "age_estimation_decline_threshold": { + "type": "integer", + "default": 18, + "minimum": 0, + "maximum": 100, + "description": "Minimum acceptable estimated age in years. An estimated age **below** this value adds an `AGE_BELOW_MINIMUM` warning and declines. Set `0` to disable the age check (the call then only runs liveness + age estimation without an age-based decision).", + "example": 18 + }, + "rotate_image": { + "type": "boolean", + "default": false, + "description": "When `true`, the service tries the input image in 90-degree increments (0, 90, 180, 270) and uses the orientation that yields the best face detection — useful for mobile captures with missing EXIF orientation. Adds latency, so only enable it if you cannot guarantee upright images.", + "example": false + }, + "save_api_request": { + "type": "boolean", + "default": true, + "description": "When `true` (default), persists the call as an API-type session — visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with the detected face added to your application's face index. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only.", + "example": true + }, + "vendor_data": { + "type": "string", + "description": "Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later.", + "example": "user-123" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={\"flow\":\"age-gate\"}`) — it is parsed into an object.", + "example": { + "flow": "age-gate" + } + } + } + }, + "example": { + "user_image": "(binary JPEG/PNG of the selfie)", + "face_liveness_score_decline_threshold": 30, + "age_estimation_decline_threshold": 18, + "rotate_image": false, + "save_api_request": true, + "vendor_data": "user-123", + "metadata": { + "flow": "age-gate" + } + } + } + } + }, + "responses": { + "200": { + "description": "Age estimation and passive liveness completed. `age_estimation.age_estimation` is the model-predicted age in years of the largest detected face (`null` if none); `age_estimation.score` is the passive-liveness score (0–100); `status` is `Approved` only when `warnings` is empty. An underage or spoofed capture still returns `200` with `status: \"Declined\"` — inspect the body, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id.", + "content": { + "application/json": { + "examples": { + "Approved": { + "summary": "Live adult face — both checks passed", + "value": { + "request_id": "0c40ba43-64ab-4e2e-b4b8-7d1f12f81bc1", + "age_estimation": { + "status": "Approved", + "method": "PASSIVE", + "score": 97.5, + "user_image": { + "entities": [ + { + "age": 27.33, + "bbox": [ + 40, + 40, + 100, + 100 + ], + "confidence": 0.7177750468254089, + "gender": "male" + } + ], + "best_angle": 0 + }, + "age_estimation": 27.33, + "warnings": [] + }, + "vendor_data": "user-123", + "metadata": { + "flow": "age-gate" + }, + "created_at": "2026-06-12T02:24:11.512941+00:00" + } + }, + "Declined - age below threshold": { + "summary": "Estimated age below `age_estimation_decline_threshold`", + "value": { + "request_id": "9be41a6f-2f4e-4f57-92f4-b3a4f6f0a1c2", + "age_estimation": { + "status": "Declined", + "method": "PASSIVE", + "score": 95.0, + "user_image": { + "entities": [ + { + "age": 16.5, + "bbox": [ + 40, + 40, + 100, + 100 + ], + "confidence": 0.7177750468254089, + "gender": "female" + } + ], + "best_angle": 0 + }, + "age_estimation": 16.5, + "warnings": [ + { + "risk": "AGE_BELOW_MINIMUM", + "feature": "LIVENESS", + "additional_data": null, + "log_type": "error", + "short_description": "Age below minimum", + "long_description": "The age of the face is below the minimum age threshold for the application." + } + ] + }, + "vendor_data": "user-123", + "metadata": null, + "created_at": "2026-06-12T02:26:40.118332+00:00" + } + }, + "Declined - liveness too low": { + "summary": "Liveness score at or below the decline threshold (possible spoof)", + "value": { + "request_id": "5f6f2f1f-7c4f-43b9-8d62-0a8f4c2d9e77", + "age_estimation": { + "status": "Declined", + "method": "PASSIVE", + "score": 5.0, + "user_image": { + "entities": [ + { + "age": 25.0, + "bbox": [ + 40, + 40, + 100, + 100 + ], + "confidence": 0.7177750468254089, + "gender": "male" + } + ], + "best_angle": 0 + }, + "age_estimation": 25.0, + "warnings": [ + { + "risk": "LOW_LIVENESS_SCORE", + "feature": "LIVENESS", + "additional_data": null, + "log_type": "error", + "short_description": "Low liveness score", + "long_description": "The liveness check resulted in a low score, indicating potential use of non-live facial representations or poor-quality biometric data." + } + ] + }, + "vendor_data": "user-123", + "metadata": null, + "created_at": "2026-06-12T02:27:02.901547+00:00" + } + } + }, + "schema": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." + }, + "age_estimation": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "Approved", + "Declined" + ], + "description": "`Approved` when no warnings were raised; `Declined` otherwise (every warning this endpoint produces declines)." + }, + "method": { + "type": "string", + "enum": [ + "PASSIVE" + ], + "description": "Liveness method. Always `PASSIVE` on this endpoint (single-image, no user challenge)." + }, + "score": { + "type": "number", + "format": "float", + "nullable": true, + "minimum": 0, + "maximum": 100, + "description": "Passive-liveness score from 0 to 100 — the model's confidence that the photo shows a live person. Compared against `face_liveness_score_decline_threshold`; at or below it the call declines with `LOW_LIVENESS_SCORE`. `null` when the liveness model returns no confidence — the call still declines with `LOW_LIVENESS_SCORE` because null is treated as 0 for the threshold comparison.", + "example": 97.5 + }, + "user_image": { + "type": "object", + "description": "Face-detection results for the analyzed image.", + "properties": { + "entities": { + "type": "array", + "description": "One entry per detected face. Empty when no face was found (then `age_estimation` is `null` and an `AGE_NOT_DETECTED` warning is added).", + "items": { + "type": "object", + "properties": { + "age": { + "type": "number", + "format": "float", + "description": "Model-estimated age of this face, in years.", + "example": 27.33 + }, + "bbox": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 4, + "maxItems": 4, + "description": "Bounding box of the detected face as `[x_min, y_min, x_max, y_max]` pixel coordinates in the processed image.", + "example": [ + 40, + 40, + 100, + 100 + ] + }, + "confidence": { + "type": "number", + "format": "float", + "minimum": 0, + "maximum": 1, + "description": "Face-detection confidence (0–1).", + "example": 0.7177750468254089 + }, + "gender": { + "type": "string", + "description": "Model-predicted gender of the detected face (`male` or `female`). Informational only.", + "example": "male" + } + } + } + }, + "best_angle": { + "type": "integer", + "nullable": true, + "description": "Rotation (degrees: 0, 90, 180, or 270) that produced the best face detection. Only non-zero when `rotate_image=true` corrected the orientation.", + "example": 0 + } + } + }, + "age_estimation": { + "type": "number", + "format": "float", + "nullable": true, + "description": "Model-predicted age in years of the **largest** detected face. `null` when no face age could be estimated (an `AGE_NOT_DETECTED` warning is added and the call declines).", + "example": 27.33 + }, + "warnings": { + "type": "array", + "description": "Empty on a clean approval. Any entry sets `status` to `Declined`.", + "items": { + "type": "object", + "properties": { + "risk": { + "type": "string", + "enum": [ + "NO_FACE_DETECTED", + "LOW_LIVENESS_SCORE", + "LIVENESS_FACE_ATTACK", + "AGE_NOT_DETECTED", + "AGE_BELOW_MINIMUM" + ], + "description": "Machine-readable risk code. `NO_FACE_DETECTED` — the liveness model found no face; `LOW_LIVENESS_SCORE` — liveness `score` at or below `face_liveness_score_decline_threshold`; `LIVENESS_FACE_ATTACK` — a presentation attack (photo/screen/mask) was detected; `AGE_NOT_DETECTED` — no face age could be estimated; `AGE_BELOW_MINIMUM` — estimated age below `age_estimation_decline_threshold`. `NO_FACE_DETECTED` and `LOW_LIVENESS_SCORE` are mutually exclusive — when no face is found only `NO_FACE_DETECTED` is emitted." + }, + "feature": { + "type": "string", + "enum": [ + "LIVENESS" + ], + "description": "Feature that raised the warning. Always `LIVENESS` on this endpoint." + }, + "additional_data": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Always `null` for age-estimation warnings." + }, + "log_type": { + "type": "string", + "enum": [ + "error" + ], + "description": "Severity. Every warning this endpoint produces is an `error` and sets `status` to `Declined`." + }, + "short_description": { + "type": "string", + "description": "Human-readable one-line summary of the risk." + }, + "long_description": { + "type": "string", + "description": "Human-readable explanation of the risk." + } + } + } + } + } + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Echo of the `vendor_data` you sent, or `null`." + }, + "metadata": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Echo of the `metadata` object you sent, or `null`." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp (UTC) of when the response was generated." + } + } + } + } + } + }, + "400": { + "description": "Validation error. Returned when `user_image` is missing, exceeds 5 MB, has an unsupported extension, or when a threshold is out of range. The body is DRF's standard field-error envelope: one array of messages per offending field.", + "content": { + "application/json": { + "examples": { + "Missing user image": { + "summary": "`user_image` not included in the form data", + "value": { + "user_image": [ + "No file was submitted." + ] + } + }, + "Unsupported file extension": { + "summary": "File extension outside tiff/jpg/jpeg/png/webp", + "value": { + "user_image": [ + "File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, webp." + ] + } + }, + "File too large": { + "summary": "Upload exceeds the 5 MB limit", + "value": { + "user_image": [ + "File size should not exceed 5 MB" + ] + } + }, + "Threshold out of range": { + "summary": "`face_liveness_score_decline_threshold` outside 0–100", + "value": { + "face_liveness_score_decline_threshold": [ + "Ensure this value is less than or equal to 100." + ] + } + } + } + } + } + }, + "403": { + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any image processing happens.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Not enough credits": { + "summary": "Organization balance cannot cover the call", + "value": { + "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "content": { + "application/json": { + "examples": { + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X POST 'https://verification.didit.me/v3/age-estimation/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'user_image=@./selfie.jpg' \\\n -F 'age_estimation_decline_threshold=18' \\\n -F 'face_liveness_score_decline_threshold=30' \\\n -F 'save_api_request=true' \\\n -F 'vendor_data=user-123'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nurl = 'https://verification.didit.me/v3/age-estimation/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('selfie.jpg', 'rb') as f:\n files = {'user_image': ('selfie.jpg', f, 'image/jpeg')}\n data = {\n 'age_estimation_decline_threshold': 18,\n 'face_liveness_score_decline_threshold': 30,\n 'save_api_request': 'true',\n 'vendor_data': 'user-123',\n }\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=60)\n\nresp.raise_for_status()\nresult = resp.json()['age_estimation']\nprint('status:', result['status'])\nprint('estimated age:', result['age_estimation'])\nprint('liveness score:', result['score'])\nfor w in result['warnings']:\n print('warning:', w['risk'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('user_image', new Blob([fs.readFileSync('./selfie.jpg')]), 'selfie.jpg');\nform.append('age_estimation_decline_threshold', '18');\nform.append('face_liveness_score_decline_threshold', '30');\nform.append('save_api_request', 'true');\nform.append('vendor_data', 'user-123');\n\nconst response = await fetch('https://verification.didit.me/v3/age-estimation/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n});\n\nif (!response.ok) throw new Error(`Age estimation failed: ${response.status}`);\nconst { age_estimation } = await response.json();\nconsole.log('status:', age_estimation.status, 'age:', age_estimation.age_estimation, 'liveness:', age_estimation.score);" + } + ] + } + }, + "/v3/poa/": { + "post": { + "summary": "Proof of Address (document OCR + address checks)", + "description": "Extract and validate a Proof of Address document — utility bills, bank statements, and government-issued documents — in one call. Returns the issuer, issue/expiry dates, the holder name, the raw and parsed/geocoded address, an `Approved`/`Declined` `status`, and a `warnings` list explaining every issue found.\n\n**Latency.** Extraction is LLM-based: typical calls take **5–15 seconds**, multi-page PDFs up to **~30 seconds**. Configure a client timeout of **at least 45 seconds** and do not retry before the call completes.\n\n**How it works.** The document (image or PDF, up to 15 MB; encrypted PDFs require `document_password`) is classified into a document type and subtype (e.g. `UTILITY_BILL` / `WATER_BILL`), and the issuer, dates, holder name, and address are extracted with LLM-based extraction. The address is parsed and geocoded into `poa_parsed_address`. PDF/EXIF forensics screen for tampering — modification after digital signing, known PDF editors, overlay-text manipulation, suspicious re-exports — surfacing `SUSPECTED_DOCUMENT_MANIPULATION` or `DOCUMENT_METADATA_MISMATCH`. Document age is validated against `poa_document_age_months`, and the language against `poa_languages_allowed`. If you pass `expected_address`, `expected_country`, or `expected_first_name`/`expected_last_name`, the extracted values are cross-checked and mismatches are flagged.\n\n**Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Hard failures always decline: `MISSING_ADDRESS_INFORMATION`, `POA_DOCUMENT_EXPIRED`, `INVALID_DOCUMENT_TYPE`, `UNABLE_TO_VALIDATE_DOCUMENT_AGE`, plus `UNABLE_TO_EXTRACT_ISSUE_DATE` and `POA_NAME_NOT_DETECTED` (not configurable on this endpoint). Five risk groups are configurable per request via the `poa_*_action` options (`DECLINE` or `NO_ACTION`, all defaulting to `DECLINE`). `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION` and `UNPARSABLE_OR_INVALID_ADDRESS` are informational on this endpoint and never decline. A document that cannot be processed at all returns `400`; a readable but problematic document returns `200` with `status: \"Declined\"` — always inspect `poa.status` and `poa.warnings`, not just the HTTP code.\n\n**Billing.** Each `200` response consumes one Proof of Address API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any document processing.\n\n**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the uploaded document is stored, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the decision additionally exposes `document_file` and the `document_metadata` forensics, which this response omits), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (a fictional \"Sandbox Power Co.\" electricity bill), no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.\n\nNote: `FUTURE_ISSUE_DATE` and `POOR_DOCUMENT_QUALITY` exist in workflow sessions but are never produced by this standalone endpoint — a future-dated document is not auto-declined here.", + "operationId": "post_v3poa", + "x-didit-sdk": { + "resource": "proof_of_address", + "method": "verify", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" + ], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "document" + ], + "properties": { + "document": { + "type": "string", + "format": "binary", + "description": "The Proof of Address document. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `pdf`, `webp`. Maximum upload size: **15 MB** (larger files are rejected with `400`). Multi-page PDFs are supported (expect higher latency); encrypted PDFs require `document_password`. Images are automatically compressed before processing." + }, + "document_password": { + "type": "string", + "description": "Password to decrypt `document` when it is an encrypted PDF. Sending an encrypted PDF without (or with a wrong) password returns `400`.", + "writeOnly": true + }, + "expected_address": { + "type": "string", + "description": "Address to verify against the document. It is geocoded and compared with the extracted address; a mismatch (or an expected address that cannot be geocoded/verified) adds `ADDRESS_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`. The geocoded form is echoed back in `expected_details_formatted_address` / `expected_details_parsed_address`.", + "example": "Av. Monseñor Tavella 1291, Salta, Argentina" + }, + "expected_country": { + "type": "string", + "description": "Country to verify against the document (ISO 3166-1 code, e.g. `ARG` or `AR`). A mismatch with the document's country adds `POA_COUNTRY_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`.", + "example": "ARG" + }, + "expected_first_name": { + "type": "string", + "description": "First name to verify against `name_on_document` (fuzzy match, transliteration-aware). A score below the match threshold adds `NAME_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`.", + "example": "Sophia" + }, + "expected_last_name": { + "type": "string", + "description": "Last name to verify against `name_on_document`, combined with `expected_first_name` for the match score.", + "example": "Martinez" + }, + "poa_languages_allowed": { + "type": "string", + "description": "Comma-separated list of accepted document languages as ISO 639-1 codes (e.g. `en,es,fr`). When omitted or blank, **all 51 supported languages** are accepted. A document in a language outside the list adds `UNSUPPORTED_DOCUMENT_LANGUAGE` (governed by `poa_unsupported_language_action`); an unknown code in the list returns `400` with the full set of supported codes.", + "example": "en,es" + }, + "poa_document_age_months": { + "type": "string", + "description": "Comma-separated `key:value` pairs setting the maximum document age in months per document type — keys: `utility_bill`, `bank_statement`, `government_issued_document`, `other_poa_document`; values: `1`–`120`, or `-1` for unlimited (no age check). Defaults when omitted: `utility_bill:3,bank_statement:3,government_issued_document:12,other_poa_document:12`. A document older than its limit declines with `POA_DOCUMENT_EXPIRED`. **Caution:** when you provide this field, list *every* type you accept — omitted types are treated as not accepted (informational `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION`) and their age check is skipped.", + "example": "utility_bill:3,bank_statement:6" + }, + "poa_name_mismatch_action": { + "type": "string", + "enum": [ + "DECLINE", + "NO_ACTION" + ], + "default": "DECLINE", + "description": "Accepted but **currently not applied** — name-mismatch warnings (`NAME_MISMATCH_WITH_PROVIDED`) follow `poa_address_mismatch_action` instead. Set `poa_address_mismatch_action` to control both name and address mismatch behavior." + }, + "poa_document_issues_action": { + "type": "string", + "enum": [ + "DECLINE", + "NO_ACTION" + ], + "default": "DECLINE", + "description": "What to do on file-integrity problems — in practice only `DOCUMENT_METADATA_MISMATCH` is produced by this endpoint." + }, + "poa_document_authenticity_action": { + "type": "string", + "enum": [ + "DECLINE", + "NO_ACTION" + ], + "default": "DECLINE", + "description": "What to do when document manipulation is suspected (`SUSPECTED_DOCUMENT_MANIPULATION`) — e.g. the PDF was modified after digital signing, processed by a known PDF editor, shows overlay-text manipulation, or has inconsistent EXIF dates." + }, + "poa_unsupported_language_action": { + "type": "string", + "enum": [ + "DECLINE", + "NO_ACTION" + ], + "default": "DECLINE", + "description": "What to do when the document language is outside `poa_languages_allowed` (`UNSUPPORTED_DOCUMENT_LANGUAGE`)." + }, + "poa_address_mismatch_action": { + "type": "string", + "enum": [ + "DECLINE", + "NO_ACTION" + ], + "default": "DECLINE", + "description": "What to do when the extracted details disagree with the `expected_*` fields: `ADDRESS_MISMATCH_WITH_PROVIDED`, `NAME_MISMATCH_WITH_PROVIDED`, and `POA_COUNTRY_MISMATCH_WITH_PROVIDED` all follow this action." + }, + "poa_issuer_not_identified_action": { + "type": "string", + "enum": [ + "DECLINE", + "NO_ACTION" + ], + "default": "DECLINE", + "description": "What to do when no issuing company/organization can be identified on the document (`ISSUER_NOT_IDENTIFIED`)." + }, + "save_api_request": { + "type": "boolean", + "default": true, + "description": "When `true` (default), persists the call as an API-type session — visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with the uploaded document stored. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only.", + "example": true + }, + "vendor_data": { + "type": "string", + "description": "Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later.", + "example": "user-123" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={\"flow\":\"onboarding\"}`) — it is parsed into an object.", + "example": { + "flow": "onboarding" + } + } + } + }, + "example": { + "document": "(binary JPEG/PNG/PDF of the utility bill or bank statement)", + "expected_address": "Av. Monseñor Tavella 1291, Salta, Argentina", + "expected_country": "ARG", + "expected_first_name": "Sophia", + "expected_last_name": "Martinez", + "poa_languages_allowed": "en,es", + "save_api_request": true, + "vendor_data": "user-123" + } + } + } + }, + "responses": { + "200": { + "description": "Document processed. `poa.status` is `Approved` or `Declined`; every detected issue is itemized in `poa.warnings`. An expired, mismatched, or suspicious document still returns `200` with `status: \"Declined\"` — inspect the body, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id.", + "content": { + "application/json": { + "examples": { + "Approved": { + "summary": "Utility bill read cleanly, no expected-details checks", + "value": { + "request_id": "4b523a8d-ed32-45e4-8aa8-a89eda459d88", + "poa": { + "issuing_state": "ARG", + "document_type": "UTILITY_BILL", + "document_subtype": "WATER_BILL", + "document_language": "es", + "issuer": "Aguas del Norte", + "issue_date": "2026-04-10", + "expiration_date": null, + "poa_address": "AVDA. MONSEÑOR TAVELLA N° 1291, B° VILLA LAVALLE CP: 4400, CAPITAL - SALTA", + "poa_formatted_address": "Av. Monseñor Tavella 1291 b, A4400 Salta, Argentina", + "poa_parsed_address": { + "street_1": "Avenida Monseñor Tavella 1291", + "street_2": "b", + "city": "Salta", + "region": "Salta", + "country": "AR", + "postal_code": "A4400", + "document_location": { + "latitude": -24.8208682, + "longitude": -65.4130954 + } + }, + "expected_details_address": null, + "expected_details_formatted_address": null, + "expected_details_parsed_address": null, + "name_on_document": "OESTE EMBOTELLADORA S.A.", + "extra_fields": { + "bank_account_number": null, + "bank_iban": null, + "bank_sort_code": null, + "bank_routing_number": null, + "bank_swift_bic": null, + "bank_branch_name": null, + "bank_branch_address": null, + "document_phone_number": null, + "additional_names": [ + "LA EMBOTELLADORA DEL NORTE SA" + ] + }, + "detected_codes": [ + { + "type": "QR_CODE", + "page_number": 1, + "position": [ + [ + 820, + 1120 + ], + [ + 1002, + 1120 + ], + [ + 1002, + 1302 + ], + [ + 820, + 1302 + ] + ], + "x": 820, + "y": 1120, + "width": 182, + "height": 182, + "page_width": 1190, + "page_height": 1684, + "raw_payload": "aHR0cHM6Ly9hZ3Vhc2RlbG5vcnRlLmNvbS9mYWN0dXJhLzk5ODgxMQ==", + "parsed_payload": "https://aguasdelnorte.com/factura/998811" + }, + { + "type": "CODE_128", + "page_number": 1, + "position": [ + [ + 112, + 1480 + ], + [ + 560, + 1480 + ], + [ + 560, + 1540 + ], + [ + 112, + 1540 + ] + ], + "x": 112, + "y": 1480, + "width": 448, + "height": 60, + "page_width": 1190, + "page_height": 1684, + "raw_payload": "MDA5OTg4MTEwMDA0Mg==", + "parsed_payload": "0099881100042" + } + ], + "status": "Approved", + "warnings": [] + }, + "vendor_data": "user-123", + "metadata": null, + "created_at": "2026-06-12T02:29:02.439655+00:00" + } + }, + "Declined - document too old": { + "summary": "Issue date older than the allowed `poa_document_age_months`", + "value": { + "request_id": "f61681b8-9422-4378-ac3e-04c6a247fb45", + "poa": { + "issuing_state": "ARG", + "document_type": "UTILITY_BILL", + "document_subtype": "WATER_BILL", + "document_language": "es", + "issuer": "Aguas del Norte", + "issue_date": "2021-02-10", + "expiration_date": null, + "poa_address": "AVDA. MONSEÑOR TAVELLA N° 1291, B° VILLA LAVALLE CP: 4400, CAPITAL - SALTA", + "poa_formatted_address": "Av. Monseñor Tavella 1291 b, A4400 Salta, Argentina", + "poa_parsed_address": { + "street_1": "Avenida Monseñor Tavella 1291", + "street_2": "b", + "city": "Salta", + "region": "Salta", + "country": "AR", + "postal_code": "A4400", + "document_location": { + "latitude": -24.8208682, + "longitude": -65.4130954 + } + }, + "expected_details_address": null, + "expected_details_formatted_address": null, + "expected_details_parsed_address": null, + "name_on_document": "OESTE EMBOTELLADORA S.A.", + "extra_fields": { + "bank_account_number": null, + "bank_iban": null, + "bank_sort_code": null, + "bank_routing_number": null, + "bank_swift_bic": null, + "bank_branch_name": null, + "bank_branch_address": null, + "document_phone_number": null, + "additional_names": [ + "LA EMBOTELLADORA DEL NORTE SA" + ] + }, + "status": "Declined", + "warnings": [ + { + "risk": "POA_DOCUMENT_EXPIRED", + "feature": "PROOF_OF_ADDRESS", + "additional_data": { + "expiration_reason": "age_window", + "max_age_months": 3, + "document_subtype": "WATER_BILL", + "document_type": "UTILITY_BILL", + "issue_date": "2021-02-10" + }, + "log_type": "error", + "short_description": "Document expired", + "long_description": "The submitted document is older than the maximum accepted age of 3 months from its issue date, which exceeds the acceptable time period for validity." + } + ] + }, + "vendor_data": "user-123", + "metadata": null, + "created_at": "2026-06-12T02:23:02.165493+00:00" + } + }, + "Declined - name mismatch": { + "summary": "`expected_first_name`/`expected_last_name` do not match `name_on_document`", + "value": { + "request_id": "0a7f2f9c-58f1-4b8e-b1e3-2f6f9d4f7a21", + "poa": { + "issuing_state": "ARG", + "document_type": "UTILITY_BILL", + "document_subtype": "WATER_BILL", + "document_language": "es", + "issuer": "Aguas del Norte", + "issue_date": "2021-02-10", + "expiration_date": null, + "poa_address": "AVDA. MONSEÑOR TAVELLA N° 1291, B° VILLA LAVALLE CP: 4400, CAPITAL - SALTA", + "poa_formatted_address": "Av. Monseñor Tavella 1291 b, A4400 Salta, Argentina", + "poa_parsed_address": { + "street_1": "Avenida Monseñor Tavella 1291", + "street_2": "b", + "city": "Salta", + "region": "Salta", + "country": "AR", + "postal_code": "A4400", + "document_location": { + "latitude": -24.8208682, + "longitude": -65.4130954 + } + }, + "expected_details_address": null, + "expected_details_formatted_address": null, + "expected_details_parsed_address": null, + "name_on_document": "OESTE EMBOTELLADORA S.A.", + "extra_fields": { + "bank_account_number": null, + "bank_iban": null, + "bank_sort_code": null, + "bank_routing_number": null, + "bank_swift_bic": null, + "bank_branch_name": null, + "bank_branch_address": null, + "document_phone_number": null, + "additional_names": [ + "LA EMBOTELLADORA DEL NORTE SA" + ] + }, + "status": "Declined", + "warnings": [ + { + "risk": "NAME_MISMATCH_WITH_PROVIDED", + "feature": "PROOF_OF_ADDRESS", + "additional_data": null, + "log_type": "error", + "short_description": "Name mismatch with provided information", + "long_description": "The full name on the document does not match the name from the user's verified identity documents, or the full name sent by API." + } + ] + }, + "vendor_data": "user-123", + "metadata": null, + "created_at": "2026-06-12T02:27:21.330172+00:00" + } + } + }, + "schema": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." + }, + "poa": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "Approved", + "Declined" + ], + "description": "`Approved` when no warning resolves to a decline; `Declined` otherwise — the `warnings` list explains why." + }, + "issuing_state": { + "type": "string", + "nullable": true, + "description": "Country the document was issued in, as extracted from the document — usually ISO 3166-1 alpha-3 (e.g. `ARG`).", + "example": "ARG" + }, + "document_type": { + "type": "string", + "enum": [ + "UTILITY_BILL", + "BANK_STATEMENT", + "GOVERNMENT_ISSUED_DOCUMENT", + "OTHER_POA_DOCUMENT", + "UNKNOWN" + ], + "description": "Detected document category. `UNKNOWN` means the document could not be classified as an acceptable Proof of Address and auto-declines with `INVALID_DOCUMENT_TYPE`. Never null — classification failures fall back to `UNKNOWN`." + }, + "document_subtype": { + "type": "string", + "description": "Finer-grained classification within `document_type` — e.g. `ELECTRICITY_BILL`, `WATER_BILL`, `GAS_BILL`, `INTERNET_BILL`, `PHONE_BILL` (utility bills); `ACCOUNT_STATEMENT`, `CREDIT_CARD_STATEMENT` (bank statements); `TAX_ASSESSMENT`, `RESIDENCY_CERTIFICATE` (government documents). `UNKNOWN` when no subtype could be determined. Never null — classification failures fall back to `UNKNOWN`.", + "example": "WATER_BILL" + }, + "document_language": { + "type": "string", + "nullable": true, + "description": "Detected document language (ISO 639-1).", + "example": "es" + }, + "issuer": { + "type": "string", + "nullable": true, + "description": "Company or organization that issued the document. `null` adds `ISSUER_NOT_IDENTIFIED`.", + "example": "Aguas del Norte" + }, + "issue_date": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Document issue date (`YYYY-MM-DD`). `null` when it could not be extracted — that declines with `UNABLE_TO_EXTRACT_ISSUE_DATE` on this endpoint.", + "example": "2021-02-10" + }, + "expiration_date": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Explicit expiry/validity date printed on the document, when present. Independent of the `poa_document_age_months` age check." + }, + "poa_address": { + "type": "string", + "nullable": true, + "description": "Address exactly as printed on the document. `null` auto-declines with `MISSING_ADDRESS_INFORMATION`." + }, + "poa_formatted_address": { + "type": "string", + "nullable": true, + "description": "Geocoded, normalized version of `poa_address`.", + "example": "Av. Monseñor Tavella 1291 b, A4400 Salta, Argentina" + }, + "poa_parsed_address": { + "type": "object", + "nullable": true, + "description": "Structured, geocoded breakdown of the document address. `null` when the address could not be parsed.", + "properties": { + "street_1": { + "type": "string", + "nullable": true, + "example": "Avenida Monseñor Tavella 1291" + }, + "street_2": { + "type": "string", + "nullable": true + }, + "city": { + "type": "string", + "nullable": true, + "example": "Salta" + }, + "region": { + "type": "string", + "nullable": true, + "example": "Salta" + }, + "country": { + "type": "string", + "nullable": true, + "description": "ISO 3166-1 alpha-2 country code.", + "example": "AR" + }, + "postal_code": { + "type": "string", + "nullable": true, + "example": "A4400" + }, + "document_location": { + "type": "object", + "nullable": true, + "properties": { + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + } + }, + "description": "Geocoded coordinates of the document address." + } + } + }, + "expected_details_address": { + "type": "string", + "nullable": true, + "description": "Echo of the `expected_address` you sent, or `null`." + }, + "expected_details_formatted_address": { + "type": "string", + "nullable": true, + "description": "Geocoded, normalized version of `expected_address`; `null` when not provided or not geocodable." + }, + "expected_details_parsed_address": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Full geocoding result for `expected_address` (street/city/region/country/postal code plus raw geocoder output and an `is_verified` flag). `null` when `expected_address` was not provided." + }, + "name_on_document": { + "type": "string", + "nullable": true, + "description": "Primary holder name extracted from the document; compared against `expected_first_name`/`expected_last_name` when provided. `null` declines with `POA_NAME_NOT_DETECTED` on this endpoint.", + "example": "OESTE EMBOTELLADORA S.A." + }, + "extra_fields": { + "type": "object", + "description": "Document-specific extras. Bank fields are populated for bank statements when printed on the document; `null` otherwise.", + "properties": { + "bank_account_number": { + "type": "string", + "nullable": true + }, + "bank_iban": { + "type": "string", + "nullable": true + }, + "bank_sort_code": { + "type": "string", + "nullable": true + }, + "bank_routing_number": { + "type": "string", + "nullable": true + }, + "bank_swift_bic": { + "type": "string", + "nullable": true + }, + "bank_branch_name": { + "type": "string", + "nullable": true + }, + "bank_branch_address": { + "type": "string", + "nullable": true + }, + "document_phone_number": { + "type": "string", + "nullable": true, + "description": "Contact phone number printed on the document." + }, + "additional_names": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Other holder names found on the document (e.g. co-account holders, previous account names)." + } + } + }, + "warnings": { + "type": "array", + "description": "Empty on a clean approval. Every entry explains one detected risk; entries with `log_type: \"error\"` set `status` to `Declined`.", + "items": { + "type": "object", + "properties": { + "risk": { + "type": "string", + "description": "Machine-readable risk code. Always-decline: `MISSING_ADDRESS_INFORMATION`, `POA_DOCUMENT_EXPIRED`, `INVALID_DOCUMENT_TYPE`, `UNABLE_TO_VALIDATE_DOCUMENT_AGE`, plus `UNABLE_TO_EXTRACT_ISSUE_DATE` and `POA_NAME_NOT_DETECTED` (fixed to decline on this endpoint). Governed by request options: `NAME_MISMATCH_WITH_PROVIDED` / `ADDRESS_MISMATCH_WITH_PROVIDED` / `POA_COUNTRY_MISMATCH_WITH_PROVIDED` (`poa_address_mismatch_action`), `POOR_DOCUMENT_QUALITY` (never produced by this endpoint) / `DOCUMENT_METADATA_MISMATCH` (`poa_document_issues_action`), `SUSPECTED_DOCUMENT_MANIPULATION` (`poa_document_authenticity_action`), `UNSUPPORTED_DOCUMENT_LANGUAGE` (`poa_unsupported_language_action`), `ISSUER_NOT_IDENTIFIED` (`poa_issuer_not_identified_action`). Informational on this endpoint: `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION`, `UNPARSABLE_OR_INVALID_ADDRESS`.", + "example": "POA_DOCUMENT_EXPIRED" + }, + "feature": { + "type": "string", + "enum": [ + "PROOF_OF_ADDRESS" + ], + "description": "Feature that raised the warning. Always `PROOF_OF_ADDRESS` on this endpoint." + }, + "additional_data": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Extra context for some risks — e.g. `POA_DOCUMENT_EXPIRED` includes `expiration_reason` (`printed_expiration` or `age_window`), `document_type`, `document_subtype`, and `issue_date`, plus `expiration_date` on the `printed_expiration` path or `max_age_months` on the `age_window` path; `SUSPECTED_DOCUMENT_MANIPULATION` includes `reason` and `detection_method`. `null` for most risks." + }, + "log_type": { + "type": "string", + "enum": [ + "error", + "information" + ], + "description": "Severity. `error` warnings drive `status` to `Declined`; `information` entries are advisory and never decline on their own. (`warning` is not produced by this endpoint — its options only accept `DECLINE` or `NO_ACTION`.)" + }, + "short_description": { + "type": "string", + "description": "Human-readable one-line summary of the risk." + }, + "long_description": { + "type": "string", + "description": "Human-readable explanation of the risk." + } + } + } + }, + "detected_codes": { + "type": "array", + "description": "QR codes and barcodes detected across every processed page of the document. Empty when none are found. A located but undecodable QR code has `parsed_payload: null`.", + "items": { + "$ref": "#/components/schemas/DocumentAIDetectedCode" + } + } + } + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Echo of the `vendor_data` you sent, or `null`." + }, + "metadata": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Echo of the `metadata` object you sent, or `null`." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp (UTC) of when the response was generated." + } + } + } + } + } + }, + "400": { + "description": "Validation or processing error. Field-level problems return DRF's standard envelope (one array of messages per offending field). Encrypted-PDF problems return `{\"detail\": ...}`. A document the extraction pipeline cannot process returns `{\"error\": [\"Error extracting POA information\"]}`.", + "content": { + "application/json": { + "examples": { + "Missing document": { + "summary": "`document` not included in the form data", + "value": { + "document": [ + "No file was submitted." + ] + } + }, + "Unsupported file extension": { + "summary": "File extension outside tiff/jpg/jpeg/png/pdf/webp", + "value": { + "document": [ + "File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, pdf, webp." + ] + } + }, + "File too large": { + "summary": "Upload exceeds the 15 MB limit", + "value": { + "document": [ + "File size should not exceed 15 MB" + ] + } + }, + "Encrypted PDF": { + "summary": "PDF uploaded without the required password", + "value": { + "detail": "The PDF is encrypted. Please upload a decrypted PDF or a photo instead." + } + }, + "Wrong PDF password": { + "summary": "`document_password` does not decrypt the PDF", + "value": { + "detail": "The PDF password is incorrect. Please provide the correct password." + } + }, + "Invalid language code": { + "summary": "`poa_languages_allowed` contains an unsupported code", + "value": { + "poa_languages_allowed": [ + "Invalid language code: 'xx'. Must be one of the supported languages: ['ar', 'bn', 'hy', 'bg', 'bs', 'ca', 'cnr', 'sq', 'zh', 'hr', 'cs', 'da', 'nl', 'en', 'et', 'fi', 'fr', 'ka', 'kk', 'de', 'el', 'he', 'hi', 'hu', 'id', 'it', 'ja', 'ko', 'ky', 'lv', 'lt', 'mk', 'mn', 'ms', 'no', 'fa', 'pl', 'pt', 'ro', 'ru', 'sr', 'sk', 'sl', 'so', 'es', 'sv', 'th', 'tr', 'uk', 'uz', 'vi']." + ] + } + }, + "Invalid document age value": { + "summary": "`poa_document_age_months` value outside 1–120 / -1", + "value": { + "poa_document_age_months": [ + "Invalid integer value for 'utility_bill': '999'. Must be -1 (unlimited) or a positive integer between 1 and 120." + ] + } + }, + "Extraction failure": { + "summary": "The document could not be processed by the extraction pipeline", + "value": { + "error": [ + "Error extracting POA information" + ] + } + } + } + } + } + }, + "403": { + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any document processing happens.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Not enough credits": { + "summary": "Organization balance cannot cover the call", + "value": { + "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "content": { + "application/json": { + "examples": { + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X POST 'https://verification.didit.me/v3/poa/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'document=@./utility-bill.pdf' \\\n -F 'expected_address=Av. Monseñor Tavella 1291, Salta, Argentina' \\\n -F 'expected_country=ARG' \\\n -F 'expected_first_name=Sophia' \\\n -F 'expected_last_name=Martinez' \\\n -F 'poa_languages_allowed=en,es' \\\n -F 'vendor_data=user-123' \\\n --max-time 45" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nurl = 'https://verification.didit.me/v3/poa/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('utility-bill.pdf', 'rb') as f:\n files = {'document': ('utility-bill.pdf', f, 'application/pdf')}\n data = {\n 'expected_address': 'Av. Monseñor Tavella 1291, Salta, Argentina',\n 'expected_country': 'ARG',\n 'expected_first_name': 'Sophia',\n 'expected_last_name': 'Martinez',\n 'poa_languages_allowed': 'en,es',\n 'vendor_data': 'user-123',\n }\n # LLM-based extraction takes 5-15s (multi-page PDFs up to ~30s) - allow headroom\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=45)\n\nresp.raise_for_status()\npoa = resp.json()['poa']\nprint('status:', poa['status'])\nprint('issuer:', poa['issuer'], '| issued:', poa['issue_date'])\nprint('address:', poa['poa_formatted_address'])\nfor w in poa['warnings']:\n print('warning:', w['risk'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('document', new Blob([fs.readFileSync('./utility-bill.pdf')]), 'utility-bill.pdf');\nform.append('expected_address', 'Av. Monseñor Tavella 1291, Salta, Argentina');\nform.append('expected_country', 'ARG');\nform.append('expected_first_name', 'Sophia');\nform.append('expected_last_name', 'Martinez');\nform.append('poa_languages_allowed', 'en,es');\nform.append('vendor_data', 'user-123');\n\n// LLM-based extraction takes 5-15s (multi-page PDFs up to ~30s) - allow >=45s\nconst response = await fetch('https://verification.didit.me/v3/poa/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n signal: AbortSignal.timeout(45_000),\n});\n\nif (!response.ok) throw new Error(`POA verification failed: ${response.status}`);\nconst { poa } = await response.json();\nconsole.log('status:', poa.status);\nconsole.log('warnings:', poa.warnings.map((w) => w.risk));" + } + ] + } + }, + "/v3/document-ai/": { + "post": { + "summary": "Document AI (custom field extraction + tamper checks)", + "description": "Extract the fields **you** define from any supporting document — payslips, proof of funds, source-of-wealth letters, tax statements, contracts, invoices — in one call. Send the file plus a `fields` definition (key, name, extraction instruction, type) and get back typed values (`text`, `number`, `date` as `YYYY-MM-DD`), an optional name-match score against the expected person or company, PDF/EXIF tamper signals, an `Approved`/`Declined`/`In Review` `status`, and a `warnings` list explaining every issue found. This is the server-to-server counterpart of the hosted Document AI workflow step (see `/core-technology/document-ai/overview`); it uses Didit's default extraction model and does not expose provider selection.\n\n**Latency.** Extraction is LLM-based: typical calls take **5–15 seconds**, multi-page PDFs up to **~30 seconds**. Configure a client timeout of **at least 45 seconds** and do not retry before the call completes.\n\n**Request format.** `multipart/form-data` only. `fields` is **one JSON-encoded array string** (1–30 items, at most 16 KB); bracket-style form arrays are rejected with `400`. Each item is `{key, name, instruction, type, required, is_full_name}` — `key` is the JSON key you get back (`^[a-z0-9_]{1,64}$`, unique), `instruction` tells the model what to read, `type` is `text` (default) | `number` | `date`, `required` raises `DOCUMENT_AI_MISSING_REQUIRED_FIELDS` when the value cannot be read, and at most one field may set `is_full_name: true` to mark the value compared against the expected name.\n\n**Name matching.** Pass `expected_first_name` + `expected_last_name` (person) **or** `expected_company_name` (+ optional `expected_company_country`) — not both. The value of the `is_full_name` field (or, when none is marked, the first text field whose key looks like a name) is compared fuzzily; `name_match_score` (0–100) and `name_on_document` are always returned when both sides exist, and a score below `document_ai_name_match_score_threshold` (default 80) adds `DOCUMENT_AI_NAME_MISMATCH`, governed by `document_ai_name_mismatch_action`.\n\n**Decision logic.** Each risk maps to a configurable action: `DECLINE` → `status: \"Declined\"`, `REVIEW` → `status: \"In Review\"`, `NO_ACTION` → informational only. Defaults mirror the workflow step: unreadable document `REVIEW`, missing required fields `REVIEW`, suspected manipulation `DECLINE`, name mismatch `NO_ACTION`, unsupported file `REVIEW`. A document that cannot be processed at all returns `400`; a readable but problematic document returns `200` with `Declined`/`In Review` — always inspect `document_ai.status` and `document_ai.warnings`, not just the HTTP code.\n\n**Billing.** Each `200` response consumes one Document AI API credit — **$0.20 per document, no free tier** (standalone APIs never have one). When the organization's balance cannot cover the call, the endpoint returns `403` before any processing. Sandbox API keys return a deterministic mock response and are not charged.\n\n**Persistence.** With `save_api_request=true` (default) the call is stored as an API-type session: the document, the extracted values, the field definitions and the name-match snapshot are kept, the session shows up in the Business Console, and `request_id` works with `GET /v3/session/{sessionId}/decision/`.", + "operationId": "post_v3document-ai", + "x-didit-sdk": { + "resource": "document_ai", + "method": "analyze", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "document", + "fields" + ], + "properties": { + "document": { + "type": "string", + "format": "binary", + "description": "The document to read. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `pdf`, `webp`, `zip`. Maximum upload size: **30 MB** (larger files are rejected with `400`). Multi-page PDFs are supported (expect higher latency); encrypted PDFs are rejected with `400`. Images are automatically compressed before processing." + }, + "fields": { + "type": "string", + "description": "One JSON-encoded array string with **1–30** field definitions (maximum 16 KB). Each item: `key` (required, `^[a-z0-9_]{1,64}$`, unique — the key returned in `extracted_data`), `name` (≤100 chars, shown in messages), `instruction` (≤500 chars, what the model should read), `type` (`text` default | `number` | `date`), `required` (boolean, default `false`), `is_full_name` (boolean, default `false`, at most one per request). Bracket-style form arrays, invalid JSON, duplicate keys, or more than one `is_full_name` return `400` before any processing or billing.", + "example": "[{\"key\": \"employer_name\", \"name\": \"Employer name\", \"instruction\": \"The employer or company issuing the payslip.\", \"type\": \"text\", \"required\": true, \"is_full_name\": false}, {\"key\": \"employee_name\", \"name\": \"Employee name\", \"instruction\": \"The full name of the employee as printed on the payslip.\", \"type\": \"text\", \"required\": true, \"is_full_name\": true}, {\"key\": \"net_pay\", \"name\": \"Net pay\", \"instruction\": \"The net amount paid to the employee for the period.\", \"type\": \"number\", \"required\": true, \"is_full_name\": false}, {\"key\": \"pay_date\", \"name\": \"Pay date\", \"instruction\": \"The payment date shown on the payslip.\", \"type\": \"date\", \"required\": false, \"is_full_name\": false}]" + }, + "expected_first_name": { + "type": "string", + "description": "First name to verify against the document's name field (fuzzy match, transliteration-aware). Use together with `expected_last_name`; cannot be combined with `expected_company_name`.", + "example": "Sophia" + }, + "expected_last_name": { + "type": "string", + "description": "Last name to verify against the document's name field, combined with `expected_first_name` for `name_match_score`.", + "example": "Martinez" + }, + "expected_company_name": { + "type": "string", + "description": "Company name to verify against the document's name field (for business documents). Cannot be combined with `expected_first_name` / `expected_last_name` (no example value on purpose — the generated request sample uses the person fields; swap them for this one when verifying a business document)." + }, + "expected_company_country": { + "type": "string", + "description": "Optional ISO 3166-1 alpha-2 country code (two ASCII letters, e.g. `FR`) that tunes company-name normalization for `expected_company_name`. Only used with `expected_company_name`." + }, + "document_ai_name_match_score_threshold": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 80, + "description": "Minimum `name_match_score` (0–100) to consider the name matched. A lower score adds `DOCUMENT_AI_NAME_MISMATCH`, governed by `document_ai_name_mismatch_action`.", + "example": 80 + }, + "document_ai_unreadable_document_action": { + "type": "string", + "enum": [ + "DECLINE", + "REVIEW", + "NO_ACTION" + ], + "default": "REVIEW", + "description": "What to do when no field could be read from the document (`DOCUMENT_AI_UNREADABLE_DOCUMENT`)." + }, + "document_ai_missing_required_fields_action": { + "type": "string", + "enum": [ + "DECLINE", + "REVIEW", + "NO_ACTION" + ], + "default": "REVIEW", + "description": "What to do when at least one `required` field could not be read (`DOCUMENT_AI_MISSING_REQUIRED_FIELDS`; `additional_data.missing_fields` lists them)." + }, + "document_ai_document_tampering_action": { + "type": "string", + "enum": [ + "DECLINE", + "REVIEW", + "NO_ACTION" + ], + "default": "DECLINE", + "description": "What to do when PDF/EXIF forensics suspect manipulation — modified after digital signing, known PDF editor, overlay-text manipulation, suspicious re-export, image anomalies (`DOCUMENT_AI_SUSPECTED_MANIPULATION`)." + }, + "document_ai_name_mismatch_action": { + "type": "string", + "enum": [ + "DECLINE", + "REVIEW", + "NO_ACTION" + ], + "default": "NO_ACTION", + "description": "What to do when `name_match_score` is below the threshold (`DOCUMENT_AI_NAME_MISMATCH`)." + }, + "document_ai_unsupported_file_action": { + "type": "string", + "enum": [ + "DECLINE", + "REVIEW", + "NO_ACTION" + ], + "default": "REVIEW", + "description": "What to do when the upload is empty or not a processable file (`DOCUMENT_AI_UNSUPPORTED_FILE`)." + }, + "save_api_request": { + "type": "boolean", + "default": true, + "description": "When `true` (default), persists the call as an API-type session — visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, with the uploaded document, the extracted values, the field definitions and the name-match snapshot stored. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only; the call is still billed.", + "example": true + }, + "vendor_data": { + "type": "string", + "description": "Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later.", + "example": "user-123" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={\"flow\":\"onboarding\"}`) — it is parsed into an object.", + "example": { + "flow": "onboarding" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Document processed. `document_ai.status` is `Approved`, `Declined`, or `In Review`; every detected issue is itemized in `document_ai.warnings`. A problematic document still returns `200` — inspect the body, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id.", + "content": { + "application/json": { + "examples": { + "Approved": { + "summary": "Payslip read cleanly, name matched", + "value": { + "request_id": "9d6a3b1e-2f0c-4a8e-9b7d-1c2e3f4a5b6c", + "document_ai": { + "status": "Approved", + "extracted_data": { + "employer_name": "Acme Payments Ltd", + "employee_name": "Sophia Martinez", + "net_pay": 3120.5, + "pay_date": "2026-07-31" + }, + "detected_codes": [ + { + "type": "CODE_128", + "page_number": 1, + "position": [ + [ + 96, + 456 + ], + [ + 96, + 616 + ], + [ + 1012, + 611 + ], + [ + 1015, + 456 + ] + ], + "x": 96, + "y": 456, + "width": 919, + "height": 160, + "page_width": 1200, + "page_height": 1700, + "raw_payload": "SU5WLTIwMjYtMDA0Mg==", + "parsed_payload": "INV-2026-0042" + } + ], + "fields": [ + { + "key": "employer_name", + "name": "Employer name", + "instruction": "The employer or company issuing the payslip.", + "type": "text", + "required": true, + "is_full_name": false + }, + { + "key": "employee_name", + "name": "Employee name", + "instruction": "The full name of the employee as printed on the payslip.", + "type": "text", + "required": true, + "is_full_name": true + }, + { + "key": "net_pay", + "name": "Net pay", + "instruction": "The net amount paid to the employee for the period.", + "type": "number", + "required": true, + "is_full_name": false + }, + { + "key": "pay_date", + "name": "Pay date", + "instruction": "The payment date shown on the payslip.", + "type": "date", + "required": false, + "is_full_name": false + } + ], + "name_match_score": 100.0, + "name_on_document": "Sophia Martinez", + "warnings": [] + }, + "vendor_data": "user-123", + "metadata": { + "flow": "onboarding" + }, + "created_at": "2026-08-19T16:40:12.318Z" + } + }, + "In Review — missing required field": { + "summary": "`net_pay` could not be read; default action REVIEW", + "value": { + "request_id": "3f2a9c7b-5d4e-4b1a-8c6f-0e9d8c7b6a5f", + "document_ai": { + "status": "In Review", + "extracted_data": { + "employer_name": "Acme Payments Ltd", + "employee_name": "Sophia Martinez", + "net_pay": null, + "pay_date": "2026-07-31" + }, + "detected_codes": [], + "fields": [ + { + "key": "employer_name", + "name": "Employer name", + "instruction": "The employer or company issuing the payslip.", + "type": "text", + "required": true, + "is_full_name": false + }, + { + "key": "employee_name", + "name": "Employee name", + "instruction": "The full name of the employee as printed on the payslip.", + "type": "text", + "required": true, + "is_full_name": true + }, + { + "key": "net_pay", + "name": "Net pay", + "instruction": "The net amount paid to the employee for the period.", + "type": "number", + "required": true, + "is_full_name": false + }, + { + "key": "pay_date", + "name": "Pay date", + "instruction": "The payment date shown on the payslip.", + "type": "date", + "required": false, + "is_full_name": false + } + ], + "name_match_score": 100.0, + "name_on_document": "Sophia Martinez", + "warnings": [ + { + "risk": "DOCUMENT_AI_MISSING_REQUIRED_FIELDS", + "feature": "DOCUMENT_AI", + "additional_data": { + "missing_fields": [ + "Net pay" + ] + }, + "log_type": "warning", + "short_description": "Required fields could not be extracted", + "long_description": "One or more fields configured as required could not be extracted from the uploaded document." + } + ] + }, + "vendor_data": null, + "metadata": null, + "created_at": "2026-08-19T16:41:05.002Z" + } + }, + "Declined — suspected manipulation": { + "summary": "PDF forensics flagged a known editor; default action DECLINE", + "value": { + "request_id": "7c1d2e3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f", + "document_ai": { + "status": "Declined", + "extracted_data": { + "employer_name": "Acme Payments Ltd", + "employee_name": "Sophia Martinez", + "net_pay": 3120.5, + "pay_date": "2026-07-31" + }, + "detected_codes": [], + "fields": [ + { + "key": "employer_name", + "name": "Employer name", + "instruction": "The employer or company issuing the payslip.", + "type": "text", + "required": true, + "is_full_name": false + }, + { + "key": "employee_name", + "name": "Employee name", + "instruction": "The full name of the employee as printed on the payslip.", + "type": "text", + "required": true, + "is_full_name": true + }, + { + "key": "net_pay", + "name": "Net pay", + "instruction": "The net amount paid to the employee for the period.", + "type": "number", + "required": true, + "is_full_name": false + }, + { + "key": "pay_date", + "name": "Pay date", + "instruction": "The payment date shown on the payslip.", + "type": "date", + "required": false, + "is_full_name": false + } + ], + "name_match_score": null, + "name_on_document": "Sophia Martinez", + "warnings": [ + { + "risk": "DOCUMENT_AI_SUSPECTED_MANIPULATION", + "feature": "DOCUMENT_AI", + "additional_data": { + "detection_method": "known_editor", + "processed_by_known_editor": "ilovepdf" + }, + "log_type": "error", + "short_description": "Suspected document manipulation", + "long_description": "The system detected signs of potential manipulation or editing in the uploaded document." + } + ] + }, + "vendor_data": null, + "metadata": null, + "created_at": "2026-08-19T16:42:40.771Z" + } + } + }, + "schema": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." + }, + "document_ai": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "Approved", + "Declined", + "In Review" + ], + "description": "`Approved` when no warning resolves to a decline or review; `Declined` when at least one risk maps to `DECLINE`; otherwise `In Review` when at least one maps to `REVIEW`. The `warnings` list explains why." + }, + "extracted_data": { + "type": "object", + "additionalProperties": true, + "description": "One entry per requested field key. Values are typed per the field `type` — strings for `text`, numbers for `number`, `YYYY-MM-DD` strings for `date` — or `null` when the value could not be read.", + "example": { + "employer_name": "Acme Payments Ltd", + "employee_name": "Sophia Martinez", + "net_pay": 3120.5, + "pay_date": "2026-07-31" + } + }, + "detected_codes": { + "type": "array", + "description": "QR codes and barcodes detected across every processed page. Empty when none are found. A located but undecodable QR code has `parsed_payload: null`.", + "items": { + "$ref": "#/components/schemas/DocumentAIDetectedCode" + } + }, + "fields": { + "type": "array", + "description": "The normalized field definitions that were used for this extraction, echoed back (also stored with the session when `save_api_request=true`).", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "instruction": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "text", + "number", + "date" + ] + }, + "required": { + "type": "boolean" + }, + "is_full_name": { + "type": "boolean" + } + } + } + }, + "name_match_score": { + "type": "number", + "nullable": true, + "minimum": 0, + "maximum": 100, + "description": "Fuzzy similarity (0–100) between `name_on_document` and the expected person/company name. Returned whenever both a name value and an expected name exist — on matches **and** mismatches; `null` otherwise.", + "example": 96.0 + }, + "name_on_document": { + "type": "string", + "nullable": true, + "description": "The name value taken from the document (the `is_full_name` field, or the first name-like text field) that was compared against the expected name. `null` when no name value was available.", + "example": "Sophia Martinez" + }, + "warnings": { + "type": "array", + "description": "Empty on a clean approval. Every entry explains one detected risk; entries with `log_type: \"error\"` set `status` to `Declined`, entries with `log_type: \"warning\"` set it to `In Review`.", + "items": { + "type": "object", + "properties": { + "risk": { + "type": "string", + "enum": [ + "DOCUMENT_AI_UNREADABLE_DOCUMENT", + "DOCUMENT_AI_MISSING_REQUIRED_FIELDS", + "DOCUMENT_AI_SUSPECTED_MANIPULATION", + "DOCUMENT_AI_NAME_MISMATCH", + "DOCUMENT_AI_UNSUPPORTED_FILE" + ], + "description": "Machine-readable risk code. Each one is governed by the matching `document_ai_*_action` request option.", + "example": "DOCUMENT_AI_MISSING_REQUIRED_FIELDS" + }, + "feature": { + "type": "string", + "enum": [ + "DOCUMENT_AI" + ], + "description": "Feature that raised the warning. Always `DOCUMENT_AI` on this endpoint." + }, + "additional_data": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Extra context for the risk: `{\"missing_fields\": [\"Net pay\"]}` for missing required fields, `{\"name_on_document\", \"score\", \"threshold\"}` for a name mismatch, and the forensic finding (`detection_method`, producer/editor details) for suspected manipulation. `null` when there is nothing to add.", + "example": { + "missing_fields": [ + "Net pay" + ] + } + }, + "log_type": { + "type": "string", + "enum": [ + "error", + "warning", + "information" + ], + "description": "Severity derived from the configured action: `error` (DECLINE), `warning` (REVIEW), `information` (NO_ACTION)." + }, + "short_description": { + "type": "string", + "description": "Human-readable one-liner for the risk.", + "example": "Required fields could not be extracted" + }, + "long_description": { + "type": "string", + "description": "Human-readable explanation, interpolated with `additional_data` where relevant." + } + } + } + } + } + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Echo of the `vendor_data` you sent, or `null`." + }, + "metadata": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Echo of the `metadata` object you sent, or `null`." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the request was processed (UTC, ISO 8601)." + } + } + } + } + } + }, + "400": { + "description": "Validation or processing error. Field-level problems return DRF's standard envelope (one array of messages per offending field). Encrypted or unreadable documents and extraction failures return `{\"error\": ...}`.", + "content": { + "application/json": { + "examples": { + "Missing document": { + "summary": "`document` not included in the form data", + "value": { + "document": [ + "No file was submitted." + ] + } + }, + "Invalid fields payload": { + "summary": "`fields` is not one JSON-encoded array string", + "value": { + "fields": [ + "fields must be a valid JSON array string." + ] + } + }, + "Duplicate field keys": { + "summary": "Two items share the same `key`", + "value": { + "fields": [ + "Every fields item must have a unique key." + ] + } + }, + "Conflicting expectations": { + "summary": "Person and company expectations sent together", + "value": { + "non_field_errors": [ + "Provide either person expected-name fields or expected_company_name, not both." + ] + } + }, + "Invalid metadata": { + "summary": "`metadata` is not a JSON object (or contains a NUL character)", + "value": { + "metadata": [ + "Metadata must be a JSON object." + ] + } + }, + "Oversized vendor_data": { + "summary": "`vendor_data` over the 2,000-byte cap", + "value": { + "vendor_data": [ + "vendor_data is too long (maximum 2000 bytes)." + ] + } + }, + "Encrypted PDF": { + "summary": "Password-protected PDF", + "value": { + "error": "The PDF is encrypted. Please upload a decrypted PDF or a photo instead." + } + }, + "Extraction failed": { + "summary": "The extraction pipeline could not process the document", + "value": { + "error": [ + "Error extracting Document AI information" + ] + } + } + }, + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "403": { + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls — and contract-billed organizations whose rate card has no Document AI API entry — return `403` with `{\"error\": ...}` before any document processing happens.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Not enough credits": { + "summary": "Organization balance cannot cover the call", + "value": { + "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + } + }, + "Contract rate missing": { + "summary": "Contract-billed organization whose rate card has no Document AI API entry", + "value": { + "error": "Document AI API is not included in your contract rate card. Contact support@didit.me to enable it." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "error": { + "type": "string" + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "content": { + "application/json": { + "examples": { + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/v3/aml/": { + "post": { + "summary": "AML screening (standalone)", + "operationId": "post_v3aml", + "x-didit-sdk": { + "resource": "aml", + "method": "screen", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "Idempotency-Key", + "in": "header", + "required": false, + "description": "Optional retry-safety key, up to 255 characters. Reusing the same key with the same request body returns the original response without running or billing a second screening. Reusing the key with a different body returns `409`.", + "schema": { + "type": "string", + "maxLength": 255 + } + } + ], + "description": "Screen a person or company against PEP, sanctions, warning/watchlist, and adverse-media datasets in one call.\n\n**Two-score model.** Each hit carries a `match_score` (0–100, identity confidence) and a `risk_score` (0–100, entity risk). Hits whose `match_score` is below `aml_match_score_threshold` (default **93**) are tagged `review_status: \"False Positive\"` and ignored for risk purposes; the highest `risk_score` among the remaining hits becomes `aml.score`. The final `aml.status` follows your thresholds: `score` > `aml_score_review_threshold` (default **100**) → `Declined`; `score` > `aml_score_approve_threshold` (default **80**) → `In Review`; otherwise `Approved`. Whenever `score` is non-zero and ≥ the approve threshold a `POSSIBLE_MATCH_FOUND` warning is added (a score of exactly 0 never warns).\n\n**Match-score weights.** `aml_name_weight` + `aml_dob_weight` + `aml_country_weight` must sum to exactly 100 (defaults 60/25/15). Components missing from the request are re-normalized away; a matching `document_number` acts as a golden key that overrides `match_score` to 100.\n\n**Latency.** Plain screenings typically complete in a few seconds. With `include_adverse_media=true` the API waits for adverse-media enrichment (an initial ~5 s pause plus up to ~25 s of polling) — configure a client timeout of **at least 60 seconds**.\n\n**Persistence.** `save_api_request` defaults to **true**: the screening is stored as a session (`request_id` works with `GET /v3/session/{sessionId}/decision/`), it appears in the console, a `status.updated` webhook fires, and hit review statuses can be updated later. With `save_api_request=false` nothing is stored and `request_id` is a transient correlation id. `include_ongoing_monitoring=true` keeps re-screening the profile and requires `save_api_request=true` (enforced with a `400`).\n\n**Idempotency.** Send an `Idempotency-Key` header when retrying after a timeout or transient error. The key is scoped to your application and the exact request body. The first completed request stores its response; later identical retries return that same response and do not run or bill another AML screening. If the original request is still running, retry later with the same key. If the same key is reused with a different body, the API returns `409`.\n\n**Billing.** One AML API credit is charged after the first successful screening. Replayed idempotent responses are not charged again; an insufficient balance returns `403` before any new screening happens.\n\n**Sandbox.** Keys from sandbox applications skip screening and billing entirely and return a mocked result — `Approved` with zero hits unless you pass `sandbox_scenario`. `save_api_request` works the same as on live keys, so a saved sandbox screening is retrievable through `GET /v3/session/{sessionId}/decision/` and `GET /v3/session/{sessionId}/generate-pdf/`.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "full_name" + ], + "properties": { + "full_name": { + "type": "string", + "minLength": 2, + "description": "Full name of the person or company to screen. Minimum **2 characters** - the screening provider's own floor. A shorter or blank name returns `400` on this field before any screening runs, so nothing is billed.", + "example": "Antonio Ejemplo Modelo" + }, + "entity_type": { + "type": "string", + "enum": [ + "person", + "company" + ], + "default": "person", + "description": "Type of entity to screen. Defaults to `person`." + }, + "date_of_birth": { + "type": "string", + "format": "date", + "description": "Date of birth (persons) or incorporation date (companies), `YYYY-MM-DD`. Improves match precision via the DOB weight.", + "example": "1972-02-29" + }, + "nationality": { + "type": "string", + "description": "ISO 3166-1 **alpha-2** country code (e.g. `ES`). Invalid codes return `400`. Improves match precision via the country weight.", + "example": "ES" + }, + "document_number": { + "type": "string", + "description": "Identity document number. Not weighted — acts as a golden key: an exact match overrides the hit's `match_score` to 100, a hard mismatch penalizes it. Sent to the screening provider as a search filter only when it is **2-50 characters**; a value outside that range is accepted and stored, and the screening still runs and is billed, just not narrowed by document number." + }, + "aml_score_approve_threshold": { + "type": "number", + "minimum": 0, + "maximum": 100, + "default": 80, + "description": "Risk-score threshold at or below which the result is `Approved`. Must be ≤ `aml_score_review_threshold` (else `400`)." + }, + "aml_score_review_threshold": { + "type": "number", + "minimum": 0, + "maximum": 100, + "default": 100, + "description": "Risk-score threshold above which the result is `Declined`. Scores between the two thresholds produce `In Review`." + }, + "aml_name_weight": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 60, + "description": "Weight of name similarity in the per-hit match score. The three weights must sum to exactly 100 (else `400`)." + }, + "aml_dob_weight": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 25, + "description": "Weight of date of birth in the per-hit match score." + }, + "aml_country_weight": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 15, + "description": "Weight of country/nationality in the per-hit match score." + }, + "aml_match_score_threshold": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "default": 93, + "description": "Per-hit cutoff: hits with `match_score` below this are `False Positive` (excluded from risk assessment); at or above are `Unreviewed` possible matches." + }, + "include_adverse_media": { + "type": "boolean", + "default": false, + "description": "Also search news media for negative coverage. Adds up to ~30 s of latency while results are gathered — use a ≥60 s client timeout." + }, + "include_ongoing_monitoring": { + "type": "boolean", + "default": false, + "description": "Keep re-screening this profile and push changes via webhook. Requires `save_api_request=true` (else `400`)." + }, + "save_api_request": { + "type": "boolean", + "default": true, + "description": "Persist the screening as a session (console visibility, decision endpoint, webhooks, hit review). Set to `false` for a stateless check." + }, + "vendor_data": { + "type": "string", + "description": "Your identifier for the screened user; echoed back and stored with the session." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Free-form JSON stored with the request and echoed back." + }, + "sandbox_scenario": { + "type": "string", + "description": "Sandbox-only scenario slug. Only accepted when the API key belongs to a sandbox-mode application; rejected on live applications. A standalone screening has no session to arm, so the slug goes on the screening request itself: `decline_aml_hit` returns one production-shaped hit and settles the screening as `Declined`, `review_aml_possible_match` settles it `In Review`, and a scenario that arms no AML outcome leaves the screening clean. See the scenario catalog at GET /v1/sandbox/scenarios/ or [Sandbox & Test Data](/integration/sandbox-testing).", + "example": "decline_aml_hit" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/aml/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -H 'Idempotency-Key: aml-user-1234-2026-08-20' \\\n -d '{\n \"full_name\": \"Antonio Ejemplo Modelo\",\n \"entity_type\": \"person\",\n \"date_of_birth\": \"1972-02-29\",\n \"nationality\": \"ES\",\n \"include_adverse_media\": false,\n \"vendor_data\": \"user-1234\"\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/aml/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n \"Idempotency-Key\": \"aml-user-1234-2026-08-20\",\n },\n json={\n \"full_name\": \"Antonio Ejemplo Modelo\",\n \"entity_type\": \"person\",\n \"date_of_birth\": \"1972-02-29\",\n \"nationality\": \"ES\",\n \"include_adverse_media\": False,\n \"vendor_data\": \"user-1234\",\n },\n timeout=60, # adverse media enrichment can add ~30s\n)\nresp.raise_for_status()\nprint(resp.json()[\"aml\"][\"status\"]) # Approved / In Review / Declined" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const res = await fetch('https://verification.didit.me/v3/aml/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n 'Idempotency-Key': 'aml-user-1234-2026-08-20',\n },\n body: JSON.stringify({\n full_name: 'Antonio Ejemplo Modelo',\n entity_type: 'person',\n date_of_birth: '1972-02-29',\n nationality: 'ES',\n include_adverse_media: false,\n vendor_data: 'user-1234',\n }),\n});\nconst data = await res.json();\nconsole.log(data.aml.status);" + } + ], + "responses": { + "200": { + "description": "Screening completed. `aml.status` reflects your thresholds; every hit at or above the internal inclusion floor is listed in `aml.hits` with its identity `match_score`, entity `risk_score`, and a `score_breakdown` explaining the match.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." + }, + "aml": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "Approved", + "In Review", + "Declined" + ], + "description": "`Declined` when `score` > `aml_score_review_threshold`; `In Review` when `score` > `aml_score_approve_threshold`; `Approved` otherwise (including when no score is available)." + }, + "total_hits": { + "type": "integer", + "description": "Number of entries in `hits`." + }, + "entity_type": { + "type": "string", + "enum": [ + "person", + "company" + ], + "description": "Echo of the screened entity type." + }, + "hits": { + "type": "array", + "items": { + "type": "object", + "description": "One screening hit. `match_score` (0–100) measures identity confidence — hits below `aml_match_score_threshold` are tagged `review_status: \"False Positive\"` and excluded from the risk assessment; hits at or above it are `\"Unreviewed\"` possible matches. `risk_score` (0–100) measures how risky the matched entity is and drives the top-level `aml.score`.", + "properties": { + "id": { + "type": "string", + "nullable": true, + "description": "Stable identifier of the matched entity in the screening datasets." + }, + "url": { + "type": "string", + "nullable": true, + "description": "Source URL for the matched entity, when available." + }, + "match": { + "type": "boolean", + "nullable": true, + "description": "Legacy exact-match flag. Prefer `match_score` + `review_status`." + }, + "score": { + "type": "number", + "nullable": true, + "description": "DEPRECATED weighted match score in the 0–1 range. Use `match_score` instead." + }, + "match_score": { + "type": "number", + "nullable": true, + "description": "Identity confidence (0–100). Compared against `aml_match_score_threshold` to split False Positives from Possible Matches." + }, + "risk_score": { + "type": "number", + "nullable": true, + "description": "Entity risk score (0–100). The highest `risk_score` among non-false-positive hits becomes the top-level `aml.score`." + }, + "target": { + "type": "boolean", + "nullable": true + }, + "caption": { + "type": "string", + "nullable": true, + "description": "Display name of the matched entity." + }, + "datasets": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + }, + "description": "Datasets the entity appears in (e.g. `PEP`, `PEP Level 1`, sanctions list names)." + }, + "features": { + "type": "object", + "nullable": true + }, + "properties": { + "type": "object", + "nullable": true, + "description": "Entity profile: `name`, `alias`, `notes`, `gender`, `birthDate`, `birthPlace`, `nationality`, `position`, and other biographical arrays (each nullable)." + }, + "rca_name": { + "type": "string", + "nullable": true, + "description": "Name of the related close associate through which the entity was matched, when the hit is an RCA." + }, + "pep_matches": { + "type": "array", + "description": "PEP list entries backing this hit.", + "items": { + "type": "object", + "properties": { + "list_name": { + "type": "string" + }, + "publisher": { + "type": "string" + }, + "source_url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "matched_name": { + "type": "string" + }, + "pep_position": { + "type": "string" + }, + "date_of_birth": { + "type": "string" + }, + "place_of_birth": { + "type": "string" + }, + "aliases": { + "type": "array", + "items": { + "type": "string" + } + }, + "education": { + "type": "array", + "items": { + "type": "string" + } + }, + "other_sources": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "sanction_matches": { + "type": "array", + "items": { + "type": "object" + }, + "description": "Sanctions list entries backing this hit (same backing-list shape as `pep_matches`, plus sanction-specific fields)." + }, + "warning_matches": { + "type": "array", + "items": { + "type": "object" + }, + "description": "Warning/watchlist entries backing this hit." + }, + "adverse_media_matches": { + "type": "array", + "description": "Adverse-media articles. Populated only when `include_adverse_media=true`.", + "items": { + "type": "object", + "properties": { + "headline": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "sentiment": { + "type": "string" + }, + "sentiment_score": { + "type": "number" + }, + "source_url": { + "type": "string" + }, + "thumbnail": { + "type": "string", + "nullable": true + }, + "country": { + "type": "string", + "nullable": true + }, + "author_name": { + "type": "string", + "nullable": true + }, + "publication_date": { + "type": "string", + "nullable": true + }, + "adverse_keywords": { + "type": "array", + "items": { + "type": "string" + } + }, + "other_sources": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "adverse_media_details": { + "type": "object", + "nullable": true, + "description": "Aggregated adverse-media sentiment for the entity (`sentiment`, `sentiment_score`, `adverse_keywords` frequency map). Populated only when `include_adverse_media=true`." + }, + "first_seen": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the entity first appeared in the datasets." + }, + "last_seen": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Most recent dataset refresh that included the entity." + }, + "linked_entities": { + "type": "array", + "description": "Known relationships of the matched entity (family, associates, companies).", + "items": { + "type": "object", + "properties": { + "name": { + "type": "array", + "items": { + "type": "string" + } + }, + "relation": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "type": "array", + "items": { + "type": "string" + } + }, + "details": { + "type": "array", + "items": { + "type": "string" + } + }, + "active": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "risk_view": { + "type": "object", + "nullable": true, + "description": "Per-dimension risk breakdown (`crimes`, `countries`, `categories`, `custom_list`), each with `score`, `weightage`, `risk_level`, and a `risk_scores` map." + }, + "additional_information": { + "type": "object", + "nullable": true, + "description": "Free-form extra context, e.g. a `flag_summary` array explaining why the entity is flagged." + }, + "review_status": { + "type": "string", + "nullable": true, + "enum": [ + "False Positive", + "Unreviewed", + "Confirmed Match", + "Inconclusive" + ], + "description": "`False Positive` — `match_score` below `aml_match_score_threshold`, excluded from risk assessment. `Unreviewed` — possible match awaiting review. `Confirmed Match` / `Inconclusive` — set manually by a reviewer (via the Business Console) on saved sessions; that review capability is not part of this spec." + }, + "score_breakdown": { + "type": "object", + "nullable": true, + "description": "How `match_score` was computed. Name, date of birth, and country each contribute `_score` × `_weight_normalized` points; weights are re-normalized when a component is missing from the screened data. The document number is not weighted — it acts as a 'golden key': `MATCH` overrides the score to 100, `NEUTRAL` leaves it unchanged, `HARD_MISMATCH` applies a penalty.", + "properties": { + "name_score": { + "type": "integer", + "nullable": true, + "description": "Raw name similarity (0–100)." + }, + "name_weight": { + "type": "integer", + "nullable": true, + "description": "Configured name weight (`aml_name_weight`)." + }, + "name_weight_normalized": { + "type": "number", + "nullable": true, + "description": "Name weight after re-normalization (0–100)." + }, + "name_contribution": { + "type": "number", + "nullable": true, + "description": "Points the name contributed to `total_score`." + }, + "dob_score": { + "type": "integer", + "nullable": true, + "description": "Date-of-birth score as a percentage of its weight (−100 to 100)." + }, + "dob_weight": { + "type": "integer", + "nullable": true, + "description": "Configured DOB weight (`aml_dob_weight`)." + }, + "dob_weight_normalized": { + "type": "number", + "nullable": true, + "description": "DOB weight after re-normalization (0–100)." + }, + "dob_contribution": { + "type": "number", + "nullable": true, + "description": "Points the DOB contributed to `total_score`." + }, + "country_score": { + "type": "integer", + "nullable": true, + "description": "Country score as a percentage of its weight (−100 to 100)." + }, + "country_weight": { + "type": "integer", + "nullable": true, + "description": "Configured country weight (`aml_country_weight`)." + }, + "country_weight_normalized": { + "type": "number", + "nullable": true, + "description": "Country weight after re-normalization (0–100)." + }, + "country_contribution": { + "type": "number", + "nullable": true, + "description": "Points the country contributed to `total_score`." + }, + "document_number_match_type": { + "type": "string", + "nullable": true, + "enum": [ + "MATCH", + "NEUTRAL", + "HARD_MISMATCH" + ], + "description": "Golden-key result for the document number comparison." + }, + "document_number_effect": { + "type": "string", + "nullable": true, + "description": "Human-readable description of the document-number effect." + }, + "total_score": { + "type": "integer", + "nullable": true, + "description": "Final weighted match score (0–100) — mirrors `match_score`." + } + } + } + } + } + }, + "score": { + "type": "number", + "nullable": true, + "description": "Overall AML risk score (0–100): the highest `risk_score` among hits that are not False Positives. Drives `status` via the two thresholds." + }, + "screened_data": { + "type": "object", + "description": "Echo of what was screened: `full_name`, `date_of_birth`, `nationality`, `document_number` (absent fields are `null`).", + "properties": { + "full_name": { + "type": "string" + }, + "date_of_birth": { + "type": "string", + "nullable": true + }, + "nationality": { + "type": "string", + "nullable": true + }, + "document_number": { + "type": "string", + "nullable": true + } + } + }, + "warnings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "description": "Feature that raised the warning." + }, + "risk": { + "type": "string", + "description": "Machine-readable risk identifier." + }, + "additional_data": { + "type": "object", + "nullable": true, + "description": "Extra context for the warning, when available." + }, + "log_type": { + "type": "string", + "description": "`warning`, `information`, or `error` — how the risk affected the outcome." + }, + "short_description": { + "type": "string", + "description": "One-line human-readable summary." + }, + "long_description": { + "type": "string", + "description": "Full human-readable explanation." + } + } + }, + "description": "Contains a `POSSIBLE_MATCH_FOUND` warning whenever `score` is non-zero and ≥ `aml_score_approve_threshold` (a score of exactly 0 never warns); empty otherwise." + } + } + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Echo of the `vendor_data` you sent." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Echo of the `metadata` you sent." + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "examples": { + "No hits (Approved)": { + "summary": "Clean screening — zero hits", + "value": { + "request_id": "d27c5509-7a66-45a9-b05e-d69d9f4c336a", + "aml": { + "status": "Approved", + "total_hits": 0, + "entity_type": "person", + "hits": [], + "score": 0, + "screened_data": { + "full_name": "Maximilian Mustermann-Doe", + "date_of_birth": "1992-03-14", + "nationality": "DE", + "document_number": null + }, + "warnings": [] + }, + "vendor_data": "user-1234", + "metadata": { + "internal_ref": "abc-123" + }, + "created_at": "2026-06-11T10:21:07.013938+00:00" + } + }, + "PEP hit (Approved — risk below approve threshold)": { + "summary": "One PEP possible match; risk score 73 stays under the default approve threshold of 80", + "value": { + "request_id": "0b7e7a3e-65a1-4f3d-9c7a-1de2f9a40111", + "aml": { + "status": "Approved", + "total_hits": 1, + "entity_type": "person", + "hits": [ + { + "id": "kDpfszrbkDdaxpMVZw3NLg", + "url": "https://news.example.org/articles/sample", + "match": false, + "score": 0.93, + "match_score": 93.0, + "risk_score": 73.0, + "target": null, + "caption": "Antonio Ejemplo Modelo", + "datasets": [ + "PEP Level 1", + "PEP" + ], + "features": null, + "rca_name": "", + "first_seen": "2026-01-07T00:00:00", + "last_seen": "2026-01-07T00:00:00", + "review_status": "Unreviewed", + "properties": { + "name": [ + "Antonio Ejemplo Modelo", + "Antonio Ejemplo" + ], + "alias": [ + "Antonio Ejemplo Modelo", + "Ejemplo Modelo, Pedro" + ], + "notes": [ + "Prime Minister of Spain" + ], + "gender": [ + "male" + ], + "birthDate": [ + "1972-02-29" + ], + "birthPlace": [ + "Madrid, Spain" + ], + "nationality": null, + "position": null + }, + "risk_view": { + "crimes": { + "score": 0.0, + "weightage": 20, + "risk_level": "Low", + "risk_scores": {} + }, + "countries": { + "score": 9.64, + "weightage": 30, + "risk_level": "Low", + "risk_scores": { + "Spain": 25.71 + } + }, + "categories": { + "score": 62.5, + "weightage": 50, + "risk_level": "High", + "risk_scores": { + "PEP": 100.0, + "PEP Level 1": 100.0 + } + }, + "custom_list": {} + }, + "score_breakdown": { + "name_score": 89, + "name_weight": 60, + "name_weight_normalized": 60.0, + "name_contribution": 53.4, + "dob_score": 100, + "dob_weight": 25, + "dob_weight_normalized": 25.0, + "dob_contribution": 25.0, + "country_score": 100, + "country_weight": 15, + "country_weight_normalized": 15.0, + "country_contribution": 15.0, + "document_number_match_type": "NEUTRAL", + "document_number_effect": "No document number provided for screening", + "total_score": 93 + }, + "pep_matches": [ + { + "aliases": [], + "education": [], + "list_name": "Presidency of the Government of Spain", + "publisher": "Presidency of the Government of Spain", + "source_url": "https://news.example.org/articles/sample", + "description": "The Presidency of the Government is the central institutional body in Spain that supports the Prime Minister and coordinates the general action of the central government.", + "matched_name": "Antonio Ejemplo Modelo", + "pep_position": "", + "date_of_birth": "1972-02-29", + "other_sources": [], + "place_of_birth": "Madrid, Spain" + } + ], + "sanction_matches": [], + "warning_matches": [], + "adverse_media_matches": [], + "adverse_media_details": null, + "linked_entities": [ + { + "name": [ + "Maria Begona Gomez Fernandez" + ], + "active": [], + "status": [], + "details": [], + "relation": [ + "spouse" + ] + } + ], + "additional_information": { + "flag_summary": [ + "Antonio Ejemplo Modelo has been flagged as a Level 1 Politically Exposed Person (PEP) from Spain, serving as the President of the Government of Spain." + ] + } + } + ], + "score": 73.0, + "screened_data": { + "full_name": "Pedro Sanchez Perez-Castejon", + "date_of_birth": "1972-02-29", + "nationality": "ES", + "document_number": null + }, + "warnings": [] + }, + "vendor_data": null, + "metadata": null, + "created_at": "2026-06-11T10:23:44.812000+00:00" + } + } + } + } + } + }, + "400": { + "description": "Validation error. Field-level problems return DRF's standard envelope (one array of messages per offending field); cross-field rules (weight sum, threshold ordering, monitoring-without-save) return `{\"error\": [...]}`.", + "content": { + "application/json": { + "examples": { + "Missing full_name": { + "summary": "`full_name` is the only required field", + "value": { + "full_name": [ + "This field is required." + ] + } + }, + "Short full_name": { + "summary": "`full_name` must be at least 2 characters", + "value": { + "full_name": [ + "Ensure this field has at least 2 characters." + ] + } + }, + "Invalid nationality": { + "summary": "`nationality` must be ISO 3166-1 alpha-2", + "value": { + "nationality": [ + "Invalid ISO 3166-1 alpha-2 country code." + ] + } + }, + "Weights do not sum to 100": { + "summary": "`aml_name_weight` + `aml_dob_weight` + `aml_country_weight` ≠ 100", + "value": { + "error": [ + "AML weights must sum to 100. Current sum: 90 (name: 50, dob: 25, country: 15)" + ] + } + }, + "Thresholds out of order": { + "summary": "Approve threshold above review threshold", + "value": { + "error": [ + "Approve threshold (90.0) cannot be greater than review threshold (80.0)" + ] + } + }, + "Monitoring without saving": { + "summary": "`include_ongoing_monitoring=true` with `save_api_request=false`", + "value": { + "error": [ + "Ongoing monitoring can only be enabled when the API request is saved." + ] + } + } + } + } + } + }, + "403": { + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any screening or provider call happens.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Not enough credits": { + "summary": "Organization balance cannot cover the call", + "value": { + "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "content": { + "application/json": { + "examples": { + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/v3/face-search/": { + "post": { + "summary": "Face Search", + "description": "Search a face image (1:N) against your application's face search index — faces enrolled from verification sessions, saved standalone API calls, direct user-profile face uploads, and your block/allow lists — and get back ranked similarity matches plus blocklist and duplicate warnings.\n\n**How it works.** The largest detected face in `user_image` is embedded and searched against the index, scoped to your application. Up to **5 matches** above the similarity floor are returned in `matches`, each with `similarity_percentage`, the originating session (`session_id`, `session_number`, `status`, `vendor_data`), `user_details` extracted during that session's document verification, `is_blocklisted` / `is_allowlisted` flags, and a `source` discriminator (`session`, `imported`, or `list_entry`).\n\n- `search_type=most_similar` (default) ranks every enrolled face by similarity — use it for deduplication and fraud-ring investigation.\n- `search_type=blocklisted_or_approved` restricts candidates to blocklisted faces, allowlisted faces, faces from approved sessions, and imported user-profile faces, ranking blocklisted entries first — use it when the primary goal is blocklist screening.\n\n`status` is `Declined` only when a `FACE_IN_BLOCKLIST` or `POSSIBLE_FACE_IN_BLOCKLIST` warning fires; `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` (`information`) and `MULTIPLE_FACES_DETECTED` (`warning`) never decline. Unlike Passive Liveness, this endpoint does **not** exclude prior sessions with the same `vendor_data` from matching. A passive-liveness analysis also runs internally and is stored with the persisted session, but its result is not part of this response.\n\n**Billing.** Each `200` response consumes one Face Search API credit (standalone APIs have no free tier). Insufficient balance returns `403` before any image processing.\n\n**Session persistence and face enrollment (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session (Business Console, `GET /v3/session/{sessionId}/decision/` via the returned `request_id`, `status.updated` webhook) and the searched face is enrolled into your face search index. Faces enrolled by Face Search calls are excluded from future Face Search results, so repeated searches never match each other. When `false`, the call is one-shot: nothing is stored, the face is not enrolled, and `match_image_url` values are internal storage paths rather than downloadable URLs.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", + "operationId": "post_v3face-search", + "x-didit-sdk": { + "resource": "face_search", + "method": "search", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" + ], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "user_image" + ], + "properties": { + "user_image": { + "type": "string", + "format": "binary", + "description": "Front-facing face image to search with. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy. The image must contain at least one detectable face — otherwise the endpoint returns `400`. When several faces are present, the largest one is searched and a `MULTIPLE_FACES_DETECTED` warning is added." + }, + "search_type": { + "type": "string", + "default": "most_similar", + "enum": [ + "most_similar", + "blocklisted_or_approved" + ], + "description": "Search policy. `most_similar` (default) ranks every enrolled face in your application by similarity. `blocklisted_or_approved` restricts candidates to blocklisted faces, allowlisted faces, faces from approved sessions, and imported user-profile faces — with blocklisted entries ranked first.", + "example": "most_similar" + }, + "rotate_image": { + "type": "boolean", + "default": false, + "description": "When `true`, the service tries 90-degree rotations of the input and uses the orientation that yields the best face detection. Useful when EXIF orientation is missing. Adds latency.", + "example": false + }, + "save_api_request": { + "type": "boolean", + "default": true, + "description": "When `true` (default), persists the call as an API-type session, emits a `status.updated` webhook, presigns `match_image_url` values, and enrolls the searched face into your face search index (Face Search enrollments are excluded from future searches). When `false`, the search is one-shot — no session is stored and the face is not enrolled.", + "example": true + }, + "vendor_data": { + "type": "string", + "description": "Optional opaque identifier (your user id, email, UUID…) stored on the persisted session and echoed back. It does not exclude same-user faces from the search results.", + "example": "user-123" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Optional JSON object stored with the session (when `save_api_request=true`) and echoed back. In multipart requests, send it as a JSON-encoded string field — it is parsed into an object.", + "example": { + "flow": "dedup_check" + } + } + } + }, + "example": { + "user_image": "(binary JPEG/PNG selfie)", + "search_type": "most_similar", + "rotate_image": false, + "save_api_request": true, + "vendor_data": "user-123", + "metadata": { + "flow": "dedup_check" + } + } + } + } + }, + "responses": { + "200": { + "description": "Face search completed. `face_search.matches` holds up to 5 similar faces ordered by similarity; an empty array means no enrolled face exceeded the similarity floor. `status` is `Declined` only on blocklist hits — duplicate matches alone return `Approved`, so inspect `matches` and `warnings`, not just `status`. When `save_api_request=true`, `request_id` is the persisted session id. Each match carries `source` (`session`, `imported`, `list_entry`, or `retained_template`), `vendor_user_id`, and `biometric_template_id`; a `retained_template` match comes from an image-free biometric template kept after the source session was deleted and has `null` session fields, `match_image_url`, and `user_details`.", + "content": { + "application/json": { + "examples": { + "Blocklist hit (Declined)": { + "summary": "The face matches a blocklisted face", + "value": { + "request_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", + "face_search": { + "status": "Declined", + "total_matches": 1, + "matches": [ + { + "session_id": "882c42d5-8a4d-4d20-8080-a22f57822c86", + "session_number": 323442, + "similarity_percentage": 99.99, + "source": "session", + "vendor_data": "user-1", + "verification_date": "2025-01-01T00:00:00Z", + "user_details": { + "full_name": "Jane Marie Doe", + "document_type": "ID", + "document_number": "X1234567" + }, + "match_image_url": "https:///face/3f6a1c2e/reference.jpg", + "status": "Approved", + "is_blocklisted": true, + "is_allowlisted": false, + "api_service": null, + "vendor_user_id": "2f7c1a9e-8b4d-4c6a-9e1f-3d5b7a9c1e2f", + "biometric_template_id": null + } + ], + "user_image": { + "entities": [ + { + "bbox": [ + 40, + 40, + 120, + 120 + ], + "confidence": 0.732973 + } + ], + "best_angle": 0 + }, + "warnings": [ + { + "risk": "FACE_IN_BLOCKLIST", + "feature": "LIVENESS", + "additional_data": { + "blocklisted_session_id": "882c42d5-8a4d-4d20-8080-a22f57822c86", + "blocklisted_session_number": 323442, + "api_service": null + }, + "log_type": "error", + "short_description": "Face in blocklist", + "long_description": "The system identified a face in the blocklist, which means the face is not allowed to be verified." + } + ] + }, + "vendor_data": "user-123", + "metadata": null, + "created_at": "2026-06-12T01:04:42.763237+00:00" + } + }, + "Duplicate found (Approved)": { + "summary": "The face already appeared in another approved session — informational, no decline", + "value": { + "request_id": "5e0c3a1f-7b2d-4c8e-9f10-2a3b4c5d6e7f", + "face_search": { + "status": "Approved", + "total_matches": 1, + "matches": [ + { + "session_id": "1f2e3d4c-5b6a-7980-1122-334455667788", + "session_number": 1024, + "similarity_percentage": 87.42, + "source": "session", + "vendor_data": "user-9", + "verification_date": "2025-11-20T09:15:00Z", + "user_details": null, + "match_image_url": "https:///face/9c8b7a6d/reference.jpg", + "status": "Approved", + "is_blocklisted": false, + "is_allowlisted": false, + "api_service": "PASSIVE_LIVENESS", + "vendor_user_id": "2f7c1a9e-8b4d-4c6a-9e1f-3d5b7a9c1e2f", + "biometric_template_id": null + } + ], + "user_image": { + "entities": [ + { + "bbox": [ + 40, + 40, + 120, + 120 + ], + "confidence": 0.732973 + } + ], + "best_angle": 0 + }, + "warnings": [ + { + "risk": "DUPLICATED_FACE", + "feature": "LIVENESS", + "additional_data": { + "duplicated_session_id": "1f2e3d4c-5b6a-7980-1122-334455667788", + "duplicated_session_number": 1024, + "api_service": "PASSIVE_LIVENESS" + }, + "log_type": "information", + "short_description": "Duplicated face from other approved session", + "long_description": "The system identified a duplicated face from another approved session, requiring further investigation." + } + ] + }, + "vendor_data": "user-123", + "metadata": null, + "created_at": "2026-06-12T01:04:42.763237+00:00" + } + }, + "No matches (Approved)": { + "summary": "No enrolled face exceeded the similarity floor", + "value": { + "request_id": "9d8c7b6a-5f4e-3d2c-1b0a-998877665544", + "face_search": { + "status": "Approved", + "total_matches": 0, + "matches": [], + "user_image": { + "entities": [ + { + "bbox": [ + 40, + 40, + 120, + 120 + ], + "confidence": 0.732973 + } + ], + "best_angle": 0 + }, + "warnings": [] + }, + "vendor_data": "user-123", + "metadata": null, + "created_at": "2026-06-12T01:04:42.763237+00:00" + } + } + }, + "schema": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." + }, + "face_search": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "Approved", + "Declined" + ], + "description": "`Declined` only when a `FACE_IN_BLOCKLIST` or `POSSIBLE_FACE_IN_BLOCKLIST` warning fired. Duplicate matches and multiple-face warnings never decline." + }, + "total_matches": { + "type": "integer", + "description": "Number of entries in `matches` (0–5).", + "example": 1 + }, + "matches": { + "type": "array", + "maxItems": 5, + "description": "Up to 5 faces from your face search index that exceed the similarity floor, ordered by similarity (with blocklisted entries ranked first when `search_type=blocklisted_or_approved`). Empty when no enrolled face is similar enough.", + "items": { + "type": "object", + "properties": { + "session_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Id of the verification session the matched face belongs to. `null` when `source` is `imported`, `list_entry`, or `retained_template`.", + "example": "882c42d5-8a4d-4d20-8080-a22f57822c86" + }, + "session_number": { + "type": "integer", + "nullable": true, + "description": "Human-friendly session number shown in the Business Console. `null` for non-session matches.", + "example": 323442 + }, + "similarity_percentage": { + "type": "number", + "format": "float", + "minimum": 0, + "maximum": 100, + "description": "Similarity between the submitted face and the matched face (0–100).", + "example": 99.99 + }, + "source": { + "type": "string", + "enum": [ + "session", + "imported", + "list_entry", + "retained_template" + ], + "description": "Where the matched face was enrolled from: `session` — a verification session or a saved standalone API call; `imported` — a face uploaded directly to a User profile; `list_entry` — a face attached to one of your lists; `retained_template` — an image-free biometric template stored on a User, listed under `/v3/biometric-templates/`." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "`vendor_data` of the matched session or linked User for imported and retained-template faces.", + "example": "user-1" + }, + "vendor_user_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Linked User id. `null` for list entries and session matches without a User.", + "example": "2f7c1a9e-4b28-4a6d-97f1-83576f80e1b4" + }, + "biometric_template_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Id of the retained biometric template. Set only when `source` is `retained_template`; `null` otherwise.", + "example": null + }, + "verification_date": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the matched face was originally captured, or when a retained template was created (`YYYY-MM-DDThh:mm:ssZ`). `null` for `list_entry` matches.", + "example": "2025-01-01T00:00:00Z" + }, + "user_details": { + "type": "object", + "nullable": true, + "description": "Identity data extracted during the matched session's document verification. `null` when the matched session has no document data (e.g. liveness-only sessions) and always `null` for `retained_template` matches. For `source: imported` matches it is populated from the User profile instead: `full_name` set, `document_type`/`document_number` null.", + "properties": { + "full_name": { + "type": "string", + "nullable": true, + "example": "Jane Marie Doe" + }, + "document_type": { + "type": "string", + "nullable": true, + "example": "ID" + }, + "document_number": { + "type": "string", + "nullable": true, + "example": "X1234567" + } + } + }, + "match_image_url": { + "type": "string", + "nullable": true, + "description": "Time-limited URL (`https:///...`) of the matched face's reference image when `save_api_request=true`. With `save_api_request=false` this is an internal storage path that is not directly downloadable. Always `null` for `retained_template` matches because retained templates contain no image.", + "example": "https:///face/3f6a1c2e/reference.jpg" + }, + "status": { + "type": "string", + "nullable": true, + "description": "Status of the matched session (`Approved`, `Declined`, `In Review`, …). `null` for `imported`/`list_entry` matches.", + "example": "Approved" + }, + "is_blocklisted": { + "type": "boolean", + "description": "`true` when the matched face is on your face blocklist. Any sufficiently similar blocklisted match declines the search." + }, + "is_allowlisted": { + "type": "boolean", + "description": "`true` when the matched face is on one of your allowlists. Allowlisted matches suppress duplicate warnings." + }, + "api_service": { + "type": "string", + "nullable": true, + "description": "Set when the matched face was enrolled by a standalone API call (e.g. `PASSIVE_LIVENESS`); `null` for regular verification sessions and imported faces.", + "example": null + } + } + } + }, + "user_image": { + "type": "object", + "description": "Face-detection results for the submitted image. Entries here carry only `bbox` and `confidence`.", + "properties": { + "entities": { + "type": "array", + "description": "One entry per detected face. The largest face is the one searched.", + "items": { + "type": "object", + "properties": { + "bbox": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 4, + "maxItems": 4, + "description": "Bounding box `[x_min, y_min, x_max, y_max]` in pixels.", + "example": [ + 40, + 40, + 120, + 120 + ] + }, + "confidence": { + "type": "number", + "format": "float", + "minimum": 0, + "maximum": 1, + "description": "Face-detection confidence (0–1).", + "example": 0.732973 + } + } + } + }, + "best_angle": { + "type": "integer", + "description": "Rotation (degrees) that produced the best face detection; non-zero only when `rotate_image=true` corrected the orientation. Never null on this endpoint (defaults to 0 when the model omits it).", + "example": 0 + } + } + }, + "warnings": { + "type": "array", + "description": "Risk signals. `FACE_IN_BLOCKLIST` / `POSSIBLE_FACE_IN_BLOCKLIST` (`error`) decline the search; `MULTIPLE_FACES_DETECTED` (`warning`) and `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` (`information`) are advisory.", + "items": { + "type": "object", + "properties": { + "risk": { + "type": "string", + "enum": [ + "MULTIPLE_FACES_DETECTED", + "FACE_IN_BLOCKLIST", + "POSSIBLE_FACE_IN_BLOCKLIST", + "DUPLICATED_FACE", + "POSSIBLE_DUPLICATED_FACE" + ], + "description": "Machine-readable risk code." + }, + "feature": { + "type": "string", + "enum": [ + "LIVENESS" + ], + "description": "Feature that raised the warning. Always `LIVENESS` on this endpoint." + }, + "additional_data": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "`null` for `MULTIPLE_FACES_DETECTED`. Blocklist hits carry `{blocklisted_session_id, blocklisted_session_number, api_service}`; duplicate hits carry `{duplicated_session_id, duplicated_session_number, api_service}` pointing at the first matching session." + }, + "log_type": { + "type": "string", + "enum": [ + "error", + "warning", + "information" + ], + "description": "Severity. `error` warnings drive `status` to `Declined`; `warning` and `information` entries are advisory and never decline on their own." + }, + "short_description": { + "type": "string", + "description": "Human-readable one-line summary of the risk." + }, + "long_description": { + "type": "string", + "description": "Human-readable explanation of the risk." + } + } + } + } + } + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Echo of the `vendor_data` you sent, or `null`." + }, + "metadata": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Echo of the `metadata` object you sent, or `null`." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp (UTC) of when the response was generated, e.g. `2026-06-12T01:04:42.763237+00:00`." + } + } + } + } + } + }, + "400": { + "description": "Validation error. Returned when `user_image` is missing, exceeds 5 MB, has an unsupported extension, or when **no face is detected** in the submitted image. Field errors use DRF's `{field: [messages]}` envelope; face-detection failures use `{\"error\": ...}`.", + "content": { + "application/json": { + "examples": { + "No face detected": { + "summary": "The image decodes fine but contains no detectable face", + "value": { + "error": "No face detected in the image" + } + }, + "Missing image": { + "summary": "`user_image` not included in the form data", + "value": { + "user_image": [ + "No file was submitted." + ] + } + }, + "Unsupported file extension": { + "summary": "File extension outside tiff/jpg/jpeg/png/webp", + "value": { + "user_image": [ + "File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, webp." + ] + } + }, + "File too large": { + "summary": "Upload exceeds the 5 MB limit", + "value": { + "user_image": [ + "File size should not exceed 5 MB" + ] + } + } + } + } + } + }, + "403": { + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any image processing happens.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Not enough credits": { + "summary": "Organization balance cannot cover the call", + "value": { + "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "content": { + "application/json": { + "examples": { + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X POST 'https://verification.didit.me/v3/face-search/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'user_image=@./selfie.jpg' \\\n -F 'search_type=most_similar' \\\n -F 'save_api_request=true' \\\n -F 'vendor_data=user-123'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nurl = 'https://verification.didit.me/v3/face-search/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('selfie.jpg', 'rb') as f:\n files = {'user_image': ('selfie.jpg', f, 'image/jpeg')}\n data = {\n 'search_type': 'most_similar',\n 'save_api_request': 'true',\n 'vendor_data': 'user-123',\n }\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=60)\n\nresp.raise_for_status()\nbody = resp.json()\nprint('status:', body['face_search']['status'])\nprint('matches:', body['face_search']['total_matches'])\nfor m in body['face_search']['matches']:\n print(f\" session={m['session_id']} similarity={m['similarity_percentage']} blocklisted={m['is_blocklisted']}\")" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('user_image', new Blob([fs.readFileSync('./selfie.jpg')]), 'selfie.jpg');\nform.append('search_type', 'most_similar');\nform.append('save_api_request', 'true');\nform.append('vendor_data', 'user-123');\n\nconst response = await fetch('https://verification.didit.me/v3/face-search/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n});\n\nif (!response.ok) throw new Error(`Face search failed: ${response.status}`);\nconst body = await response.json();\nconsole.log('status:', body.face_search.status, 'matches:', body.face_search.total_matches);\nbody.face_search.matches.forEach(m =>\n console.log(` session=${m.session_id} similarity=${m.similarity_percentage} blocklisted=${m.is_blocklisted}`)\n);" + } + ] + } + }, + "/v3/sessions/": { + "get": { + "summary": "List verification sessions (KYC and/or KYB) with filters and pagination", + "description": "Returns a paginated list of verification sessions for the application identified by your API key, newest first (descending `session_number`).\n\n**Canonical URL:** the route is `/v3/sessions/` (trailing slash). Requests to `/v3/sessions` without the slash receive a `301` redirect to the slashed URL with the query string preserved; most HTTP clients follow it transparently on GET.\n\n**When to use this endpoint vs webhooks.** Use this endpoint for dashboards, back-office search, reconciliation, and batch exports. Do not poll it to track the progress of individual verifications - configure [webhooks](/integration/webhooks) instead, which push `status.updated` events the moment a session changes. For one session's full result, call `GET /v3/session/{sessionId}/decision/`.\n\n**KYC and KYB sessions.** `session_kind` selects which kinds are listed:\n- `user` (default) - User Verification (KYC) sessions only. Backward-compatible behavior.\n- `business` - Business Verification (KYB) sessions only. Rows have a different, smaller shape (see the response schema).\n- `all` - both kinds in one response: the KYC page and the KYB page for the same `limit`/`offset` are concatenated in `results` (KYC rows first), so a page can contain up to `2 x limit` rows. `count` is the sum of both kinds and `next`/`previous` reflect the KYC-side pagination. For strict pagination of mixed data, call `user` and `business` separately.\n\nEvery row carries a `session_kind` discriminator (`\"user\"` or `\"business\"`).\n\n**Pagination.** Standard limit/offset: `limit` (default 50) and `offset` (default 0), with `next`/`previous` ready-made page URLs. For KYC rows `count` is exact. For KYB rows `count` is computed with a bounded scan and is capped at 100 - when there are more than 100 matches it stays at 100, so iterate using `next` (or by checking whether a full page came back) rather than computing total pages from `count`.\n\n**Filter semantics.** All filters combine with AND and apply to KYC rows. When `session_kind` is `business` or `all`, the KYB side honors the shared parameters `vendor_data`, `status`, `workflow_id`, `country` (registry-company country code), `date_from`, `date_to`, `search` (company name, registration number — type registration numbers without separators, e.g. `DPC001` not `DPC-001` — vendor_data, session number, or session UUID), `warning`, and `organization_name`; the remaining KYC-only filters are ignored for KYB rows. Comma-separated parameters (`status`, `country`, `document_type`, `region`, `workflow_id`, `document_name`, `warning`, `organization_name`, `session_type`) match any of the listed values (OR within the parameter). `vendor_data` is an exact match - use `search` for partial matching. Unknown values in list filters simply return no rows (no error); malformed typed values (`date_from`, `date_to`, `didit_internal_id`, `api_service`, `session_kind`) return `400` with a per-field error object.\n\n**Search.** `search` adapts to the input: a full UUID looks up `session_id`; digits (with optional `+ - . space`) match session-number prefix, document/personal numbers, phone-number prefix, or `vendor_data`; values containing `@` match email prefixes; any other text matches names (accent-insensitive) and `vendor_data`.\n\nNo fixed per-endpoint rate limit is enforced, but keep list polling infrequent and rely on webhooks for real-time updates.", + "operationId": "get_v3_sessions", + "x-didit-sdk": { + "resource": "sessions", + "method": "list", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "session_kind", + "in": "query", + "required": false, + "description": "Which kind of sessions to list: `user` (KYC only, default), `business` (KYB only), or `all` (both, concatenated - see the description for merged-pagination semantics). Matching is case-insensitive (`BUSINESS` works); any value other than a case variant of `user`/`business`/`all` returns `400`.", + "schema": { + "type": "string", + "enum": [ + "user", + "business", + "all" + ], + "default": "user" + }, + "example": "all" + }, + { + "name": "limit", + "in": "query", + "required": false, + "description": "Maximum number of rows per page (per kind when `session_kind=all`). Defaults to 50.", + "schema": { + "type": "integer", + "default": 50, + "minimum": 1 + }, + "example": 20 + }, + { + "name": "offset", + "in": "query", + "required": false, + "description": "Number of rows to skip before the first returned row. Defaults to 0. Prefer following the `next`/`previous` URLs from the response.", + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + }, + "example": 20 + }, + { + "name": "status", + "in": "query", + "required": false, + "description": "Comma-separated list of session statuses to include, e.g. `Approved,In Review`. Values are case-sensitive; see [Verification Statuses](/integration/verification-statuses). Unknown values match nothing.", + "schema": { + "type": "string" + }, + "example": "Approved,In Review" + }, + { + "name": "vendor_data", + "in": "query", + "required": false, + "description": "Exact-match filter on the `vendor_data` you sent at session creation (your own user/business identifier). For partial matches use `search` instead.", + "schema": { + "type": "string" + }, + "example": "your-user-id-123" + }, + { + "name": "search", + "in": "query", + "required": false, + "description": "Smart free-text search. A full UUID matches `session_id`; numeric input matches session-number prefix, document/personal numbers, phone prefix, or `vendor_data`; input containing `@` matches email prefixes; other text matches names (accent-insensitive) and `vendor_data`. Free text containing digits additionally matches document/personal-number search fields and session-number prefixes.", + "schema": { + "type": "string" + }, + "example": "jane doe" + }, + { + "name": "workflow_id", + "in": "query", + "required": false, + "description": "Comma-separated workflow UUIDs (find them on the Workflows page in the Console). Malformed UUIDs in the list are ignored; if none are valid the result is empty.", + "schema": { + "type": "string" + }, + "example": "9f9b1234-aaaa-bbbb-cccc-1234567890ab" + }, + { + "name": "date_from", + "in": "query", + "required": false, + "description": "Only sessions created on or after this date (UTC), format `YYYY-MM-DD`. Malformed dates return `400`.", + "schema": { + "type": "string", + "format": "date" + }, + "example": "2026-06-01" + }, + { + "name": "date_to", + "in": "query", + "required": false, + "description": "Only sessions created up to this date (UTC), format `YYYY-MM-DD`. For KYC rows the entire day is included; for KYB rows the comparison is `created_at <= date_to` (midnight), so use the next day's date to include a full day of KYB sessions.", + "schema": { + "type": "string", + "format": "date" + }, + "example": "2026-06-30" + }, + { + "name": "country", + "in": "query", + "required": false, + "description": "Comma-separated country filter. For KYC rows: ISO 3166-1 alpha-3 codes of the ID document's issuing state (e.g. `ESP,USA`), which may differ from nationality. For KYB rows: the registry company's country code.", + "schema": { + "type": "string" + }, + "example": "ESP,USA" + }, + { + "name": "document_type", + "in": "query", + "required": false, + "description": "Comma-separated ID-document type codes verified in the session (KYC rows only): `P` (Passport), `ID` (Identity Card), `DL` (Driver's License), `RP` (Residence Permit), `SSC` (Social Security Card), `HIC` (Health Insurance Card), `WP` (Work Permit), `TC` (Tax Card), `VISA`, `PSC` (Public Service Card), `BC` (Birth Certificate), `OTHER`. Note the response field `document_type` contains the display name, not the code.", + "schema": { + "type": "string" + }, + "example": "P,ID" + }, + { + "name": "document_name", + "in": "query", + "required": false, + "description": "Exact document names; multiple names can be passed comma-separated, but the value is only split on commas when it contains more than one ` - ` separator (a lone name with an embedded comma is matched whole). Names are as recognized by document verification (front or back side), e.g. `Spain - Id Card (2021)`. KYC rows only.", + "schema": { + "type": "string" + }, + "example": "Spain - Id Card (2021)" + }, + { + "name": "region", + "in": "query", + "required": false, + "description": "Comma-separated document region/state values (for documents issued per region, such as US driver's licenses). KYC rows only.", + "schema": { + "type": "string" + }, + "example": "CA,TX" + }, + { + "name": "didit_internal_id", + "in": "query", + "required": false, + "description": "Filter by Didit's internal user identifier (the `didit_internal_id` response field) to list all sessions of one verified user. Must be a valid UUID; malformed values return `400`. KYC rows only.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "d9e78474-6c8d-4f2a-bb90-6f5c2f3a1147" + }, + { + "name": "session_type", + "in": "query", + "required": false, + "description": "Comma-separated session creation channels: `API`, `HOSTED`, `MIGRATED`. The special value `ALL` (or omitting the parameter) disables the filter. KYC rows only.", + "schema": { + "type": "string" + }, + "example": "HOSTED" + }, + { + "name": "api_service", + "in": "query", + "required": false, + "description": "Filter standalone API sessions by service. Invalid values return `400`. KYC rows only.", + "schema": { + "type": "string", + "enum": [ + "ID_VERIFICATION", + "FACE_MATCH", + "AGE_ESTIMATION", + "FACE_SEARCH", + "POA", + "AML", + "KYB_REGISTRY", + "PASSIVE_LIVENESS", + "DATABASE_VALIDATION", + "PHONE_VERIFICATION", + "EMAIL_VERIFICATION" + ] + }, + "example": "AML" + }, + { + "name": "screened_full_name", + "in": "query", + "required": false, + "description": "Accent-insensitive name search inside AML and database-validation screened data. When combined with `api_service=AML` or `api_service=DATABASE_VALIDATION`, the search is restricted to that feature's screened data. KYC rows only.", + "schema": { + "type": "string" + }, + "example": "Jane Doe" + }, + { + "name": "warning", + "in": "query", + "required": false, + "description": "Comma-separated warning risk codes; returns sessions that raised any of them (e.g. `POSSIBLE_FRAUD`). Risk codes are listed per feature in the [warnings documentation](/integration/webhooks). Applies to KYC rows and, with business warning codes, to KYB rows.", + "schema": { + "type": "string" + }, + "example": "POSSIBLE_FRAUD" + }, + { + "name": "status_initialized_as_in_review", + "in": "query", + "required": false, + "description": "When `true`, only sessions whose decision initially landed in `In Review` (useful to audit manual-review volume). KYC rows only.", + "schema": { + "type": "boolean" + }, + "example": true + }, + { + "name": "is_kyc_to_be_reviewed", + "in": "query", + "required": false, + "description": "`true` returns sessions whose document verification is flagged for manual review; `false` returns sessions with document verification explicitly not flagged. Omit to disable. KYC rows only.", + "schema": { + "type": "boolean" + }, + "example": true + }, + { + "name": "euuid", + "in": "query", + "required": false, + "description": "Filter by the end user's euuid (UUID assigned to the verifying user, used by Console deep links). Must be a valid UUID; malformed values return a 400 field error. KYC rows only.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "0f8fad5b-d9cb-469f-a165-70867728950e" + }, + { + "name": "organization_name", + "in": "query", + "required": false, + "description": "Comma-separated organization names. Console-oriented; with an API key (already scoped to one application) it only matches your own organization's name.", + "schema": { + "type": "string" + }, + "example": "Acme Inc" + } + ], + "responses": { + "200": { + "description": "Paginated session list. `results` items are KYC rows, KYB rows, or both depending on `session_kind`; discriminate with each row's `session_kind` field.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "count", + "next", + "previous", + "results" + ], + "properties": { + "count": { + "type": "integer", + "description": "Number of sessions matching the filters. Exact for KYC rows; for KYB rows it is capped at 100 (use `next` to know whether more pages exist). For `session_kind=all` it is the sum of both kinds.", + "example": 2 + }, + "next": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Absolute URL of the next page, or `null` on the last page.", + "example": "https://verification.didit.me/v3/sessions/?limit=20&offset=20" + }, + "previous": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Absolute URL of the previous page, or `null` on the first page.", + "example": null + }, + "results": { + "type": "array", + "description": "Session rows, newest first. With `session_kind=all`, KYC rows come first, then KYB rows.", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/UserSessionListItem" + }, + { + "$ref": "#/components/schemas/BusinessSessionListItem" + } + ], + "discriminator": { + "propertyName": "session_kind", + "mapping": { + "user": "#/components/schemas/UserSessionListItem", + "business": "#/components/schemas/BusinessSessionListItem" + } + } + } + } + } + }, + "examples": { + "KYC sessions (default)": { + "summary": "session_kind=user - one completed and one not-started session", + "value": { + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "session_id": "5b3720ed-d429-42ef-b67f-37ea805f48ee", + "session_kind": "user", + "session_number": 720, + "session_url": "https://verify.didit.me/session/CuXoBYCYVO5Y", + "portrait_image": "https:///portraits/5b3720ed.jpg?X-Amz-Expires=3600&X-Amz-Signature=...", + "document_type": "Passport", + "full_name": "Jane Marie Doe", + "country": "ESP", + "status": "Approved", + "vendor_data": "your-user-id-123", + "didit_internal_id": "d9e78474-6c8d-4f2a-bb90-6f5c2f3a1147", + "created_at": "2026-06-09T12:26:54.328364Z", + "features": [ + { + "feature": "ID_VERIFICATION", + "status": "Approved" + }, + { + "feature": "LIVENESS", + "status": "Approved" + }, + { + "feature": "FACE_MATCH", + "status": "Approved" + }, + { + "feature": "AML", + "status": "Approved" + }, + { + "feature": "IP_ANALYSIS", + "status": "Approved" + } + ], + "phone_number": { + "number": "+14155552671", + "is_verified": true + }, + "email_address": { + "email": "jane@example.com", + "is_verified": true + }, + "additional_details": { + "aml": { + "total_hits": 0, + "entity_type": "person", + "is_adverse_media_fetched": false + }, + "liveness": { + "method": "PASSIVE", + "score": 0.98 + }, + "face_match": { + "score": 0.95 + } + } + }, + { + "session_id": "038ad62f-5ebd-46c2-afe9-4b2956bf687f", + "session_kind": "user", + "session_number": 719, + "session_url": "https://verify.didit.me/session/7RGKiO6ioGH6", + "portrait_image": null, + "document_type": null, + "full_name": null, + "country": null, + "status": "Not Started", + "vendor_data": "your-user-id-124", + "didit_internal_id": "eb4e679d-ca4b-461f-9685-687bac29ebdb", + "created_at": "2026-06-09T08:02:23.343053Z", + "features": [ + { + "feature": "ID_VERIFICATION", + "status": "Not Finished" + }, + { + "feature": "LIVENESS", + "status": "Not Finished" + }, + { + "feature": "FACE_MATCH", + "status": "Not Finished" + }, + { + "feature": "AML", + "status": "Not Finished" + }, + { + "feature": "IP_ANALYSIS", + "status": "Not Finished" + } + ], + "phone_number": null, + "email_address": null, + "additional_details": null + } + ] + } + }, + "Mixed KYC + KYB (session_kind=all)": { + "summary": "session_kind=all - KYC row followed by a KYB row", + "value": { + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "session_id": "5b3720ed-d429-42ef-b67f-37ea805f48ee", + "session_kind": "user", + "session_number": 720, + "session_url": "https://verify.didit.me/session/CuXoBYCYVO5Y", + "portrait_image": null, + "document_type": "Identity Card", + "full_name": "Jane Marie Doe", + "country": "ESP", + "status": "In Review", + "vendor_data": "your-user-id-123", + "didit_internal_id": "d9e78474-6c8d-4f2a-bb90-6f5c2f3a1147", + "created_at": "2026-06-09T12:26:54.328364Z", + "features": [ + { + "feature": "ID_VERIFICATION", + "status": "In Review" + }, + { + "feature": "LIVENESS", + "status": "Approved" + } + ], + "phone_number": null, + "email_address": null, + "additional_details": { + "liveness": { + "method": "PASSIVE", + "score": 0.98 + } + } + }, + { + "session_id": "8a1f63c2-1b9e-4f3a-9c41-2f6a1f0b7d52", + "session_kind": "business", + "session_number": 42, + "status": "Approved", + "vendor_data": "your-company-id-77", + "created_at": "2026-06-08T09:14:02.118450Z", + "workflow_id": "9f9b1234-aaaa-bbbb-cccc-1234567890ab", + "workflow_type": "kyb", + "workflow_label": "Standard KYB", + "company_name": "Acme Holdings S.L.", + "registration_number": "B12345678", + "country": "ES" + } + ] + } + } + } + } + } + }, + "301": { + "description": "Redirect to the canonical trailing-slash URL. Returned when the request path is `/v3/sessions` (no trailing slash); the `Location` header points to `/v3/sessions/` with the query string preserved. GET clients typically follow it automatically." + }, + "400": { + "description": "Invalid query parameter. The body is an object keyed by the offending parameter.", + "content": { + "application/json": { + "examples": { + "Invalid session_kind": { + "summary": "session_kind not one of user/business/all", + "value": { + "session_kind": "Must be \"user\", \"business\", or \"all\"." + } + }, + "Invalid date": { + "summary": "Malformed date_from/date_to", + "value": { + "date_from": [ + "Enter a valid date." + ] + } + }, + "Invalid didit_internal_id": { + "summary": "didit_internal_id is not a UUID", + "value": { + "didit_internal_id": [ + "Enter a valid UUID." + ] + } + }, + "Invalid api_service": { + "summary": "api_service not in the allowed choices", + "value": { + "api_service": [ + "Select a valid choice. FOO is not one of the available choices." + ] + } + } + } + } + } + }, + "403": { + "description": "Missing, malformed, or revoked API key. This API never returns `401`; all authentication failures are `403`.\n\n**Deleted application:** for a few minutes after an application is deleted, a request made with one of its API keys can answer `{\"detail\": \"This application was deleted.\", \"code\": \"application_deleted\"}`. Do not retry: the application and its API keys no longer exist. After those few minutes, the key is refused like any other invalid key.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "example": "You do not have permission to perform this action." + }, + "code": { + "type": "string", + "description": "Machine-readable reason, present only on some failures. `application_deleted`: the API key belongs to an application that was deleted.", + "example": "application_deleted" + } + }, + "required": [ + "detail" + ] + }, + "examples": { + "Forbidden": { + "summary": "Missing or invalid x-api-key", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "ApplicationDeleted": { + "summary": "API key of a deleted application", + "value": { + "detail": "This application was deleted.", + "code": "application_deleted" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -s 'https://verification.didit.me/v3/sessions/?status=Approved,In%20Review&date_from=2026-06-01&limit=20' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nurl = \"https://verification.didit.me/v3/sessions/\"\nheaders = {\"x-api-key\": \"YOUR_API_KEY\"}\nparams = {\n \"session_kind\": \"user\", # \"user\" (default) | \"business\" | \"all\"\n \"status\": \"Approved,In Review\", # comma-separated, OR within the list\n \"date_from\": \"2026-06-01\",\n \"limit\": 20,\n}\n\nwhile url:\n response = requests.get(url, headers=headers, params=params)\n response.raise_for_status()\n page = response.json()\n for session in page[\"results\"]:\n print(session[\"session_kind\"], session[\"session_id\"], session[\"status\"])\n url, params = page[\"next\"], None # \"next\" already carries the query string" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const params = new URLSearchParams({\n session_kind: 'all',\n status: 'Approved,In Review',\n date_from: '2026-06-01',\n limit: '20',\n});\n\nconst response = await fetch(\n `https://verification.didit.me/v3/sessions/?${params}`,\n { headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\n\nconst { count, next, results } = await response.json();\nfor (const session of results) {\n // session.session_kind is \"user\" (KYC) or \"business\" (KYB)\n console.log(session.session_kind, session.session_id, session.status);\n}" + } + ] + } + }, + "/v3/session/{sessionId}/share/": { + "post": { + "summary": "Mint a share token for a finished verification session", + "description": "Mint a short-lived JWT that lets a specific Didit application import this finished session. Pair with `POST /v3/session/import-shared/`.", + "operationId": "post_v3_session_share", + "x-didit-sdk": { + "resource": "sessions", + "method": "share", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "responses": { + "200": { + "description": "Share token minted.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "share_token": { + "type": "string", + "description": "HS256-signed JWT. Pass this verbatim as `share_token` to [`POST /v3/session/import-shared/`](/sessions-api/import-shared-session) on the target application.", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "for_application_id": { + "type": "string", + "format": "uuid", + "description": "Echoes the target application UUID that the token is bound to.", + "example": "a5f3bca2-46e2-411e-90ef-a580900a57ee" + }, + "session_kind": { + "type": "string", + "enum": [ + "user", + "business" + ], + "description": "Whether the source session is a User Verification (KYC) or Business Verification (KYB) session." + } + } + }, + "examples": { + "User session": { + "summary": "Share a KYC session", + "value": { + "share_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uX2lkIjoiMjBiYjNjM2ItNjE0Ni00MzlmLTg0YTQtYmQzMGQwMGFjNmEyIiwiZnJvbV9hcHBsaWNhdGlvbl9pZCI6ImRiZDIwZTM0LTQyZTktNGYyYy1iYTkxLWNmMDc2MjAxNmY2NCIsImZvcl9hcHBsaWNhdGlvbl9pZCI6ImE1ZjNiY2EyLTQ2ZTItNDExZS05MGVmLWE1ODA5MDBhNTdlZSIsImlhdCI6MTc1MzYzMDY2NiwiZXhwIjoxNzUzNjM0MjY2fQ.JJ9pNE_hqZsOtbR0XYZIWw4JzidjdEl279iUrsIkhGE", + "for_application_id": "a5f3bca2-46e2-411e-90ef-a580900a57ee", + "session_kind": "user" + } + }, + "Business session": { + "summary": "Share a KYB session", + "value": { + "share_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "for_application_id": "a5f3bca2-46e2-411e-90ef-a580900a57ee", + "session_kind": "business" + } + } + } + } + } + }, + "401": { + "description": "Missing or invalid credentials. Unlike most v3 endpoints, this endpoint authenticates through the permission decorator and returns `401` when no valid token/key is presented.", + "content": { + "application/json": { + "examples": { + "Unauthorized": { + "summary": "Unauthorized", + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + } + } + } + } + }, + "404": { + "description": "No session with the given `session_id` exists in your application.", + "content": { + "application/json": { + "examples": { + "Not Found": { + "summary": "Session not found", + "value": { + "detail": "Not found." + } + } + } + } + } + }, + "400": { + "description": "Validation error. Field errors are keyed by field name; the not-finished error arrives under a `detail` key (as an array).", + "content": { + "application/json": { + "examples": { + "Wrong status": { + "summary": "Source session is not finished", + "value": { + "detail": [ + "Only finished sessions (\"Approved\", \"Declined\", \"In Review\") can be shared." + ] + } + }, + "Target missing": { + "summary": "Target application does not exist", + "value": { + "for_application_id": [ + "Target application does not exist." + ] + } + }, + "Self-share": { + "summary": "Target is the calling application", + "value": { + "for_application_id": [ + "Cannot share a session with the same application." + ] + } + }, + "TTL bounds": { + "summary": "ttl_in_seconds outside bounds", + "value": { + "ttl_in_seconds": [ + "Ensure this value is greater than or equal to 60." + ] + } + } + } + } + } + }, + "403": { + "description": "The credentials are valid but lack the `write:sessions` permission for this application.", + "content": { + "application/json": { + "examples": { + "No Permission": { + "value": { + "detail": "You do not have permission to perform this action." + }, + "summary": "No Permission" + } + } + } + } + } + }, + "parameters": [ + { + "in": "path", + "name": "sessionId", + "required": true, + "description": "UUID of the source verification session to mint a token for.", + "schema": { + "type": "string", + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } + } + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "for_application_id" + ], + "properties": { + "for_application_id": { + "type": "string", + "format": "uuid", + "description": "UUID of the Didit application that will redeem the token. Must exist, must not be deleted, and must differ from the calling application. Find it in the Business Console under **Settings → Application**.", + "example": "a5f3bca2-46e2-411e-90ef-a580900a57ee" + }, + "ttl_in_seconds": { + "type": "integer", + "description": "Token lifetime, in seconds. Minimum `60`, maximum `86400` (24 h). Defaults to `3600` (1 h).", + "minimum": 60, + "maximum": 86400, + "default": 3600, + "example": 3600 + } + } + }, + "example": { + "for_application_id": "a5f3bca2-46e2-411e-90ef-a580900a57ee", + "ttl_in_seconds": 7200 + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/share/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"for_application_id\": \"a5f3bca2-46e2-411e-90ef-a580900a57ee\",\n \"ttl_in_seconds\": 7200\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/share/\",\n headers={\n 'x-api-key': 'YOUR_API_KEY',\n \"Content-Type\": \"application/json\",\n },\n json={\n \"for_application_id\": \"a5f3bca2-46e2-411e-90ef-a580900a57ee\",\n \"ttl_in_seconds\": 7200,\n },\n)\nresponse.raise_for_status()\nshare_token = response.json()[\"share_token\"]" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const response = await fetch(\n 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/share/',\n {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n for_application_id: 'a5f3bca2-46e2-411e-90ef-a580900a57ee',\n ttl_in_seconds: 7200,\n }),\n },\n);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst { share_token } = await response.json();" + } + ] + } + }, + "/v3/session/import-shared/": { + "post": { + "summary": "Redeem a share token to clone a verification session into your application", + "description": "Redeem a share token to clone a KYC or KYB session into the calling application with a fresh `session_id`. Not idempotent — a token redeems once per receiver.", + "operationId": "post_v3_session_import_shared", + "x-didit-sdk": { + "resource": "sessions", + "method": "import_shared", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "responses": { + "201": { + "description": "Session cloned. The body is the V3 decision payload for both kinds, discriminated by `session_kind`: `\"user\"` (KYC) with list-based feature blocks such as `id_verifications`, `liveness_checks`, `aml_screenings`; `\"business\"` (KYB) with blocks like `registry_checks` and `key_people_checks`. Both include `shared_from_session` — the `session_id` of the original session in the source application. With `trust_review: false` the cloned session's `status` is forced to `In Review`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionDecision" + }, + "examples": { + "User session imported": { + "summary": "Cloned KYC session (truncated — full V3 decision payload in reality)", + "value": { + "session_id": "11111111-2222-3333-4444-555555555555", + "session_kind": "user", + "shared_from_session": "99999999-8888-7777-6666-555555555555", + "session_number": 43762, + "session_url": null, + "status": "In Review", + "workflow_id": "9f9b1234-aaaa-bbbb-cccc-1234567890ab", + "vendor_data": "user-1", + "created_at": "2026-05-17T08:42:11Z", + "expires_at": "2026-05-24T08:42:11Z" + } + }, + "Business session imported": { + "summary": "Cloned KYB session (truncated — full V3 KYB decision payload in reality)", + "value": { + "session_id": "22222222-3333-4444-5555-666666666666", + "session_kind": "business", + "shared_from_session": "88888888-7777-6666-5555-444444444444", + "session_number": 982, + "session_url": null, + "status": "Approved", + "workflow_id": "9f9b1234-aaaa-bbbb-cccc-1234567890ab", + "vendor_data": "company-1" + } + } + } + } + } + }, + "400": { + "description": "Validation error (token invalid, expired, wrong target, or references a session that no longer exists).", + "content": { + "application/json": { + "examples": { + "Invalid Share Token": { + "summary": "Invalid JWT", + "value": { + "share_token": [ + "Invalid share token." + ] + } + }, + "Expired Share Token": { + "summary": "Token past `exp`", + "value": { + "share_token": [ + "Share token has expired." + ] + } + }, + "Wrong audience": { + "summary": "Token not bound to this application", + "value": { + "share_token": [ + "This token is not valid for this application." + ] + } + }, + "Original missing": { + "summary": "Source session was deleted", + "value": { + "share_token": [ + "Original session does not exist." + ] + } + } + } + } + } + }, + "403": { + "description": "Missing/invalid API key (this endpoint returns `403`, never `401`, for authentication failures), or this source session has already been imported into the calling application (one redeem per receiver).", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "summary": "Forbidden", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Already imported": { + "summary": "Source already imported once", + "value": { + "detail": "This session has already been shared with your application." + } + } + } + } + } + }, + "404": { + "description": "`workflow_id` does not belong to the calling application.", + "content": { + "application/json": { + "examples": { + "Workflow not found": { + "summary": "Workflow unknown", + "value": { + "detail": "Workflow does not exist for this application." + } + } + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "share_token", + "trust_review", + "workflow_id" + ], + "properties": { + "share_token": { + "type": "string", + "description": "JWT share token issued by [`POST /v3/session/{sessionId}/share/`](/sessions-api/share-session).", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + }, + "trust_review": { + "type": "boolean", + "description": "If `true`, the cloned session keeps the source's final `status`. If `false`, it is forced into `In Review`.", + "example": false + }, + "workflow_id": { + "type": "string", + "format": "uuid", + "description": "UUID of a workflow in the calling application. Cross-application IDs are rejected with `404`.", + "example": "9f9b1234-aaaa-bbbb-cccc-1234567890ab" + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Optional override for the cloned session's `vendor_data`.", + "example": "user-1" + } + } + }, + "example": { + "share_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "trust_review": false, + "workflow_id": "9f9b1234-aaaa-bbbb-cccc-1234567890ab", + "vendor_data": "user-1" + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST \\\n https://verification.didit.me/v3/session/import-shared/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"share_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n \"trust_review\": false,\n \"workflow_id\": \"9f9b1234-aaaa-bbbb-cccc-1234567890ab\",\n \"vendor_data\": \"user-1\"\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/session/import-shared/\",\n headers={\n 'x-api-key': 'YOUR_API_KEY',\n \"Content-Type\": \"application/json\",\n },\n json={\n \"share_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n \"trust_review\": False,\n \"workflow_id\": \"9f9b1234-aaaa-bbbb-cccc-1234567890ab\",\n \"vendor_data\": \"user-1\",\n },\n)\nresponse.raise_for_status()\nimported = response.json()" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const response = await fetch(\n 'https://verification.didit.me/v3/session/import-shared/',\n {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n share_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',\n trust_review: false,\n workflow_id: '9f9b1234-aaaa-bbbb-cccc-1234567890ab',\n vendor_data: 'user-1',\n }),\n },\n);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst imported = await response.json();" + } + ] + } + }, + "/v3/kyb/search/": { + "post": { + "summary": "KYB registry company search (standalone)", + "operationId": "post_v3_kyb_search", + "x-didit-sdk": { + "resource": "kyb", + "method": "search", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [], + "description": "Search official company registries for candidate companies by name and/or registration number. This is step 1 of the two-step registry flow: **search** returns lightweight candidates, then `POST /v3/kyb/select/` retrieves the full profile of one candidate.\n\n**Search billing.** Successful searches using a paid company-data source cost USD 0.50 per request, regardless of candidate count. Empty results, failures and results served only by other sources are not charged. The applicable price is reserved before searching; insufficient balance returns 403 with code `insufficient_credits`. Contract rates apply where agreed, and an unconfigured contract search rate returns 403. Sandbox searches are free. Repeating a successful search can incur another charge; callback delivery does not add a charge. Profile retrieval is billed separately. This search fee also applies to v2. Candidates always come back with `fetch_status: \"pending\"`: the full company data is only fetched (and billed) on select.\n\n**`kyb_response_id` is ephemeral.** It is a per-search handle, not a stable company id. Select promptly after searching and re-search to get a fresh handle instead of storing it.\n\n**`tier` picks the provider chain.** `tier` names the registry tier the search runs on: `basic` (the default, the Lite chain), `shareholders` or `ubo`. The search uses the provider chain that serves that tier, and it is not always the same chain the Lite search reads, so an empty Lite search is not proof that the registry does not hold the company — search again at the tier you need. A country that does not offer the tier you name is searched at the highest tier it offers up to that one. Retrieve the candidate from the endpoint that sells that tier: `POST /v3/kyb/select/` (Lite), `POST /v3/kyb/shareholders/` or `POST /v3/kyb/ubo/`.\n\n**Authentication.** `POST /v3/kyb/search/` (and `/v2/`) takes your application's API key in the `x-api-key` header. The hosted `POST /v1/kyb/search/` is the session-authenticated route a hosted verification flow calls: it answers `403` to an API key and takes no `tier` — the workflow's KYB registry configuration picks the chain.\n\n**Two modes.**\n- *Synchronous (default):* the API polls the registry until the search resolves and returns the final candidate list (`search_resolved: true`).\n- *Asynchronous:* pass `webhook_url` and the API returns immediately — usually with `search_status: \"pending\"` and an empty candidate list — then POSTs a `kyb.registry_search.resolved` callback to your URL when the registry finishes. The callback body is `{\"event_id\", \"event_type\": \"kyb.registry_search.resolved\", \"request_id\", \"vendor_data\", \"metadata\", \"search_status\", \"search_resolved\", \"kyb_registry\", \"timestamp\", \"created_at\"}` — note `timestamp` and `created_at` in the callback are epoch integers, unlike this endpoint's ISO `created_at` — and is sent **unsigned** (header `X-Didit-Unsigned-Callback: true`, no `X-Signature`).\n\n**Country codes.** ISO 3166-1 alpha-2 (`GB`, `DE`), plus `XX-YY` subdivision codes where registries are regional — e.g. `US-CA` for California.\n\n**Sandbox.** Keys from sandbox applications return one static candidate without contacting any registry.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "country_code" + ], + "properties": { + "country_code": { + "type": "string", + "maxLength": 10, + "description": "ISO 3166-1 alpha-2 code of the country whose company registry to search. For example, `GB`. Where company registries operate at state or province level, append the ISO 3166-2 subdivision code as `XX-YY`: `US-CA` searches the California registry. United States searches always require the subdivision — a bare `US` is rejected. Case-insensitive; normalized to upper case.", + "example": "GB" + }, + "name": { + "type": "string", + "maxLength": 255, + "description": "Company name to search for. Either `name` or `registration_number` is required.", + "example": "Tesco" + }, + "registration_number": { + "type": "string", + "maxLength": 100, + "description": "Registry registration number to search for. Either `name` or `registration_number` is required." + }, + "tier": { + "type": "string", + "enum": [ + "basic", + "shareholders", + "ubo" + ], + "default": "basic", + "description": "Registry tier whose provider chain this search runs on. `basic` (default) searches the country's Lite chain; `shareholders` and `ubo` search that tier's ownership chain, which can be a different source, so an empty Lite search does not prove the registry lacks the company. A country that does not offer the named tier is searched at the highest tier it offers up to that one. Retrieve the candidate from the endpoint that sells the tier you searched: `POST /v3/kyb/select/` (Lite), `POST /v3/kyb/shareholders/` or `POST /v3/kyb/ubo/`.", + "example": "basic" + }, + "search_type": { + "type": "string", + "enum": [ + "contains" + ], + "default": "contains", + "description": "Name-matching strategy applied by the registry search. `contains` is the only accepted value: no registry source behind the search offers a prefix or fuzzy mode, so `start_with` and `fuzzy` are rejected with `400` (`not a valid choice`)." + }, + "vendor_data": { + "type": "string", + "description": "Your identifier for this lookup; echoed back and included in the webhook callback." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Free-form JSON; echoed back and included in the webhook callback." + }, + "webhook_url": { + "type": "string", + "format": "uri", + "maxLength": 500, + "description": "Switches the search to asynchronous mode: the API returns immediately and POSTs an unsigned `kyb.registry_search.resolved` callback to this URL when the candidate list is ready." + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/kyb/search/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"country_code\": \"GB\",\n \"name\": \"Tesco\",\n \"search_type\": \"contains\",\n \"vendor_data\": \"company-1234\"\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/kyb/search/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"country_code\": \"GB\", \"name\": \"Tesco\"},\n timeout=60, # synchronous mode waits for the registry to resolve\n)\nresp.raise_for_status()\ncandidates = resp.json()[\"kyb_registry\"][\"companies\"]\nkyb_response_id = candidates[0][\"kyb_response_id\"] # pass to /v3/kyb/select/" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const res = await fetch('https://verification.didit.me/v3/kyb/search/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ country_code: 'GB', name: 'Tesco' }),\n});\nconst data = await res.json();\nconst kybResponseId = data.kyb_registry.companies[0].kyb_response_id;" + } + ], + "responses": { + "200": { + "description": "Search accepted. Synchronous mode returns the resolved candidate list; webhook mode returns immediately (often `search_status: \"pending\"`) and delivers the final list via the `kyb.registry_search.resolved` callback.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "With `webhook_url`: the persisted search-request id, echoed as `request_id` in the `kyb.registry_search.resolved` callback. Without `webhook_url`: a transient correlation UUID (the search is not stored)." + }, + "kyb_registry": { + "$ref": "#/components/schemas/KYBRegistrySearchResults" + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Echo of the `vendor_data` you sent." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Echo of the `metadata` you sent." + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "examples": { + "Resolved search": { + "summary": "Synchronous mode — final candidate list", + "value": { + "request_id": "0c7e9a40-3a13-4f7e-8a44-9d2e2f1c5b6a", + "kyb_registry": { + "companies": [ + { + "kyb_response_id": "69aeeb95febb0f1704042259", + "name": "Tesco PLC", + "registration_number": "00445790", + "status": "active", + "type": "Public Limited Company", + "risk_level": null, + "fetch_status": "pending" + }, + { + "kyb_response_id": "69aeeb95febb0f170404225a", + "name": "E-TESCO LTD", + "registration_number": "15168476", + "status": "active", + "type": "Private Limited Company", + "risk_level": null, + "fetch_status": "pending" + } + ], + "pagination": { + "total": 2, + "page": 1, + "per_page": 25 + }, + "search_status": "resolved", + "search_resolved": true + }, + "vendor_data": "company-1234", + "metadata": null, + "created_at": "2026-06-11T10:40:00.000000+00:00" + } + }, + "Pending search (webhook mode)": { + "summary": "`webhook_url` provided — candidates arrive via the kyb.registry_search.resolved callback", + "value": { + "request_id": "5f2d8c1b-7e4a-4b9c-9f0d-3a6e8c2b1d4f", + "kyb_registry": { + "companies": [], + "pagination": { + "total": 0, + "page": 1, + "per_page": 25 + }, + "search_status": "pending", + "search_resolved": false + }, + "vendor_data": "company-1234", + "metadata": { + "source": "onboarding" + }, + "created_at": "2026-06-11T10:41:00.000000+00:00" + } + } + } + } + } + }, + "400": { + "description": "Validation error. Field-level problems use DRF's standard envelope; the name/registration-number rule reports under `non_field_errors`.", + "content": { + "application/json": { + "examples": { + "Missing search criteria": { + "summary": "Neither `name` nor `registration_number` provided", + "value": { + "non_field_errors": [ + "Either 'name' or 'registration_number' is required." + ] + } + }, + "Invalid country_code": { + "summary": "Must be `XX` (alpha-2) or `XX-YY` subdivision", + "value": { + "country_code": [ + "country_code must be a 2-letter ISO code." + ] + } + }, + "Invalid search_type": { + "summary": "Only `contains` is accepted", + "value": { + "search_type": [ + "\"start_with\" is not a valid choice." + ] + } + }, + "Invalid webhook_url": { + "summary": "`webhook_url` must be a valid URL", + "value": { + "webhook_url": [ + "Enter a valid URL." + ] + } + } + } + } + } + }, + "403": { + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Also returned before searching when the applicable search fee cannot be covered (`code: \"insufficient_credits\"`), or when a contract search rate is missing.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "content": { + "application/json": { + "examples": { + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + }, + "502": { + "description": "The KYB registry provider is temporarily unavailable - a 5xx response, timeout, or connection failure from the upstream registry. The search is not billed. Retry later, or, in Didit's hosted verification flow, the user is automatically offered manual company entry instead of a retry-only error.", + "content": { + "application/json": { + "examples": { + "Registry provider outage": { + "summary": "The registry provider did not answer the search", + "value": { + "detail": "The KYB registry provider is temporarily unavailable. Please try again shortly.", + "code": "kyb_registry_provider_unavailable" + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/v3/kyb/select/": { + "post": { + "summary": "KYB registry company select — Lite tier (standalone)", + "operationId": "post_v3_kyb_select", + "x-didit-sdk": { + "resource": "kyb", + "method": "select", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [], + "description": "Retrieve the **Lite** registry profile of one candidate returned by `POST /v3/kyb/search/` — company details, registered address, identifiers, status, activity and the officers, plus the raw registry payload. Shareholders and ultimate beneficial owners are sold by `POST /v3/kyb/shareholders/` and `POST /v3/kyb/ubo/`.\n\n**Pairing with search.** Pass the candidate's `kyb_response_id` exactly as returned by a search of the same application. The handle is ephemeral and scoped to that search — select promptly, and re-search for a fresh handle rather than storing it.\n\n**Billing.** Each select is billed at the country's Lite price (any search fee is separate); see [KYB registry pricing](/getting-started/kyb-registry-pricing). The balance is checked before any registry call (`403` when short) and the charge is recorded against the created business session.\n\n**Persistence is mandatory.** Every select creates a business session (its id is the `request_id`) so billing and retrieval can be tracked; it appears in the console. `save_api_request` is accepted for symmetry but must be `true` (the default) — sending `false` returns `400`.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KYBRegistryCandidateRequest" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/kyb/select/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"kyb_response_id\": \"69aeeb95febb0f1704042259\",\n \"vendor_data\": \"company-1234\"\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os, requests\n\nsearch = requests.post(\n \"https://verification.didit.me/v3/kyb/search/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"country_code\": \"GB\", \"name\": \"Tesco\"},\n timeout=60,\n).json()\nkyb_response_id = search[\"kyb_registry\"][\"companies\"][0][\"kyb_response_id\"]\n\nresp = requests.post(\n \"https://verification.didit.me/v3/kyb/select/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"kyb_response_id\": kyb_response_id, \"vendor_data\": \"company-1234\"},\n timeout=90, # waits for the registry to resolve the full profile\n)\nresp.raise_for_status()\ncompany = resp.json()[\"kyb_registry\"]\nprint(company[\"company_name\"], company[\"registry_status\"], company[\"status\"])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const res = await fetch('https://verification.didit.me/v3/kyb/select/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n kyb_response_id: '69aeeb95febb0f1704042259',\n vendor_data: 'company-1234',\n }),\n});\nconst data = await res.json();\nconsole.log(data.kyb_registry.company_name, data.kyb_registry.registry_status);" + } + ], + "responses": { + "200": { + "description": "Company retrieved and billed. `kyb_registry` carries the full profile; check `data_resolved` — when `false` the registry was still processing and the stored record completes automatically once it resolves.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Id of the business session created for this retrieval. The stored record (visible in the console) keeps updating if the registry resolves after the response." + }, + "registry_tier_requested": { + "type": "string", + "enum": [ + "basic", + "shareholders", + "ubo" + ], + "description": "The tier this endpoint sells: `basic` (Lite) for select, `shareholders`, `ubo`." + }, + "registry_tier_delivered": { + "type": "string", + "enum": [ + "basic", + "shareholders", + "ubo" + ], + "description": "The tier actually delivered and billed. Lower than requested when the country's registries hold no ownership data, or the registry has no ownership record for this company." + }, + "kyb_registry": { + "type": "object", + "description": "Full registry company profile.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Didit's id for this company record." + }, + "node_id": { + "type": "string", + "example": "feature_kyb_registry" + }, + "status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review" + ], + "description": "Didit's assessment of the registry check (e.g. `In Review` when the company is not active in the registry or ownership data could not be derived)." + }, + "registry_status": { + "type": "string", + "nullable": true, + "enum": [ + "active", + "dissolved", + "deregistered", + "see full details", + "authorised", + "appointed representative", + "unauthorised", + "inactive", + "no longer authorised", + "closed", + "struck off" + ], + "description": "Normalized company status as reported by the registry." + }, + "data_resolved": { + "type": "boolean", + "description": "`true` once the registry returned the full profile. `false` when the registry was still processing after polling — the stored record completes automatically once the registry resolves." + }, + "company_name": { + "type": "string", + "example": "Tesco PLC" + }, + "registration_number": { + "type": "string", + "nullable": true, + "example": "00445790" + }, + "country_code": { + "type": "string", + "description": "ISO 3166-1 alpha-2.", + "example": "GB" + }, + "region": { + "type": "string", + "nullable": true, + "description": "Subdivision code when the registry is regional (e.g. `CA` for `US-CA`); empty otherwise." + }, + "company_type": { + "type": "string", + "nullable": true, + "example": "Public Limited Company" + }, + "incorporation_date": { + "type": "string", + "format": "date", + "nullable": true, + "example": "1947-11-27" + }, + "registered_address": { + "type": "string", + "nullable": true, + "example": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA" + }, + "tax_number": { + "type": "string", + "nullable": true, + "example": "GB412 8925 93" + }, + "risk_level": { + "type": "string", + "nullable": true, + "description": "Registry-reported risk level, when available." + }, + "verification_status": { + "type": "string", + "nullable": true, + "description": "Registry verification status, when available." + }, + "is_from_registry": { + "type": "boolean", + "description": "Always `true` for this endpoint — the profile was retrieved from an official registry." + }, + "fetch_status": { + "type": "string", + "enum": [ + "pending", + "resolved" + ], + "description": "Registry fetch lifecycle. Mirrored by `data_resolved`." + }, + "alternative_names": { + "type": "string", + "nullable": true, + "description": "Alternative, trading, former or original-language company names as text, when reported." + }, + "nature_of_business": { + "type": "string", + "nullable": true, + "description": "Activity description or classification code, when reported." + }, + "registered_capital": { + "type": "string", + "nullable": true + }, + "registered_capital_amount": { + "type": "string", + "nullable": true, + "description": "Decimal amount as a string, when reported." + }, + "registered_capital_currency": { + "type": "string", + "nullable": true, + "description": "ISO 4217 code, when reported." + }, + "website": { + "type": "string", + "nullable": true, + "example": "https://www.tesco.com/" + }, + "email": { + "type": "string", + "nullable": true, + "example": "customer.service@tesco.co.uk" + }, + "phone": { + "type": "string", + "nullable": true, + "example": "03301231688" + }, + "legal_entity_identifier": { + "type": "string", + "nullable": true, + "description": "LEI, when reported." + }, + "location_of_registration": { + "type": "string", + "nullable": true, + "description": "Registry/jurisdiction the company is registered with, when reported." + }, + "vat_number": { + "type": "string", + "nullable": true, + "description": "EU VAT number, when collected during the hosted registry step or edited afterwards. At select time this is typically not yet set — VAT and tax ID validation runs when the end user submits the registry step in the hosted flow." + }, + "vat_validation_status": { + "type": "string", + "enum": [ + "valid", + "invalid", + "format_valid", + "invalid_format", + "could_not_validate", + "not_applicable" + ], + "description": "Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session." + }, + "vat_validated_number": { + "type": "string", + "nullable": true, + "description": "The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran." + }, + "vat_validation_source": { + "type": "string", + "nullable": true, + "enum": [ + "eu_vies", + "pl_vat_white_list", + "no_brreg", + "ch_uid_register", + "br_receita_federal", + "co_rues", + "cr_hacienda", + "ec_sri", + "py_dnit", + "format_rules", + null + ], + "description": "The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran." + }, + "vat_validated_name": { + "type": "string", + "nullable": true, + "description": "Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." + }, + "vat_validated_address": { + "type": "string", + "nullable": true, + "description": "Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." + }, + "vat_checked_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Timestamp of the check. `null` when no check ran." + }, + "financial_summary": { + "type": "object", + "nullable": true, + "description": "Registry-reported share/capital details (currency, total shares, share classes), when available." + }, + "officers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "example": "MELISSA BETHELL" + }, + "designation": { + "type": "string", + "nullable": true, + "description": "Raw registry designation (e.g. `Director`, `Secretary`)." + }, + "role": { + "type": "string", + "description": "Normalized role derived from the designation. Full 17-value model set; registry parsing on this endpoint typically emits director, non_executive_director, secretary, chairman, or company_officer.", + "enum": [ + "director", + "non_executive_director", + "secretary", + "chairman", + "ubo", + "shareholder", + "representative", + "founder", + "legal_advisor", + "authorized_signatory", + "trustee", + "beneficiary", + "company_officer", + "settlor", + "protector", + "investor", + "other" + ] + }, + "nationality": { + "type": "string", + "nullable": true + }, + "is_active": { + "type": "boolean", + "description": "`false` when the registry marks the appointment as resigned." + }, + "kyc_status": { + "type": "string", + "nullable": true, + "enum": [ + "Approved", + "Declined", + "Pending" + ], + "description": "Status of an associated KYC session, when one was started for this officer. `null` otherwise." + }, + "kyc_session_url": { + "type": "string", + "nullable": true, + "description": "Verification URL of the associated KYC session, when one exists." + }, + "first_name": { + "type": "string", + "nullable": true, + "description": "Given name when explicitly reported by the registry; not inferred from the full name." + }, + "last_name": { + "type": "string", + "nullable": true, + "description": "Family name when explicitly reported by the registry." + }, + "address": { + "type": "string", + "nullable": true, + "description": "Officer address reported by the registry." + }, + "appointment_date": { + "type": "string", + "nullable": true, + "description": "Date the officer was appointed.", + "format": "date" + }, + "termination_date": { + "type": "string", + "nullable": true, + "description": "Date the appointment ended, when reported.", + "format": "date" + }, + "date_of_birth": { + "type": "string", + "nullable": true, + "description": "Complete registry-reported birth date. Null when only a birth year or month is known.", + "format": "date" + }, + "date_of_birth_year": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 9999, + "description": "Registry-reported birth year, including when the full date is unavailable." + }, + "date_of_birth_month": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 12, + "description": "Registry-reported birth month. Null when only the year is known." + } + } + }, + "description": "Directors, secretaries, and other officers extracted from the registry." + }, + "beneficial_owners": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "first_name": { + "type": "string", + "nullable": true + }, + "last_name": { + "type": "string", + "nullable": true + }, + "entity_type": { + "type": "string", + "enum": [ + "person", + "company" + ], + "description": "Whether the owner is a natural person or a corporate entity." + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Normalized roles (e.g. `ubo`, `shareholder`)." + }, + "ownership_min_shares": { + "type": "string", + "nullable": true, + "description": "Lower bound of the ownership band reported by the registry (e.g. `25%`)." + }, + "ownership_max_shares": { + "type": "string", + "nullable": true, + "description": "Upper bound of the ownership band reported by the registry." + }, + "is_active": { + "type": "boolean" + }, + "kyc_status": { + "type": "string", + "nullable": true, + "enum": [ + "Approved", + "Declined", + "Pending" + ] + }, + "kyc_session_url": { + "type": "string", + "nullable": true + }, + "effective_ownership_percent": { + "type": "number", + "nullable": true, + "description": "Parsed ownership percentage (max bound, falling back to min bound)." + }, + "company_name": { + "type": "string", + "nullable": true + }, + "registration_number": { + "type": "string", + "nullable": true + }, + "country_of_birth": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "nullable": true + }, + "nationality": { + "type": "string", + "nullable": true + }, + "designation": { + "type": "string", + "nullable": true + }, + "voting_min_shares": { + "type": "string", + "nullable": true + }, + "voting_max_shares": { + "type": "string", + "nullable": true + }, + "date_of_birth": { + "type": "string", + "nullable": true, + "description": "Complete birth date when reported.", + "format": "date" + }, + "appointment_date": { + "type": "string", + "nullable": true, + "description": "Appointment date when reported.", + "format": "date" + }, + "shareholding": { + "$ref": "#/components/schemas/KYBRegistryShareholding" + } + } + }, + "description": "Beneficial owners / persons with significant control extracted from the registry. Can be empty when the registry does not expose ownership." + }, + "addresses": { + "type": "array", + "nullable": true, + "description": "Every address the registry states, one canonical shape. Null when it states none.", + "items": { + "$ref": "#/components/schemas/KYBRegistryAddress" + } + }, + "industries": { + "type": "array", + "nullable": true, + "description": "The activities the registry classifies the company under, one canonical shape. Null when it states none.", + "items": { + "$ref": "#/components/schemas/KYBRegistryIndustry" + } + }, + "accounts": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts." + }, + "registry_data": { + "type": "object", + "description": "Raw record returned by the registry source, kept for reviewers. Its shape differs by country and source and is not a stable contract: read the canonical fields (`addresses`, `industries`, `ownership`, `beneficial_owners[].shareholding`) instead." + }, + "user_provided_data": { + "type": "object", + "nullable": true, + "description": "Always `null` on this endpoint (used by the hosted KYB flow when end users edit registry data)." + }, + "confirmed_by_user_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Always `null` on this endpoint." + }, + "last_console_edit_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Set when a console reviewer edits the record later." + }, + "is_editable": { + "type": "boolean", + "description": "Whether the registry-sourced data can still be edited in the hosted flow/console." + }, + "registry_tier_delivered": { + "type": "string", + "enum": [ + "basic", + "shareholders", + "ubo" + ], + "description": "Same as the top-level `registry_tier_delivered`." + }, + "registry_ownership_status": { + "type": "string", + "nullable": true, + "enum": [ + "not_requested", + "delivered", + "unavailable" + ], + "description": "`delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `not_requested` on the Lite tier." + }, + "ownership": { + "$ref": "#/components/schemas/KYBRegistryOwnership" + }, + "registry_tier": { + "type": "string", + "nullable": true, + "description": "The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`." + } + } + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Echo of the `vendor_data` you sent." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Echo of the `metadata` you sent." + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "examples": { + "Resolved company": { + "summary": "Full registry profile (officer list truncated for brevity)", + "value": { + "request_id": "9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e", + "kyb_registry": { + "uuid": "f1e2d3c4-b5a6-4789-9c0d-1e2f3a4b5c6d", + "node_id": "feature_kyb_registry", + "status": "In Review", + "registry_status": "active", + "data_resolved": true, + "company_name": "Tesco PLC", + "registration_number": "00445790", + "country_code": "GB", + "region": "", + "company_type": "Public Limited Company", + "incorporation_date": "1947-11-27", + "registered_address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", + "tax_number": "GB412 8925 93", + "risk_level": "low risk", + "verification_status": "verified", + "is_from_registry": true, + "fetch_status": "resolved", + "alternative_names": "TESCO STORES (HOLDINGS) PUBLIC LIMITED COMPANY", + "nature_of_business": null, + "registered_capital": null, + "registered_capital_amount": null, + "registered_capital_currency": null, + "website": "https://www.tesco.com/", + "email": "customer.service@tesco.co.uk", + "phone": "03301231688", + "legal_entity_identifier": null, + "location_of_registration": null, + "financial_summary": { + "currency": "Pound (£)", + "total_number_of_shares": "8174378721", + "meta_detail": { + "Class_Of_Share": "Ordinary" + } + }, + "officers": [ + { + "uuid": "a1b2c3d4-e5f6-4789-9a0b-1c2d3e4f5a6b", + "name": "CHRISTOPHER JON TAYLOR", + "designation": "Secretary", + "role": "secretary", + "nationality": null, + "is_active": true, + "kyc_status": null, + "kyc_session_url": null + }, + { + "uuid": "b2c3d4e5-f6a7-4890-8b1c-2d3e4f5a6b7c", + "name": "MELISSA BETHELL", + "designation": "Director", + "role": "director", + "nationality": "British", + "is_active": true, + "kyc_status": null, + "kyc_session_url": null + } + ], + "beneficial_owners": [], + "addresses": [ + { + "address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", + "type": "legal_entity_registry_address", + "description": "registered office address" + } + ], + "industries": null, + "accounts": { + "last_account": "26 February 2025", + "next_account": "26 February 2026", + "due_by_date": "26 August 2026" + }, + "registry_data": { + "name": "Tesco PLC", + "registration_number": "00445790", + "status": "active", + "type": "Public Limited Company", + "incorporation_date": "1947-11-27", + "tax_number": "GB412 8925 93", + "country_code": "GB", + "fetch_status": "resolved", + "industries_detail": [ + { + "code": "47110", + "name": "Retail Sale In Non-specialised Stores With Food, Beverages Or Tobacco Predominating", + "description": null, + "meta_detail": [] + } + ] + }, + "user_provided_data": null, + "confirmed_by_user_at": null, + "last_console_edit_at": null, + "is_editable": true + }, + "vendor_data": "company-1234", + "metadata": null, + "created_at": "2026-06-11T10:45:00.000000+00:00" + } + }, + "Pending (registry still resolving)": { + "summary": "Select stayed pending after polling — sparse fields, auto-completes server-side", + "value": { + "request_id": "9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e", + "kyb_registry": { + "uuid": "7c0a4f7e-2b1d-4f6a-9c3e-8d5b2a1f0e9d", + "node_id": "feature_kyb_registry", + "status": "Not Finished", + "registry_status": null, + "data_resolved": false, + "company_name": "Tesco PLC", + "registration_number": "00445790", + "country_code": "GB", + "region": null, + "company_type": null, + "incorporation_date": null, + "registered_address": null, + "tax_number": null, + "risk_level": null, + "verification_status": null, + "is_from_registry": true, + "fetch_status": "pending", + "alternative_names": null, + "nature_of_business": null, + "registered_capital": null, + "registered_capital_amount": null, + "registered_capital_currency": null, + "website": null, + "email": null, + "phone": null, + "legal_entity_identifier": null, + "location_of_registration": null, + "financial_summary": {}, + "officers": [], + "beneficial_owners": [], + "addresses": null, + "industries": null, + "accounts": null, + "registry_data": {}, + "user_provided_data": null, + "confirmed_by_user_at": null, + "last_console_edit_at": null, + "is_editable": true + }, + "vendor_data": "company-1234", + "metadata": null, + "created_at": "2026-06-11T10:45:00.000000+00:00" + } + } + } + } + } + }, + "400": { + "description": "Validation error. Field-level problems use DRF's standard envelope.", + "content": { + "application/json": { + "examples": { + "Missing kyb_response_id": { + "summary": "`kyb_response_id` is required", + "value": { + "kyb_response_id": [ + "This field is required." + ] + } + }, + "save_api_request=false rejected": { + "summary": "Selects are always persisted", + "value": { + "save_api_request": [ + "KYB registry select must be saved so billing and retrieval can be tracked." + ] + } + } + } + } + } + }, + "403": { + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any screening or provider call happens.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Not enough credits": { + "summary": "Organization balance cannot cover the call", + "value": { + "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "content": { + "application/json": { + "examples": { + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/v3/passive-liveness/": { + "post": { + "summary": "Passive Liveness", + "description": "Detect presentation attacks — printed photos, screen replays, masks, deepfakes — from a single face image. No video, motion, or user interaction required.\n\n**How it works.** The image is analyzed by a biometric model; the largest detected face is evaluated and a liveness `score` (0–100) is computed. `status` is `Declined` when any `error`-severity warning fires:\n- `NO_FACE_DETECTED` — the liveness model found no face,\n- `LOW_LIVENESS_SCORE` — score at or below `face_liveness_score_decline_threshold` (default `30`),\n- `LIVENESS_FACE_ATTACK` — the model detected a presentation attack,\n- `FACE_IN_BLOCKLIST` / `POSSIBLE_FACE_IN_BLOCKLIST` — the face matches an entry on your face blocklist.\n\n`MULTIPLE_FACES_DETECTED` is a `warning`-severity signal (does not decline), and `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` are `information`-severity signals that the same face already appeared in another approved session (they never decline). `face_quality` and `face_luminance` (0–100, nullable) describe capture quality.\n\n**Blocklist and duplicate screening.** Runs on **every** call — with or without `save_api_request` — by matching the detected face against your application's face search index. Prior faces with the same `vendor_data` are excluded from the entire index search — so re-running liveness for the same user does not trigger `DUPLICATED_FACE`, and a blocklisted face originating from a session with the caller's own `vendor_data` is also NOT flagged (`FACE_IN_BLOCKLIST` screening is bypassed for same-`vendor_data` faces).\n\n**Billing.** Each `200` response consumes one Passive Liveness API credit (standalone APIs have no free tier). Insufficient balance returns `403` before any image processing.\n\n**Session persistence and face enrollment (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session (Business Console, `GET /v3/session/{sessionId}/decision/` via the returned `request_id`, `status.updated` webhook), the reference image and face crop are stored, and the detected face is **enrolled into your face search index** so later Face Search calls and duplicate checks can match it. When `false`, the call is one-shot: nothing is stored and the face is not enrolled (blocklist/duplicate screening still runs).\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", + "operationId": "post_v3passive-liveness", + "x-didit-sdk": { + "resource": "passive_liveness", + "method": "check", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" + ], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "user_image" + ], + "properties": { + "user_image": { + "type": "string", + "format": "binary", + "description": "Front-facing face image (e.g. a selfie). Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy. The image should contain a single subject — when several faces are present, the largest one is evaluated and a `MULTIPLE_FACES_DETECTED` warning is added." + }, + "face_liveness_score_decline_threshold": { + "type": "number", + "format": "float", + "default": 30, + "minimum": 0, + "maximum": 100, + "description": "Liveness score threshold (0–100). A computed score **at or below** this value emits a `LOW_LIVENESS_SCORE` warning and sets `status` to `Declined`. Default `30`; raise it for a stricter anti-spoofing posture. Values outside 0–100 return `400`.", + "example": 30 + }, + "rotate_image": { + "type": "boolean", + "default": false, + "description": "When `true`, the service tries 90-degree rotations of the input and uses the orientation that yields the best face detection. Useful when EXIF orientation is missing. Adds latency.", + "example": false + }, + "save_api_request": { + "type": "boolean", + "default": true, + "description": "When `true` (default), persists the call as an API-type session, stores the face images, emits a `status.updated` webhook, and enrolls the detected face into your face search index. When `false`, the call is one-shot — nothing is stored and the face is not enrolled. Blocklist and duplicate screening run either way.", + "example": true + }, + "vendor_data": { + "type": "string", + "description": "Optional opaque identifier (your user id, email, UUID…) stored on the persisted session and echoed back. Also used to exclude prior sessions with the same `vendor_data` from the duplicate-face check, so re-verifying the same user does not raise `DUPLICATED_FACE`.", + "example": "user-123" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Optional JSON object stored with the session (when `save_api_request=true`) and echoed back. In multipart requests, send it as a JSON-encoded string field — it is parsed into an object.", + "example": { + "flow": "withdrawal" + } + } + } + }, + "example": { + "user_image": "(binary JPEG/PNG selfie)", + "face_liveness_score_decline_threshold": 30, + "rotate_image": false, + "save_api_request": true, + "vendor_data": "user-123", + "metadata": { + "flow": "withdrawal" + } + } + } + } + }, + "responses": { + "200": { + "description": "Passive liveness completed. `liveness.score` is the model's 0–100 confidence that the image shows a live person; `status` is `Approved` unless an `error`-severity warning fired. A spoofed or blocklisted face still returns `200` — inspect `liveness.status` and `liveness.warnings`, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id.", + "content": { + "application/json": { + "examples": { + "Approved": { + "summary": "Live face — no warnings", + "value": { + "request_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", + "liveness": { + "status": "Approved", + "method": "PASSIVE", + "score": 95.0, + "user_image": { + "entities": [ + { + "bbox": [ + 661, + 728, + 1688, + 2188 + ], + "confidence": 0.732973, + "age": 26.91, + "gender": "male", + "race": null + } + ], + "best_angle": 0 + }, + "warnings": [], + "face_quality": 84.21, + "face_luminance": 50.33 + }, + "vendor_data": "user-123", + "metadata": null, + "created_at": "2026-06-12T01:04:42.763237+00:00" + } + }, + "Declined - presentation attack": { + "summary": "The model flagged a spoof (print, replay, mask…)", + "value": { + "request_id": "f1c8c9b2-d3e4-4f5a-9b8c-7d6e5f4a3b2c", + "liveness": { + "status": "Declined", + "method": "PASSIVE", + "score": 12.5, + "user_image": { + "entities": [ + { + "bbox": [ + 100, + 120, + 420, + 540 + ], + "confidence": 0.81, + "age": 30.1, + "gender": "female", + "race": null + } + ], + "best_angle": 0 + }, + "warnings": [ + { + "risk": "LOW_LIVENESS_SCORE", + "feature": "LIVENESS", + "additional_data": null, + "log_type": "error", + "short_description": "Low liveness score", + "long_description": "The liveness check resulted in a low score, indicating potential use of non-live facial representations or poor-quality biometric data." + }, + { + "risk": "LIVENESS_FACE_ATTACK", + "feature": "LIVENESS", + "additional_data": null, + "log_type": "error", + "short_description": "Liveness Face Attack", + "long_description": "The system detected a potential attempt to bypass the liveness check." + } + ], + "face_quality": 72.4, + "face_luminance": 48.2 + }, + "vendor_data": "user-123", + "metadata": null, + "created_at": "2026-06-12T01:04:42.763237+00:00" + } + }, + "Declined - face on blocklist": { + "summary": "Live face, but it matches a blocklist entry", + "value": { + "request_id": "0b54a1de-22aa-4e0f-9c84-5a2b1f3c4d5e", + "liveness": { + "status": "Declined", + "method": "PASSIVE", + "score": 93.75, + "user_image": { + "entities": [ + { + "bbox": [ + 40, + 40, + 100, + 100 + ], + "confidence": 0.722082, + "age": 27.0, + "gender": "male", + "race": null + } + ], + "best_angle": 0 + }, + "warnings": [ + { + "risk": "FACE_IN_BLOCKLIST", + "feature": "LIVENESS", + "additional_data": { + "blocklisted_session_id": "882c42d5-8a4d-4d20-8080-a22f57822c86", + "blocklisted_session_number": 3242, + "api_service": null + }, + "log_type": "error", + "short_description": "Face in blocklist", + "long_description": "The system identified a face in the blocklist, which means the face is not allowed to be verified." + } + ], + "face_quality": null, + "face_luminance": null + }, + "vendor_data": "user-123", + "metadata": null, + "created_at": "2026-06-12T01:04:42.763237+00:00" + } + } + }, + "schema": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." + }, + "liveness": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "Approved", + "Declined" + ], + "description": "`Declined` when at least one `error`-severity warning fired (no face, low score, presentation attack, or a blocklist hit); `Approved` otherwise. `warning`/`information` entries (multiple faces, duplicates) never decline on their own." + }, + "method": { + "type": "string", + "enum": [ + "PASSIVE" + ], + "description": "Always `PASSIVE` for this endpoint." + }, + "score": { + "type": "number", + "format": "float", + "nullable": true, + "minimum": 0, + "maximum": 100, + "description": "Liveness confidence (0–100, two decimals) that the image depicts a live person rather than a spoof. `null` when the model returned no confidence.", + "example": 95.0 + }, + "user_image": { + "type": "object", + "description": "Face-detection results for the submitted image.", + "properties": { + "entities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "bbox": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 4, + "maxItems": 4, + "description": "Bounding box of the detected face as `[x_min, y_min, x_max, y_max]` pixel coordinates in the processed image.", + "example": [ + 661, + 728, + 1688, + 2188 + ] + }, + "confidence": { + "type": "number", + "format": "float", + "minimum": 0, + "maximum": 1, + "description": "Face-detection confidence (0–1).", + "example": 0.732973 + }, + "age": { + "type": "number", + "format": "float", + "description": "Model-estimated age of the detected face, in years. Informational only — it does not affect the liveness decision.", + "example": 26.91 + }, + "gender": { + "type": "string", + "description": "Model-predicted gender of the detected face (`male` or `female`). Informational only.", + "example": "male" + }, + "race": { + "type": "string", + "nullable": true, + "description": "Reserved field — always `null` in responses from this endpoint.", + "example": null + } + } + }, + "description": "One entry per detected face. The largest face is the one evaluated." + }, + "best_angle": { + "type": "integer", + "nullable": true, + "description": "Rotation (degrees) that produced the best face detection; non-zero only when `rotate_image=true` corrected the orientation.", + "example": 0 + } + } + }, + "warnings": { + "type": "array", + "description": "Risk signals. `error` severity (declines): `NO_FACE_DETECTED`, `LOW_LIVENESS_SCORE` (score at or below the threshold), `LIVENESS_FACE_ATTACK`, `FACE_IN_BLOCKLIST`, `POSSIBLE_FACE_IN_BLOCKLIST`. `warning` severity (review, no decline): `MULTIPLE_FACES_DETECTED`. `information` severity (no decline): `DUPLICATED_FACE`, `POSSIBLE_DUPLICATED_FACE` — the same face already appeared in another approved session.", + "items": { + "type": "object", + "properties": { + "risk": { + "type": "string", + "enum": [ + "NO_FACE_DETECTED", + "LOW_LIVENESS_SCORE", + "LIVENESS_FACE_ATTACK", + "MULTIPLE_FACES_DETECTED", + "FACE_IN_BLOCKLIST", + "POSSIBLE_FACE_IN_BLOCKLIST", + "DUPLICATED_FACE", + "POSSIBLE_DUPLICATED_FACE" + ], + "description": "Machine-readable risk code." + }, + "feature": { + "type": "string", + "enum": [ + "LIVENESS" + ], + "description": "Feature that raised the warning. Always `LIVENESS` on this endpoint." + }, + "additional_data": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "`null` for most warnings. Blocklist hits carry `{blocklisted_session_id, blocklisted_session_number, api_service}`; duplicate hits carry `{duplicated_session_id, duplicated_session_number, api_service}` pointing at the matching session." + }, + "log_type": { + "type": "string", + "enum": [ + "error", + "warning", + "information" + ], + "description": "Severity. `error` warnings drive `status` to `Declined`; `warning` and `information` entries are advisory and never decline on their own." + }, + "short_description": { + "type": "string", + "description": "Human-readable one-line summary of the risk." + }, + "long_description": { + "type": "string", + "description": "Human-readable explanation of the risk." + } + } + } + }, + "face_quality": { + "type": "number", + "format": "float", + "nullable": true, + "minimum": 0, + "maximum": 100, + "description": "Capture-quality score of the detected face, normalized to 0–100. `null` when the model could not produce a reliable value.", + "example": 84.21 + }, + "face_luminance": { + "type": "number", + "format": "float", + "nullable": true, + "minimum": 0, + "maximum": 100, + "description": "Brightness of the detected face, normalized to 0–100. `null` when unavailable.", + "example": 50.33 + } + } + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Echo of the `vendor_data` you sent, or `null`." + }, + "metadata": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Echo of the `metadata` object you sent, or `null`." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp (UTC) of when the response was generated, e.g. `2026-06-12T01:04:42.763237+00:00`." + } + } + } + } + } + }, + "400": { + "description": "Validation error. Returned when `user_image` is missing, exceeds 5 MB, has an unsupported extension, fails server-side image decoding, or when an option is out of range. Field errors use DRF's `{field: [messages]}` envelope; image-decoding failures use `{\"error\": ...}`.", + "content": { + "application/json": { + "examples": { + "Missing image": { + "summary": "`user_image` not included in the form data", + "value": { + "user_image": [ + "No file was submitted." + ] + } + }, + "Invalid image format": { + "summary": "Allowed extension but undecodable image content", + "value": { + "error": "Invalid user image format.", + "code": "invalid_image" + } + }, + "Unsupported file extension": { + "summary": "File extension outside tiff/jpg/jpeg/png/webp", + "value": { + "user_image": [ + "File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, webp." + ] + } + }, + "File too large": { + "summary": "Upload exceeds the 5 MB limit", + "value": { + "user_image": [ + "File size should not exceed 5 MB" + ] + } + } + } + } + } + }, + "403": { + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any image processing happens.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Not enough credits": { + "summary": "Organization balance cannot cover the call", + "value": { + "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "content": { + "application/json": { + "examples": { + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X POST 'https://verification.didit.me/v3/passive-liveness/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'user_image=@./selfie.jpg' \\\n -F 'face_liveness_score_decline_threshold=30' \\\n -F 'save_api_request=true' \\\n -F 'vendor_data=user-123'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nurl = 'https://verification.didit.me/v3/passive-liveness/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('selfie.jpg', 'rb') as f:\n files = {'user_image': ('selfie.jpg', f, 'image/jpeg')}\n data = {\n 'face_liveness_score_decline_threshold': 30,\n 'save_api_request': 'true',\n 'vendor_data': 'user-123',\n }\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=60)\n\nresp.raise_for_status()\nbody = resp.json()\nprint('status:', body['liveness']['status'])\nprint('score:', body['liveness']['score'])\nfor w in body['liveness']['warnings']:\n print('warning:', w['risk'], '-', w['log_type'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('user_image', new Blob([fs.readFileSync('./selfie.jpg')]), 'selfie.jpg');\nform.append('face_liveness_score_decline_threshold', '30');\nform.append('save_api_request', 'true');\nform.append('vendor_data', 'user-123');\n\nconst response = await fetch('https://verification.didit.me/v3/passive-liveness/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n});\n\nif (!response.ok) throw new Error(`Passive liveness failed: ${response.status}`);\nconst body = await response.json();\nconsole.log('status:', body.liveness.status, 'score:', body.liveness.score);\nbody.liveness.warnings.forEach(w => console.log('warning:', w.risk, '-', w.log_type));" + } + ] + } + }, + "/v3/database-validation/": { + "post": { + "summary": "Database Validation (standalone)", + "operationId": "post_v3database-validation", + "x-didit-sdk": { + "resource": "database_validation", + "method": "validate", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [], + "description": "Validate a person's identity data against official government and registry sources — CPF in Brazil, RENAPER in Argentina, DNI registries in Spain and Peru, INE in Mexico, and 100+ more services across 60+ countries.\n\n**Service selection.** Each country exposes one or more catalog services identified by a `service_id` (e.g. `bra_cpf`, `arg_renaper`, `pan_cedula_sib`). Pin the services you want with the `services` array — if you omit it, exactly **one** default service runs (the longest-established live service for the country). Sending `services` also switches the response to the extended shape with `services_used` and `match_score`. Discover services, their required fields, and prices via `GET /v1/organization/database-validation-countries/` (a catalog endpoint not documented in this spec) or the Business Console workflow editor. Some services are flagged `requires_consent=true` in the catalog and demand `consent=true`; others require onboarding for your organization before they can be called.\n\n**Input fields.** `identification_number` is a universal field that maps to the right country-specific field automatically (see its description). Country format rules are enforced before any provider is called (e.g. BRA CPF must be exactly 11 digits) and format failures are never billed. Biometric services need a `selfie` upload (multipart only); Argentina RENAPER additionally requires `gender`.\n\n**Results.** Each service returns a field-by-field `validation` map plus a vendor-neutral `outcome_code` (`MATCH`, `PARTIAL_MATCH`, `NO_MATCH`, `DOCUMENT_NOT_FOUND`, `INVALID_DOCUMENT_FORMAT`, `INVALID_INPUT`, `MINOR_BLOCKED`, `DECEASED`, `BIOMETRIC_NO_MATCH`, `BIOMETRIC_IMAGE_UNUSABLE`, `INCONCLUSIVE`, `DOCUMENT_SUPERSEDED`, `REGISTRY_UNAVAILABLE`, `REGISTRY_ERROR`). Note the distinctions: `NO_MATCH` is a definitive mismatch, `INCONCLUSIVE` means the registry could not confirm either way (review, not decline), and `BIOMETRIC_IMAGE_UNUSABLE` is a technical selfie problem (retake), unlike the definitive `BIOMETRIC_NO_MATCH`. The overall `match_type` aggregates across services (any full → `full_match`, else any partial → `partial_match`, else `no_match`) and `no_match_action`/`partial_match_action` translate it into the final `status`. `validation_type` is derived: `two_by_two` when two or more distinct services produced a full match (on the identification number, on full name + date of birth together, or on the address without contradictions), else `one_by_one`.\n\n**Persistence.** `save_api_request` defaults to **true**: the result is stored as a session (`request_id` works with `GET /v3/session/{sessionId}/decision/`), appears in the console, and fires a `status.updated` webhook. It also controls the `validations` shape — per-service objects when saved, a merged `{field: match}` map when not.\n\n**Billing.** Per-service: each service that returns a billable result is charged its own catalog price. Failed or skipped services are not billed, and requests rejected with `400` cost nothing. The pre-flight balance check covers the sum of the selected services' prices (`403` when short).\n\n**Failures.** When no selected service returns a usable result, the API returns `400` with `validation_errors` if every unanswered attempt explicitly refused the input (`provider_rejected_input`, `retryable: false`). Correct the input before trying again. Otherwise it returns `502` with `validation_errors` (`empty_provider_response`, `retryable: true`), including mixed input rejection and unavailable fallback attempts; nothing is billed (a session is still recorded with status `Not Finished` when `save_api_request=true`). When only some services fail, the `200` response carries their failures in `database_validation.errors`.\n\n**Sandbox.** Keys from sandbox applications skip the registry calls and billing and return a static `full_match` response — the sandbox payload always includes `services_used` and `match_score: 100` regardless of how `services` was sent, so don't validate the live extended-shape gating or the 0.0–1.0 `match_score` fraction against sandbox responses.\n\nSend the request as `application/json`, or as `multipart/form-data` when uploading a `selfie`.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "issuing_state" + ], + "properties": { + "issuing_state": { + "type": "string", + "description": "ISO 3166-1 **alpha-3** country code of the registry to validate against (e.g. `BRA`, `ESP`, `COL`). Determines which services are available. Unsupported codes return `400` with the full list of valid options.", + "example": "BRA" + }, + "services": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Catalog `service_id`s to run for this country (e.g. `[\"bra_cpf\"]`). Also accepts a single string or a comma-separated/JSON-encoded string. **If omitted or empty, exactly one default service runs** — the longest-established live service for the country; newer, biometric, and pay-as-you-go services must be named explicitly. Sending `services` switches the response to the extended shape (adds `services_used` and `match_score`). Every id must exist and be live for the `issuing_state`; services that require onboarding return `400` until activated for your organization. Discover available services per country via `GET /v1/organization/database-validation-countries/` (a catalog endpoint not documented in this spec) or the Business Console workflow editor.", + "example": [ + "bra_cpf" + ] + }, + "validation_type": { + "type": "string", + "deprecated": true, + "description": "DEPRECATED. Accepted for backward compatibility but ignored — the response `validation_type` is now derived from how many services full-matched. Use `services` to pin specific services." + }, + "consent": { + "type": "boolean", + "default": false, + "description": "Set to `true` when the end user has explicitly consented to the selected validation services. Required for services flagged `requires_consent=true` in the catalog — without it those services return `400` with `services_requiring_consent`." + }, + "identification_number": { + "type": "string", + "description": "Universal identification number — automatically mapped to the correct country-specific field, so you can use it instead of `personal_number`/`tax_number`/`document_number`: ARG→document_number (DNI), BOL→document_number (CI), BRA→tax_number (CPF, 11 digits), CHL→personal_number (RUT), COL→personal_number (Cédula), CRI→personal_number (Cédula), DOM→personal_number (Cédula, 11 digits), ECU→personal_number (Cédula, 10 digits), ESP→personal_number (DNI/NIE), GTM→document_number (DPI), HND→document_number (DNI), MEX→personal_number (CURP, 18 chars), PAN→personal_number (Cédula), PER→personal_number (DNI, 8 digits), PRY→document_number (CI), SLV→document_number (DUI), URY→personal_number (CI), VEN→document_number (Cédula). It never overrides an explicitly provided country-specific field." + }, + "first_name": { + "type": "string", + "description": "The individual's first name. Required by some services/countries.", + "example": "John" + }, + "last_name": { + "type": "string", + "description": "The individual's last name. Required by some services/countries.", + "example": "Doe" + }, + "middle_name": { + "type": "string", + "description": "Middle name, used by some country services (AUS, NZL, …)." + }, + "full_name": { + "type": "string", + "description": "Full name — some services accept this in lieu of first/last name (e.g. CHN services, which match the native-script name)." + }, + "date_of_birth": { + "type": "string", + "format": "date", + "description": "Date of birth, `YYYY-MM-DD`. Required by many services.", + "example": "1980-01-01" + }, + "personal_number": { + "type": "string", + "description": "Government-issued unique personal identifier. Used by: CHL (RUT), COL (Cédula), CRI, DOM, ECU, ESP (DNI/NIE), MEX (CURP), PAN (Cédula), PER (DNI), URY. Consider `identification_number` instead." + }, + "tax_number": { + "type": "string", + "description": "Tax identification number. Used by: BRA (CPF, 11 digits). Consider `identification_number` instead." + }, + "document_number": { + "type": "string", + "description": "Document number. Used by: ARG (DNI), BOL (CI), GTM (DPI), HND (DNI), PRY (CI), SLV (DUI), VEN (Cédula). Consider `identification_number` instead." + }, + "document_type": { + "type": "string", + "enum": [ + "P", + "DL", + "ID", + "RP", + "SSC", + "HIC", + "WP", + "TC", + "VISA", + "PSC", + "BC", + "OTHER" + ], + "description": "Type of document being validated: `P` passport, `DL` driver license, `ID` national ID, `RP` residence permit (plus `SSC`, `HIC`, `WP`, `TC`, `VISA`, `PSC`, `BC`, `OTHER`). Required by some services (e.g. ESP)." + }, + "expiration_date": { + "type": "string", + "format": "date", + "description": "Document expiration date, `YYYY-MM-DD`. Required for ESP (Spain) validation.", + "example": "2030-01-15" + }, + "date_of_issue": { + "type": "string", + "format": "date", + "description": "Document issue date, `YYYY-MM-DD` (fecha de expedición). Required for COL (Colombia) cédula validation.", + "example": "2015-06-20" + }, + "nationality": { + "type": "string", + "description": "Nationality as ISO 3166-1 alpha-3. Required by some services." + }, + "gender": { + "type": "string", + "enum": [ + "M", + "F", + "X" + ], + "description": "Gender: `M`, `F`, or `X` (other/unknown). Required for Argentina's RENAPER validation (`arg_renaper`)." + }, + "address": { + "description": "Residential address. Prefer the structured object `{\"street_1\":\"123 Main St\",\"street_2\":\"Apt 4B\",\"city\":\"Springfield\",\"region\":\"IL\",\"postal_code\":\"62701\",\"country\":\"US\"}`; a complete single-line string is still accepted. Required by address-verification services; when a service defines address requirements, missing parts return `400` with `address_fields_required_by`.", + "oneOf": [ + { + "type": "object", + "properties": { + "street_1": { + "type": "string" + }, + "street_2": { + "type": "string" + }, + "city": { + "type": "string" + }, + "region": { + "type": "string" + }, + "postal_code": { + "type": "string" + }, + "country": { + "type": "string" + } + } + }, + { + "type": "string", + "description": "Complete single-line address (legacy)." + } + ] + }, + "address_element_1": { + "type": "string", + "description": "Street address including street number and type. If omitted, derived from `address`." + }, + "address_element_2": { + "type": "string", + "description": "Apartment, unit, building, floor, or extra address line. Only send when you have it explicitly." + }, + "address_element_3": { + "type": "string", + "description": "City, suburb, district, locality, or neighborhood. If omitted, derived from `address`." + }, + "address_element_4": { + "type": "string", + "description": "State, province, region, or town. If omitted, derived from `address`." + }, + "address_element_5": { + "type": "string", + "description": "Postcode or postal code. `postal_code` is accepted as an alias." + }, + "postal_code": { + "type": "string", + "description": "Postal code for address-based services (alias of `address_element_5`)." + }, + "driver_license_number": { + "type": "string", + "description": "Driver licence number for government document-verification services (AUS, NZL, IND)." + }, + "driver_license_card_number": { + "type": "string", + "description": "Physical driver licence card number, where required separately from the licence number (AUS)." + }, + "driver_license_state": { + "type": "string", + "description": "Driver licence state or territory of issue (AUS)." + }, + "driver_license_version": { + "type": "string", + "description": "Driver licence version code (NZL)." + }, + "passport_number": { + "type": "string", + "description": "Passport number for government passport-verification services (AUS, NZL, IND, CHN)." + }, + "passport_expiration_date": { + "type": "string", + "format": "date", + "description": "Passport expiry date, `YYYY-MM-DD`, for passport-verification services." + }, + "passport_file_number": { + "type": "string", + "description": "Passport file number (IND)." + }, + "passport_issue_country": { + "type": "string", + "description": "Issuing country of the passport (ISO 3166-1 alpha-3) for passport-verification services." + }, + "medicare_card_number": { + "type": "string", + "description": "Medicare card number (AUS)." + }, + "immi_card_number": { + "type": "string", + "description": "Australian ImmiCard number." + }, + "immi_card_expiry_date": { + "type": "string", + "format": "date", + "description": "Australian ImmiCard expiry date." + }, + "citizenship_certificate_number": { + "type": "string", + "description": "Citizenship certificate number (AUS)." + }, + "birth_registration_number": { + "type": "string", + "description": "Birth-certificate registration number (AUS)." + }, + "birth_registration_date": { + "type": "string", + "format": "date", + "description": "Birth-certificate registration date (AUS)." + }, + "birth_registration_state": { + "type": "string", + "description": "Birth-certificate registration state (AUS)." + }, + "marriage_certificate_number": { + "type": "string", + "description": "Marriage certificate number (AUS)." + }, + "change_of_name_certificate_number": { + "type": "string", + "description": "Change-of-name certificate number (AUS)." + }, + "first_partner_name": { + "type": "string", + "description": "First name of the partner on a marriage certificate (AUS)." + }, + "last_partner_name": { + "type": "string", + "description": "Last name of the partner on a marriage certificate (AUS)." + }, + "voter_id": { + "type": "string", + "description": "Voter registration number (IND EPIC, IRL)." + }, + "epic_card": { + "type": "string", + "description": "India EPIC voter card number." + }, + "pan": { + "type": "string", + "description": "India PAN (Permanent Account Number)." + }, + "national_id": { + "type": "string", + "description": "National ID number (CHN, MYS, KEN, KHM, NGA, ZAF, …)." + }, + "cic": { + "type": "string", + "description": "Mexican INE/IFE Código de Identificación de Credencial (CIC), 9 digits. Used by the MEX INE credential-validity service (`mex_ine_vigencia`)." + }, + "identificador_ciudadano": { + "type": "string", + "description": "Mexican INE Identificador del Ciudadano (9 digits). Used with `cic` for modern INE models (E/F/G/H) in `mex_ine_vigencia`." + }, + "ocr": { + "type": "string", + "description": "Mexican INE OCR number (13 digits, back of card). Used with `cic` for Model D in `mex_ine_vigencia`." + }, + "voter_number": { + "type": "string", + "description": "Mexican INE Clave de Elector (18 chars). Used with `emission_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`." + }, + "emission_number": { + "type": "string", + "description": "Mexican INE Número de Emisión. Used with `voter_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`." + }, + "bvn": { + "type": "string", + "description": "Nigerian Bank Verification Number." + }, + "bank_card_number": { + "type": "string", + "description": "Bank card number (CHN bank-card verification)." + }, + "ssn": { + "type": "string", + "description": "Social Security Number (USA)." + }, + "phone": { + "type": "string", + "description": "Phone number for phone-verification services." + }, + "landline": { + "type": "string", + "description": "Landline number for phone-verification services." + }, + "email": { + "type": "string", + "format": "email", + "description": "Email address for identity-verification services." + }, + "country_of_residence": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country of residence. Used by global identity-enrichment services (e.g. `glb_identity_enrichment`) to focus the lookup.", + "example": "US" + }, + "partial_match_action": { + "type": "string", + "enum": [ + "DECLINE", + "NO_ACTION" + ], + "default": "NO_ACTION", + "description": "What a `partial_match` does to `database_validation.status`. Defaults to `NO_ACTION` (status stays `Approved`, with a warning)." + }, + "no_match_action": { + "type": "string", + "enum": [ + "DECLINE", + "NO_ACTION" + ], + "default": "DECLINE", + "description": "What a `no_match` does to `database_validation.status`. Defaults to `DECLINE`." + }, + "save_api_request": { + "type": "boolean", + "default": true, + "description": "Persist the validation as a session (console visibility, decision endpoint, `status.updated` webhook). Also changes the `validations` response shape: per-service objects when `true` (default), a merged field map when `false`." + }, + "vendor_data": { + "type": "string", + "description": "Your identifier for the validated user; echoed back and stored with the session." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Free-form JSON stored with the request and echoed back." + } + } + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "issuing_state" + ], + "properties": { + "issuing_state": { + "type": "string", + "description": "ISO 3166-1 **alpha-3** country code of the registry to validate against (e.g. `BRA`, `ESP`, `COL`). Determines which services are available. Unsupported codes return `400` with the full list of valid options.", + "example": "BRA" + }, + "services": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Catalog `service_id`s to run for this country (e.g. `[\"bra_cpf\"]`). Also accepts a single string or a comma-separated/JSON-encoded string. **If omitted or empty, exactly one default service runs** — the longest-established live service for the country; newer, biometric, and pay-as-you-go services must be named explicitly. Sending `services` switches the response to the extended shape (adds `services_used` and `match_score`). Every id must exist and be live for the `issuing_state`; services that require onboarding return `400` until activated for your organization. Discover available services per country via `GET /v1/organization/database-validation-countries/` (a catalog endpoint not documented in this spec) or the Business Console workflow editor.", + "example": [ + "bra_cpf" + ] + }, + "validation_type": { + "type": "string", + "deprecated": true, + "description": "DEPRECATED. Accepted for backward compatibility but ignored — the response `validation_type` is now derived from how many services full-matched. Use `services` to pin specific services." + }, + "consent": { + "type": "boolean", + "default": false, + "description": "Set to `true` when the end user has explicitly consented to the selected validation services. Required for services flagged `requires_consent=true` in the catalog — without it those services return `400` with `services_requiring_consent`." + }, + "identification_number": { + "type": "string", + "description": "Universal identification number — automatically mapped to the correct country-specific field, so you can use it instead of `personal_number`/`tax_number`/`document_number`: ARG→document_number (DNI), BOL→document_number (CI), BRA→tax_number (CPF, 11 digits), CHL→personal_number (RUT), COL→personal_number (Cédula), CRI→personal_number (Cédula), DOM→personal_number (Cédula, 11 digits), ECU→personal_number (Cédula, 10 digits), ESP→personal_number (DNI/NIE), GTM→document_number (DPI), HND→document_number (DNI), MEX→personal_number (CURP, 18 chars), PAN→personal_number (Cédula), PER→personal_number (DNI, 8 digits), PRY→document_number (CI), SLV→document_number (DUI), URY→personal_number (CI), VEN→document_number (Cédula). It never overrides an explicitly provided country-specific field." + }, + "first_name": { + "type": "string", + "description": "The individual's first name. Required by some services/countries.", + "example": "John" + }, + "last_name": { + "type": "string", + "description": "The individual's last name. Required by some services/countries.", + "example": "Doe" + }, + "middle_name": { + "type": "string", + "description": "Middle name, used by some country services (AUS, NZL, …)." + }, + "full_name": { + "type": "string", + "description": "Full name — some services accept this in lieu of first/last name (e.g. CHN services, which match the native-script name)." + }, + "date_of_birth": { + "type": "string", + "format": "date", + "description": "Date of birth, `YYYY-MM-DD`. Required by many services.", + "example": "1980-01-01" + }, + "personal_number": { + "type": "string", + "description": "Government-issued unique personal identifier. Used by: CHL (RUT), COL (Cédula), CRI, DOM, ECU, ESP (DNI/NIE), MEX (CURP), PAN (Cédula), PER (DNI), URY. Consider `identification_number` instead." + }, + "tax_number": { + "type": "string", + "description": "Tax identification number. Used by: BRA (CPF, 11 digits). Consider `identification_number` instead." + }, + "document_number": { + "type": "string", + "description": "Document number. Used by: ARG (DNI), BOL (CI), GTM (DPI), HND (DNI), PRY (CI), SLV (DUI), VEN (Cédula). Consider `identification_number` instead." + }, + "document_type": { + "type": "string", + "enum": [ + "P", + "DL", + "ID", + "RP", + "SSC", + "HIC", + "WP", + "TC", + "VISA", + "PSC", + "BC", + "OTHER" + ], + "description": "Type of document being validated: `P` passport, `DL` driver license, `ID` national ID, `RP` residence permit (plus `SSC`, `HIC`, `WP`, `TC`, `VISA`, `PSC`, `BC`, `OTHER`). Required by some services (e.g. ESP)." + }, + "expiration_date": { + "type": "string", + "format": "date", + "description": "Document expiration date, `YYYY-MM-DD`. Required for ESP (Spain) validation.", + "example": "2030-01-15" + }, + "date_of_issue": { + "type": "string", + "format": "date", + "description": "Document issue date, `YYYY-MM-DD` (fecha de expedición). Required for COL (Colombia) cédula validation.", + "example": "2015-06-20" + }, + "nationality": { + "type": "string", + "description": "Nationality as ISO 3166-1 alpha-3. Required by some services." + }, + "gender": { + "type": "string", + "enum": [ + "M", + "F", + "X" + ], + "description": "Gender: `M`, `F`, or `X` (other/unknown). Required for Argentina's RENAPER validation (`arg_renaper`)." + }, + "address": { + "description": "Residential address. Prefer the structured object `{\"street_1\":\"123 Main St\",\"street_2\":\"Apt 4B\",\"city\":\"Springfield\",\"region\":\"IL\",\"postal_code\":\"62701\",\"country\":\"US\"}`; a complete single-line string is still accepted. Required by address-verification services; when a service defines address requirements, missing parts return `400` with `address_fields_required_by`.", + "oneOf": [ + { + "type": "object", + "properties": { + "street_1": { + "type": "string" + }, + "street_2": { + "type": "string" + }, + "city": { + "type": "string" + }, + "region": { + "type": "string" + }, + "postal_code": { + "type": "string" + }, + "country": { + "type": "string" + } + } + }, + { + "type": "string", + "description": "Complete single-line address (legacy)." + } + ] + }, + "address_element_1": { + "type": "string", + "description": "Street address including street number and type. If omitted, derived from `address`." + }, + "address_element_2": { + "type": "string", + "description": "Apartment, unit, building, floor, or extra address line. Only send when you have it explicitly." + }, + "address_element_3": { + "type": "string", + "description": "City, suburb, district, locality, or neighborhood. If omitted, derived from `address`." + }, + "address_element_4": { + "type": "string", + "description": "State, province, region, or town. If omitted, derived from `address`." + }, + "address_element_5": { + "type": "string", + "description": "Postcode or postal code. `postal_code` is accepted as an alias." + }, + "postal_code": { + "type": "string", + "description": "Postal code for address-based services (alias of `address_element_5`)." + }, + "driver_license_number": { + "type": "string", + "description": "Driver licence number for government document-verification services (AUS, NZL, IND)." + }, + "driver_license_card_number": { + "type": "string", + "description": "Physical driver licence card number, where required separately from the licence number (AUS)." + }, + "driver_license_state": { + "type": "string", + "description": "Driver licence state or territory of issue (AUS)." + }, + "driver_license_version": { + "type": "string", + "description": "Driver licence version code (NZL)." + }, + "passport_number": { + "type": "string", + "description": "Passport number for government passport-verification services (AUS, NZL, IND, CHN)." + }, + "passport_expiration_date": { + "type": "string", + "format": "date", + "description": "Passport expiry date, `YYYY-MM-DD`, for passport-verification services." + }, + "passport_file_number": { + "type": "string", + "description": "Passport file number (IND)." + }, + "passport_issue_country": { + "type": "string", + "description": "Issuing country of the passport (ISO 3166-1 alpha-3) for passport-verification services." + }, + "medicare_card_number": { + "type": "string", + "description": "Medicare card number (AUS)." + }, + "immi_card_number": { + "type": "string", + "description": "Australian ImmiCard number." + }, + "immi_card_expiry_date": { + "type": "string", + "format": "date", + "description": "Australian ImmiCard expiry date." + }, + "citizenship_certificate_number": { + "type": "string", + "description": "Citizenship certificate number (AUS)." + }, + "birth_registration_number": { + "type": "string", + "description": "Birth-certificate registration number (AUS)." + }, + "birth_registration_date": { + "type": "string", + "format": "date", + "description": "Birth-certificate registration date (AUS)." + }, + "birth_registration_state": { + "type": "string", + "description": "Birth-certificate registration state (AUS)." + }, + "marriage_certificate_number": { + "type": "string", + "description": "Marriage certificate number (AUS)." + }, + "change_of_name_certificate_number": { + "type": "string", + "description": "Change-of-name certificate number (AUS)." + }, + "first_partner_name": { + "type": "string", + "description": "First name of the partner on a marriage certificate (AUS)." + }, + "last_partner_name": { + "type": "string", + "description": "Last name of the partner on a marriage certificate (AUS)." + }, + "voter_id": { + "type": "string", + "description": "Voter registration number (IND EPIC, IRL)." + }, + "epic_card": { + "type": "string", + "description": "India EPIC voter card number." + }, + "pan": { + "type": "string", + "description": "India PAN (Permanent Account Number)." + }, + "national_id": { + "type": "string", + "description": "National ID number (CHN, MYS, KEN, KHM, NGA, ZAF, …)." + }, + "cic": { + "type": "string", + "description": "Mexican INE/IFE Código de Identificación de Credencial (CIC), 9 digits. Used by the MEX INE credential-validity service (`mex_ine_vigencia`)." + }, + "identificador_ciudadano": { + "type": "string", + "description": "Mexican INE Identificador del Ciudadano (9 digits). Used with `cic` for modern INE models (E/F/G/H) in `mex_ine_vigencia`." + }, + "ocr": { + "type": "string", + "description": "Mexican INE OCR number (13 digits, back of card). Used with `cic` for Model D in `mex_ine_vigencia`." + }, + "voter_number": { + "type": "string", + "description": "Mexican INE Clave de Elector (18 chars). Used with `emission_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`." + }, + "emission_number": { + "type": "string", + "description": "Mexican INE Número de Emisión. Used with `voter_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`." + }, + "bvn": { + "type": "string", + "description": "Nigerian Bank Verification Number." + }, + "bank_card_number": { + "type": "string", + "description": "Bank card number (CHN bank-card verification)." + }, + "ssn": { + "type": "string", + "description": "Social Security Number (USA)." + }, + "phone": { + "type": "string", + "description": "Phone number for phone-verification services." + }, + "landline": { + "type": "string", + "description": "Landline number for phone-verification services." + }, + "email": { + "type": "string", + "format": "email", + "description": "Email address for identity-verification services." + }, + "country_of_residence": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country of residence. Used by global identity-enrichment services (e.g. `glb_identity_enrichment`) to focus the lookup.", + "example": "US" + }, + "partial_match_action": { + "type": "string", + "enum": [ + "DECLINE", + "NO_ACTION" + ], + "default": "NO_ACTION", + "description": "What a `partial_match` does to `database_validation.status`. Defaults to `NO_ACTION` (status stays `Approved`, with a warning)." + }, + "no_match_action": { + "type": "string", + "enum": [ + "DECLINE", + "NO_ACTION" + ], + "default": "DECLINE", + "description": "What a `no_match` does to `database_validation.status`. Defaults to `DECLINE`." + }, + "save_api_request": { + "type": "boolean", + "default": true, + "description": "Persist the validation as a session (console visibility, decision endpoint, `status.updated` webhook). Also changes the `validations` response shape: per-service objects when `true` (default), a merged field map when `false`." + }, + "vendor_data": { + "type": "string", + "description": "Your identifier for the validated user; echoed back and stored with the session." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Free-form JSON stored with the request and echoed back." + }, + "selfie": { + "type": "string", + "format": "binary", + "description": "Selfie image (`jpg`, `jpeg`, `png`, `webp`; max 2 MB — images are compressed to ~1.5 MB before upload to the registry) for biometric validation services, e.g. Argentina RENAPER (`arg_renaper`) and Panama SIB (`pan_cedula_sib`, `pan_cedula_sib_plus`). Required whenever a selected service lists it as a required field. Only available via `multipart/form-data`." + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/database-validation/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"issuing_state\": \"BRA\",\n \"services\": [\"bra_cpf\"],\n \"identification_number\": \"12345678900\",\n \"first_name\": \"John\",\n \"last_name\": \"Doe\",\n \"date_of_birth\": \"1980-01-01\",\n \"vendor_data\": \"user-1234\"\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/database-validation/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\n \"issuing_state\": \"BRA\",\n \"services\": [\"bra_cpf\"],\n \"identification_number\": \"12345678900\",\n \"first_name\": \"John\",\n \"last_name\": \"Doe\",\n \"date_of_birth\": \"1980-01-01\",\n \"vendor_data\": \"user-1234\",\n },\n timeout=45,\n)\nresp.raise_for_status()\ndv = resp.json()[\"database_validation\"]\nprint(dv[\"status\"], dv[\"match_type\"]) # e.g. Approved full_match\n\n# Biometric services need a selfie via multipart/form-data:\n# requests.post(..., data={\"issuing_state\": \"ARG\", \"services\": '[\"arg_renaper\"]',\n# \"identification_number\": \"12345678\", \"gender\": \"M\", ...},\n# files={\"selfie\": open(\"selfie.jpg\", \"rb\")})" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const res = await fetch('https://verification.didit.me/v3/database-validation/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n issuing_state: 'BRA',\n services: ['bra_cpf'],\n identification_number: '12345678900',\n first_name: 'John',\n last_name: 'Doe',\n date_of_birth: '1980-01-01',\n vendor_data: 'user-1234',\n }),\n});\nconst data = await res.json();\nconsole.log(data.database_validation.status, data.database_validation.match_type);" + } + ], + "responses": { + "200": { + "description": "Validation completed — at least one selected service returned a usable result. Inspect `database_validation.match_type` for the aggregate outcome, `validations` for the per-service field comparisons and `outcome_code`s, and `errors` (when present) for services that failed.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." + }, + "database_validation": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "Approved", + "Declined", + "In Review" + ], + "description": "`Declined` when the overall `match_type` triggers a `DECLINE` action (`no_match_action` defaults to `DECLINE`, `partial_match_action` to `NO_ACTION`); `In Review` when no service produced a usable comparison, for example when the only selected service came back `REGISTRY_UNAVAILABLE` or `INCONCLUSIVE`; `Approved` otherwise. A request that selected several services can be `Approved` on one service's match while another was unavailable, so check `errors` and the per-service `outcome_code`s too." + }, + "issuing_state": { + "type": "string", + "description": "Echo of the ISO 3166-1 alpha-3 country validated against.", + "example": "BRA" + }, + "validation_type": { + "type": "string", + "enum": [ + "one_by_one", + "two_by_two" + ], + "description": "Derived from the results: `two_by_two` when ≥2 distinct services produced a full match (on the identification number, on full name + date of birth together, or on the address without contradictions); `one_by_one` otherwise. (The request field of the same name is deprecated and ignored.)" + }, + "screened_data": { + "type": "object", + "description": "Echo of the input fields that were sent to the selected services (dates normalized to `YYYY-MM-DD`; a `selfie`, when sent, is returned as a pre-signed URL; `consent_obtained: true` is added when consent was given)." + }, + "match_type": { + "type": "string", + "nullable": true, + "enum": [ + "full_match", + "partial_match", + "no_match" + ], + "description": "Overall result aggregated across services with field-level comparisons: any `full_match` → `full_match`; else any `partial_match` → `partial_match`; else `no_match`. Outcome-only services, such as biometric criminal screening, can return `null`; inspect each item in `validations` for its `outcome_code`." + }, + "validations": { + "description": "Shape depends on `save_api_request`. **`true` (default):** an array of per-service objects (`service_id`, `service_name`, `validation`, `outcome_code`, `outcome_detail`, `source_data`). **`false`:** a single flat object merging the field-level results of all services (`{\"full_name\": \"full_match\", ...}`).", + "oneOf": [ + { + "type": "array", + "items": { + "type": "object", + "description": "Per-service validation result (returned when `save_api_request=true`, the default).", + "properties": { + "service_id": { + "type": "string", + "description": "Catalog id of the service that produced this result (e.g. `bra_cpf`).", + "example": "bra_cpf" + }, + "service_name": { + "type": "string", + "description": "Human-readable catalog name of the service.", + "example": "Brazil - CPF status check" + }, + "validation": { + "type": "object", + "description": "Field-by-field comparison between your input and the registry record. Keys are canonical field names (`full_name`, `date_of_birth`, `identification_number`, `address`, …); values are `full_match`, `partial_match`, or `no_match`.", + "additionalProperties": { + "type": "string", + "enum": [ + "full_match", + "partial_match", + "no_match" + ] + } + }, + "outcome_code": { + "type": "string", + "enum": [ + "MATCH", + "PARTIAL_MATCH", + "NO_MATCH", + "DOCUMENT_NOT_FOUND", + "INVALID_DOCUMENT_FORMAT", + "INVALID_INPUT", + "MINOR_BLOCKED", + "DECEASED", + "BIOMETRIC_NO_MATCH", + "BIOMETRIC_IMAGE_UNUSABLE", + "INCONCLUSIVE", + "DOCUMENT_SUPERSEDED", + "REGISTRY_UNAVAILABLE", + "REGISTRY_ERROR" + ], + "description": "Vendor-neutral outcome of the registry lookup. `MATCH`/`PARTIAL_MATCH`/`NO_MATCH` describe the comparison; `DOCUMENT_NOT_FOUND` means the identifier is not in the registry; `INCONCLUSIVE` means the registry could not confirm either way (routed to review — NOT a no-match); `BIOMETRIC_NO_MATCH` is a definitive face mismatch while `BIOMETRIC_IMAGE_UNUSABLE` is a technical image problem (retake the selfie); `DECEASED`/`MINOR_BLOCKED` are registry-policy outcomes; `DOCUMENT_SUPERSEDED` means the identity matched but the edition of the document presented is not the one on file (a newer edition has been issued); `INVALID_DOCUMENT_FORMAT`/`INVALID_INPUT` mean the registry itself rejected the data it was sent; `REGISTRY_UNAVAILABLE`/`REGISTRY_ERROR` indicate upstream failures." + }, + "outcome_detail": { + "type": "string", + "nullable": true, + "description": "Raw upstream status detail backing the outcome code (e.g. an HTTP or registry status code)." + }, + "source_data": { + "type": "object", + "nullable": true, + "description": "Cleaned, normalized view of the registry record using canonical field names (`identification_number`, `first_name`, `last_name`, `date_of_birth`, plus registry-specific flags). Image fields, when present, are returned as pre-signed URLs." + } + } + } + }, + { + "type": "object", + "additionalProperties": { + "type": "string", + "enum": [ + "full_match", + "partial_match", + "no_match" + ] + }, + "description": "Merged field-level results (stateless `save_api_request=false` shape)." + } + ] + }, + "warnings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "description": "Feature that raised the warning." }, - "issuing_authority": { + "risk": { "type": "string", + "description": "Machine-readable risk identifier." + }, + "additional_data": { + "type": "object", "nullable": true, - "description": "The authority behind the wallet, e.g. `Danish Agency for Digital Government`." + "description": "Extra context for the warning, when available." }, - "issuing_country": { + "log_type": { "type": "string", - "description": "ISO 3166-1 alpha-3 country the wallet was offered in." + "description": "`warning`, `information`, or `error` — how the risk affected the outcome." }, - "credential_type": { + "short_description": { "type": "string", - "nullable": true, - "description": "Credential the wallet presented, e.g. `person_identification`." + "description": "One-line human-readable summary." }, - "level_of_assurance": { + "long_description": { "type": "string", - "enum": [ - "low", - "substantial", - "high" - ], - "nullable": true, - "description": "The level of assurance the wallet asserted. A sign-in that asserts less than the level requested fails instead of downgrading." + "description": "Full human-readable explanation." + } + } + }, + "description": "`DATABASE_VALIDATION_NO_MATCH` or `DATABASE_VALIDATION_PARTIAL_MATCH` when applicable; empty on a full match." + }, + "errors": { + "type": "array", + "description": "Present only when some (but not all) selected services failed. Each entry carries `service_id`, `code`, `message`, and `retryable`. A service reported here is absent from `validations` and `services_used`, and is not billed. This array is the only place it appears, so inspect it even when `status` is `Approved`.", + "items": { + "type": "object", + "properties": { + "service_id": { + "type": "string" }, - "verified_at": { + "code": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "The wallet's authentication instant." + "example": "empty_provider_response" }, - "signature_valid": { - "type": "boolean", - "nullable": true, - "description": "Whether the signed assertion the wallet returned validated." + "message": { + "type": "string" }, - "attributes": { - "type": "object", - "additionalProperties": true, - "description": "The attributes the wallet shared, keyed as the wallet's catalog entry lists them." + "retryable": { + "type": "boolean", + "description": "`true` when the service produced no answer and retrying it later can succeed; `false` when the source refused the data it was sent and the input must be corrected first.", + "example": true + } + } + } + }, + "services_used": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Catalog ids of the services that returned a billable result. **Only present when the request explicitly sent `services`.**" + }, + "match_score": { + "type": "number", + "description": "Fraction (0.0–1.0) of attempted services that produced a full match (on the identification number, on full name + date of birth together, or on the address without contradictions). **Only present when the request explicitly sent `services`.**" + } + } + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Echo of the `vendor_data` you sent." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Echo of the `metadata` you sent." + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "examples": { + "Full match (Approved)": { + "summary": "Default save_api_request=true — per-service `validations` objects", + "value": { + "request_id": "7f9a1c0e-1f2b-4f6e-9d3a-2b1c0e7f9a1c", + "database_validation": { + "status": "Approved", + "issuing_state": "BRA", + "validation_type": "one_by_one", + "screened_data": { + "tax_number": "12345678900", + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "1980-01-01" + }, + "match_type": "full_match", + "validations": [ + { + "validation": { + "full_name": "full_match", + "date_of_birth": "full_match", + "identification_number": "full_match" + }, + "outcome_code": "MATCH", + "outcome_detail": "200", + "service_id": "bra_cpf", + "service_name": "Brazil - CPF status check", + "source_data": { + "identification_number": "12345678900", + "first_name": "JOHN", + "last_name": "DOE", + "date_of_birth": "1980-01-01", + "lgpd_minor": false, + "minor_under_16": false, + "minor_under_18": false + } + } + ], + "warnings": [] + }, + "vendor_data": "user-1234", + "metadata": null, + "created_at": "2026-06-11T10:30:00.000000+00:00" + } + }, + "Explicit services (extended shape)": { + "summary": "Request sent `services` — response adds `services_used` and `match_score`", + "value": { + "request_id": "b3e7c1a2-9d4f-4f0b-8a6c-5e2d1f0a9b3e", + "database_validation": { + "status": "Approved", + "issuing_state": "BRA", + "validation_type": "one_by_one", + "screened_data": { + "tax_number": "12345678900", + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "1980-01-01" + }, + "match_type": "full_match", + "validations": [ + { + "validation": { + "full_name": "full_match", + "date_of_birth": "full_match", + "identification_number": "full_match" + }, + "outcome_code": "MATCH", + "outcome_detail": "200", + "service_id": "bra_cpf", + "service_name": "Brazil - CPF status check", + "source_data": { + "identification_number": "12345678900", + "first_name": "JOHN", + "last_name": "DOE", + "date_of_birth": "1980-01-01", + "lgpd_minor": false, + "minor_under_16": false, + "minor_under_18": false + } + } + ], + "warnings": [], + "services_used": [ + "bra_cpf" + ], + "match_score": 1.0 + }, + "vendor_data": "user-1234", + "metadata": null, + "created_at": "2026-06-11T10:30:00.000000+00:00" + } + }, + "No match (Declined)": { + "summary": "Registry record does not match — default `no_match_action=DECLINE`", + "value": { + "request_id": "0a1b2c3d-4e5f-6789-abcd-ef0123456789", + "database_validation": { + "status": "Declined", + "issuing_state": "BRA", + "validation_type": "one_by_one", + "screened_data": { + "tax_number": "12345678900", + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "1980-01-01" + }, + "match_type": "no_match", + "validations": [ + { + "validation": { + "full_name": "no_match", + "date_of_birth": "no_match", + "identification_number": "no_match" + }, + "outcome_code": "NO_MATCH", + "outcome_detail": "200", + "service_id": "bra_cpf", + "service_name": "Brazil - CPF status check", + "source_data": { + "identification_number": "00987654321", + "first_name": "TOTALLY", + "last_name": "DIFFERENT NAME", + "date_of_birth": "1999-01-01", + "lgpd_minor": false, + "minor_under_16": false, + "minor_under_18": false + } + } + ], + "warnings": [ + { + "feature": "DATABASE_VALIDATION", + "risk": "DATABASE_VALIDATION_NO_MATCH", + "additional_data": null, + "log_type": "error", + "short_description": "Database validation no match", + "long_description": "The system identified a no match in the database validation, requiring further investigation." + } + ] + }, + "vendor_data": null, + "metadata": null, + "created_at": "2026-06-11T10:31:00.000000+00:00" + } + }, + "Stateless (save_api_request=false)": { + "summary": "`validations` collapses to a merged field map; nothing is stored", + "value": { + "request_id": "c4d5e6f7-8901-2345-6789-abcdef012345", + "database_validation": { + "status": "Approved", + "issuing_state": "BRA", + "validation_type": "one_by_one", + "screened_data": { + "tax_number": "12345678900", + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "1980-01-01" + }, + "match_type": "full_match", + "validations": { + "full_name": "full_match", + "date_of_birth": "full_match", + "identification_number": "full_match" + }, + "warnings": [] + }, + "vendor_data": null, + "metadata": null, + "created_at": "2026-06-11T10:32:00.000000+00:00" + } + } + } + } + } + }, + "400": { + "description": "Validation error - nothing is billed. Preflight field errors occur before any external call. If every unanswered attempt explicitly refuses the input, the response instead includes `validation_errors` with `code: provider_rejected_input` and `retryable: false`; correct the input before retrying. Field-level problems use DRF's standard envelope; catalog problems return a `services` array (plus `requires_onboarding` or `services_requiring_consent` when relevant); missing-field errors include `fields_required_by` mapping each missing field to the services that demand it; services that need at least one extra identifier (e.g. `usa_states_credit_bureau`, which cannot verify a name alone) return `any_field_required_by` mapping each service to the fields of which at least one must be present; format problems checked per service return `invalid_fields_by_service`.", + "content": { + "application/json": { + "examples": { + "Invalid issuing_state": { + "summary": "Country not supported — message lists every valid alpha-3 code", + "value": { + "error": [ + "Invalid issuing state. Valid options are: ARE, ARG, AUS, AUT, BEL, BGD, BOL, BRA, CAN, CHE, CHL, CHN, CIV, COL, CRI, CZE, DEU, DNK, DOM, ECU, ESP, FIN, FRA, GBR, GHA, GLB, GRC, GTM, HKG, HND, IDN, IND, IRL, ITA, KEN, KHM, MAR, MEX, MYS, NGA, NLD, NOR, NZL, OMN, PAN, PER, PHL, POL, PRT, PRY, QAT, SGP, SLV, SVK, SWE, THA, UGA, URY, USA, VEN, ZAF, ZMB, ZWE" + ] + } + }, + "Missing required fields": { + "summary": "Union of required fields across selected services, with the universal-field hint", + "value": { + "error": [ + "Missing required fields: tax_number (or use 'identification_number' which maps to 'tax_number' for BRA)" + ], + "fields_required_by": { + "tax_number": [ + "bra_cpf" + ] + } + } + }, + "Unknown service id": { + "summary": "`services` contains an id that is not in the catalog", + "value": { + "services": [ + "Unknown service_id(s): ['bogus_service']" + ] + } + }, + "Service not live for country": { + "summary": "`services` names a service from another country (or a stub entry)", + "value": { + "services": [ + "Services not currently live for PER: ['bra_cpf']. Available services: ['per_dni', 'per_residential', 'per_tax_registration']" + ] + } + }, + "Service requires onboarding": { + "summary": "Service exists but is not activated for your organization yet", + "value": { + "services": [ + "Services require onboarding before they can be enabled for AUS: ['aus_australia_driver_licence']. Contact Didit support to activate them." + ], + "requires_onboarding": [ + "aus_australia_driver_licence" + ] + } + }, + "Consent missing": { + "summary": "A selected service is flagged requires_consent=true and `consent` was not `true`", + "value": { + "consent": [ + "Explicit end-user consent is required for these database validation services: ['chn_national_id']. Send `consent=true`." + ], + "services_requiring_consent": [ + "chn_national_id" + ] + } + }, + "Country format rule violation": { + "summary": "Identifier fails the country's format rule (length/digits/regex)", + "value": { + "tax_number": [ + "Invalid tax number for BRA: Brazilian CPF (exactly 11 digits). Must be exactly 11 characters long (got 10)." + ] + } + }, + "Service-specific format violation": { + "summary": "A field fails the selected service's own format requirements", + "value": { + "error": [ + "One or more fields do not match the format required by the selected service." + ], + "invalid_fields_by_service": { + "chn_national_id": [ + "national_id_format" + ] + } + } + }, + "Input refused by every attempt": { + "summary": "Correct the input before retrying", + "value": { + "error": "The source refused to run this query with the supplied input.", + "validation_errors": [ + { + "service_id": "bra_cpf", + "code": "provider_rejected_input", + "message": "The source refused to run this query with the supplied input.", + "retryable": false + } + ] + } + } + }, + "schema": { + "type": "object", + "additionalProperties": true, + "properties": { + "error": { + "description": "Request-level explanation or preflight field errors.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "validation_errors": { + "type": "array", + "description": "Present when every unanswered attempt refused the input after preflight.", + "items": { + "type": "object", + "properties": { + "service_id": { + "type": "string" + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean", + "description": "Whether retrying later can resolve this service failure. False means correct the supplied input first." + } + } + } + } + } + } + } + } + }, + "403": { + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any screening or provider call happens.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Not enough credits": { + "summary": "Organization balance cannot cover the call", + "value": { + "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "content": { + "application/json": { + "examples": { + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + }, + "502": { + "description": "No selected service returned a usable result. Inspect each validation_errors entry before retrying; HTTP 502 alone does not establish a transient failure. An empty_provider_response with retryable: true indicates unavailable or unexplained results, including a refused primary followed by an unavailable fallback. Legacy provider_invalid_input and provider_validation_error entries instead require corrected input and may omit retryable. Nothing is billed. When save_api_request=true, the failed attempt is recorded as a session with status Not Finished. Partial failures return 200 with database_validation.errors.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "validation_errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "service_id": { + "type": "string" + }, + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean", + "description": "Whether retrying later can resolve this service failure. False means correct the supplied input first." + } + } + } + } + } + }, + "examples": { + "Provider returned no result": { + "summary": "Every selected service failed", + "value": { + "error": "Database validation could not be performed. The external validation service did not return a result. Please try again later.", + "validation_errors": [ + { + "service_id": "bra_cpf", + "code": "empty_provider_response", + "message": "The provider did not return a database validation result.", + "retryable": true + } + ] + } + } + } + } + } + } + } + } + }, + "/v3/phone/send/": { + "post": { + "summary": "Send Phone Code", + "description": "Send a one-time passcode (OTP) to a phone number over WhatsApp, SMS, or another supported channel, then verify it with [`POST /v3/phone/check/`](/standalone-apis/phone-check).\n\n**How send and check pair up.** Verification state is keyed by your application plus the E.164 `phone_number` — the check call does not take `request_id`. Each new send creates a pending verification that lives for **5 minutes**: call the check with the same number and the code the user received before the window closes. Calling send again for the same number while a verification is pending re-sends a code for that same verification (`status: \"Retry\"`, same `request_id`). At most one retry is attached this way (two sends total); a further send starts a fresh verification with a new `request_id`. The 5-minute window is measured from the first send and is not extended by retries.\n\n**Delivery channels.** `options.preferred_channel` selects the channel (`whatsapp` by default; `sms`, `telegram`, `voice`, `rcs`, `viber`, and `zalo` are also supported). When the preferred channel is unavailable for the destination country or number, delivery automatically falls back to SMS — the send still reports `Success`, and the channel actually used is reported by the check response (`phone.verification_method` and the `lifecycle` events).\n\n**Branding.** SMS codes carry your application's SMS sender name, set in the Business Console, at no extra cost. Pass `options.sms_sender_name` to sign one code with another name, or `options.use_white_label_customization: true` to fall back to your white-label public name when no sender name is set. WhatsApp codes use a fixed message; connect your own WhatsApp Business number in the Business Console to send them from your business name and number, billed at the Didit fee only.\n\n**Send statuses.** `Success` — OTP sent for a new verification; `Retry` — OTP re-sent for the pending verification; `Blocked` — the anti-fraud layer refused to send (see `reason`, e.g. `repeated_attempts`, `suspicious`, `spam`). A `Blocked` send immediately finalizes the verification as `Declined` with a high-risk warning, so a follow-up check returns `Expired or Not Found`.\n\n**Billing.** The price depends on the destination country and the channel that actually delivers the message — see [Phone Verification Pricing](/getting-started/phone-verification-pricing). Only `Blocked` sends are guaranteed free: any other unbilled send attempt is charged at verification finalization or expiration, even if delivery was never confirmed or the delivery provider reported the message undeliverable. Checks are always free. As an anti-abuse measure, phone verification is disabled until your organization's first top-up (`403`).\n\n**Session persistence.** Every new verification is persisted as an API-type session: `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the verification appears in the Business Console, and `status.updated` webhooks fire as it progresses.\n\n**Sandbox.** Sandbox API keys skip delivery and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Success` payload with a random `request_id`; no message is sent and nothing is persisted. Use code `123456` on the sandbox check.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limits.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints, plus an anti-abuse cap of 4 send attempts per phone number per hour; exceeding either returns `429`.", + "operationId": "post_v3phone_send", + "x-didit-sdk": { + "resource": "phone", + "method": "send_code", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/phone/send/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"phone_number\": \"+14155552671\",\n \"options\": {\n \"code_size\": 6,\n \"preferred_channel\": \"whatsapp\",\n \"locale\": \"en-US\"\n },\n \"vendor_data\": \"user-1234\"\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/phone/send/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n },\n json={\n \"phone_number\": \"+14155552671\",\n \"options\": {\n \"code_size\": 6,\n \"preferred_channel\": \"whatsapp\",\n \"locale\": \"en-US\",\n },\n \"vendor_data\": \"user-1234\",\n },\n timeout=15,\n)\nresp.raise_for_status()\nprint(resp.json()) # {request_id, status, reason, vendor_data, metadata}\n# Then verify with POST /v3/phone/check/ using the same phone_number" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const res = await fetch('https://verification.didit.me/v3/phone/send/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n phone_number: '+14155552671',\n options: { code_size: 6, preferred_channel: 'whatsapp', locale: 'en-US' },\n vendor_data: 'user-1234',\n }),\n});\nif (!res.ok) throw new Error(`Phone send failed: ${res.status}`);\nconst data = await res.json();\nconsole.log(data); // { request_id, status, reason, vendor_data, metadata }\n// Then verify with POST /v3/phone/check/ using the same phone_number" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "phone_number": { + "type": "string", + "maxLength": 20, + "description": "Recipient phone number in E.164 format — leading `+` and country code (e.g., `+14155552671`). Numbers are validated and normalized to E.164; unparseable or invalid numbers return `400` with a `phone_number` field error.", + "example": "+14155552671" + }, + "options": { + "type": "object", + "description": "OTP delivery options. All fields are optional.", + "properties": { + "code_size": { + "type": "integer", + "minimum": 4, + "maximum": 8, + "default": 6, + "description": "Number of digits in the OTP." + }, + "locale": { + "type": "string", + "maxLength": 5, + "pattern": "^[a-z]{2,3}(-[A-Z]{2,3})?$", + "description": "BCP-47 locale used to localize the verification message (e.g., `en-US`, `fr`, `es`). Invalid formats return `400`.", + "example": "en-US" + }, + "preferred_channel": { + "type": "string", + "enum": [ + "whatsapp", + "sms", + "telegram", + "voice", + "rcs", + "viber", + "zalo" + ], + "default": "whatsapp", + "description": "Preferred delivery channel. If the channel is unavailable for the destination country or number, the message automatically falls back to SMS — the send still reports `Success`." + }, + "use_white_label_customization": { + "type": "boolean", + "default": false, + "description": "When `true`, the send is white-labelled: if your application has no SMS sender name, the SMS names your white-label public name instead (\"123456 is your verification code for Acme Bank\"). Your SMS sender name, when set, is used with or without this flag. Applies to SMS messages, including an SMS fallback from another channel; WhatsApp codes are sent with a fixed message and are not branded. Defaults to `false`." + }, + "sms_sender_name": { + "type": "string", + "nullable": true, + "maxLength": 30, + "example": "Acme Bank", + "description": "The name this code's SMS carries (\"123456 is your verification code for Acme Bank\"), instead of your application's SMS sender name. Free. 1 to 30 characters: letters, digits, spaces and ' & - ! only, not only digits, with single spaces between words; any other value returns `400` and no code is sent. Omitted, `null` or blank, your application's SMS sender name is used. Each application can start using up to two new names per day (UTC); past that, the code is sent with your application's name. Applies to SMS messages, including an SMS fallback from another channel; WhatsApp codes are not branded.", + "pattern": "^$|^(?![\\d ]+$)[\\p{L}\\p{N}'&!-]+( [\\p{L}\\p{N}'&!-]+)*$" + } + } + }, + "signals": { + "type": "object", + "description": "Optional device and network signals about the end user, forwarded to the anti-fraud layer to improve detection of abusive or automated traffic. All fields are optional.", + "properties": { + "ip": { + "type": "string", + "description": "IP address of the end user's device. IPv4 or IPv6.", + "example": "192.0.2.1" + }, + "device_id": { + "type": "string", + "maxLength": 255, + "description": "Unique identifier of the end user's device. Android: `ANDROID_ID`; iOS: `identifierForVendor`.", + "example": "8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2" + }, + "device_platform": { + "type": "string", + "enum": [ + "android", + "ios", + "ipados", + "tvos", + "web" + ], + "description": "Platform of the end user's device." + }, + "device_model": { + "type": "string", + "maxLength": 255, + "description": "Model of the end user's device.", + "example": "iPhone17,2" + }, + "os_version": { + "type": "string", + "maxLength": 64, + "description": "Operating-system version of the end user's device.", + "example": "18.0.1" + }, + "app_version": { + "type": "string", + "maxLength": 64, + "description": "Version of your application.", + "example": "1.2.34" + }, + "user_agent": { + "type": "string", + "maxLength": 512, + "description": "User agent of the end user's browser or app.", + "example": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1" + } + } + }, + "vendor_data": { + "type": "string", + "description": "Optional caller-controlled identifier (your internal user id, an email, a UUID, etc.) persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. Use it to correlate Didit's `request_id` with your user record." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Optional free-form JSON object persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console." + } + }, + "required": [ + "phone_number" + ] + }, + "examples": { + "WhatsApp (default)": { + "summary": "6-digit OTP via WhatsApp, falling back to SMS automatically", + "value": { + "phone_number": "+14155552671", + "options": { + "preferred_channel": "whatsapp", + "locale": "en-US" + }, + "vendor_data": "user-1234" + } + }, + "SMS with custom code length": { + "summary": "Force SMS delivery with a 4-digit code", + "value": { + "phone_number": "+34699999999", + "options": { + "preferred_channel": "sms", + "code_size": 4, + "locale": "es" + } + } + }, + "With fraud signals": { + "summary": "Forward device signals to strengthen anti-fraud screening", + "value": { + "phone_number": "+14155552671", + "signals": { + "ip": "192.0.2.1", + "device_id": "8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2", + "device_platform": "ios", + "device_model": "iPhone17,2", + "os_version": "18.0.1", + "app_version": "1.2.34" + }, + "vendor_data": "user-1234" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Send acknowledged. Inspect `status`: `Success` and `Retry` mean a code is on its way; `Blocked` means the anti-fraud layer refused and the verification is already finalized as `Declined`. `request_id` is the persisted session id (same id on a `Retry`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PhoneVerificationSendResponse" + }, + "examples": { + "Success": { + "summary": "OTP sent for a new verification", + "value": { + "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", + "status": "Success", + "reason": null, + "vendor_data": "user-1234", + "metadata": null + } + }, + "Retry": { + "summary": "Second send for the same number inside the 5-minute window — same request_id", + "value": { + "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", + "status": "Retry", + "reason": null, + "vendor_data": "user-1234", + "metadata": null + } + }, + "Blocked": { + "summary": "Anti-fraud refusal — verification finalized as Declined, nothing billed", + "value": { + "request_id": "0d8af1f4-0c43-46d7-b75c-6e4e22c0a7b3", + "status": "Blocked", + "reason": "repeated_attempts", + "vendor_data": "user-1234", + "metadata": null + } + } + } + } + } + }, + "400": { + "description": "Validation error. Malformed input returns DRF's field-error envelope (one array of messages per offending field, nested under `options` for option errors). A number that parses but is rejected at send time (unreachable, or an ineligible line type such as a short code) returns a `{\"detail\": ...}` envelope instead.", + "content": { + "application/json": { + "examples": { + "Invalid phone number (field error)": { + "summary": "Number is not valid E.164", + "value": { + "phone_number": [ + "Invalid phone number provided." + ] + } + }, + "Missing phone number": { + "summary": "`phone_number` not included in the body", + "value": { + "phone_number": [ + "This field is required." + ] + } + }, + "Invalid options": { + "summary": "Out-of-range `code_size`, malformed `locale`, unknown `preferred_channel`", + "value": { + "options": { + "code_size": [ + "Ensure this value is less than or equal to 8." + ], + "locale": [ + "Ensure this field has no more than 5 characters." + ], + "preferred_channel": [ + "\"carrier_pigeon\" is not a valid choice." + ] + } + } + }, + "Number rejected at send time": { + "summary": "Valid E.164 shape but the number cannot receive an OTP", + "value": { + "detail": "Invalid phone number provided." + } + }, + "Ineligible line type": { + "summary": "Line type cannot receive an OTP (e.g., short code)", + "value": { + "detail": "Invalid phone line type provided." + } + } + } + } + } + }, + "403": { + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment (`{\"detail\": ...}`) — this API never returns `401`. Also returned before any send when the organization has never topped up (anti-abuse gate) or when its balance cannot cover the destination's send price (`{\"error\": ...}`).", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Top-up required": { + "summary": "Phone verification stays disabled until the organization's first top-up", + "value": { + "detail": "To protect against abuse, phone verification is disabled until your first top-up." + } + }, + "Not enough credits": { + "summary": "Organization balance cannot cover the send price", + "value": { + "error": "You don't have enough credits to perform this request." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. Additionally, an anti-abuse cap of 4 verification attempts per phone number per hour applies upstream. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. Note: the `X-RateLimit-*`/`Retry-After` headers accompany only the 300/min write-limit 429; the per-number cap 429 carries no rate-limit headers.", + "content": { + "application/json": { + "examples": { + "Per-number cap": { + "summary": "More than 4 verification attempts for the same number in the last hour", + "value": { + "detail": "Maximum verification attempts reached for this phone number. Only 4 authentication attempts are allowed per hour. Try again later or use a different number." + } + }, + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + }, + "502": { + "description": "The phone verification provider is temporarily unavailable - a 5xx response, timeout, or connection failure that survived the built-in retries. Safe to retry; nothing is billed and no attempt is consumed.", + "content": { + "application/json": { + "examples": { + "Provider outage": { + "summary": "The provider did not answer the send request", + "value": { + "detail": "Phone verification is temporarily unavailable. Please try again shortly.", + "code": "phone_provider_unavailable" + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string" + } + } + } + } + } + }, + "500": { + "description": "Unexpected failure while creating the verification that is not a known provider outage. Safe to retry.", + "content": { + "application/json": { + "examples": { + "Provider error": { + "summary": "OTP delivery could not be initiated", + "value": { + "detail": "Error creating phone verification" + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/phone/check/": { + "post": { + "summary": "Check Phone Code", + "description": "Verify the OTP delivered by [`POST /v3/phone/send/`](/standalone-apis/phone-send) and get the final verification result plus phone intelligence: carrier name and line type, virtual (VoIP) and disposable flags, and duplicate usage of the number across your sessions.\n\n**How the check finds the verification.** Matching is by your application plus the E.164 `phone_number` — `request_id` is not an input. The most recent pending verification created within the last **5 minutes** is checked. If there is none (never sent, already finalized, or older than 5 minutes) the endpoint returns `200` with `status: \"Expired or Not Found\"`.\n\n**Attempt budget.** Each verification allows **3 code attempts**. The first two wrong codes return `status: \"Failed\"` with the attempts remaining and `phone: null`; the third wrong code finalizes the verification as `Declined` with a `VERIFICATION_CODE_ATTEMPTS_EXCEEDED` warning and returns the full phone report.\n\n**Outcomes.** `Approved` — correct code and no declining risk. `Declined` — terminal: the code was correct but a declining risk matched (a `DECLINE` action below, a blocklisted or high-risk number), or the attempt budget was exhausted. `Failed` — wrong code, attempts remaining. `Expired or Not Found` — nothing to check. On `Approved`/`Declined` the `request_id` equals the send's `request_id` (the session id) and `phone` carries the full report (`carrier`, `is_virtual`, `is_disposable`, `warnings`, `lifecycle`, `matches`); on `Failed` and `Expired or Not Found` the `request_id` is a one-off random UUID.\n\n**Risk actions.** `duplicated_phone_number_action`, `disposable_number_action`, and `voip_number_action` decide what happens when the corresponding risk is detected on a correct code: `DECLINE` flips the final status to `Declined`; `NO_ACTION` (default) records the risk in `phone.warnings` without affecting the status.\n\n**Billing.** Checks are free — delivery is billed on the send side (see [Phone Verification Pricing](/getting-started/phone-verification-pricing)).\n\n**Session persistence.** A finalized check updates the session created by the send (visible in the Business Console, queryable via `GET /v3/session/{sessionId}/decision/`) and fires a `status.updated` webhook.\n\n**Sandbox.** Sandbox API keys skip all processing: any well-formed code (4–8 digits, `123456` included) returns a static `Approved` payload with a simplified flat `phone` object (`status`, `phone_number`, `country_code`, `carrier_name`, `line_type`, `flags`); malformed input still returns `400`. Nothing is persisted.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limits.** Shared write budget of 300 requests/min per API key, plus the upstream anti-abuse cap of 4 attempts per number per hour; exceeding either returns `429`.\n\n**Social footprint.** Set `enable_social` to `true` to add a `phone_social` block: the online platforms this number is registered on, with per-category counts and any profile details available. `profiles_registered: 0` means the platforms were checked and the number was found on none of them - a valid, billable answer. The block is absent when the add-on was not requested or the check could not be completed, and it is not billed in that case. Billing adds one `phone_social` unit per request on top of this endpoint's own price.", + "operationId": "post_v3phone_check", + "x-didit-sdk": { + "resource": "phone", + "method": "check_code", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/phone/check/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"phone_number\": \"+14155552671\",\n \"code\": \"123456\",\n \"voip_number_action\": \"DECLINE\"\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/phone/check/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n },\n json={\n \"phone_number\": \"+14155552671\", # same number as the send call\n \"code\": \"123456\",\n \"voip_number_action\": \"DECLINE\",\n },\n timeout=15,\n)\nresp.raise_for_status()\nresult = resp.json()\nprint(result[\"status\"]) # Approved / Declined / Failed / Expired or Not Found\nif result[\"status\"] in (\"Approved\", \"Declined\"):\n print(result[\"phone\"][\"carrier\"], result[\"phone\"][\"is_virtual\"])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const res = await fetch('https://verification.didit.me/v3/phone/check/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n phone_number: '+14155552671', // same number as the send call\n code: '123456',\n voip_number_action: 'DECLINE',\n }),\n});\nconst data = await res.json();\nif (data.status === 'Approved') {\n // full phone report is in data.phone (carrier, is_virtual, matches, ...)\n}" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "phone_number": { + "type": "string", + "maxLength": 20, + "description": "The same phone number used in the matching [`POST /v3/phone/send/`](/standalone-apis/phone-send) call, in E.164 format. This is what links the check to the send.", + "example": "+14155552671" + }, + "code": { + "type": "string", + "minLength": 4, + "maxLength": 8, + "pattern": "^[0-9]{4,8}$", + "description": "The OTP the end user received. Must be 4–8 numeric digits — anything else returns `400` without consuming an attempt. A well-formed but wrong code returns `200` with `status: \"Failed\"` and consumes one of the 3 attempts.", + "example": "123456" + }, + "duplicated_phone_number_action": { + "type": "string", + "enum": [ + "NO_ACTION", + "DECLINE" + ], + "default": "NO_ACTION", + "description": "What to do when the same number was already used by a different user (different `vendor_data`) of your application. `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings` and fills `phone.matches`." + }, + "disposable_number_action": { + "type": "string", + "enum": [ + "NO_ACTION", + "DECLINE" + ], + "default": "NO_ACTION", + "description": "What to do when the carrier lookup flags the number as a temporary/burner line (`phone.is_disposable`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings`." + }, + "voip_number_action": { + "type": "string", + "enum": [ + "NO_ACTION", + "DECLINE" + ], + "default": "NO_ACTION", + "description": "What to do when the carrier lookup reports a virtual line type — `voip`, `isp`, or `vpn` (`phone.is_virtual`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings`." + }, + "enable_social": { + "type": "boolean", + "default": false, + "description": "When true, also look up the social footprint of the number - the messaging, social, e-commerce and professional platforms it is registered on. Billed as one `phone_social` unit per request in addition to this endpoint's own price.", + "example": true + }, + "phone_no_social_presence_action": { + "type": "string", + "enum": [ + "NO_ACTION", + "DECLINE" + ], + "default": "NO_ACTION", + "description": "Action when the social footprint check finds the number registered on none of the platforms it covers. Only NO_ACTION and DECLINE are accepted on this endpoint; REVIEW is available on the workflow configuration key of the same name." + } + }, + "required": [ + "phone_number", + "code" + ] + }, + "examples": { + "Default": { + "summary": "Record any risks but never auto-decline", + "value": { + "phone_number": "+14155552671", + "code": "123456" + } + }, + "Strict risk policy": { + "summary": "Auto-decline VoIP and disposable lines", + "value": { + "phone_number": "+14155552671", + "code": "123456", + "voip_number_action": "DECLINE", + "disposable_number_action": "DECLINE" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Check completed — wrong codes and missing verifications also return `200`; inspect `status`, not the HTTP code. `phone` is populated only on finalized outcomes (`Approved`/`Declined`), `null` on `Failed`, and absent on `Expired or Not Found`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PhoneVerificationCheckResponse" + }, + "examples": { + "Approved": { + "summary": "Correct code — full phone report with carrier, lifecycle, and matches", + "value": { + "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", + "status": "Approved", + "message": "The verification code is correct.", + "phone": { + "status": "Approved", + "phone_number_prefix": "+1", + "phone_number": "4155552671", + "full_number": "+14155552671", + "country_code": "US", + "country_name": "United States", + "carrier": { + "name": "AT&T", + "type": "mobile" + }, + "is_disposable": false, + "is_virtual": false, + "verification_method": "whatsapp", + "verification_attempts": 1, + "verified_at": "2026-06-12T01:24:47.311323Z", + "warnings": [], + "lifecycle": [ + { + "type": "PHONE_VERIFICATION_MESSAGE_SENT", + "timestamp": "2026-06-12T01:23:39.580554+00:00", + "details": { + "status": "Success", + "reason": null, + "channel": "whatsapp", + "actual_channel": "whatsapp" + }, + "fee": 0.0709 + }, + { + "type": "PHONE_DELIVERY_DELIVERED", + "timestamp": "2026-06-12T01:23:43.118220+00:00", + "details": { + "channel": "whatsapp", + "status": "delivered" + }, + "fee": 0 + }, + { + "type": "VALID_CODE_ENTERED", + "timestamp": "2026-06-12T01:24:47.311201+00:00", + "details": { + "code_tried": "123456", + "status": "Approved" + }, + "fee": 0 + }, + { + "type": "PHONE_VERIFICATION_APPROVED", + "timestamp": "2026-06-12T01:24:47.384292+00:00", + "details": null, + "fee": 0 + } + ], + "matches": [] + }, + "vendor_data": "user-1234", + "metadata": null, + "created_at": "2026-06-12T01:24:47.401719+00:00" + } + }, + "Failed (attempts remaining)": { + "summary": "Wrong code — the user can retry; request_id here is a one-off UUID", + "value": { + "request_id": "11111111-2222-3333-4444-555555555555", + "status": "Failed", + "message": "The verification code is incorrect. Attempts remaining: 2", + "phone": null, + "vendor_data": "user-1234", + "metadata": null, + "created_at": "2026-06-12T01:24:21.512340+00:00" + } + }, + "Declined (max attempts)": { + "summary": "Third wrong code — verification finalized as Declined", + "value": { + "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", + "status": "Declined", + "message": "The verification code is incorrect. Maximum attempts reached.", + "phone": { + "status": "Declined", + "phone_number_prefix": "+1", + "phone_number": "4155552671", + "full_number": "+14155552671", + "country_code": "US", + "country_name": "United States", + "carrier": { + "name": "AT&T", + "type": "mobile" + }, + "is_disposable": false, + "is_virtual": false, + "verification_method": "whatsapp", + "verification_attempts": 1, + "verified_at": null, + "warnings": [ + { + "feature": "PHONE", + "risk": "VERIFICATION_CODE_ATTEMPTS_EXCEEDED", + "additional_data": null, + "log_type": "error", + "short_description": "Verification code attempts exceeded", + "long_description": "The system detected that the phone number has exceeded the maximum number of verification code attempts." + } + ], + "lifecycle": [ + { + "type": "PHONE_VERIFICATION_MESSAGE_SENT", + "timestamp": "2026-06-12T01:23:39.580554+00:00", + "details": { + "status": "Success", + "reason": null, + "channel": "whatsapp", + "actual_channel": "whatsapp" + }, + "fee": 0.0709 + }, + { + "type": "INVALID_CODE_ENTERED", + "timestamp": "2026-06-12T01:24:01.118220+00:00", + "details": { + "code_tried": "111111", + "status": "Failed" + }, + "fee": 0 + }, + { + "type": "INVALID_CODE_ENTERED", + "timestamp": "2026-06-12T01:24:21.512340+00:00", + "details": { + "code_tried": "222222", + "status": "Failed" + }, + "fee": 0 + }, + { + "type": "INVALID_CODE_ENTERED", + "timestamp": "2026-06-12T01:24:47.311201+00:00", + "details": { + "code_tried": "333333", + "status": "Failed" + }, + "fee": 0 + }, + { + "type": "PHONE_VERIFICATION_DECLINED", + "timestamp": "2026-06-12T01:24:47.384292+00:00", + "details": { + "reason": "VERIFICATION_CODE_ATTEMPTS_EXCEEDED" + }, + "fee": 0 + } + ], + "matches": [] + }, + "vendor_data": "user-1234", + "metadata": null, + "created_at": "2026-06-12T01:24:47.401719+00:00" + } + }, + "Declined (risk policy)": { + "summary": "Correct code, but the line is VoIP and voip_number_action=DECLINE", + "value": { + "request_id": "7f1c9d2e-44a1-4b6e-9a3e-5b8e6f1c2d3a", + "status": "Declined", + "message": "The verification code is correct.", + "phone": { + "status": "Declined", + "phone_number_prefix": "+1", + "phone_number": "5005550006", + "full_number": "+15005550006", + "country_code": "US", + "country_name": "United States", + "carrier": { + "name": "CLEC LLC", + "type": "voip" + }, + "is_disposable": false, + "is_virtual": true, + "verification_method": "sms", + "verification_attempts": 1, + "verified_at": "2026-06-12T01:24:47.311323Z", + "warnings": [ + { + "feature": "PHONE", + "risk": "VOIP_NUMBER_DETECTED", + "additional_data": null, + "log_type": "error", + "short_description": "VoIP number detected", + "long_description": "The system detected that the phone number is a VoIP number, which is not allowed." + } + ], + "lifecycle": [ + { + "type": "PHONE_VERIFICATION_MESSAGE_SENT", + "timestamp": "2026-06-12T01:23:39.580554+00:00", + "details": { + "status": "Success", + "reason": null, + "channel": "whatsapp", + "actual_channel": "sms" + }, + "fee": 0.0709 + }, + { + "type": "VALID_CODE_ENTERED", + "timestamp": "2026-06-12T01:24:47.311201+00:00", + "details": { + "code_tried": "123456", + "status": "Approved" + }, + "fee": 0 + }, + { + "type": "PHONE_VERIFICATION_DECLINED", + "timestamp": "2026-06-12T01:24:47.384292+00:00", + "details": { + "reason": "VOIP_NUMBER_DETECTED" + }, + "fee": 0 + } + ], + "matches": [] + }, + "vendor_data": "user-1234", + "metadata": null, + "created_at": "2026-06-12T01:24:47.401719+00:00" + } + }, + "Expired or Not Found": { + "summary": "No pending verification for this number in the last 5 minutes", + "value": { + "request_id": "69cc35c6-280a-47a6-ab8c-4f32abc02ed0", + "status": "Expired or Not Found", + "message": "No pending phone verification found in the last 5 minutes.", + "vendor_data": null, + "metadata": null, + "created_at": "2026-06-12T01:24:47.311323+00:00" + } + } + } + } + } + }, + "400": { + "description": "Validation error — DRF's field-error envelope: one array of messages per offending field.", + "content": { + "application/json": { + "examples": { + "Invalid code format": { + "summary": "`code` is not 4–8 numeric digits", + "value": { + "code": [ + "Invalid code format. Enter 4 to 8 numeric digits." + ] + } + }, + "Invalid phone number": { + "summary": "`phone_number` is not valid E.164", + "value": { + "phone_number": [ + "Invalid phone number provided." + ] + } + }, + "Invalid action": { + "summary": "Risk actions accept only NO_ACTION or DECLINE", + "value": { + "voip_number_action": [ + "\"REVIEW\" is not a valid choice." + ] + } + } + } + } + } + }, + "403": { + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — this API never returns `401`. Checks are free, so there is no credit check here.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. Additionally, an anti-abuse cap of 4 verification attempts per phone number per hour applies upstream. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. Note: the `X-RateLimit-*`/`Retry-After` headers accompany only the 300/min write-limit 429; the per-number cap 429 carries no rate-limit headers.", + "content": { + "application/json": { + "examples": { + "Per-number cap": { + "summary": "More than 4 verification attempts for the same number in the last hour", + "value": { + "detail": "Maximum verification attempts reached for this phone number. Only 4 authentication attempts are allowed per hour. Try again later or use a different number." + } + }, + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + }, + "502": { + "description": "The phone verification provider is temporarily unavailable - a 5xx response, timeout, or connection failure that survived the built-in retries. Safe to retry; the attempt budget is not consumed.", + "content": { + "application/json": { + "examples": { + "Provider outage": { + "summary": "The provider did not answer the check request", + "value": { + "detail": "Phone verification is temporarily unavailable. Please try again shortly.", + "code": "phone_provider_unavailable" + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string" + } + } + } + } + } + }, + "500": { + "description": "Unexpected failure while verifying the code upstream that is not a known provider outage. Safe to retry; the attempt budget is only consumed by completed checks.", + "content": { + "application/json": { + "examples": { + "Provider error": { + "summary": "Code verification could not be completed", + "value": { + "detail": "Error checking phone verification" + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/email/send/": { + "post": { + "summary": "Send Email Code", + "description": "Send a one-time passcode (OTP) to an email address, then verify it with [`POST /v3/email/check/`](/standalone-apis/email-check).\n\n**How send and check pair up.** Verification state is keyed by your application plus the `email` address — the check call does not take `request_id`. Each new send creates a pending verification that lives for **5 minutes**: call the check with the same address and the code the user received before the window closes. Calling send again for the same address while a verification is pending generates a fresh code for that same verification (`status: \"Retry\"`, same `request_id`); the previous code stops working. At most one retry is attached this way (two sends total); a further send starts a fresh verification with a new `request_id`. The 5-minute window is measured from the first send and is not extended by retries.\n\n**Deliverability pre-check.** Before anything is sent, the address goes through syntax and DNS/MX validation. Addresses that cannot receive mail return `200` with `status: \"Undeliverable\"` and `reason: \"email_can_not_be_delivered\"` — no email is sent, nothing is billed, and the verification is immediately finalized as `Declined` (a follow-up check returns `Expired or Not Found`). A downstream send failure reports the same way.\n\n**Code format and branding.** Codes are 4–8 characters (`options.code_size`, default 6), numeric by default; set `options.alphanumeric_code: true` for uppercase letters and digits (the check comparison is case-insensitive). The email template is localized via `options.locale` (54 supported languages) and can use your application's white-label branding via `options.use_white_label_customization`.\n\n**Billing.** One Email Verification API credit per successful send (`status: \"Success\"`), charged at send time. `Retry` and `Undeliverable` sends are free, and checks are free.\n\n**Session persistence.** Every new verification is persisted as an API-type session: `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the verification appears in the Business Console, and `status.updated` webhooks fire as it progresses.\n\n**Sandbox.** Sandbox API keys skip delivery and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Success` payload with a random `request_id`; no email is sent and nothing is persisted. Use code `123456` on the sandbox check.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", + "operationId": "post_v3email_send", + "x-didit-sdk": { + "resource": "email", + "method": "send_code", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/email/send/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"email\": \"alice@example.com\",\n \"options\": { \"code_size\": 6, \"locale\": \"en\" },\n \"vendor_data\": \"user-1234\"\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/email/send/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n },\n json={\n \"email\": \"alice@example.com\",\n \"options\": {\"code_size\": 6, \"locale\": \"en\"},\n \"vendor_data\": \"user-1234\",\n },\n timeout=15,\n)\nresp.raise_for_status()\nprint(resp.json()) # {request_id, status, reason, vendor_data, metadata}\n# Then verify with POST /v3/email/check/ using the same email" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const res = await fetch('https://verification.didit.me/v3/email/send/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n email: 'alice@example.com',\n options: { code_size: 6, locale: 'en' },\n vendor_data: 'user-1234',\n }),\n});\nif (!res.ok) throw new Error(`Email send failed: ${res.status}`);\nconst data = await res.json();\nconsole.log(data); // { request_id, status, reason, vendor_data, metadata }\n// Then verify with POST /v3/email/check/ using the same email" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Recipient email address. Malformed addresses return `400`; syntactically valid addresses that cannot receive mail (failed DNS/MX validation) return `200` with `status: \"Undeliverable\"`.", + "example": "alice@example.com" + }, + "options": { + "type": "object", + "description": "OTP format, localization, and branding options. All fields are optional.", + "properties": { + "code_size": { + "type": "integer", + "minimum": 4, + "maximum": 8, + "default": 6, + "description": "Number of characters in the OTP (digits by default; uppercase letters and digits when `alphanumeric_code` is `true`)." + }, + "alphanumeric_code": { + "type": "boolean", + "default": false, + "description": "When `true`, the OTP is composed of uppercase letters `A–Z` and digits `0–9` instead of digits only. The `/v3/email/check/` comparison is case-insensitive." + }, + "locale": { + "type": "string", + "maxLength": 5, + "enum": [ + "en", + "ar", + "bn", + "bg", + "bs", + "ca", + "cs", + "da", + "de", + "el", + "es", + "et", + "fa", + "fi", + "fr", + "he", + "hi", + "hr", + "hu", + "hy", + "id", + "it", + "ja", + "ka", + "kk", + "ko", + "ky", + "lt", + "lv", + "cnr", + "mk", + "mn", + "ms", + "nl", + "no", + "pl", + "pt-BR", + "pt", + "ro", + "ru", + "sk", + "sl", + "so", + "sq", + "sr", + "sv", + "th", + "tr", + "uk", + "uz", + "vi", + "zh-CN", + "zh-TW", + "zh" + ], + "description": "Language of the OTP email template. Defaults to `en`. Unsupported values return `400` listing all supported locales.", + "example": "en" + }, + "use_white_label_customization": { + "type": "boolean", + "default": false, + "description": "When `true`, the OTP email uses your application's white-label customization (logo, colors, sender branding) instead of the default Didit template." + } + } + }, + "signals": { + "type": "object", + "description": "Optional device and network signals about the end user, forwarded to the anti-fraud layer to improve detection of abusive or automated traffic. All fields are optional.", + "properties": { + "ip": { + "type": "string", + "description": "IP address of the end user's device. IPv4 or IPv6.", + "example": "192.0.2.1" + }, + "device_id": { + "type": "string", + "maxLength": 255, + "description": "Unique identifier of the end user's device. Android: `ANDROID_ID`; iOS: `identifierForVendor`.", + "example": "8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2" + }, + "device_platform": { + "type": "string", + "enum": [ + "android", + "ios", + "ipados", + "tvos", + "web" + ], + "description": "Platform of the end user's device." + }, + "device_model": { + "type": "string", + "maxLength": 255, + "description": "Model of the end user's device.", + "example": "iPhone17,2" + }, + "os_version": { + "type": "string", + "maxLength": 64, + "description": "Operating-system version of the end user's device.", + "example": "18.0.1" + }, + "app_version": { + "type": "string", + "maxLength": 64, + "description": "Version of your application.", + "example": "1.2.34" + }, + "user_agent": { + "type": "string", + "maxLength": 512, + "description": "User agent of the end user's browser or app.", + "example": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1" + } + } + }, + "vendor_data": { + "type": "string", + "description": "Optional caller-controlled identifier (your internal user id, an email, a UUID, etc.) persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. Use it to correlate Didit's `request_id` with your user record." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Optional free-form JSON object persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console." + } + }, + "required": [ + "email" + ] + }, + "examples": { + "Numeric 6-digit code": { + "summary": "Default — 6-digit numeric code in English", + "value": { + "email": "alice@example.com", + "options": { + "locale": "en" + }, + "vendor_data": "user-1234" + } + }, + "Alphanumeric 8-character code": { + "summary": "Uppercase letters + digits, Spanish template", + "value": { + "email": "bob@example.com", + "options": { + "code_size": 8, + "alphanumeric_code": true, + "locale": "es" + } + } + }, + "White-label branding": { + "summary": "Send the OTP email with your application's white-label customization", + "value": { + "email": "carol@example.com", + "options": { + "locale": "en", + "use_white_label_customization": true + }, + "vendor_data": "user-5678" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Send acknowledged. Inspect `status`: `Success` and `Retry` mean a code is on its way; `Undeliverable` means the address cannot receive mail and the verification is already finalized as `Declined`. `request_id` is the persisted session id (same id on a `Retry`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailVerificationSendResponse" + }, + "examples": { + "Success": { + "summary": "OTP emailed for a new verification (billed)", + "value": { + "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", + "status": "Success", + "reason": null, + "vendor_data": "user-1234", + "metadata": null + } + }, + "Retry": { + "summary": "Second send inside the 5-minute window — fresh code, same request_id, free", + "value": { + "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", + "status": "Retry", + "reason": null, + "vendor_data": "user-1234", + "metadata": null + } + }, + "Undeliverable": { + "summary": "Address failed DNS/MX validation — verification finalized as Declined, nothing billed", + "value": { + "request_id": "cb1a5011-344d-444d-bdce-218904745d7f", + "status": "Undeliverable", + "reason": "email_can_not_be_delivered", + "vendor_data": "user-1234", + "metadata": null + } + } + } + } + } + }, + "400": { + "description": "Validation error — DRF's field-error envelope: one array of messages per offending field, nested under `options` for option errors.", + "content": { + "application/json": { + "examples": { + "Invalid email": { + "summary": "Malformed email address", + "value": { + "email": [ + "Enter a valid email address." + ] + } + }, + "Unsupported locale": { + "summary": "`options.locale` outside the supported set", + "value": { + "options": { + "locale": [ + "Invalid locale. Supported locales are en, ar, bn, bg, bs, ca, cs, da, de, el, es, et, fa, fi, fr, he, hi, hr, hu, hy, id, it, ja, ka, kk, ko, ky, lt, lv, cnr, mk, mn, ms, nl, no, pl, pt-BR, pt, ro, ru, sk, sl, so, sq, sr, sv, th, tr, uk, uz, vi, zh-CN, zh-TW, zh." + ] + } + } + }, + "Code size out of range": { + "summary": "`options.code_size` outside 4–8", + "value": { + "options": { + "code_size": [ + "Ensure this value is less than or equal to 8." + ] + } + } + } + } + } + } + }, + "403": { + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment (`{\"detail\": ...}`) — this API never returns `401`. Also returned before any send when the organization's balance cannot cover one Email Verification API credit (`{\"error\": ...}`).", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Not enough credits": { + "summary": "Organization balance cannot cover the send price", + "value": { + "error": "You don't have enough credits to perform this request." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "content": { + "application/json": { + "examples": { + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + }, + "503": { + "description": "The email provider could not be reached (an outage or a timeout), so no code was sent and nothing was billed or counted as a send attempt. Repeat the same request.", + "content": { + "application/json": { + "examples": { + "Email delivery unavailable": { + "summary": "The email provider failed after one automatic retry", + "value": { + "detail": "Email delivery is temporarily unavailable. Please try again." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/email/check/": { + "post": { + "summary": "Check Email Code", + "description": "Verify the OTP delivered by [`POST /v3/email/send/`](/standalone-apis/email-send) and get the final verification result plus email intelligence: deliverability, breach exposure (`is_breached` and the `breaches` list from a breach-intelligence database), disposable-provider detection, and duplicate usage of the address across your sessions.\n\n**How the check finds the verification.** Matching is by your application plus the `email` address — `request_id` is not an input. The most recent pending verification created within the last **5 minutes** is checked. If there is none (never sent, already finalized, undeliverable at send time, or older than 5 minutes) the endpoint returns `200` with `status: \"Expired or Not Found\"`.\n\n**Attempt budget.** Each verification allows **3 code attempts**. The first two wrong codes return `status: \"Failed\"` with the attempts remaining and `email: null`; the third wrong code finalizes the verification as `Declined` with an `EMAIL_CODE_ATTEMPTS_EXCEEDED` warning and returns the full email report. Codes are compared case-insensitively (relevant for `alphanumeric_code` sends); only the most recently sent code is valid.\n\n**Outcomes.** `Approved` — correct code and no declining risk. `Declined` — terminal: the code was correct but a declining risk matched (a `DECLINE` action below, a blocklisted address, or the address found undeliverable at finalization), or the attempt budget was exhausted. `Failed` — wrong code, attempts remaining. `Expired or Not Found` — nothing to check. On `Approved`/`Declined` the `request_id` equals the send's `request_id` (the session id) and `email` carries the full report (`is_breached`, `breaches`, `is_disposable`, `is_undeliverable`, `warnings`, `lifecycle`, `matches`); on `Failed` and `Expired or Not Found` the `request_id` is a one-off random UUID.\n\n**Risk actions.** `duplicated_email_action`, `breached_email_action`, and `disposable_email_action` decide what happens when the corresponding risk is detected on a correct code: `DECLINE` flips the final status to `Declined`; `NO_ACTION` (default) records the risk in `email.warnings` without affecting the status.\n\n**Billing.** Checks are free — the credit is consumed by the successful send.\n\n**Session persistence.** A finalized check updates the session created by the send (visible in the Business Console, queryable via `GET /v3/session/{sessionId}/decision/`) and fires a `status.updated` webhook.\n\n**Sandbox.** Sandbox API keys skip all processing: any well-formed code (`123456` included) returns a static `Approved` payload with a simplified `email` object (`status`, `email`, `is_breached`, `is_disposable`, `is_undeliverable`); malformed input still returns `400`. Nothing is persisted.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.\n\n**Social footprint.** Set `enable_social` to `true` to add a `email_social` block: the online platforms this address is registered on, with per-category counts and any profile details available. `profiles_registered: 0` means the platforms were checked and the address was found on none of them - a valid, billable answer. The block is absent when the add-on was not requested or the check could not be completed, and it is not billed in that case. Billing adds one `email_social` unit per request on top of this endpoint's own price.", + "operationId": "post_v3email_check", + "x-didit-sdk": { + "resource": "email", + "method": "check_code", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/email/check/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"email\": \"alice@example.com\",\n \"code\": \"123456\",\n \"disposable_email_action\": \"DECLINE\"\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/email/check/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n },\n json={\n \"email\": \"alice@example.com\", # same address as the send call\n \"code\": \"123456\",\n \"disposable_email_action\": \"DECLINE\",\n },\n timeout=15,\n)\nresp.raise_for_status()\nresult = resp.json()\nprint(result[\"status\"]) # Approved / Declined / Failed / Expired or Not Found\nif result[\"status\"] in (\"Approved\", \"Declined\"):\n print(result[\"email\"][\"is_breached\"], result[\"email\"][\"is_disposable\"])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const res = await fetch('https://verification.didit.me/v3/email/check/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n email: 'alice@example.com', // same address as the send call\n code: '123456',\n disposable_email_action: 'DECLINE',\n }),\n});\nconst data = await res.json();\nif (data.status === 'Approved') {\n // full email report is in data.email (breaches, is_disposable, matches, ...)\n}" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "The same email address used in the matching [`POST /v3/email/send/`](/standalone-apis/email-send) call. This is what links the check to the send.", + "example": "alice@example.com" + }, + "code": { + "type": "string", + "maxLength": 10, + "description": "The OTP the end user received: 4–8 digits, or 4–8 letters/digits when `alphanumeric_code: true` was used at send time. Comparison is case-insensitive.", + "example": "123456" + }, + "duplicated_email_action": { + "type": "string", + "enum": [ + "NO_ACTION", + "DECLINE" + ], + "default": "NO_ACTION", + "description": "What to do when the same address was already used and approved by a different user (only previously Approved verifications count) (different `vendor_data`) of your application. `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings` and fills `email.matches`." + }, + "breached_email_action": { + "type": "string", + "enum": [ + "NO_ACTION", + "DECLINE" + ], + "default": "NO_ACTION", + "description": "What to do when the address appears in known data breaches (`email.is_breached`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings`." + }, + "disposable_email_action": { + "type": "string", + "enum": [ + "NO_ACTION", + "DECLINE" + ], + "default": "NO_ACTION", + "description": "What to do when the domain belongs to a disposable/temporary-mail provider (`email.is_disposable`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings`." + }, + "enable_social": { + "type": "boolean", + "default": false, + "description": "When true, also look up the social footprint of the address - the email, social, professional, e-commerce and entertainment platforms it is registered on. Billed as one `email_social` unit per request in addition to this endpoint's own price.", + "example": true + }, + "email_no_social_presence_action": { + "type": "string", + "enum": [ + "NO_ACTION", + "DECLINE" + ], + "default": "NO_ACTION", + "description": "Action when the social footprint check finds the address registered on none of the platforms it covers. Only NO_ACTION and DECLINE are accepted on this endpoint; REVIEW is available on the workflow configuration key of the same name." + } + }, + "required": [ + "email", + "code" + ] + }, + "examples": { + "Default": { + "summary": "Record any risks but never auto-decline", + "value": { + "email": "alice@example.com", + "code": "123456" + } + }, + "Strict risk policy": { + "summary": "Auto-decline disposable and breached inboxes", + "value": { + "email": "alice@example.com", + "code": "123456", + "disposable_email_action": "DECLINE", + "breached_email_action": "DECLINE" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Check completed — wrong codes and missing verifications also return `200`; inspect `status`, not the HTTP code. `email` is populated only on finalized outcomes (`Approved`/`Declined`), `null` on `Failed`, and absent on `Expired or Not Found`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailVerificationCheckResponse" + }, + "examples": { + "Approved": { + "summary": "Correct code — clean address, full email report", + "value": { + "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", + "status": "Approved", + "message": "The verification code is correct.", + "email": { + "status": "Approved", + "email": "alice@example.com", + "is_breached": false, + "breaches": [], + "is_disposable": false, + "is_undeliverable": false, + "verification_attempts": 1, + "verified_at": "2026-06-12T01:24:47.311323Z", + "warnings": [], + "lifecycle": [ + { + "type": "EMAIL_VERIFICATION_MESSAGE_SENT", + "timestamp": "2026-06-12T01:23:39.580554+00:00", + "details": { + "status": "Success", + "reason": null + }, + "fee": 0.03 + }, + { + "type": "VALID_CODE_ENTERED", + "timestamp": "2026-06-12T01:24:47.311201+00:00", + "details": { + "code_tried": "123456", + "status": "Approved" + }, + "fee": 0 + }, + { + "type": "EMAIL_VERIFICATION_APPROVED", + "timestamp": "2026-06-12T01:24:47.384292+00:00", + "details": null, + "fee": 0 + } + ], + "matches": [] + }, + "vendor_data": "user-1234", + "metadata": null, + "created_at": "2026-06-12T01:24:47.401719+00:00" + } + }, + "Approved with breach exposure": { + "summary": "Correct code; breach exposure recorded as a warning (breached_email_action=NO_ACTION)", + "value": { + "request_id": "3a1f2b6c-9d4e-4f7a-8b2c-1e5d6f7a8b9c", + "status": "Approved", + "message": "The verification code is correct.", + "email": { + "status": "Approved", + "email": "bob@example.com", + "is_breached": true, + "breaches": [ + { + "name": "ExampleApp", + "domain": "exampleapp.com", + "breach_date": "2019-03-21", + "breach_emails_count": 763117, + "description": "In March 2019, the social platform ExampleApp suffered a data breach that exposed email addresses and passwords.", + "logo_path": "https:///logos/ExampleApp.png", + "data_classes": [ + "email_addresses", + "passwords" + ], + "is_verified": true + } + ], + "is_disposable": false, + "is_undeliverable": false, + "verification_attempts": 1, + "verified_at": "2026-06-12T01:24:47.311323Z", + "warnings": [ + { + "feature": "EMAIL", + "risk": "BREACHED_EMAIL_DETECTED", + "additional_data": null, + "log_type": "information", + "short_description": "Breached email detected", + "long_description": "This email address was found in one or more known data breaches." + } + ], + "lifecycle": [ + { + "type": "EMAIL_VERIFICATION_MESSAGE_SENT", + "timestamp": "2026-06-12T01:23:39.580554+00:00", + "details": { + "status": "Success", + "reason": null + }, + "fee": 0.03 + }, + { + "type": "VALID_CODE_ENTERED", + "timestamp": "2026-06-12T01:24:47.311201+00:00", + "details": { + "code_tried": "835126", + "status": "Approved" + }, + "fee": 0 + }, + { + "type": "EMAIL_VERIFICATION_APPROVED", + "timestamp": "2026-06-12T01:24:47.384292+00:00", + "details": null, + "fee": 0 + } + ], + "matches": [] + }, + "vendor_data": "user-5678", + "metadata": null, + "created_at": "2026-06-12T01:24:47.401719+00:00" + } + }, + "Failed (attempts remaining)": { + "summary": "Wrong code — the user can retry; request_id here is a one-off UUID", + "value": { + "request_id": "11111111-2222-3333-4444-555555555555", + "status": "Failed", + "message": "The verification code is incorrect. Attempts remaining: 2", + "email": null, + "vendor_data": "user-1234", + "metadata": null, + "created_at": "2026-06-12T01:24:21.512340+00:00" + } + }, + "Declined (risk policy)": { + "summary": "Correct code, but the address is disposable and disposable_email_action=DECLINE", + "value": { + "request_id": "8c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f", + "status": "Declined", + "message": "The verification code is correct.", + "email": { + "status": "Declined", + "email": "tempuser42@mailinator.com", + "is_breached": false, + "breaches": [], + "is_disposable": true, + "is_undeliverable": false, + "verification_attempts": 1, + "verified_at": "2026-06-12T01:24:47.311323Z", + "warnings": [ + { + "feature": "EMAIL", + "risk": "DISPOSABLE_EMAIL_DETECTED", + "additional_data": null, + "log_type": "error", + "short_description": "Disposable email detected", + "long_description": "The system detected that the email is disposable, which is not allowed." + } + ], + "lifecycle": [ + { + "type": "EMAIL_VERIFICATION_MESSAGE_SENT", + "timestamp": "2026-06-12T01:23:39.580554+00:00", + "details": { + "status": "Success", + "reason": null + }, + "fee": 0.03 + }, + { + "type": "VALID_CODE_ENTERED", + "timestamp": "2026-06-12T01:24:47.311201+00:00", + "details": { + "code_tried": "123456", + "status": "Approved" + }, + "fee": 0 + }, + { + "type": "EMAIL_VERIFICATION_DECLINED", + "timestamp": "2026-06-12T01:24:47.384292+00:00", + "details": { + "reason": "DISPOSABLE_EMAIL_DETECTED" + }, + "fee": 0 + } + ], + "matches": [] + }, + "vendor_data": "user-1234", + "metadata": null, + "created_at": "2026-06-12T01:24:47.401719+00:00" + } + }, + "Expired or Not Found": { + "summary": "No pending verification for this address in the last 5 minutes", + "value": { + "request_id": "88808a77-06e9-4cb0-85f5-9c052ddfd987", + "status": "Expired or Not Found", + "message": "No pending email verification found in the last 5 minutes.", + "vendor_data": null, + "metadata": null, + "created_at": "2026-06-12T01:24:59.887904+00:00" + } + } + } + } + } + }, + "400": { + "description": "Validation error — DRF's field-error envelope: one array of messages per offending field.", + "content": { + "application/json": { + "examples": { + "Invalid email": { + "summary": "Malformed email address", + "value": { + "email": [ + "Enter a valid email address." + ] + } + }, + "Missing code": { + "summary": "`code` not included in the body", + "value": { + "code": [ + "This field is required." + ] + } + } + } + } + } + }, + "403": { + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — this API never returns `401`. Checks are free, so there is no credit check here.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "content": { + "application/json": { + "examples": { + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/workflows/": { + "get": { + "summary": "List workflows", + "description": "List your workflows, newest first, in a limit/offset pagination envelope (`{count, next, previous, results}`, default page size 50). Each row is one version; `workflow_id` is the stable group ID and new sessions use the latest `published` version.", + "operationId": "list_workflows", + "x-didit-sdk": { + "resource": "workflows", + "method": "list", + "auth": "api_key", + "server": true + }, + "tags": [ + "Workflows" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 50 + }, + "description": "Page size (number of workflows per page)." + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 0 + }, + "description": "Number of rows to skip before the first returned workflow." + } + ], + "responses": { + "200": { + "description": "One page of workflows ordered by `created_at` descending, wrapped in the pagination envelope. Each item in `results` is a `WorkflowListItem`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "description": "Total number of workflow versions for the application." + }, + "next": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL of the next page, or `null` on the last page." + }, + "previous": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL of the previous page, or `null` on the first page." + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkflowListItem" + } + } + } + }, + "examples": { + "Two workflows": { + "summary": "A default KYC workflow and a draft KYC + AML workflow", + "value": { + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "uuid": "a1b2c3d4-5678-90ab-cdef-111111111111", + "workflow_id": "a1b2c3d4-5678-90ab-cdef-111111111111", + "workflow_label": "Standard KYC", + "workflow_type": "kyc", + "is_default": true, + "is_archived": false, + "is_white_label_enabled": false, + "total_price": 0.15, + "min_price": 0.15, + "max_price": 0.15, + "features": "OCR + LIVENESS + FACE_MATCH", + "is_simple_workflow": true, + "is_editable": false, + "workflow_url": "https://verify.didit.me/u/_yljPOx8RWG3UAz5ZRQGVQ", + "max_retry_attempts": 3, + "retry_window_days": 7, + "session_expiration_time": 604800, + "version": 2, + "status": "published", + "has_draft": false, + "created_by": { + "name": "Alex Mateo", + "email": "alex@example.com", + "type": "user" + }, + "updated_at": "2026-04-12T10:30:00Z" + }, + { + "uuid": "b2c3d4e5-6789-01bc-defa-222222222222", + "workflow_id": "b2c3d4e5-6789-01bc-defa-222222222222", + "workflow_label": "Full Verification + AML", + "workflow_type": "kyc", + "is_default": false, + "is_archived": false, + "is_white_label_enabled": false, + "total_price": 0.25, + "min_price": 0.25, + "max_price": 0.25, + "features": "OCR + LIVENESS + FACE_MATCH + AML", + "is_simple_workflow": true, + "is_editable": true, + "workflow_url": "https://verify.didit.me/u/Aq3kPOx8RWG3UAz5ZRQGVQ", + "max_retry_attempts": 3, + "retry_window_days": 7, + "session_expiration_time": 604800, + "version": 1, + "status": "draft", + "has_draft": true, + "created_by": null, + "updated_at": "2026-04-15T18:05:21Z" + } + ] + } + } + } + } + } + }, + "403": { + "description": "The API key is missing, malformed, expired, or belongs to a different application. Body: `{\"detail\": \"You do not have permission to perform this action.\"}`. Refresh the token via the [Auth API](/auth-api/get-credentials) and retry.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Invalid token": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. Retry with exponential backoff and honour any `Retry-After` header." + }, + "500": { + "description": "Unexpected server error while building the list. The request can be safely retried." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X GET 'https://verification.didit.me/v3/workflows/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Accept: application/json'" + }, + { + "lang": "Python", + "label": "Python (requests)", + "source": "import requests\n\nresp = requests.get(\n \"https://verification.didit.me/v3/workflows/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n timeout=10,\n)\nresp.raise_for_status()\npage = resp.json()\nfor workflow in page[\"results\"]:\n print(workflow[\"uuid\"], workflow[\"workflow_label\"], workflow[\"status\"])" + }, + { + "lang": "JavaScript", + "label": "Node.js (fetch)", + "source": "const res = await fetch(\"https://verification.didit.me/v3/workflows/\", {\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n});\nif (!res.ok) throw new Error(`Didit ${res.status}`);\nconst page = await res.json();\npage.results.forEach((w) => console.log(w.uuid, w.workflow_label, w.status));" + } + ] + }, + "post": { + "summary": "Create workflow", + "description": "Create a simple workflow. Send `features` in execution order (ordering rules apply); defaults to `published` v1. The body accepts ONLY the fields listed in `SimpleWorkflowRequest` — any other key (including `workflow_type`) is rejected with a 400. Not idempotent — each call creates a new workflow. Maximum 50 workflows per application.", + "operationId": "create_workflow", + "x-didit-sdk": { + "resource": "workflows", + "method": "create", + "auth": "api_key", + "server": true + }, + "tags": [ + "Workflows" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SimpleWorkflowRequest" + }, + "examples": { + "Simple KYC": { + "summary": "Linear ID, liveness, and face match workflow", + "value": { + "workflow_label": "Standard KYC", + "features": [ + { + "feature": "OCR", + "config": { + "duplicated_user_action": "review" + } + }, + { + "feature": "LIVENESS", + "config": { + "face_liveness_method": "passive" + } + }, + { + "feature": "FACE_MATCH", + "config": { + "face_match_score_decline_threshold": 40, + "face_match_score_review_threshold": 60 + } + } + ] + } + }, + "KYC + AML": { + "summary": "Run AML after ID verification", + "value": { + "workflow_label": "KYC with AML", + "features": [ + { + "feature": "OCR" + }, + { + "feature": "AML", + "config": { + "aml_score_review_threshold": 70, + "aml_score_approve_threshold": 30 + } + } + ] + } + }, + "Questionnaire workflow": { + "summary": "Run a questionnaire created with POST /v3/questionnaires/", + "value": { + "workflow_label": "Source of Funds", + "features": [ + { + "feature": "QUESTIONNAIRE", + "config": { + "questionnaire_uuid": "11111111-2222-3333-4444-555555555555", + "review_questionnaire_manually": true + } + } + ] + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Workflow created. The response body is the FULL workflow configuration — the same raw `VerificationSettings` serialization returned by `GET /v3/workflows/{settings_uuid}/` (~190 fields: every feature toggle, threshold, action and the generated `workflow_graph`), not the compact list item. Store the `uuid` (also exposed as `workflow_id`) so you can reference it from later session-creation calls. Note: `features` is a `\" + \"`-joined string and prices are JSON numbers.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowDetail" + }, + "examples": { + "Created (abbreviated)": { + "summary": "Key fields of the full configuration object", + "value": { + "uuid": "c3d4e5f6-7890-12cd-ef34-333333333333", + "workflow_id": "c3d4e5f6-7890-12cd-ef34-333333333333", + "workflow_label": "Standard KYC", + "workflow_type": null, + "is_default": false, + "is_archived": false, + "total_price": 0.15, + "min_price": 0.15, + "max_price": 0.15, + "features": "OCR + LIVENESS + FACE_MATCH", + "is_simple_workflow": false, + "is_editable": false, + "workflow_url": "https://verify.didit.me/u/w9TkPOx8RWG3UAz5ZRQGVQ", + "workflow_graph": { + "start_node": "ocr", + "nodes": "... one node per feature plus a final status node ..." + }, + "max_retry_attempts": 3, + "retry_window_days": 7, + "session_expiration_time": 604800, + "version": 1, + "status": "published", + "published_at": "2026-05-17T10:30:00Z", + "created_at": "2026-05-17T10:30:00Z", + "updated_at": "2026-05-17T10:30:00Z" + } + } + } + } + } + }, + "400": { + "description": "Validation error. Common causes: a top-level key outside the accepted whitelist (e.g. `workflow_type`), an unknown `feature` value, a feature placed before its dependency (e.g., `FACE_MATCH` before `OCR`), a passive feature (`AML`, `IP_ANALYSIS`, `DATABASE_VALIDATION`, `KYB_KEY_PEOPLE`) used as the first step, a `QUESTIONNAIRE` feature whose `config.questionnaire_uuid` does not exist for this application, or an out-of-range threshold. Body is a DRF validation error object — keys are field names, values are arrays of human-readable messages.", + "content": { + "application/json": { + "examples": { + "Missing feature": { + "value": { + "features": [ + "At least one feature is required." + ] + } + }, + "Unknown top-level field": { + "value": { + "workflow_type": [ + "The v3 workflow API only accepts `workflow_label`, `is_default`, `status`, `features`, `is_white_label_enabled`, `is_desktop_allowed`, `max_retry_attempts`, `retry_window_days`, `session_expiration_time`, `face_liveness_max_attempts`, `face_match_max_attempts`." + ] + } + }, + "Unknown feature": { + "value": { + "features": [ + "Invalid feature 'FOO'. Allowed values: AGE_ESTIMATION, AML, DATABASE_VALIDATION, EMAIL_VERIFICATION, FACE_MATCH, IP_ANALYSIS, KYB_DOCUMENTS, KYB_KEY_PEOPLE, KYB_REGISTRY, LIVENESS, NFC, OCR, PHONE_VERIFICATION, PROOF_OF_ADDRESS, QUESTIONNAIRE." + ] + } + }, + "Ordering": { + "value": { + "features": [ + "Feature 'FACE_MATCH' must appear after one of: OCR." + ] + } + }, + "Questionnaire not found": { + "value": { + "features": "Questionnaire '11111111-2222-3333-4444-555555555555' was not found for this application. Create the questionnaire first, then use its questionnaire_id." + } + } + } + } + } + }, + "403": { + "description": "The API key is missing, malformed, expired, or scoped to a different application. Refresh via the [Auth API](/auth-api/get-credentials) and retry.", + "content": { + "application/json": { + "examples": { + "Invalid token": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "415": { + "description": "Unsupported media type. Set `Content-Type: application/json` and send a JSON body." + }, + "429": { + "description": "Rate limit exceeded. Retry with exponential backoff." + }, + "500": { + "description": "Unexpected server error while creating the workflow. Safe to retry." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X POST 'https://verification.didit.me/v3/workflows/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"workflow_label\": \"Standard KYC\",\n \"features\": [\n {\"feature\": \"OCR\"},\n {\"feature\": \"LIVENESS\", \"config\": {\"face_liveness_method\": \"passive\"}},\n {\"feature\": \"FACE_MATCH\", \"config\": {\"face_match_score_decline_threshold\": 40, \"face_match_score_review_threshold\": 60}}\n ]\n }'" + }, + { + "lang": "Python", + "label": "Python (requests)", + "source": "import requests\n\npayload = {\n \"workflow_label\": \"Standard KYC\",\n \"features\": [\n {\"feature\": \"OCR\"},\n {\"feature\": \"LIVENESS\", \"config\": {\"face_liveness_method\": \"passive\"}},\n {\"feature\": \"FACE_MATCH\"},\n ],\n}\nresp = requests.post(\n \"https://verification.didit.me/v3/workflows/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n json=payload,\n timeout=10,\n)\nresp.raise_for_status()\nworkflow = resp.json()\nprint(\"Workflow id:\", workflow[\"uuid\"])" + }, + { + "lang": "JavaScript", + "label": "Node.js (fetch)", + "source": "const res = await fetch(\"https://verification.didit.me/v3/workflows/\", {\n method: \"POST\",\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n workflow_label: \"Standard KYC\",\n features: [\n { feature: \"OCR\" },\n { feature: \"LIVENESS\", config: { face_liveness_method: \"passive\" } },\n { feature: \"FACE_MATCH\" },\n ],\n }),\n});\nif (!res.ok) throw new Error(`Didit ${res.status}: ${await res.text()}`);\nconst workflow = await res.json();\nconsole.log(\"Workflow id:\", workflow.uuid);" + } + ] + } + }, + "/v3/workflows/{settings_uuid}/": { + "get": { + "summary": "Get workflow", + "description": "Fetch full configuration for one workflow version by `settings_uuid`: feature toggles, thresholds, allowed documents, graph, retry policy, callbacks, and pricing.", + "operationId": "get_workflow", + "x-didit-sdk": { + "resource": "workflows", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "tags": [ + "Workflows" + ], + "parameters": [ + { + "name": "settings_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Per-version UUID of the workflow (the `uuid` value returned by the list endpoint). Each version of a workflow has its own `settings_uuid`." + } + ], + "responses": { + "200": { + "description": "Full workflow configuration. The shape is the raw `VerificationSettings` serialization (~190 fields: every feature toggle, threshold, warning action, allowed-documents config, the `workflow_graph`, retry policy, callbacks and pricing). The example below is abbreviated. Notes: `features` is a `\" + \"`-joined string (e.g. `\"OCR + LIVENESS\"`), prices are JSON numbers, and `is_editable` is `true` only for `draft` versions.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowDetail" + }, + "examples": { + "KYC workflow with face match (abbreviated)": { + "value": { + "uuid": "a1b2c3d4-5678-90ab-cdef-111111111111", + "workflow_id": "a1b2c3d4-5678-90ab-cdef-111111111111", + "workflow_label": "Standard KYC", + "workflow_type": "kyc", + "is_default": true, + "is_archived": false, + "is_liveness_enabled": true, + "face_liveness_method": "passive", + "face_liveness_score_decline_threshold": 50, + "face_liveness_score_review_threshold": 70, + "is_face_match_enabled": true, + "face_match_score_decline_threshold": 40, + "face_match_score_review_threshold": 60, + "is_aml_enabled": false, + "is_phone_verification_enabled": false, + "is_email_verification_enabled": false, + "is_database_validation_enabled": false, + "is_ip_analysis_enabled": false, + "is_nfc_enabled": false, + "duplicated_user_action": "no_action", + "documents_allowed": { + "ESP": { + "P": { + "sides": 1, + "enabled": 1, + "subtypes": [ + "EPASSPORT", + "PASSPORT_GENERIC" + ], + "preferred_characters": "latin", + "expiration_check_mode": "strict" + } + } + }, + "max_retry_attempts": 3, + "retry_window_days": 7, + "session_expiration_time": 604800, + "version": 2, + "status": "published", + "published_at": "2026-04-12T10:30:00Z", + "is_simple_workflow": false, + "is_editable": false, + "workflow_url": "https://verify.didit.me/u/_yljPOx8RWG3UAz5ZRQGVQ", + "total_price": 0.15, + "min_price": 0.15, + "max_price": 0.15, + "features": "OCR + LIVENESS + FACE_MATCH", + "created_at": "2025-01-15T10:30:00Z", + "updated_at": "2026-04-12T10:30:00Z" + } + } + } + } + } + }, + "403": { + "description": "API key is missing, malformed, expired, or scoped to a different application.", + "content": { + "application/json": { + "examples": { + "Invalid token": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No workflow with this `settings_uuid` exists for the authenticated application. Either the UUID is wrong or it belongs to another application — Didit deliberately does not distinguish the two cases to avoid leaking the existence of cross-application records.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. Retry with exponential backoff." + }, + "500": { + "description": "Unexpected server error. Safe to retry." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X GET 'https://verification.didit.me/v3/workflows/a1b2c3d4-5678-90ab-cdef-111111111111/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "Python", + "label": "Python (requests)", + "source": "import requests\n\nresp = requests.get(\n f\"https://verification.didit.me/v3/workflows/{settings_uuid}/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n timeout=10,\n)\nresp.raise_for_status()\nworkflow = resp.json()\nprint(workflow[\"workflow_label\"], workflow[\"status\"])" + }, + { + "lang": "JavaScript", + "label": "Node.js (fetch)", + "source": "const res = await fetch(`https://verification.didit.me/v3/workflows/${settingsUuid}/`, {\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n});\nif (!res.ok) throw new Error(`Didit ${res.status}`);\nconst workflow = await res.json();\nconsole.log(workflow.workflow_label, workflow.status);" + } + ] + }, + "patch": { + "summary": "Update workflow", + "description": "Update a workflow. Versioning depends on the `status` field you send and the current state of the version: (1) no `status` on a **published** version → the published version is updated in place (same `uuid`, same `version`); (2) no `status` on a **draft** → the draft is updated and immediately published; (3) `status: \"published\"` on a **published** version → a NEW published version is created under the same `workflow_id` (new `uuid`, `version` + 1) and returned, and any existing draft in the group is deleted; (4) `status: \"draft\"` keeps a draft as a draft (autosave) but is rejected with 400 on a published version (`\"Cannot revert a published version to draft.\"`). The body accepts the same strict field whitelist as create — unknown keys are rejected with 400.", + "operationId": "update_workflow", + "x-didit-sdk": { + "resource": "workflows", + "method": "update", + "auth": "api_key", + "server": true + }, + "tags": [ + "Workflows" + ], + "parameters": [ + { + "name": "settings_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Per-version UUID of the workflow to update." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SimpleWorkflowUpdateRequest" + }, + "examples": { + "Replace features": { + "summary": "Send the full desired feature order", + "value": { + "features": [ + { + "feature": "OCR" + }, + { + "feature": "AML" + } + ] + } + }, + "Rename": { + "summary": "Rename a workflow", + "value": { + "workflow_label": "Premium KYC" + } + }, + "Set as default": { + "summary": "Make this workflow the application's default", + "value": { + "is_default": true + } + }, + "Update config": { + "summary": "Change feature configuration", + "value": { + "features": [ + { + "feature": "OCR" + }, + { + "feature": "LIVENESS", + "config": { + "face_liveness_method": "passive" + } + } + ] + } + }, + "Publish a draft": { + "summary": "Promote a draft to published", + "value": { + "status": "published" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Workflow updated. The body is the FULL configuration of the resulting version (same raw `VerificationSettings` shape as `GET /v3/workflows/{settings_uuid}/`). When the update created a new version (`status: \"published\"` sent on a published row) the returned `uuid` and `version` differ from the ones you patched — use the returned `uuid` for subsequent calls.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowDetail" + } + } + } + }, + "400": { + "description": "Validation error — a top-level key outside the accepted whitelist, an unknown feature, invalid ordering, out-of-range threshold, referencing a non-existent questionnaire, or `status: \"draft\"` sent for a published version (`\"Cannot revert a published version to draft.\"`). The response body is a DRF validation error keyed by field name." + }, + "403": { + "description": "API key missing or invalid.", + "content": { + "application/json": { + "examples": { + "Invalid token": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No workflow with this `settings_uuid` exists for the authenticated application." + }, + "415": { + "description": "Unsupported media type. Set `Content-Type: application/json`." + }, + "429": { + "description": "Rate limit exceeded. Retry with exponential backoff." + }, + "500": { + "description": "Unexpected server error. Safe to retry." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X PATCH 'https://verification.didit.me/v3/workflows/a1b2c3d4-5678-90ab-cdef-111111111111/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"workflow_label\": \"Premium KYC\"}'" + }, + { + "lang": "Python", + "label": "Python (requests)", + "source": "import requests\n\nresp = requests.patch(\n f\"https://verification.didit.me/v3/workflows/{settings_uuid}/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n json={\"workflow_label\": \"Premium KYC\"},\n timeout=10,\n)\nresp.raise_for_status()\nprint(resp.json()[\"workflow_label\"])" + }, + { + "lang": "JavaScript", + "label": "Node.js (fetch)", + "source": "const res = await fetch(`https://verification.didit.me/v3/workflows/${settingsUuid}/`, {\n method: \"PATCH\",\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ workflow_label: \"Premium KYC\" }),\n});\nif (!res.ok) throw new Error(`Didit ${res.status}`);\nconst workflow = await res.json();\nconsole.log(workflow.workflow_label);" + } + ] + }, + "delete": { + "summary": "Delete workflow", + "description": "Permanently delete one workflow version (no undo). Existing sessions keep their verification results but their workflow reference is cleared; other versions in the same `workflow_id` group are not deleted.", + "operationId": "delete_workflow", + "x-didit-sdk": { + "resource": "workflows", + "method": "delete", + "auth": "api_key", + "server": true + }, + "tags": [ + "Workflows" + ], + "parameters": [ + { + "name": "settings_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Per-version UUID of the workflow to delete." + } + ], + "responses": { + "204": { + "description": "Workflow deleted. The response body is empty." + }, + "403": { + "description": "API key missing or invalid.", + "content": { + "application/json": { + "examples": { + "Invalid token": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No workflow with this `settings_uuid` exists for the authenticated application.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. Retry with exponential backoff." + }, + "500": { + "description": "Unexpected server error. Safe to retry." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X DELETE 'https://verification.didit.me/v3/workflows/a1b2c3d4-5678-90ab-cdef-111111111111/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "Python", + "label": "Python (requests)", + "source": "import requests\n\nresp = requests.delete(\n f\"https://verification.didit.me/v3/workflows/{settings_uuid}/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n timeout=10,\n)\nresp.raise_for_status() # raises on 4xx/5xx; 204 is success" + }, + { + "lang": "JavaScript", + "label": "Node.js (fetch)", + "source": "const res = await fetch(`https://verification.didit.me/v3/workflows/${settingsUuid}/`, {\n method: \"DELETE\",\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n});\nif (res.status !== 204) throw new Error(`Didit ${res.status}`);" + } + ] + } + }, + "/v3/questionnaires/": { + "get": { + "summary": "List questionnaires", + "description": "List your questionnaires, newest first, in a limit/offset pagination envelope (`{count, next, previous, results}`, default page size 50). Each row is one version; `questionnaire_group_id` is the stable group ID resolved to the latest `published` version.", + "operationId": "list_questionnaires", + "x-didit-sdk": { + "resource": "questionnaires", + "method": "list", + "auth": "api_key", + "server": true + }, + "tags": [ + "Questionnaires" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 50 + }, + "description": "Page size (number of questionnaires per page)." + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 0 + }, + "description": "Number of rows to skip before the first returned questionnaire." + } + ], + "responses": { + "200": { + "description": "One page of questionnaires ordered by `created_at` descending, wrapped in the pagination envelope. Each item in `results` is a `QuestionnaireListItem`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "description": "Total number of questionnaire versions for the application." + }, + "next": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL of the next page, or `null` on the last page." + }, + "previous": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL of the previous page, or `null` on the first page." + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionnaireListItem" + } + } + } + }, + "examples": { + "One questionnaire": { + "summary": "A simple bilingual questionnaire", + "value": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "uuid": "11111111-2222-3333-4444-555555555555", + "title": "KYC Questionnaire", + "description": "Additional verification questions", + "languages": [ + "en", + "es" + ], + "default_language": "en", + "is_active": true, + "is_simple_questionnaire": true, + "created_at": "2026-04-23T09:55:00Z", + "updated_at": "2026-04-23T10:00:00Z", + "question_types": [ + "SHORT_TEXT", + "MULTIPLE_CHOICE", + "FILE_UPLOAD" + ], + "workflow_names": [], + "questionnaire_group_id": "11111111-2222-3333-4444-555555555555", + "version": 1, + "status": "published", + "published_at": "2026-04-23T10:00:00Z", + "is_editable": false + } + ] + } + } + } + } + } + }, + "403": { + "description": "API key missing, malformed, expired, or scoped to a different application.", + "content": { + "application/json": { + "examples": { + "Invalid token": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. Retry with exponential backoff." + }, + "500": { + "description": "Unexpected server error while building the list. Safe to retry." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X GET 'https://verification.didit.me/v3/questionnaires/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "Python", + "label": "Python (requests)", + "source": "import requests\n\nresp = requests.get(\n \"https://verification.didit.me/v3/questionnaires/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n timeout=10,\n)\nresp.raise_for_status()\npage = resp.json()\nfor q in page[\"results\"]:\n print(q[\"uuid\"], q[\"title\"], q[\"status\"])" + }, + { + "lang": "JavaScript", + "label": "Node.js (fetch)", + "source": "const res = await fetch(\"https://verification.didit.me/v3/questionnaires/\", {\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n});\nif (!res.ok) throw new Error(`Didit ${res.status}`);\nconst page = await res.json();\npage.results.forEach((q) => console.log(q.uuid, q.title, q.status));" + } + ] + }, + "post": { + "summary": "Create questionnaire", + "description": "Create a simple linear questionnaire. Send `form_elements` in display order; branching and repeatable groups require the Console. Defaults to `published` v1.", + "operationId": "create_questionnaire", + "x-didit-sdk": { + "resource": "questionnaires", + "method": "create", + "auth": "api_key", + "server": true + }, + "tags": [ + "Questionnaires" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SimpleQuestionnaireRequest" + }, + "examples": { + "Simple": { + "summary": "Basic questionnaire", + "value": { + "title": "Customer Onboarding", + "description": "Additional information needed for verification", + "default_language": "en", + "languages": [ + "en" + ], + "is_active": true, + "form_elements": [ + { + "id": "occupation", + "element_type": "short_text", + "label": { + "en": "What is your occupation?" + }, + "is_required": true, + "placeholder": { + "en": "Software engineer" + } + }, + { + "id": "source_of_funds", + "element_type": "multiple_choice", + "label": { + "en": "Source of funds" + }, + "is_required": true, + "options": [ + { + "value": "employment", + "label": { + "en": "Employment" + } + }, + { + "value": "business", + "label": { + "en": "Business" + } + }, + { + "value": "investments", + "label": { + "en": "Investments" + } + }, + { + "value": "other", + "label": { + "en": "Other" + } + } + ] + } + ] + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Questionnaire created. The response includes the `questionnaire_id` — store it to reference this questionnaire from workflows or future update/delete calls.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QuestionnaireDetail" + }, + "examples": { + "Created": { + "value": { + "questionnaire_id": "11111111-2222-3333-4444-555555555555", + "title": "Customer Onboarding", + "description": "Additional information needed for verification", + "languages": [ + "en" + ], + "default_language": "en", + "is_active": true, + "is_simple_questionnaire": true, + "graph": { + "start_node": "occupation", + "nodes": { + "occupation": { + "element_type": "SHORT_TEXT", + "is_required": true, + "title": { + "en": "What is your occupation?" + }, + "next": "source_of_funds" + }, + "source_of_funds": { + "element_type": "MULTIPLE_CHOICE", + "is_required": true, + "title": { + "en": "Source of funds" + }, + "choices": [ + { + "value": "employment", + "label": { + "en": "Employment" + } }, - "portrait": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "Short-lived presigned URL to the portrait the wallet shared, where it shares one." + { + "value": "business", + "label": { + "en": "Business" + } + } + ], + "next": null + } + } + }, + "sections": [], + "questionnaire_group_id": "11111111-2222-3333-4444-555555555555", + "version": 1, + "status": "published", + "published_at": "2026-04-23T10:00:00Z", + "is_editable": false + } + } + } + } + } + }, + "400": { + "description": "Validation error. Common causes: an unknown `element_type`, a translatable field missing a translation for one of `languages`, `languages` missing `en`, `default_language` not in `languages`, an option without a `label`, a question that has `branches`/`required_if`/`repeatable_config` (not supported by this endpoint), or sending a raw `graph` instead of `form_elements`. The body is a DRF validation error keyed by field name.", + "content": { + "application/json": { + "examples": { + "Graph not accepted": { + "value": { + "graph": "The v3 questionnaire API only accepts simple `form_elements`, not `graph`." + } + }, + "Missing label": { + "value": { + "form_elements": [ + { + "label": [ + "This field is required." + ] + } + ] + } + } + } + } + } + }, + "403": { + "description": "API key missing, malformed, expired, or scoped to a different application.", + "content": { + "application/json": { + "examples": { + "Invalid token": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "415": { + "description": "Unsupported media type. Set `Content-Type: application/json` and send a JSON body." + }, + "429": { + "description": "Rate limit exceeded. Retry with exponential backoff." + }, + "500": { + "description": "Unexpected server error while creating the questionnaire. Safe to retry." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X POST 'https://verification.didit.me/v3/questionnaires/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"title\": \"Customer Onboarding\",\n \"default_language\": \"en\",\n \"languages\": [\"en\"],\n \"form_elements\": [\n {\n \"id\": \"occupation\",\n \"element_type\": \"short_text\",\n \"label\": {\"en\": \"What is your occupation?\"},\n \"is_required\": true\n }\n ]\n }'" + }, + { + "lang": "Python", + "label": "Python (requests)", + "source": "import requests\n\npayload = {\n \"title\": \"Customer Onboarding\",\n \"default_language\": \"en\",\n \"languages\": [\"en\"],\n \"form_elements\": [\n {\n \"id\": \"occupation\",\n \"element_type\": \"short_text\",\n \"label\": {\"en\": \"What is your occupation?\"},\n \"is_required\": True,\n },\n ],\n}\nresp = requests.post(\n \"https://verification.didit.me/v3/questionnaires/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n json=payload,\n timeout=10,\n)\nresp.raise_for_status()\nquestionnaire = resp.json()\nprint(\"Questionnaire id:\", questionnaire[\"questionnaire_id\"])" + }, + { + "lang": "JavaScript", + "label": "Node.js (fetch)", + "source": "const res = await fetch(\"https://verification.didit.me/v3/questionnaires/\", {\n method: \"POST\",\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n title: \"Customer Onboarding\",\n default_language: \"en\",\n languages: [\"en\"],\n form_elements: [\n {\n id: \"occupation\",\n element_type: \"short_text\",\n label: { en: \"What is your occupation?\" },\n is_required: true,\n },\n ],\n }),\n});\nif (!res.ok) throw new Error(`Didit ${res.status}: ${await res.text()}`);\nconst questionnaire = await res.json();\nconsole.log(\"Questionnaire id:\", questionnaire.questionnaire_id);" + } + ] + } + }, + "/v3/questionnaires/{questionnaire_uuid}/": { + "get": { + "summary": "Get questionnaire", + "description": "Fetch full questionnaire detail for one version by `questionnaire_uuid`: title, `languages`, canonical `graph`, derived `sections`, and version metadata.", + "operationId": "get_questionnaire", + "x-didit-sdk": { + "resource": "questionnaires", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "tags": [ + "Questionnaires" + ], + "parameters": [ + { + "name": "questionnaire_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Per-version UUID of the questionnaire (the `uuid` value returned by the list endpoint, also returned as `questionnaire_id` on detail endpoints)." + } + ], + "responses": { + "200": { + "description": "Full questionnaire detail including the canonical `graph`, the derived `sections` view, version metadata, and `is_editable`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QuestionnaireDetail" + }, + "examples": { + "Published questionnaire": { + "value": { + "questionnaire_id": "11111111-2222-3333-4444-555555555555", + "title": "Customer Onboarding", + "description": "Additional information needed for verification", + "languages": [ + "en" + ], + "default_language": "en", + "is_active": true, + "is_simple_questionnaire": true, + "graph": { + "start_node": "occupation", + "nodes": { + "occupation": { + "element_type": "SHORT_TEXT", + "is_required": true, + "title": { + "en": "What is your occupation?" + }, + "next": "source_of_funds" + }, + "source_of_funds": { + "element_type": "MULTIPLE_CHOICE", + "is_required": true, + "title": { + "en": "Source of funds" + }, + "choices": [ + { + "value": "employment", + "label": { + "en": "Employment" + } }, - "face_match_score": { - "type": "number", - "nullable": true, - "description": "Selfie against the wallet portrait, 0-100, where the wallet shares one." + { + "value": "business", + "label": { + "en": "Business" + } + } + ], + "next": null + } + } + }, + "sections": [ + { + "title": {}, + "description": {}, + "items": [ + { + "uuid": "11111111-2222-3333-4444-666666666666", + "value": "occupation", + "element_type": "SHORT_TEXT", + "is_required": true, + "title": { + "en": "What is your occupation?" } } + ] + } + ], + "questionnaire_group_id": "11111111-2222-3333-4444-555555555555", + "version": 1, + "status": "published", + "published_at": "2026-04-23T10:00:00Z", + "is_editable": false + } + } + } + } + } + }, + "403": { + "description": "API key missing, malformed, expired, or scoped to a different application.", + "content": { + "application/json": { + "examples": { + "Invalid token": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No questionnaire with this `questionnaire_uuid` exists for the authenticated application. Either the UUID is wrong or it belongs to another application — the API deliberately does not distinguish the two cases.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. Retry with exponential backoff." + }, + "500": { + "description": "Unexpected server error. Safe to retry." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X GET 'https://verification.didit.me/v3/questionnaires/11111111-2222-3333-4444-555555555555/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "Python", + "label": "Python (requests)", + "source": "import requests\n\nresp = requests.get(\n f\"https://verification.didit.me/v3/questionnaires/{questionnaire_uuid}/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n timeout=10,\n)\nresp.raise_for_status()\nquestionnaire = resp.json()\nprint(questionnaire[\"title\"], questionnaire[\"status\"])" + }, + { + "lang": "JavaScript", + "label": "Node.js (fetch)", + "source": "const res = await fetch(`https://verification.didit.me/v3/questionnaires/${questionnaireUuid}/`, {\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n});\nif (!res.ok) throw new Error(`Didit ${res.status}`);\nconst questionnaire = await res.json();\nconsole.log(questionnaire.title, questionnaire.status);" + } + ] + }, + "patch": { + "summary": "Update questionnaire", + "description": "Update a questionnaire. Versioning depends on the `status` field you send and the current state of the version: (1) no `status` on a **published** version → the published version is updated in place (same `questionnaire_id`, same `version`); (2) no `status` on a **draft** → the draft is updated and immediately published; (3) `status: \"published\"` on a **published** version → a NEW published version is created under the same `questionnaire_group_id` (new `questionnaire_id`, `version` + 1) and returned, and any existing draft in the group is deleted; (4) `status: \"draft\"` keeps a draft as a draft (autosave) but is rejected with 400 on a published version (`\"Cannot revert a published version to draft.\"`).", + "operationId": "update_questionnaire", + "x-didit-sdk": { + "resource": "questionnaires", + "method": "update", + "auth": "api_key", + "server": true + }, + "tags": [ + "Questionnaires" + ], + "parameters": [ + { + "name": "questionnaire_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Per-version UUID of the questionnaire to update." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SimpleQuestionnaireUpdateRequest" + }, + "examples": { + "Rename": { + "summary": "Rename a questionnaire", + "value": { + "title": "Updated Customer Onboarding" + } + }, + "Replace questions": { + "summary": "Replace the full linear question list", + "value": { + "form_elements": [ + { + "id": "occupation", + "element_type": "short_text", + "label": { + "en": "What is your occupation?" + }, + "is_required": true + }, + { + "id": "source_of_funds", + "element_type": "multiple_choice", + "label": { + "en": "Source of funds" + }, + "options": [ + { + "value": "employment", + "label": { + "en": "Employment" + } + }, + { + "value": "business", + "label": { + "en": "Business" + } + } + ] + } + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Questionnaire updated. The body is the latest state of the resulting version — the in-place updated version, or the newly created published version when you sent `status: \"published\"` on a published row (in that case `questionnaire_id` and `version` differ from the ones you patched). `questionnaire_id` in the response is the per-version UUID; use `questionnaire_group_id` if you need the stable group identifier.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QuestionnaireDetail" + } + } + } + }, + "400": { + "description": "Validation error — unknown `element_type`, missing translation, unsupported branching/conditional fields, `languages` missing `en`, `default_language` not in `languages`, option without a label, sending a raw `graph`, or `status: \"draft\"` on a published version (`\"Cannot revert a published version to draft.\"`). Response body is a DRF validation error keyed by field name." + }, + "403": { + "description": "API key missing or invalid.", + "content": { + "application/json": { + "examples": { + "Invalid token": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No questionnaire with this `questionnaire_uuid` exists for the authenticated application." + }, + "415": { + "description": "Unsupported media type. Set `Content-Type: application/json`." + }, + "429": { + "description": "Rate limit exceeded. Retry with exponential backoff." + }, + "500": { + "description": "Unexpected server error. Safe to retry." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X PATCH 'https://verification.didit.me/v3/questionnaires/11111111-2222-3333-4444-555555555555/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"title\": \"Updated Customer Onboarding\"}'" + }, + { + "lang": "Python", + "label": "Python (requests)", + "source": "import requests\n\nresp = requests.patch(\n f\"https://verification.didit.me/v3/questionnaires/{questionnaire_uuid}/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n json={\"title\": \"Updated Customer Onboarding\"},\n timeout=10,\n)\nresp.raise_for_status()\nprint(resp.json()[\"questionnaire_id\"], resp.json()[\"status\"])" + }, + { + "lang": "JavaScript", + "label": "Node.js (fetch)", + "source": "const res = await fetch(`https://verification.didit.me/v3/questionnaires/${questionnaireUuid}/`, {\n method: \"PATCH\",\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ title: \"Updated Customer Onboarding\" }),\n});\nif (!res.ok) throw new Error(`Didit ${res.status}`);\nconst q = await res.json();\nconsole.log(q.questionnaire_id, q.status);" + } + ] + }, + "delete": { + "summary": "Delete questionnaire", + "description": "Permanently delete one questionnaire version (no undo). Stored questionnaire responses linked to this version are deleted with it, and sessions referencing it lose the link; other versions in the same group are not deleted. Workflows that referenced it keep running but the questionnaire step can no longer load it.", + "operationId": "delete_questionnaire", + "x-didit-sdk": { + "resource": "questionnaires", + "method": "delete", + "auth": "api_key", + "server": true + }, + "tags": [ + "Questionnaires" + ], + "parameters": [ + { + "name": "questionnaire_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Per-version UUID of the questionnaire to delete." + } + ], + "responses": { + "204": { + "description": "Questionnaire deleted. The response body is empty." + }, + "403": { + "description": "API key missing or invalid.", + "content": { + "application/json": { + "examples": { + "Invalid token": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No questionnaire with this `questionnaire_uuid` exists for the authenticated application.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. Retry with exponential backoff." + }, + "500": { + "description": "Unexpected server error. Safe to retry." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl -X DELETE 'https://verification.didit.me/v3/questionnaires/11111111-2222-3333-4444-555555555555/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "Python", + "label": "Python (requests)", + "source": "import requests\n\nresp = requests.delete(\n f\"https://verification.didit.me/v3/questionnaires/{questionnaire_uuid}/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n timeout=10,\n)\nresp.raise_for_status() # raises on 4xx/5xx; 204 is success" + }, + { + "lang": "JavaScript", + "label": "Node.js (fetch)", + "source": "const res = await fetch(`https://verification.didit.me/v3/questionnaires/${questionnaireUuid}/`, {\n method: \"DELETE\",\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n});\nif (res.status !== 204) throw new Error(`Didit ${res.status}`);" + } + ] + } + }, + "/v3/users/": { + "get": { + "summary": "List users", + "description": "List [users](/entities/users) Didit assembles from KYC sessions sharing the same `vendor_data` (free-form string, NOT a UUID), plus users created via the API. Paginated, ordered by `last_session_at` desc. This endpoint does not support filtering or search parameters — fetch pages and filter client-side.", + "operationId": "list_users", + "x-didit-sdk": { + "resource": "users", + "method": "list", + "auth": "api_key", + "server": true + }, + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50, + "minimum": 1 + }, + "description": "Page size. Defaults to 50 when omitted. There is no enforced maximum, but keep pages small for latency." + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + }, + "description": "Zero-based offset of the first record to return. Prefer following the `next` URL from the previous page over computing this by hand." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/users/?limit=50' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/users/',\n headers={'x-api-key': 'YOUR_API_KEY'},\n params={'limit': 50},\n)\nresp.raise_for_status()\npage = resp.json()\nfor user in page['results']:\n print(user['vendor_data'], user['status'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const url = new URL('https://verification.didit.me/v3/users/');\nurl.searchParams.set('limit', '50');\n\nconst page = await fetch(url, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n}).then((r) => r.json());\n\nconsole.log(page.count, page.results.length);" + } + ], + "responses": { + "200": { + "description": "Paginated list of users with verification status, session counts, and feature breakdown.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "description": "Number of matching rows — exact up to 100, capped at 100 beyond that for performance. Do not use it to detect the last page; rely on `next` being `null` instead." + }, + "next": { + "type": "string", + "nullable": true, + "description": "Absolute URL of the next page, or `null` on the last page." + }, + "previous": { + "type": "string", + "nullable": true, + "description": "Absolute URL of the previous page, or `null` on the first page." + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserListItem" + } + } + } + }, + "examples": { + "Example": { + "value": { + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "didit_internal_id": "f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418", + "vendor_data": "user-abc-123", + "display_name": null, + "full_name": "John Michael Doe", + "date_of_birth": "1990-05-15", + "effective_name": "John Michael Doe", + "status": "ACTIVE", + "portrait_image_url": "https:///...", + "session_count": 3, + "approved_count": 2, + "declined_count": 0, + "in_review_count": 1, + "issuing_states": [ + "USA" + ], + "approved_emails": [ + "john@example.com" + ], + "approved_phones": [ + "+14155551234" + ], + "features": { + "ID_VERIFICATION": "Approved", + "LIVENESS": "Approved", + "FACE_MATCH": "Approved", + "AML": "Approved" + }, + "features_list": [ + { + "feature": "ID_VERIFICATION", + "status": "Approved" + }, + { + "feature": "LIVENESS", + "status": "Approved" + }, + { + "feature": "FACE_MATCH", + "status": "Approved" + }, + { + "feature": "AML", + "status": "Approved" + } + ], + "last_session_at": "2025-06-15T10:30:00Z", + "last_activity_at": "2025-06-15T10:30:00Z", + "first_session_at": "2025-06-01T08:00:00Z", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "name": "VIP", + "color": "#2567FF" + } + ], + "created_at": "2025-06-01T08:00:00Z" + }, + { + "didit_internal_id": "6ed99d53-e8f5-4cf8-9ac1-4a506cb40f6b", + "vendor_data": null, + "display_name": "Jane S.", + "full_name": "Jane Elizabeth Smith", + "date_of_birth": "1985-11-22", + "effective_name": "Jane S.", + "status": "FLAGGED", + "portrait_image_url": null, + "session_count": 1, + "approved_count": 0, + "declined_count": 0, + "in_review_count": 1, + "issuing_states": [], + "approved_emails": [], + "approved_phones": [], + "features": { + "ID_VERIFICATION": "In Review" + }, + "features_list": [ + { + "feature": "ID_VERIFICATION", + "status": "In Review" + } + ], + "last_session_at": "2025-06-20T14:00:00Z", + "last_activity_at": "2025-06-20T14:05:00Z", + "first_session_at": "2025-06-20T14:00:00Z", + "tags": [], + "created_at": "2025-06-20T14:00:00Z" + } + ] + } + } + } + } + } + }, + "401": { + "description": "No credentials supplied. Requests to `/v3/users/*` paths without an `x-api-key` header (or `Authorization: Bearer` token) are rejected by the authentication middleware before reaching the API.", + "content": { + "application/json": { + "examples": { + "Missing credentials": { + "value": { + "detail": "You must be authenticated with a valid access token to access this endpoint." + } + } + } + } + } + }, + "403": { + "description": "Invalid or revoked API key, or the key cannot list users for this application. Note: on this endpoint an *invalid* key returns `403` (not `401`).", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Rate limited": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/users/{vendor_data}/": { + "get": { + "summary": "Get user", + "description": "Fetch one [user](/entities/users) by `vendor_data` (exact match). Adds `metadata`, `comments`, and `updated_at` to the list view, and switches `tags` to the detailed tag-link shape.", + "operationId": "get_user", + "x-didit-sdk": { + "resource": "users", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "vendor_data", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Your unique identifier for the user — a free-form string (NOT a UUID). This is the same value you passed as `vendor_data` when creating the session that first introduced this user, matched exactly as sent.", + "example": "user-abc-123" + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/users/user-abc-123/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/users/user-abc-123/',\n headers={'x-api-key': 'YOUR_API_KEY'},\n)\nresp.raise_for_status()\nuser = resp.json()\nprint(user['didit_internal_id'], user['status'], user['session_count'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const vendorData = encodeURIComponent('user-abc-123');\nconst user = await fetch(\n `https://verification.didit.me/v3/users/${vendorData}/`,\n { headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n).then((r) => r.json());\n\nconsole.log(user.didit_internal_id, user.status);" + } + ], + "responses": { + "200": { + "description": "Full user detail including metadata, comments/activity log, and aggregated session data.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDetailItem" + }, + "examples": { + "Active user": { + "value": { + "didit_internal_id": "f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418", + "vendor_data": "user-abc-123", + "display_name": null, + "full_name": "John Michael Doe", + "date_of_birth": "1990-05-15", + "effective_name": "John Michael Doe", + "status": "ACTIVE", + "metadata": { + "tier": "premium", + "source": "website" + }, + "portrait_image_url": "https:///...", + "session_count": 3, + "approved_count": 2, + "declined_count": 0, + "in_review_count": 1, + "issuing_states": [ + "USA" + ], + "approved_emails": [ + "john@example.com" + ], + "approved_phones": [ + "+14155551234" + ], + "features": { + "ID_VERIFICATION": "Approved", + "LIVENESS": "Approved", + "FACE_MATCH": "Approved", + "AML": "Approved" + }, + "features_list": [ + { + "feature": "ID_VERIFICATION", + "status": "Approved" + }, + { + "feature": "LIVENESS", + "status": "Approved" + }, + { + "feature": "FACE_MATCH", + "status": "Approved" + }, + { + "feature": "AML", + "status": "Approved" + } + ], + "last_session_at": "2025-06-15T10:30:00Z", + "last_activity_at": "2025-06-15T10:30:00Z", + "first_session_at": "2025-06-01T08:00:00Z", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "tag": { + "uuid": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b", + "name": "VIP", + "color": "#2567FF", + "description": null, + "source": "custom", + "created_at": "2025-05-01T09:00:00Z", + "updated_at": "2025-05-01T09:00:00Z" + }, + "added_by_email": "analyst@acme.com", + "added_by_name": "Jane Analyst", + "created_at": "2025-06-02T11:00:00Z" + } + ], + "comments": [ + { + "uuid": "c1111111-2222-4333-8444-555555555555", + "comment_type": "STATUS_CHANGED", + "comment": null, + "actor_email": "analyst@acme.com", + "actor_name": "Jane Analyst", + "previous_status": "FLAGGED", + "new_status": "ACTIVE", + "previous_value": null, + "new_value": null, + "changed_fields": [], + "metadata": null, + "mentioned_emails": [], + "created_at": "2025-06-01T10:30:00Z" + } + ], + "created_at": "2025-06-01T08:00:00Z", + "updated_at": "2025-06-15T10:30:00Z" + } + } + } + } + } + }, + "401": { + "description": "No credentials supplied. Requests to `/v3/users/*` paths without an `x-api-key` header (or `Authorization: Bearer` token) are rejected by the authentication middleware before reaching the API.", + "content": { + "application/json": { + "examples": { + "Missing credentials": { + "value": { + "detail": "You must be authenticated with a valid access token to access this endpoint." + } + } + } + } + } + }, + "403": { + "description": "Invalid or revoked API key, or the key cannot read this application's users. Note: on this endpoint an *invalid* key returns `403` (not `401`).", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No user with the supplied `vendor_data` exists for this application.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Rate limited": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "patch": { + "summary": "Update user", + "description": "Partial update of a user: `display_name`, `full_name`, `date_of_birth`, `status`, `metadata`, `approved_emails`/`approved_phones`/`issuing_states`. `metadata` fully replaces. Changing `status` here works like [Update User Status](#patch-/v3/users/-vendor_data-/update-status/): it records a `STATUS_CHANGED` activity with the optional `reason`, and `BLOCKED` adds the `vendor_data` to the system blocklist (moving away from `BLOCKED` removes that entry). Changes to `display_name`, `full_name`, `date_of_birth` or `metadata` are recorded as a `PROFILE_UPDATED` activity.", + "operationId": "update_user", + "x-didit-sdk": { + "resource": "users", + "method": "update", + "auth": "api_key", + "server": true + }, + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "vendor_data", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Your unique identifier for the user (free-form string, NOT a UUID).", + "example": "user-abc-123" + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH 'https://verification.didit.me/v3/users/user-abc-123/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"display_name\": \"Jane S.\", \"status\": \"FLAGGED\"}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.patch(\n 'https://verification.didit.me/v3/users/user-abc-123/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'display_name': 'Jane S.', 'status': 'FLAGGED'},\n)\nresp.raise_for_status()\nprint(resp.json()['status'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/users/user-abc-123/', {\n method: 'PATCH',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ display_name: 'Jane S.', status: 'FLAGGED' }),\n});\nconst user = await resp.json();\nconsole.log(user.status);" + } + ], + "responses": { + "200": { + "description": "User updated. The full updated user record (same shape as Get User) is returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDetailItem" + }, + "examples": { + "Updated": { + "value": { + "didit_internal_id": "f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418", + "vendor_data": "user-abc-123", + "display_name": "Jane S.", + "full_name": "Jane Elizabeth Smith", + "date_of_birth": "1985-11-22", + "effective_name": "Jane S.", + "status": "FLAGGED", + "metadata": { + "tier": "premium" + }, + "portrait_image_url": "https:///...", + "session_count": 3, + "approved_count": 2, + "declined_count": 0, + "in_review_count": 1, + "issuing_states": [ + "USA" + ], + "approved_emails": [ + "john@example.com" + ], + "approved_phones": [ + "+14155551234" + ], + "features": { + "ID_VERIFICATION": "Approved", + "LIVENESS": "Approved", + "FACE_MATCH": "Approved", + "AML": "Approved" + }, + "features_list": [ + { + "feature": "ID_VERIFICATION", + "status": "Approved" + }, + { + "feature": "LIVENESS", + "status": "Approved" + }, + { + "feature": "FACE_MATCH", + "status": "Approved" + }, + { + "feature": "AML", + "status": "Approved" + } + ], + "last_session_at": "2025-06-15T10:30:00Z", + "last_activity_at": "2025-06-15T10:30:00Z", + "first_session_at": "2025-06-01T08:00:00Z", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "tag": { + "uuid": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b", + "name": "VIP", + "color": "#2567FF", + "description": null, + "source": "custom", + "created_at": "2025-05-01T09:00:00Z", + "updated_at": "2025-05-01T09:00:00Z" + }, + "added_by_email": "analyst@acme.com", + "added_by_name": "Jane Analyst", + "created_at": "2025-06-02T11:00:00Z" + } + ], + "comments": [], + "created_at": "2025-06-01T08:00:00Z", + "updated_at": "2025-06-15T10:30:00Z" + } + } + } + } + } + }, + "400": { + "description": "Invalid body — typically an unknown `status` or a `date_of_birth` not in `YYYY-MM-DD`.", + "content": { + "application/json": { + "examples": { + "Bad status": { + "value": { + "status": [ + "\"INVALID\" is not a valid choice." + ] + } + }, + "Bad date_of_birth": { + "value": { + "date_of_birth": [ + "Date has wrong format. Use one of these formats instead: YYYY-MM-DD." + ] + } + }, + "Reason too long": { + "value": { + "reason": [ + "Ensure this field has no more than 1000 characters." + ] + } + } + } + } + } + }, + "401": { + "description": "No credentials supplied. Requests to `/v3/users/*` paths without an `x-api-key` header (or `Authorization: Bearer` token) are rejected by the authentication middleware before reaching the API.", + "content": { + "application/json": { + "examples": { + "Missing credentials": { + "value": { + "detail": "You must be authenticated with a valid access token to access this endpoint." + } + } + } + } + } + }, + "403": { + "description": "Invalid or revoked API key, or the key cannot update users for this application. Note: on this endpoint an *invalid* key returns `403` (not `401`).", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No user with the supplied `vendor_data` exists for this application.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Rate limited": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "full_name": { + "type": "string", + "nullable": true, + "maxLength": 512, + "description": "Full name of the user" + }, + "display_name": { + "type": "string", + "nullable": true, + "description": "Custom display name for this user" + }, + "date_of_birth": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Date of birth in YYYY-MM-DD format" + }, + "status": { + "type": "string", + "enum": [ + "ACTIVE", + "FLAGGED", + "BLOCKED" + ], + "description": "User lifecycle status. A change is recorded as a `STATUS_CHANGED` activity, and `BLOCKED` adds the `vendor_data` to the system blocklist (moving away from `BLOCKED` removes that entry)." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Arbitrary JSON object you attach to the user. **Fully replaces** the existing metadata on update — merge client-side if you only want to add keys." + }, + "approved_emails": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Verified email addresses for this user, e.g. `[\"john@example.com\"]`. Fully replaces the stored list." + }, + "approved_phones": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Verified phone numbers for this user. Fully replaces the stored list." + }, + "issuing_states": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Issuing countries (ISO 3166-1 alpha-3), e.g. `[\"USA\"]`. Fully replaces the stored list." + }, + "reason": { + "type": "string", + "nullable": true, + "maxLength": 1000, + "description": "Why the status changed. Recorded on the status-change entry of the user's activity (`comments[].comment`, and **Activity** in the Business Console), only when the status actually changes. Webhooks do not carry it." + } + } + }, + "examples": { + "Rename": { + "summary": "Set display name", + "value": { + "display_name": "Jane S." + } + }, + "Override status": { + "summary": "Flag the user for review", + "value": { + "status": "FLAGGED" + } + }, + "Add metadata": { + "summary": "Replace metadata JSON", + "value": { + "metadata": { + "tier": "premium", + "risk_level": "low" + } + } + }, + "Fix extracted DOB": { + "summary": "Correct extracted profile fields", + "value": { + "full_name": "Jane Elizabeth Smith", + "date_of_birth": "1985-11-22" + } + }, + "Flag with reason": { + "summary": "Flag the user and record why", + "value": { + "status": "FLAGGED", + "reason": "Address does not match the billing country" + } + } + } + } + } + } + } + }, + "/v3/users/{vendor_data}/generate-pdf/": { + "get": { + "summary": "Download the full user history PDF report", + "description": "Render and download one compliance-ready PDF containing the full verification history of the user identified by `vendor_data`. It is the same document the Console produces from **User detail → Actions → Download PDF**, so periodic compliance exports no longer need a person clicking in the Console.\n\n**What the report contains.** A cover page with the profile summary (name, date of birth, issuing states, approved emails and phones), the latest status of every feature, the session counters and an index of the sessions included — followed by the standard report of every included session, in chronological order (oldest first). Each session report is the same document `GET /v3/session/{sessionId}/generate-pdf/` returns for that session.\n\n**Eligible sessions.** Only User Verification (KYC) sessions in `Approved`, `Declined`, `In Review` or `Kyc Expired` are included; sessions in any other status are skipped. If the user has no session in one of those statuses the endpoint returns `403` — see below.\n\n**Cap.** The report is capped at the **20 most recent** reportable sessions. When a user has more, the newest 20 are included and the cover page states how many older sessions were left out. Use `GET /v3/sessions/?vendor_data=…` for the complete list as JSON.\n\n**Branding.** If your application has white-label customization, the cover and every session report are rendered with your logo and privacy-policy URL; otherwise they carry Didit branding. Reports are rendered in English — there is no language parameter.\n\n**No caching.** Every call re-renders the PDF from the current data, so a report generated after a manual review reflects the reviewer's decision. Two calls for the same user can produce byte-different files — archive the downloaded file if you need an immutable copy.\n\n**Latency.** Rendering is synchronous and downloads every stored image of every included session, so a user with many media-heavy sessions can take tens of seconds. Use a generous client read timeout (120 s recommended) and stream the body to disk. Generation stops at a server-side time budget instead of running until your client gives up: if that budget is reached, the response is still a valid PDF containing the most recent sessions, and the cover states how many were omitted.\n\n**Scope.** The user is resolved inside the application that owns the API key. A key can never export a user of another application — such a user does not exist for it and the endpoint returns `404`.\n\n**Trailing slash.** The canonical route ends with a trailing slash (`…/generate-pdf/`). A request without it is served directly with the same response — there is no `301` redirect, so no client has to follow redirects and `curl` does not need `-L`. Use the slashed URL as in the samples.", + "operationId": "get_user_generate_pdf", + "x-didit-sdk": { + "resource": "users", + "method": "generate_pdf", + "auth": "api_key", + "server": true + }, + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "vendor_data", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Your unique identifier for the user — a free-form string (NOT a UUID). This is the same value you passed as `vendor_data` when creating the user's sessions, matched exactly as sent. URL-encode it; a value containing `/` cannot be addressed by this route.", + "example": "user-abc-123" + } + ], + "responses": { + "200": { + "description": "The rendered PDF document, returned directly as binary `application/pdf` — there is no JSON wrapper and no download-URL indirection. The body starts with the `%PDF` magic bytes. Save it to a `.pdf` file or stream it through to your caller.", + "headers": { + "Content-Disposition": { + "description": "`attachment; filename=user_{didit_internal_id}.pdf` — the file is named after the user's stable Didit identifier (`didit_internal_id` on `GET /v3/users/{vendor_data}/`), not after `vendor_data`.", + "schema": { + "type": "string", + "example": "attachment; filename=user_f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418.pdf" + } + }, + "Content-Length": { + "description": "Total size of the PDF in bytes; the body is not chunked.", + "schema": { + "type": "integer", + "example": 148213 + } + } + }, + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "401": { + "description": "No credentials supplied. Requests to `/v3/users/*` paths without an `x-api-key` header (or `Authorization: Bearer` token) are rejected by the authentication middleware before reaching the API.", + "content": { + "application/json": { + "examples": { + "Missing credentials": { + "value": { + "detail": "You must be authenticated with a valid access token to access this endpoint." + } + } + } + } + } + }, + "403": { + "description": "Authorization or eligibility failure. Distinguish the two by the `detail` string:\n\n- `\"You do not have permission to perform this action.\"` — the API key is invalid or revoked, or the request carries a Console user token instead of an application API key.\n- `\"This user has no sessions in review, declined, approved or kyc expired to report.\"` — the user exists in your application but none of its sessions is in a reportable status yet (for example every session is still `In Progress`, or the user was created through `POST /v3/users/create/` and has never verified). Wait for a `status.updated` webhook before requesting the report.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "example": "This user has no sessions in review, declined, approved or kyc expired to report." + } + } + }, + "examples": { + "Invalid credentials": { + "summary": "Revoked or invalid API key, or a Console user token", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "No reportable sessions": { + "summary": "User exists but has no Approved / Declined / In Review / Kyc Expired session", + "value": { + "detail": "This user has no sessions in review, declined, approved or kyc expired to report." + } + } + } + } + } + }, + "404": { + "description": "No user with the supplied `vendor_data` exists in the application behind the API key. Users of other applications are invisible to this key and return the same `404`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "example": "User not found." + } + } + }, + "examples": { + "Unknown user": { + "summary": "vendor_data does not match any user of this application", + "value": { + "detail": "User not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. PDF generation has a dedicated limit of **50 requests per minute per credential** (API key, Bearer token, or source IP when unauthenticated), shared between this endpoint and `GET /v3/session/{sessionId}/generate-pdf/`, in addition to the global 600/min GET limit. Wait `Retry-After` seconds (also reflected in the `X-RateLimit-*` headers), then retry. Reports are not cached server-side, so download each report once and store the file instead of re-fetching it.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying the request.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + }, + "X-RateLimit-Limit": { + "description": "Maximum number of PDF generations allowed in the current window (50).", + "schema": { + "type": "integer", + "example": 50 + } + }, + "X-RateLimit-Remaining": { + "description": "Requests remaining in the current window.", + "schema": { + "type": "integer", + "example": 0 + } + }, + "X-RateLimit-Reset": { + "description": "Unix timestamp (seconds) at which the current window resets.", + "schema": { + "type": "integer", + "example": 1750000000 + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable explanation of the rate-limit breach.", + "example": "Session PDF generation rate limit exceeded. You can make up to 50 requests per minute." + } + } + }, + "examples": { + "Throttled": { + "summary": "More than 50 PDF generations in one minute from the same credential", + "value": { + "detail": "Session PDF generation rate limit exceeded. You can make up to 50 requests per minute." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl --fail \\\n 'https://verification.didit.me/v3/users/user-abc-123/generate-pdf/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n --output user-history.pdf" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresponse = requests.get(\n \"https://verification.didit.me/v3/users/user-abc-123/generate-pdf/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n stream=True,\n timeout=120,\n)\nresponse.raise_for_status()\nwith open(\"user-history.pdf\", \"wb\") as fh:\n for chunk in response.iter_content(chunk_size=8192):\n fh.write(chunk)" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "import { writeFile } from 'node:fs/promises';\n\nconst vendorData = encodeURIComponent('user-abc-123');\nconst response = await fetch(\n `https://verification.didit.me/v3/users/${vendorData}/generate-pdf/`,\n { headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n);\nif (!response.ok) throw new Error(`PDF generation failed: HTTP ${response.status}`);\nawait writeFile('user-history.pdf', Buffer.from(await response.arrayBuffer()));" + } + ] + } + }, + "/v3/users/delete/": { + "post": { + "summary": "Batch delete users", + "description": "**Permanently delete** users: the user records are removed permanently. For everyday blocking prefer [Update User Status](#patch-/v3/users/-vendor_data-/update-status/) with `BLOCKED`. Select users with `vendor_data_list`, with `didit_internal_id_list`, or with both at once; or pass `delete_all: true`. This endpoint never narrows `delete_all` with filters. The Console's People filters are refused with a `400` (see the `400` response), so a narrowed intent does not turn into a full wipe. Two exceptions: `filter_has_approved_sessions` and `filter_has_declined_sessions` are not refused and not applied, so with `delete_all: true` they still delete every user; do not send them.\n\n**Biometric templates:** every image-free biometric template retained for these users after session deletion (see `/v3/biometric-templates/`) is purged before the user is deleted, so the person stops matching in duplicate detection and Face Search. If a template cannot be purged because a biometric store is unavailable, the call returns `503`, no user in the request is deleted, and repeating the request finishes the job.", + "operationId": "batch_delete_users", + "x-didit-sdk": { + "resource": "users", + "method": "batch_delete", + "auth": "api_key", + "server": true + }, + "tags": [ + "Users" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Provide `vendor_data_list`, `didit_internal_id_list`, or `delete_all: true`. The two lists can be combined and are OR'd. When `delete_all` is `true` it takes precedence and both lists are ignored.", + "properties": { + "vendor_data_list": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Your own identifiers to delete, matched exactly as sent." + }, + "didit_internal_id_list": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Didit internal user IDs to delete - the `didit_internal_id` returned by the create and get endpoints." + }, + "delete_all": { + "type": "boolean", + "default": false, + "description": "If `true`, deletes every user in the application." + } + } + }, + "examples": { + "Specific users": { + "summary": "Delete users by vendor_data", + "value": { + "vendor_data_list": [ + "user-123", + "user-456" + ] + } + }, + "By Didit internal ID": { + "summary": "Delete users by didit_internal_id", + "value": { + "didit_internal_id_list": [ + "3f1c0d3e-8f0a-4c2e-9a3d-9b1f5f4a2c11" + ] + } + }, + "All users": { + "summary": "Wipe every user in this application", + "value": { + "delete_all": true + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/users/delete/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"vendor_data_list\": [\"user-123\", \"user-456\"]}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/users/delete/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'vendor_data_list': ['user-123', 'user-456']},\n)\nresp.raise_for_status()\nprint('deleted', resp.json()['deleted'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/users/delete/', {\n method: 'POST',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ vendor_data_list: ['user-123', 'user-456'] }),\n});\nconst body = await resp.json();\nconsole.log('deleted', body.deleted);" + } + ], + "responses": { + "200": { + "description": "Users deleted. Returns the number of users actually deleted.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "deleted": { + "type": "integer", + "description": "Number of users deleted (excludes vendor_data values that didn't match anything)." + } + } + }, + "examples": { + "Deleted some": { + "value": { + "deleted": 2 + } + }, + "Nothing matched": { + "value": { + "deleted": 0 + } + } + } + } + } + }, + "400": { + "description": "Validation error; nothing is deleted. Errors are keyed by field name. With no selector (neither list and `delete_all` not `true`) the body is `{\"vendor_data_list\": [\"This field is required unless delete_all is true.\"]}`; an empty list is refused under its own key with `\"This list may not be empty.\"`. A Console-only filter (`filter_search`, `filter_status`, `filter_country`, `filter_date_from`, `filter_date_to`, `filter_tag`, `filter_latest_verification_status`, `filter_risk`, `filter_has_sessions`, `filter_has_open_alerts`, `filter_aml_monitoring`, `filter_can_sign_in`, `filter_auth_status`, `filter_auth_method` or `filter_auth_mfa_enabled`) is refused whenever it carries a value, even `false`, with an object keyed by each such field. Any other unknown `filter_*` key is refused with `\"Unknown filter.\"`.", + "content": { + "application/json": { + "examples": { + "Missing selector": { + "summary": "Neither list nor delete_all provided", + "value": { + "vendor_data_list": [ + "This field is required unless delete_all is true." + ] + } + }, + "Console-only filter": { + "value": { + "filter_has_sessions": "This filter is Console-only and is not supported by this endpoint." + } + } + } + } + } + }, + "401": { + "description": "No credentials supplied. Requests to `/v3/users/*` paths without an `x-api-key` header (or `Authorization: Bearer` token) are rejected by the authentication middleware before reaching the API.", + "content": { + "application/json": { + "examples": { + "Missing credentials": { + "value": { + "detail": "You must be authenticated with a valid access token to access this endpoint." + } + } + } + } + } + }, + "403": { + "description": "Invalid or revoked API key, or the key cannot delete users for this application. Note: on this endpoint an *invalid* key returns `403` (not `401`).", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Rate limited": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + }, + "503": { + "description": "A biometric store was unavailable while retaining or purging a biometric template. Nothing is deleted; repeat the same request. Retries are idempotent and converge on a single template.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Retryable": { + "value": { + "detail": "The biometric-template operation could not finish because an external store is unavailable. The operation is retryable." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/sessions/delete/": { + "post": { + "summary": "Bulk delete KYC sessions by session_number", + "description": "Bulk delete User Verification (KYC) sessions by their numeric `session_number`, or every KYC session in your application with `delete_all: true`.\n\n`session_number` is the human-friendly incrementing counter shown in the Console session list and returned as `session_number` by the create-session and decision endpoints; it is **not** the `session_id` UUID. To delete a session by UUID, or to delete a Business Verification (KYB) session, which this endpoint never touches, use `DELETE /v3/session/{sessionId}/delete/` instead.\n\n**Up to 20 sessions are deleted in the request, more run as a job.** The number of matching sessions decides, whether they are selected by `session_numbers` or with `delete_all`:\n- Up to 20 matching sessions: they are deleted before the response, which is `200` with one entry per session in `results[]`.\n- More than 20: the response is `202` with a deletion job, and the sessions are deleted in the background. Poll `GET /v3/sessions/delete/{job_id}/` until `status` is `COMPLETED` or `FAILED`. A job reports counts per outcome, never which sessions it deleted. A `delete_all` job deletes only the sessions created before the request; sessions started while it runs are not deleted.\n\nEach deleted session gets exactly the same treatment as the single-session delete endpoint: the session, every verification attempt it holds, its decision and extracted data, its related feature records (ID verification, face, liveness, face match, and so on) and all of its stored files are deleted, it disappears from every read endpoint, and previously issued media URLs stop resolving. Webhook deliveries still queued for a deleted session, and their retries, are dropped; webhooks already delivered are not affected. Blocklist entries are **not** removed, credits already consumed are **not** refunded, and the parent User entities are **not** deleted (use `POST /v3/users/delete/` for those); no webhook is emitted for the deletion. Deletion is permanent and irreversible.\n\n**Selection semantics:**\n- With `delete_all: true` you can narrow the selection with the filters of `GET /v3/sessions/`, each prefixed with `filter_` (for example `filter_status`, `filter_workflow_id`, `filter_date_from`). Filters that take several values in List Sessions (`status`, `workflow_id`, `country`, `region`, `document_type`, `document_name`, `session_type`, `warning`, `organization_name`) accept a comma-separated string or an array; the others take one value. An empty value is ignored. Filters apply only together with `delete_all`, and a deletion job applies the same filters.\n- An unknown `filter_*` key, or a filter value that cannot be read, rejects the whole request with `400` before anything is deleted, so a mistyped filter never widens the deletion to every session.\n- Entries in `session_numbers` that do not match a live KYC session in your application (unknown numbers, already-deleted sessions, or sessions owned by another application) are **silently skipped**: they do not appear in `results` and are not counted in a job's `total`.\n- Validation failures (a non-numeric entry, an empty list, neither `session_numbers` nor `delete_all` provided, or a template retention the application cannot honour) reject the **whole request** with `400` and delete nothing.\n- Sessions are deleted in groups. If a group cannot be deleted (for example because a storage service is unavailable), every session in that group is reported as `face_retention_outcome: failed_retryable` and stays in place; the other groups are unaffected. Repeat the request for those sessions. A job retries such a group by itself.\n- There is no documented cap on the list length; the shared write rate limit (below) is the practical bound.\n\n**Face biometric data:** the optional `retain_face_embeddings`, `face_retention_days`, `face_retention_deadline`, `deletion_instruction`, and `instruction_id` fields have the same meaning as on `DELETE /v3/session/{sessionId}/delete/` and apply to every matched session. Each result reports its own `face_retention_outcome`.\n\n**Response:** `200 OK` with `results[]` for up to 20 matching sessions, or `202 Accepted` with a deletion job for more. Every result confirms deletion except `face_retention_outcome: failed_retryable`, which reports a session that was NOT deleted and should be retried. Earlier versions of this endpoint returned `204 No Content`; update clients that assert on `204`.\n\n**Idempotency:** repeating the same request deletes nothing more: numbers that no longer match a session are skipped.\n\n**`delete_all: true` (destructive):** deletes **every** remaining KYC session in the application; `session_numbers` is ignored when set. There is no confirmation step and no undo.\n\n**Authentication:** API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`); this API never returns `401`.\n\n**Rate limit:** shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.\n\nThis endpoint accepts API keys only. Business Console user Bearer tokens are rejected with `403` (unlike the single-session delete, which accepts both).", + "operationId": "batch_delete_sessions", + "x-didit-sdk": { + "resource": "sessions", + "method": "batch_delete", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "session_numbers": { + "type": "array", + "description": "Numeric `session_number` values of the KYC sessions to delete, as digit-only strings (JSON numbers are also accepted and coerced). Required unless `delete_all` is `true`; an empty list returns `400`. Any non-digit entry rejects the whole request with `400` (unless `delete_all` is `true`, in which case validation is skipped and the entries are ignored). Numbers that do not match a live session in your application are silently skipped.", + "items": { + "type": "string", + "pattern": "^[0-9]+$", + "example": "1001" + }, + "minItems": 1, + "example": [ + "1001", + "1002", + "1003" + ] + }, + "delete_all": { + "type": "boolean", + "description": "When `true`, every remaining KYC session in the calling application is deleted and `session_numbers` is ignored, unless you narrow the selection with `filter_*` keys. More than 20 matching sessions run as a deletion job (`202`) that covers only the sessions created before the request. Defaults to `false`. Irreversible; there is no confirmation step.", + "default": false, + "example": false + }, + "retain_face_embeddings": { + "type": "boolean", + "nullable": true, + "description": "Override the application's `face_retention_policy` for this deletion. `true` keeps one image-free face biometric template anchored to the session's User after the session is deleted; `false` deletes the face embedding with the session; omit or `null` to follow the application policy. Ignored for KYB sessions and for sessions without a face embedding.", + "example": true + }, + "face_retention_days": { + "type": "integer", + "minimum": 1, + "maximum": 3650, + "description": "Finite retention duration, in days, for a retained template. Required when a template is retained unless the application already sets `face_retention_days`. The template's `expires_at` is the earliest of this duration, the application's general data-retention window, and `face_retention_deadline`.", + "example": 365 + }, + "face_retention_deadline": { + "type": "string", + "format": "date-time", + "description": "Optional hard expiry for a retained template. Must be in the future.", + "example": "2027-08-28T00:00:00Z" + }, + "deletion_instruction": { + "type": "string", + "enum": [ + "operational_session_delete", + "privacy_erasure" + ], + "default": "operational_session_delete", + "description": "The class of instruction you are giving. `operational_session_delete` deletes the session and retains a template only when the policy or `retain_face_embeddings` says so. `privacy_erasure` purges every retained biometric template anchored to the session's User before deleting the session, cannot be overridden by the application policy, and cannot be combined with `retain_face_embeddings: true` (`400`).", + "example": "operational_session_delete" + }, + "instruction_id": { + "type": "string", + "maxLength": 255, + "description": "Your durable reference for this deletion instruction (for example an erasure-ticket id). Recorded on the audit trail and on any retained template. Generated by Didit when omitted.", + "example": "dsar-2026-0142" + }, + "filter_status": { + "description": "Narrows a `delete_all` to the sessions that match the `status` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "filter_vendor_data": { + "description": "Narrows a `delete_all` to the sessions that match the `vendor_data` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated text, so several values are not matched as alternatives. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "type": "string" + }, + "filter_search": { + "description": "Narrows a `delete_all` to the sessions that match the `search` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated text, so several values are not matched as alternatives. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "type": "string" + }, + "filter_workflow_id": { + "description": "Narrows a `delete_all` to the sessions that match the `workflow_id` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "filter_date_from": { + "description": "Narrows a `delete_all` to the sessions that match the `date_from` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "type": "string", + "format": "date" + }, + "filter_date_to": { + "description": "Narrows a `delete_all` to the sessions that match the `date_to` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "type": "string", + "format": "date" + }, + "filter_country": { + "description": "Narrows a `delete_all` to the sessions that match the `country` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "filter_document_type": { + "description": "Narrows a `delete_all` to the sessions that match the `document_type` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "filter_document_name": { + "description": "Narrows a `delete_all` to the sessions that match the `document_name` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "filter_region": { + "description": "Narrows a `delete_all` to the sessions that match the `region` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "filter_didit_internal_id": { + "description": "Narrows a `delete_all` to the sessions that match the `didit_internal_id` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "type": "string", + "format": "uuid" + }, + "filter_session_type": { + "description": "Narrows a `delete_all` to the sessions that match the `session_type` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "filter_api_service": { + "description": "Narrows a `delete_all` to the sessions that match the `api_service` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "type": "string" + }, + "filter_screened_full_name": { + "description": "Narrows a `delete_all` to the sessions that match the `screened_full_name` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated text, so several values are not matched as alternatives. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "type": "string" + }, + "filter_warning": { + "description": "Narrows a `delete_all` to the sessions that match the `warning` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "filter_status_initialized_as_in_review": { + "description": "Narrows a `delete_all` to the sessions that match the `status_initialized_as_in_review` filter of [List Sessions](/sessions-api/list-sessions), with the same values. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "type": "boolean" + }, + "filter_is_kyc_to_be_reviewed": { + "description": "Narrows a `delete_all` to the sessions that match the `is_kyc_to_be_reviewed` filter of [List Sessions](/sessions-api/list-sessions), with the same values. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "type": "boolean" + }, + "filter_euuid": { + "description": "Narrows a `delete_all` to the sessions that match the `euuid` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "type": "string", + "format": "uuid" + }, + "filter_organization_name": { + "description": "Narrows a `delete_all` to the sessions that match the `organization_name` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. With an API key it can only match your own organization's name, so it selects all of your sessions or none. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + } + }, + "examples": { + "Specific sessions": { + "summary": "Delete specific sessions by number", + "value": { + "session_numbers": [ + "1001", + "1002", + "1003" + ] + } + }, + "All sessions": { + "summary": "Delete every KYC session in the application", + "value": { + "delete_all": true + } + }, + "Delete and retain templates": { + "summary": "Delete sessions but keep image-free biometric templates for 365 days", + "value": { + "session_numbers": [ + "1001", + "1002" + ], + "retain_face_embeddings": true, + "face_retention_days": 365, + "instruction_id": "post-approval-cleanup-2026-08" + } + }, + "Privacy erasure": { + "summary": "Delete sessions and purge every retained template for their Users", + "value": { + "session_numbers": [ + "1001" + ], + "deletion_instruction": "privacy_erasure", + "instruction_id": "dsar-2026-0142" + } + }, + "Filtered delete_all": { + "summary": "Delete every Declined session of one workflow created in September 2026", + "value": { + "delete_all": true, + "filter_status": [ + "Declined" + ], + "filter_workflow_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "filter_date_from": "2026-09-01", + "filter_date_to": "2026-09-30" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Up to 20 sessions matched and were processed in the request. One result per matched session; numbers that matched nothing (unknown or already deleted) are skipped and do not appear. A result confirms deletion except when `face_retention_outcome` is `failed_retryable`: that session was NOT deleted and should be retried.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "session_id": { + "type": "string", + "format": "uuid", + "description": "The deleted session." + }, + "session_number": { + "type": "integer", + "nullable": true, + "description": "The deleted session's number." + }, + "face_retention_outcome": { + "type": "string", + "enum": [ + "retained_with_user", + "deleted", + "none", + "ineligible_no_vendor_user", + "failed_retryable" + ], + "description": "What happened to the session's face biometric data. `deleted`: the face embedding was deleted with the session (default). `retained_with_user`: the session is deleted and one image-free biometric template stays anchored to the User; see `biometric_template_uuid`. `none`: the session had no face embedding (for example a document-only KYC session without a liveness selfie; KYB sessions are never selected by this endpoint). `ineligible_no_vendor_user`: retention was requested but the session has no linked User, so the embedding was deleted with the session. `failed_retryable` (batch endpoint only): the group this session was deleted with could not be deleted (a storage service, a biometric store, or a monitoring provider was unavailable); this session is still in place, repeat the request for it." + }, + "biometric_template_uuid": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Id of the retained biometric template when `face_retention_outcome` is `retained_with_user`; `null` otherwise. Manage it through `/v3/biometric-templates/{template_uuid}/`." + }, + "biometric_template_expires_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the retained biometric template is deleted, when `face_retention_outcome` is `retained_with_user`; `null` when no template is retained." + }, + "deleted_child_verifications": { + "type": "integer", + "description": "Always `0` on this endpoint, which deletes KYC sessions only. Deleting a Business Verification (KYB) session with `DELETE /v3/session/{sessionId}/delete/` reports the verifications it created that were deleted with it." + } + } + } + } + } + }, + "examples": { + "Mixed outcomes": { + "value": { + "results": [ + { + "session_id": "4c5c7f3a-3b1d-4b7e-9c39-2b4f7e6a1d02", + "session_number": 1001, + "face_retention_outcome": "deleted", + "biometric_template_uuid": null, + "biometric_template_expires_at": null, + "deleted_child_verifications": 0 + }, + { + "session_id": "b8e1d2c3-4f5a-4b6c-8d7e-9f0a1b2c3d4e", + "session_number": 1002, + "face_retention_outcome": "retained_with_user", + "biometric_template_uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", + "biometric_template_expires_at": "2027-10-09T10:15:00Z", + "deleted_child_verifications": 0 + }, + { + "session_id": "7a1e9c2d-5f4b-4c3a-8e2d-1b0a9f8e7d6c", + "session_number": 1003, + "face_retention_outcome": "ineligible_no_vendor_user", + "biometric_template_uuid": null, + "biometric_template_expires_at": null, + "deleted_child_verifications": 0 + }, + { + "session_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", + "session_number": 1004, + "face_retention_outcome": "failed_retryable", + "biometric_template_uuid": null, + "biometric_template_expires_at": null, + "deleted_child_verifications": 0 + } + ] + } + } + } + } + } + }, + "202": { + "description": "More than 20 sessions matched. They are deleted in the background; poll `GET /v3/sessions/delete/{job_id}/` until `status` is `COMPLETED` or `FAILED`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionDeletionJob" + }, + "examples": { + "Job accepted": { + "value": { + "job_id": "5b8f2c1e-7d3a-4f6b-9e2c-1a0d8f7e6c5b", + "status": "PENDING", + "total": 250, + "processed": 0, + "outcomes": { + "deleted": 0, + "none": 0, + "retained_with_user": 0, + "ineligible_no_vendor_user": 0, + "failed_retryable": 0, + "already_deleted": 0 + }, + "deleted_child_verifications": 0, + "error": null, + "created_at": "2026-10-09T10:15:00Z", + "completed_at": null + } + } + } + } + } + }, + "400": { + "description": "Validation error — nothing was deleted. Error values are arrays of message strings keyed by field name.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "session_numbers": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": { + "description": "Other keys: a `filter_*` key that is not a List Sessions filter (`\"Unknown filter.\"`), or a `filter_*` key whose value cannot be read (keyed by that `filter_*` key, with an array of messages)." + } + }, + "examples": { + "Missing selector": { + "summary": "Neither session_numbers nor delete_all provided", + "value": { + "session_numbers": [ + "This field is required unless delete_all is true." + ] + } + }, + "Empty list": { + "summary": "session_numbers is an empty array", + "value": { + "session_numbers": [ + "This list may not be empty." + ] + } + }, + "Non-numeric entry": { + "summary": "session_numbers contains a non-digit string", + "value": { + "session_numbers": [ + "All session_numbers must be numeric." + ] + } + }, + "Privacy erasure cannot retain": { + "summary": "deletion_instruction privacy_erasure combined with retain_face_embeddings true", + "value": { + "retain_face_embeddings": [ + "Privacy erasure cannot retain biometric templates." + ] + } + }, + "No finite retention duration": { + "summary": "Retention requested without face_retention_days on the request or the application", + "value": { + "detail": "A finite biometric-template retention duration is required." + } + }, + "Unknown filter": { + "summary": "A filter_* key that List Sessions does not have", + "value": { + "filter_reviewer": "Unknown filter." + } + }, + "Unreadable filter value": { + "summary": "A filter value that cannot be read", + "value": { + "filter_date_from": [ + "Enter a valid date." + ] + } + } + } + } + } + }, + "403": { + "description": "Missing or invalid API key. This API returns `403` for authentication failures — never `401`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Missing or invalid API key": { + "summary": "No x-api-key header, or the key is invalid", + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Shared write rate limit exceeded (300 POST/PATCH/DELETE requests per minute per API key). Inspect `Retry-After` and the `X-RateLimit-*` response headers before retrying.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Rate limited": { + "summary": "Write budget exhausted", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST \\\n https://verification.didit.me/v3/sessions/delete/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"session_numbers\": [\"1001\", \"1002\", \"1003\"]\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/sessions/delete/\",\n headers={\n \"x-api-key\": \"YOUR_API_KEY\",\n \"Content-Type\": \"application/json\",\n },\n json={\"session_numbers\": [\"1001\", \"1002\", \"1003\"]},\n)\nresponse.raise_for_status()\nif response.status_code == 202: # more than 20 sessions: poll the job\n job_id = response.json()[\"job_id\"]\nelse: # 200: one result per matched session\n print(response.json()[\"results\"])\n\n# Destructive: delete every KYC session in the application\n# requests.post(\n# \"https://verification.didit.me/v3/sessions/delete/\",\n# headers={\"x-api-key\": \"YOUR_API_KEY\"},\n# json={\"delete_all\": True},\n# )" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const response = await fetch(\n 'https://verification.didit.me/v3/sessions/delete/',\n {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ session_numbers: ['1001', '1002', '1003'] }),\n },\n);\nif (response.status !== 200 && response.status !== 202) throw new Error(`HTTP ${response.status}`);\nconst body = await response.json();\n// 200: body.results, one per matched session. 202: poll /v3/sessions/delete/${body.job_id}/" + } + ] + } + }, + "/v3/sessions/delete/{job_id}/": { + "get": { + "summary": "Get a bulk session deletion job", + "description": "Progress of a bulk session deletion that `POST /v3/sessions/delete/` answered with `202` (more than 20 matching sessions, selected by `session_numbers` or with `delete_all`). Poll every few seconds while `status` is `PENDING` or `PROCESSING`; stop when it is `COMPLETED` or `FAILED`.\n\nThe job reports how many sessions ended in each outcome and how many it has processed, never which sessions it deleted. A `FAILED` job carries the reason in `error`; the sessions it had not reached are still in place, so repeat the deletion request to delete them.\n\n**Authentication:** API key in the `x-api-key` header. A job is only visible to the application that created it; any other `job_id` returns `404`. Missing or invalid credentials return `403`; this API never returns `401`.", + "operationId": "get_session_deletion_job", + "x-didit-sdk": { + "resource": "sessions", + "method": "retrieve_deletion_job", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The `job_id` returned with the `202` from `POST /v3/sessions/delete/`." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl 'https://verification.didit.me/v3/sessions/delete/JOB_ID/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import time\nimport requests\n\nurl = 'https://verification.didit.me/v3/sessions/delete/JOB_ID/'\nwhile True:\n job = requests.get(url, headers={'x-api-key': 'YOUR_API_KEY'}).json()\n if job['status'] in ('COMPLETED', 'FAILED'):\n break\n time.sleep(3)\nprint(job['status'], job['outcomes'], job['error'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const url = 'https://verification.didit.me/v3/sessions/delete/JOB_ID/';\nlet job;\ndo {\n await new Promise((r) => setTimeout(r, 3000));\n job = await (await fetch(url, { headers: { 'x-api-key': 'YOUR_API_KEY' } })).json();\n} while (job.status === 'PENDING' || job.status === 'PROCESSING');\nconsole.log(job.status, job.outcomes, job.error);" + } + ], + "responses": { + "200": { + "description": "The job.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionDeletionJob" + }, + "examples": { + "Processing": { + "value": { + "job_id": "5b8f2c1e-7d3a-4f6b-9e2c-1a0d8f7e6c5b", + "status": "PROCESSING", + "total": 250, + "processed": 100, + "outcomes": { + "deleted": 82, + "none": 18, + "retained_with_user": 0, + "ineligible_no_vendor_user": 0, + "failed_retryable": 0, + "already_deleted": 0 + }, + "deleted_child_verifications": 0, + "error": null, + "created_at": "2026-10-09T10:15:00Z", + "completed_at": null + } + }, + "Completed": { + "value": { + "job_id": "5b8f2c1e-7d3a-4f6b-9e2c-1a0d8f7e6c5b", + "status": "COMPLETED", + "total": 250, + "processed": 250, + "outcomes": { + "deleted": 204, + "none": 44, + "retained_with_user": 0, + "ineligible_no_vendor_user": 0, + "failed_retryable": 0, + "already_deleted": 2 + }, + "deleted_child_verifications": 0, + "error": null, + "created_at": "2026-10-09T10:15:00Z", + "completed_at": "2026-10-09T10:16:40Z" + } + }, + "Failed": { + "value": { + "job_id": "5b8f2c1e-7d3a-4f6b-9e2c-1a0d8f7e6c5b", + "status": "FAILED", + "total": 250, + "processed": 100, + "outcomes": { + "deleted": 82, + "none": 18, + "retained_with_user": 0, + "ineligible_no_vendor_user": 0, + "failed_retryable": 0, + "already_deleted": 0 + }, + "deleted_child_verifications": 0, + "error": "The deletion stopped before it finished. The sessions it had not reached are still in place; repeat the request to delete them.", + "created_at": "2026-10-09T10:15:00Z", + "completed_at": "2026-10-09T10:15:52Z" + } + } + } + } + } + }, + "403": { + "description": "Missing or invalid API key. This API returns `403` for authentication failures, never `401`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Missing or invalid API key": { + "summary": "No x-api-key header, or the key is invalid", + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No deletion job with this `job_id` exists for the calling application. Jobs are only visible to the application that created them." + } + } + } + }, + "/v3/sessions/{session_id}/reviews/": { + "get": { + "summary": "List session reviews and activity log", + "description": "Return the audit trail / activity feed for a User Verification (KYC) session, newest first, in a paginated envelope (`count` / `next` / `previous` / `results`, 50 entries per page by default).\n\nFeed entries are written by every actor that touches the session: manual decisions made through `PATCH /v3/session/{session_id}/update-status/` (which records `previous_status` → `new_status`), Console reviewer activity (comments with `@email` mentions, KYC/POA data edits, file uploads/removals, tag changes, detail-page views), AML ongoing-monitoring matches, and notes appended via `POST` on this same path. Reading this feed is the API-side equivalent of the **Activity** panel in the Didit Console session view.\n\nOnly User Verification (KYC) sessions are addressable here; Business Verification (KYB) session IDs return `404`.", + "operationId": "list_session_reviews", + "x-didit-sdk": { + "resource": "sessions.reviews", + "method": "list", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "description": "UUID of the User Verification (KYC) session. Must belong to the same application as the API key — cross-application UUIDs, deleted sessions, and Business Verification (KYB) session IDs all return `404`.", + "schema": { + "type": "string", + "format": "uuid", + "example": "8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "description": "Page size. Defaults to `50`.", + "schema": { + "type": "integer", + "default": 50, + "example": 50 + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "description": "Number of entries to skip from the start of the (ordered) feed. Defaults to `0`. The `next` / `previous` URLs in the response carry the right values for walking the feed.", + "schema": { + "type": "integer", + "default": 0, + "example": 0 + } + }, + { + "name": "ordering", + "in": "query", + "required": false, + "description": "Sort order. The feed defaults to newest first (`-created_at`); pass `created_at` to read it oldest first. Unknown values are silently ignored (default order is used); other serializer field names are also accepted.", + "schema": { + "type": "string", + "default": "-created_at", + "example": "created_at" + } + } + ], + "responses": { + "200": { + "description": "Paginated activity feed, newest first by default. `count` is the total number of entries; `next` / `previous` are ready-made page URLs (`null` at the ends). A `count` of `0` with empty `results` means no activity has been recorded yet.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "description": "Total number of feed entries for this session (across all pages).", + "example": 3 + }, + "next": { + "type": "string", + "nullable": true, + "description": "URL of the next page, or `null` on the last page.", + "example": null + }, + "previous": { + "type": "string", + "nullable": true, + "description": "URL of the previous page, or `null` on the first page.", + "example": null + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReviewItem" + }, + "description": "Feed entries for this page." + } + } + }, + "examples": { + "DecisionTrail": { + "summary": "Manual approval plus API-added notes", + "value": { + "count": 3, + "next": null, + "previous": null, + "results": [ + { + "uuid": "de0561bc-5583-4409-9171-1a31a4c8a5f4", + "activity_type": "STATUS_UPDATED", + "actor_display": "API Client", + "actor_email": null, + "actor_type": "API_KEY", + "new_status": "Approved", + "previous_status": "In Review", + "comment": "Document re-checked manually; address matches.", + "mentioned_emails": [], + "previous_value": { + "status": "In Review" + }, + "new_value": { + "status": "Approved" + }, + "changed_fields": [ + "status" + ], + "metadata": null, + "created_at": "2026-06-12T00:19:31.918735Z" + }, + { + "uuid": "35370f58-5098-44ee-8a06-8c9af80b2455", + "activity_type": "STATUS_UPDATED", + "actor_display": "Unknown", + "actor_email": null, + "actor_type": "CONSOLE_USER", + "new_status": null, + "previous_status": null, + "comment": "Note without status.", + "mentioned_emails": [], + "previous_value": null, + "new_value": null, + "changed_fields": [], + "metadata": null, + "created_at": "2026-06-12T00:18:44.504157Z" + }, + { + "uuid": "00338f82-f1c5-4f57-8614-c49bb258af1a", + "activity_type": "STATUS_UPDATED", + "actor_display": "Unknown", + "actor_email": null, + "actor_type": "CONSOLE_USER", + "new_status": "In Review", + "previous_status": null, + "comment": "Flagging for manual document re-check.", + "mentioned_emails": [], + "previous_value": null, + "new_value": null, + "changed_fields": [], + "metadata": null, + "created_at": "2026-06-12T00:18:44.466537Z" + } + ] + } + }, + "PaginatedFirstPage": { + "summary": "First page with limit=2 — next holds the follow-up URL", + "value": { + "count": 4, + "next": "https://verification.didit.me/v3/sessions/8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d/reviews/?limit=2&offset=2", + "previous": null, + "results": [ + { + "uuid": "de0561bc-5583-4409-9171-1a31a4c8a5f4", + "activity_type": "STATUS_UPDATED", + "actor_display": "API Client", + "actor_email": null, + "actor_type": "API_KEY", + "new_status": "Approved", + "previous_status": "In Review", + "comment": "Document re-checked manually; address matches.", + "mentioned_emails": [], + "previous_value": { + "status": "In Review" + }, + "new_value": { + "status": "Approved" + }, + "changed_fields": [ + "status" + ], + "metadata": null, + "created_at": "2026-06-12T00:19:31.918735Z" + }, + { + "uuid": "bd4e6227-bdd2-42b2-a726-1d12f785a457", + "activity_type": "STATUS_UPDATED", + "actor_display": "Unknown", + "actor_email": null, + "actor_type": "CONSOLE_USER", + "new_status": null, + "previous_status": null, + "comment": null, + "mentioned_emails": [], + "previous_value": null, + "new_value": null, + "changed_fields": [], + "metadata": null, + "created_at": "2026-06-12T00:18:44.540238Z" + } + ] + } + }, + "Empty": { + "summary": "Session with no recorded activity yet", + "value": { + "count": 0, + "next": null, + "previous": null, + "results": [] + } + } + } + } + } + }, + "403": { + "description": "Missing, malformed, expired, or revoked `x-api-key`. Authentication is checked before the session lookup, so an invalid key returns `403` even when the session UUID does not exist.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "example": "You do not have permission to perform this action." + } + } + }, + "examples": { + "InvalidKey": { + "summary": "Missing or invalid x-api-key header", + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No User Verification (KYC) session with this UUID is visible to your application: the UUID does not exist, the session was deleted, or it belongs to a different application. Business Verification (KYB) session IDs also return `404` here — the reviews feed is only addressable for KYC sessions.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "example": "Session not found." + } + } + }, + "examples": { + "NotFound": { + "summary": "Unknown, deleted, or cross-application session UUID", + "value": { + "detail": "Session not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. Inspect `Retry-After` and `X-RateLimit-*` response headers. See [Rate limiting](/integration/rate-limiting)." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -s 'https://verification.didit.me/v3/sessions/8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d/reviews/?limit=50' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "Python", + "label": "Python (requests)", + "source": "import os, requests\n\nsession_id = \"8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d\"\nurl = f\"https://verification.didit.me/v3/sessions/{session_id}/reviews/\"\n\nwhile url: # walk every page of the feed\n resp = requests.get(url, headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]}, timeout=10)\n resp.raise_for_status()\n page = resp.json()\n for entry in page[\"results\"]:\n print(entry[\"created_at\"], entry[\"activity_type\"], entry[\"actor_display\"], entry.get(\"new_status\"))\n url = page[\"next\"]" + }, + { + "lang": "JavaScript", + "label": "Node.js (fetch)", + "source": "const sessionId = \"8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d\";\nconst res = await fetch(\n `https://verification.didit.me/v3/sessions/${sessionId}/reviews/`,\n { headers: { 'x-api-key': 'YOUR_API_KEY' } },\n);\nif (!res.ok) throw new Error(`Didit ${res.status}`);\nconst { count, results } = await res.json();\nconsole.log(`${count} feed entries`);\nfor (const entry of results) {\n console.log(entry.created_at, entry.activity_type, entry.actor_display, entry.new_status ?? \"\");\n}" + } + ] + }, + "post": { + "summary": "Add a review note to a session's audit trail", + "description": "Append a note to a User Verification (KYC) session's review / activity feed. **This endpoint only records an entry — it never changes the session.** The session's `status` is left untouched, no status-transition validation runs, and no webhook fires. To actually approve, decline, or re-open a session (which also writes a fully-attributed `STATUS_UPDATED` entry to this feed), use `PATCH /v3/session/{session_id}/update-status/` instead.\n\nBoth body fields are optional and an empty JSON object is accepted. `new_status` is stored purely as a label on the entry and accepts any of the ten session status values (case-sensitive). `@email` mentions inside `comment` are stored as plain text only — they trigger no notifications on this endpoint, and the entry's `mentioned_emails` stays empty.\n\nNote on attribution: entries created here carry no actor information. In the feed they show `actor_display: \"Unknown\"`, `actor_type: \"CONSOLE_USER\"`, and `activity_type: \"STATUS_UPDATED\"` (storage defaults), and the `201` body's `user` field is `\"Unknown\"`.", + "operationId": "create_session_review", + "x-didit-sdk": { + "resource": "sessions.reviews", + "method": "create", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "Content-Type", + "in": "header", + "required": false, + "description": "Set to `application/json` when sending a JSON body.", + "schema": { + "type": "string", + "example": "application/json" + } + }, + { + "name": "session_id", + "in": "path", + "required": true, + "description": "UUID of the User Verification (KYC) session. Must belong to the same application as the API key — cross-application UUIDs, deleted sessions, and Business Verification (KYB) session IDs all return `404`.", + "schema": { + "type": "string", + "format": "uuid", + "example": "8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "new_status": { + "type": "string", + "nullable": true, + "enum": [ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User" + ], + "description": "Optional status label to record on the entry. Accepts any session status value, case-sensitive (`\"approved\"` is rejected with `400`). Recording a status here does **not** change the session's actual status — use the update-status endpoint for that.", + "example": "In Review" + }, + "comment": { + "type": "string", + "nullable": true, + "description": "Optional free-text note (no enforced length limit). Stored verbatim; `@email` mentions are not processed on this endpoint.", + "example": "Flagging for manual document re-check." + } + } + }, + "examples": { + "NoteWithStatusLabel": { + "summary": "Note carrying a status label (session status unchanged)", + "value": { + "new_status": "In Review", + "comment": "Flagging for manual document re-check." + } + }, + "CommentOnly": { + "summary": "Plain note without a status label", + "value": { + "comment": "Customer emailed an updated utility bill; awaiting upload." + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Entry recorded on the session's audit trail. The session itself is unchanged — its `status` keeps its previous value and no webhook is sent. The new entry also appears at the top of `GET …/reviews/` (with `actor_display: \"Unknown\"`).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "user": { + "type": "string", + "description": "Display name of the entry's author. Always `\"Unknown\"` for entries created through this endpoint, because it records no actor attribution.", + "example": "Unknown" + }, + "new_status": { + "type": "string", + "nullable": true, + "enum": [ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User" + ], + "description": "The status label recorded on the entry, or `null` if none was sent. The session's actual status is not affected.", + "example": "In Review" + }, + "comment": { + "type": "string", + "nullable": true, + "description": "The note recorded on the entry, or `null` if none was sent.", + "example": "Flagging for manual document re-check." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the entry was written, ISO 8601 UTC.", + "example": "2026-06-12T00:18:44.466537Z" + } + } + }, + "examples": { + "NoteWithStatusLabel": { + "summary": "Note with a status label", + "value": { + "user": "Unknown", + "new_status": "In Review", + "comment": "Flagging for manual document re-check.", + "created_at": "2026-06-12T00:18:44.466537Z" + } + }, + "CommentOnly": { + "summary": "Plain note", + "value": { + "user": "Unknown", + "new_status": null, + "comment": "Note without status.", + "created_at": "2026-06-12T00:18:44.504157Z" + } + } + } + } + } + }, + "400": { + "description": "Validation error, returned as a per-field map of messages. The usual cause is a `new_status` value that is not one of the ten allowed statuses — the check is case-sensitive.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "new_status": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "\"approved\" is not a valid choice." + ] + } + } + }, + "examples": { + "InvalidStatusChoice": { + "summary": "Lowercase / unknown status value", + "value": { + "new_status": [ + "\"approved\" is not a valid choice." + ] + } + } + } + } + } + }, + "403": { + "description": "Missing, malformed, expired, or revoked `x-api-key`. Authentication is checked before the session lookup, so an invalid key returns `403` even when the session UUID does not exist.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "example": "You do not have permission to perform this action." + } + } + }, + "examples": { + "InvalidKey": { + "summary": "Missing or invalid x-api-key header", + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No User Verification (KYC) session with this UUID is visible to your application: the UUID does not exist, the session was deleted, or it belongs to a different application. Business Verification (KYB) session IDs also return `404` here — the reviews feed is only addressable for KYC sessions.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "example": "Session not found." + } + } + }, + "examples": { + "NotFound": { + "summary": "Unknown, deleted, or cross-application session UUID", + "value": { + "detail": "Session not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. Inspect `Retry-After` and `X-RateLimit-*` response headers. See [Rate limiting](/integration/rate-limiting)." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "# Records an audit-trail note only -- the session's status does NOT change.\ncurl -s -X POST https://verification.didit.me/v3/sessions/8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d/reviews/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"new_status\": \"In Review\",\n \"comment\": \"Flagging for manual document re-check.\"\n }'" + }, + { + "lang": "Python", + "label": "Python (requests)", + "source": "import os, requests\n\n# Records an audit-trail note only -- the session's status does NOT change.\n# To approve/decline a session use PATCH /v3/session/{session_id}/update-status/.\nsession_id = \"8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d\"\nresp = requests.post(\n f\"https://verification.didit.me/v3/sessions/{session_id}/reviews/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n },\n json={\n \"new_status\": \"In Review\",\n \"comment\": \"Flagging for manual document re-check.\",\n },\n timeout=10,\n)\nresp.raise_for_status()\nprint(resp.json()) # {'user': 'Unknown', 'new_status': 'In Review', ...}" + }, + { + "lang": "JavaScript", + "label": "Node.js (fetch)", + "source": "// Records an audit-trail note only -- the session's status does NOT change.\n// To approve/decline a session use PATCH /v3/session/{session_id}/update-status/.\nconst sessionId = \"8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d\";\nconst res = await fetch(\n `https://verification.didit.me/v3/sessions/${sessionId}/reviews/`,\n {\n method: \"POST\",\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n new_status: \"In Review\",\n comment: \"Flagging for manual document re-check.\",\n }),\n },\n);\nif (!res.ok) throw new Error(`Didit ${res.status}: ${await res.text()}`);\nconsole.log(await res.json()); // { user: 'Unknown', new_status: 'In Review', ... }" + } + ] + } + }, + "/v3/billing/balance/": { + "get": { + "summary": "Get credit balance", + "description": "Get current pre-paid credit balance in USD (organization-wide) plus auto-refill config. Amounts are decimal strings — parse with a decimal library.", + "operationId": "get_billing_balance", + "x-didit-sdk": { + "resource": "billing", + "method": "retrieve_balance", + "auth": "api_key", + "server": true + }, + "tags": [ + "Billing" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/billing/balance/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nfrom decimal import Decimal\n\nimport requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/billing/balance/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\ndata = resp.json()\nbalance = Decimal(data['balance'])\nif balance < Decimal('50'):\n print(f'Low balance: ${balance} - top up before launching new sessions.')\nelse:\n print(f'Balance OK: ${balance}')" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/billing/balance/', {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Billing balance failed: ${resp.status}`);\nconst { balance, auto_refill_enabled, auto_refill_threshold } = await resp.json();\nconsole.log({ balance, auto_refill_enabled, auto_refill_threshold });" + } + ], + "responses": { + "200": { + "description": "Current credit balance plus the organization's auto-refill configuration.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "balance", + "auto_refill_enabled" + ], + "properties": { + "balance": { + "type": "string", + "description": "Current pre-paid credit balance in USD, as a fixed-point decimal string. Decreases as verifications are billed and increases when a top-up settles." + }, + "auto_refill_enabled": { + "type": "boolean", + "description": "When `true`, Didit will automatically charge the saved payment method to add credits once the balance crosses `auto_refill_threshold`. Configured from Console → Billing." + }, + "auto_refill_amount": { + "type": "string", + "nullable": true, + "description": "USD amount that will be charged on each auto-refill. `null` when auto-refill is disabled or unset." + }, + "auto_refill_threshold": { + "type": "string", + "nullable": true, + "description": "Trigger threshold in USD. Auto-refill fires when `balance` falls below this value. `null` when auto-refill is disabled or unset." + } + } + }, + "examples": { + "Healthy balance with auto-refill": { + "value": { + "balance": "1234.5600", + "auto_refill_enabled": true, + "auto_refill_amount": "500.00", + "auto_refill_threshold": "100.00" + } + }, + "Low balance, manual top-up only": { + "value": { + "balance": "12.0000", + "auto_refill_enabled": false, + "auto_refill_amount": null, + "auto_refill_threshold": null + } + } + } + } + } + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the key cannot read billing data for this organization. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Throttled": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/billing/top-up/": { + "post": { + "summary": "Top up credits", + "description": "Add credits via a hosted checkout session (**$50 minimum**); not available on annual plans. Redirect the customer to the returned `checkout_session_url`.", + "operationId": "billing_top_up", + "x-didit-sdk": { + "resource": "billing", + "method": "top_up", + "auth": "api_key", + "server": true + }, + "tags": [ + "Billing" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "amount_in_dollars" + ], + "properties": { + "amount_in_dollars": { + "type": "number", + "minimum": 50, + "description": "Amount to charge, in USD. **Must be at least 50.** Tax is added automatically at checkout based on the customer's billing address." + }, + "success_url": { + "type": "string", + "format": "uri", + "description": "URL the hosted checkout redirects the customer to after a successful payment. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. Append the literal placeholder `{CHECKOUT_SESSION_ID}` if you want the hosted checkout to substitute the session ID." + }, + "cancel_url": { + "type": "string", + "format": "uri", + "description": "URL the hosted checkout redirects the customer to when they abort or close the Checkout window. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable." + } + } + }, + "examples": { + "Minimum top-up": { + "value": { + "amount_in_dollars": 50 + } + }, + "Custom amount with redirects": { + "value": { + "amount_in_dollars": 500, + "success_url": "https://yourapp.com/billing/top-up/success", + "cancel_url": "https://yourapp.com/billing/top-up/cancel" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/billing/top-up/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"amount_in_dollars\": 500,\n \"success_url\": \"https://yourapp.com/billing/success\",\n \"cancel_url\": \"https://yourapp.com/billing/cancel\"\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/billing/top-up/',\n headers={\n 'x-api-key': os.environ['DIDIT_API_KEY'],\n 'Content-Type': 'application/json',\n },\n json={\n 'amount_in_dollars': 500,\n 'success_url': 'https://yourapp.com/billing/success',\n 'cancel_url': 'https://yourapp.com/billing/cancel',\n },\n timeout=10,\n)\nresp.raise_for_status()\nsession = resp.json()\nprint('Redirect customer to:', session['checkout_session_url'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/billing/top-up/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n amount_in_dollars: 500,\n success_url: 'https://yourapp.com/billing/success',\n cancel_url: 'https://yourapp.com/billing/cancel',\n }),\n});\nif (!resp.ok) throw new Error(`Top-up failed: ${resp.status}`);\nconst { checkout_session_url } = await resp.json();\nwindow.location.assign(checkout_session_url);" + } + ], + "responses": { + "200": { + "description": "Hosted checkout session created. Redirect the user to `checkout_session_url` to complete payment.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "checkout_session_id", + "checkout_session_url" + ], + "properties": { + "checkout_session_id": { + "type": "string", + "description": "Hosted checkout session identifier (e.g. `cs_test_a1B2c3...`). Useful for server-side reconciliation." + }, + "checkout_session_url": { + "type": "string", + "format": "uri", + "description": "Single-use hosted payment page URL. Redirect the customer here to complete payment." + } + } + }, + "examples": { + "Created": { + "value": { + "checkout_session_id": "cs_test_a1B2c3D4e5F6g7H8i9J0", + "checkout_session_url": "https://checkout.example.com/c/pay/cs_test_a1B2c3D4e5F6g7H8i9J0" + } + } + } + } + } + }, + "400": { + "description": "Invalid payload — the amount is missing, not a number, below the $50 minimum, or the organization is on an annual plan. The error body is a JSON array.", + "content": { + "application/json": { + "examples": { + "Missing amount": { + "value": [ + "amount_in_dollars is required." + ] + }, + "Not a number": { + "value": [ + "amount_in_dollars must be a number." + ] + }, + "Below minimum": { + "value": [ + "Amount must be at least $50." + ] + }, + "Annual plan": { + "value": [ + "You have an annual plan. Contact your account manager to top up." + ] + } + } + } + } + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the key cannot create top-up sessions for this organization. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Throttled": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/webhook/destinations/": { + "get": { + "summary": "List webhook destinations", + "description": "List all webhook destinations (unpaginated array, oldest first). Each item carries the destination's configuration, its delivery metrics for the last 30 days (`metrics_window_days`) with one `daily_points` entry per day, and its health (`health_status`, `failing_streak`, `failing_since`, `last_success_at`). The list has no `summary` object and never includes the signing secret: each item shows only `secret_last4`. Fetch a single destination (`GET /v3/webhook/destinations/{destination_uuid}/`) to read `secret_shared_key`.", + "operationId": "list_webhook_destinations", + "x-didit-sdk": { + "resource": "webhooks.destinations", + "method": "list", + "auth": "api_key", + "server": true + }, + "tags": [ + "Webhook" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/webhook/destinations/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/webhook/destinations/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nfor dest in resp.json():\n print(f\"{dest['label']:30s} {dest['url']:50s} {dest['health_status']}\")" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/webhook/destinations/', {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nconst destinations = await resp.json();\nfor (const d of destinations) {\n console.log(d.label, d.url, d.health_status, `${d.error_rate_percentage}% errors`);\n}" + } + ], + "responses": { + "200": { + "description": "List of webhook destinations (unpaginated, oldest first).", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Stable destination identifier; use as `{destination_uuid}` for update/delete." + }, + "label": { + "type": "string", + "description": "Human-readable name shown in the Console." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Destination URL that receives `POST` requests with the webhook payload." + }, + "enabled": { + "type": "boolean", + "description": "When `false`, Didit silently skips delivery to this destination." + }, + "webhook_version": { + "type": "string", + "enum": [ + "v1", + "v2", + "v3" + ], + "description": "Payload schema version sent to this destination. `v3` is the current/recommended format." + }, + "subscribed_events": { + "$ref": "#/components/schemas/WebhookSubscribedEvents" + }, + "subscribed_events_count": { + "type": "integer", + "description": "Cached length of `subscribed_events`, useful for UI rendering." + }, + "deliveries_count": { + "type": "integer", + "description": "Delivery attempts (retries included) made to this destination in the last 30 days." + }, + "failed_deliveries_count": { + "type": "integer", + "description": "Delivery attempts in the last 30 days that the destination answered with HTTP 400 or above, or did not answer (timeout, refused connection or blocked address)." + }, + "average_response_time_ms": { + "type": "integer", + "nullable": true, + "description": "Average response time in milliseconds over the last 30 days, rounded to an integer. `null` when there were no deliveries in that window." + }, + "error_rate_percentage": { + "type": "integer", + "description": "`round(failed_deliveries_count / deliveries_count * 100)` over the last 30 days; `0` when there are no deliveries." + }, + "last_delivery_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Timestamp of the most recent delivery attempt (success or failure). `null` if never delivered." + }, + "metrics_window_days": { + "type": "integer", + "description": "Length in days of the window that `deliveries_count`, `failed_deliveries_count`, `average_response_time_ms`, `error_rate_percentage` and `daily_points` cover. Always `30`." + }, + "health_status": { + "type": "string", + "enum": [ + "healthy", + "degraded", + "failing" + ], + "description": "Delivery health. `failing`: 3 or more consecutive events failed their first delivery attempt since the destination last answered successfully (see `failing_streak`). `degraded`: not failing, but a delivery attempt failed in the last 24 hours and no later attempt has delivered that event; a retry or resend that delivers it clears this. `healthy`: neither. Independent of `enabled`. A failed attempt is one answered with HTTP 400 or above, or not answered at all." + }, + "failing_streak": { + "type": "integer", + "description": "How many consecutive events failed their first delivery attempt since the destination last answered successfully, within the last 30 days. Retries are not counted, and any successful attempt, including a retry or a resend, ends the streak. `0` when no event has failed since the last success." + }, + "failing_since": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the first failed attempt of the current `failing_streak` was made. `null` when `failing_streak` is `0`." + }, + "last_success_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the destination last answered a delivery with an HTTP status under 400, within the last 30 days. `null` if it has not in that window." + }, + "environment": { + "type": "string", + "enum": [ + "live", + "sandbox" + ], + "description": "Environment of the application that owns the destination." + }, + "daily_points": { + "type": "array", + "description": "One entry per UTC day of the last 30 days, oldest first, including days with no deliveries. Counts every attempt, retries included.", + "items": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date", + "description": "The UTC day." + }, + "total_deliveries": { + "type": "integer", + "description": "Delivery attempts made that day." + }, + "failed_deliveries": { + "type": "integer", + "description": "Attempts made that day that were answered with HTTP 400 or above, or not answered at all." + } + } + } + }, + "secret_last4": { + "type": "string", + "description": "Last four characters of the destination's current signing secret, so you can tell secrets apart. Not enough to verify a signature." + }, + "created_by_email": { + "type": "string", + "nullable": true, + "description": "Email of the Business Console member who created the destination. `null` for a destination created with an API key." + }, + "created_by_name": { + "type": "string", + "nullable": true, + "description": "Name of the Business Console member who created the destination. `null` for a destination created with an API key." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + } + }, + "examples": { + "Two destinations": { + "value": [ + { + "uuid": "11111111-2222-3333-4444-555555555555", + "label": "Production session webhooks", + "url": "https://yourapp.com/webhooks/didit", + "enabled": true, + "webhook_version": "v3", + "subscribed_events": [ + "status.updated", + "data.updated" + ], + "subscribed_events_count": 2, + "deliveries_count": 12453, + "failed_deliveries_count": 14, + "average_response_time_ms": 187, + "error_rate_percentage": 0, + "last_delivery_at": "2026-05-17T09:21:18Z", + "metrics_window_days": 30, + "health_status": "healthy", + "failing_streak": 0, + "failing_since": null, + "last_success_at": "2026-05-17T09:21:18Z", + "environment": "live", + "daily_points": [ + { + "date": "2026-04-18", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-04-19", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-04-20", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-04-21", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-04-22", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-04-23", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-04-24", + "total_deliveries": 415, + "failed_deliveries": 9 + }, + { + "date": "2026-04-25", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-04-26", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-04-27", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-04-28", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-04-29", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-04-30", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-05-01", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-05-02", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-05-03", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-05-04", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-05-05", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-05-06", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-05-07", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-05-08", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-05-09", + "total_deliveries": 415, + "failed_deliveries": 5 + }, + { + "date": "2026-05-10", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-05-11", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-05-12", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-05-13", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-05-14", + "total_deliveries": 415, + "failed_deliveries": 0 + }, + { + "date": "2026-05-15", + "total_deliveries": 416, + "failed_deliveries": 0 + }, + { + "date": "2026-05-16", + "total_deliveries": 416, + "failed_deliveries": 0 + }, + { + "date": "2026-05-17", + "total_deliveries": 416, + "failed_deliveries": 0 + } + ], + "secret_last4": "0xnQ", + "created_by_email": null, + "created_by_name": null, + "created_at": "2026-01-04T08:00:00Z", + "updated_at": "2026-04-22T14:09:11Z" + }, + { + "uuid": "22222222-3333-4444-5555-666666666666", + "label": "Staging mirror", + "url": "https://staging.yourapp.com/webhooks/didit", + "enabled": false, + "webhook_version": "v3", + "subscribed_events": [ + "status.updated" + ], + "subscribed_events_count": 1, + "deliveries_count": 0, + "failed_deliveries_count": 0, + "average_response_time_ms": null, + "error_rate_percentage": 0, + "last_delivery_at": null, + "metrics_window_days": 30, + "health_status": "healthy", + "failing_streak": 0, + "failing_since": null, + "last_success_at": null, + "environment": "live", + "daily_points": [ + { + "date": "2026-04-18", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-04-19", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-04-20", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-04-21", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-04-22", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-04-23", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-04-24", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-04-25", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-04-26", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-04-27", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-04-28", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-04-29", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-04-30", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-01", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-02", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-03", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-04", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-05", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-06", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-07", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-08", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-09", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-10", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-11", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-12", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-13", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-14", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-15", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-16", + "total_deliveries": 0, + "failed_deliveries": 0 + }, + { + "date": "2026-05-17", + "total_deliveries": 0, + "failed_deliveries": 0 + } + ], + "secret_last4": "k9Qe", + "created_by_email": "reviewer@example.com", + "created_by_name": "Example Reviewer", + "created_at": "2026-03-12T10:15:02Z", + "updated_at": "2026-03-12T10:15:02Z" + } + ] + } + } + } + } + }, + "403": { + "description": "API key missing, malformed, expired, or scoped to a different application. Didit returns 403 (never 401) for all authentication failures on this endpoint.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry.", + "content": { + "application/json": { + "examples": { + "Throttled": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "post": { + "summary": "Create webhook destination", + "description": "Create a webhook destination. The response includes the auto-generated `secret_shared_key` HMAC signing secret (also readable later via `GET /v3/webhook/destinations/{destination_uuid}/`). Always send `subscribed_events` with at least one event (no wildcards): an explicit empty list is rejected with 400, and omitting the field creates a destination subscribed to nothing, which never receives a webhook. The `(application, url)` pair must be unique among the application's destinations.", + "operationId": "create_webhook_destination", + "x-didit-sdk": { + "resource": "webhooks.destinations", + "method": "create", + "auth": "api_key", + "server": true + }, + "tags": [ + "Webhook" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/webhook/destinations/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"label\": \"Production session webhooks\",\n \"url\": \"https://yourapp.com/webhooks/didit\",\n \"webhook_version\": \"v3\",\n \"subscribed_events\": [\"status.updated\", \"data.updated\"]\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/webhook/destinations/',\n headers={\n 'x-api-key': os.environ['DIDIT_API_KEY'],\n 'Content-Type': 'application/json',\n },\n json={\n 'label': 'Production session webhooks',\n 'url': 'https://yourapp.com/webhooks/didit',\n 'webhook_version': 'v3',\n 'subscribed_events': ['status.updated', 'data.updated'],\n },\n timeout=10,\n)\nresp.raise_for_status()\ndestination = resp.json()\n# Store secret_shared_key in your secret manager.\nprint('Destination UUID:', destination['uuid'])\nprint('Signing secret:', destination['secret_shared_key'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/webhook/destinations/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n label: 'Production session webhooks',\n url: 'https://yourapp.com/webhooks/didit',\n webhook_version: 'v3',\n subscribed_events: ['status.updated', 'data.updated'],\n }),\n});\nif (!resp.ok) throw new Error(await resp.text());\nconst destination = await resp.json();\nconsole.log('Store this secret securely:', destination.secret_shared_key);" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "label", + "url" + ], + "properties": { + "label": { + "type": "string", + "maxLength": 255, + "description": "Human-readable name displayed in Console → Webhooks. Helps distinguish destinations when you have several (e.g. \"Prod\", \"Staging mirror\")." + }, + "url": { + "type": "string", + "format": "uri", + "description": "HTTPS URL that will receive `POST` requests. Must be unique among the application's destinations. Use a stable, hardened endpoint: Didit retries failed deliveries on a backoff schedule." + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "When `false`, the destination is saved but Didit will not push any webhooks to it. Useful for staging configs you want to keep parked." + }, + "webhook_version": { + "type": "string", + "enum": [ + "v1", + "v2", + "v3" + ], + "description": "Payload schema version Didit will send. `v3` is the current/recommended format; older versions are kept for backward compatibility with legacy integrations." + }, + "subscribed_events": { + "allOf": [ + { + "$ref": "#/components/schemas/WebhookSubscribedEvents" + } + ], + "description": "Events to deliver. Effectively required: an explicit `[]` is rejected with 400, and omitting the field creates a destination with no subscriptions that receives nothing." + } + } + }, + "examples": { + "session_updates": { + "summary": "Session status and data updates", + "value": { + "label": "Production session webhooks", + "url": "https://yourapp.com/webhooks/didit", + "webhook_version": "v3", + "subscribed_events": [ + "status.updated", + "data.updated" + ] + } + }, + "transaction_updates": { + "summary": "Transaction monitoring updates", + "value": { + "label": "Transaction monitoring webhooks", + "url": "https://yourapp.com/webhooks/didit-transactions", + "webhook_version": "v3", + "subscribed_events": [ + "transaction.created", + "transaction.status.updated" + ] + } + }, + "entity_updates": { + "summary": "User and Business entity updates", + "value": { + "label": "Entity lifecycle webhooks", + "url": "https://yourapp.com/webhooks/didit-entities", + "webhook_version": "v3", + "subscribed_events": [ + "user.status.updated", + "user.data.updated", + "business.status.updated", + "business.data.updated", + "activity.created" + ] + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Destination created. `secret_shared_key` is the HMAC signing secret — store it now.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Stable destination identifier. Use as `{destination_uuid}` in subsequent calls." + }, + "label": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "enabled": { + "type": "boolean" + }, + "webhook_version": { + "type": "string", + "enum": [ + "v1", + "v2", + "v3" + ] + }, + "secret_shared_key": { + "type": "string", + "description": "Auto-generated HMAC signing secret bound to this destination (43-character URL-safe token). Use it to verify the signature headers on incoming webhooks. Always returned in full to API-key callers; Console users need the `read:webhooks` permission, otherwise they see an empty string." + }, + "secret_last4": { + "type": "string", + "description": "Last four characters of the destination's current signing secret, so you can tell secrets apart. Not enough to verify a signature." + }, + "previous_secret_expires_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Set for 24 hours after the destination's signing secret is rotated in the Business Console. Until this time, every delivery to the destination also carries `X-Signature-Previous`, `X-Signature-V2-Previous` and `X-Signature-Simple-Previous`, made with the replaced secret (see [Signature verification](/integration/webhooks#signature-verification)). `null` when no rotation happened in the last 24 hours." + }, + "subscribed_events": { + "$ref": "#/components/schemas/WebhookSubscribedEvents" + }, + "created_by_email": { + "type": "string", + "nullable": true, + "description": "Email of the Business Console member who created the destination. `null` for a destination created with an API key." + }, + "created_by_name": { + "type": "string", + "nullable": true, + "description": "Name of the Business Console member who created the destination. `null` for a destination created with an API key." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "health_status": { + "type": "string", + "enum": [ + "healthy", + "degraded", + "failing" + ], + "description": "Delivery health. `failing`: 3 or more consecutive events failed their first delivery attempt since the destination last answered successfully (see `failing_streak`). `degraded`: not failing, but a delivery attempt failed in the last 24 hours and no later attempt has delivered that event; a retry or resend that delivers it clears this. `healthy`: neither. Independent of `enabled`. A failed attempt is one answered with HTTP 400 or above, or not answered at all." + }, + "failing_streak": { + "type": "integer", + "description": "How many consecutive events failed their first delivery attempt since the destination last answered successfully, within the last 30 days. Retries are not counted, and any successful attempt, including a retry or a resend, ends the streak. `0` when no event has failed since the last success." + }, + "failing_since": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the first failed attempt of the current `failing_streak` was made. `null` when `failing_streak` is `0`." + }, + "last_success_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the destination last answered a delivery with an HTTP status under 400, within the last 30 days. `null` if it has not in that window." + }, + "environment": { + "type": "string", + "enum": [ + "live", + "sandbox" + ], + "description": "Environment of the application that owns the destination." + }, + "summary": { + "type": "object", + "description": "Delivery metrics for the last `metrics_window_days` (30) days. `last_delivery_at` is the most recent attempt, whenever it was made.", + "properties": { + "total_deliveries": { + "type": "integer" + }, + "failed_deliveries": { + "type": "integer" + }, + "error_rate_percentage": { + "type": "integer" + }, + "min_response_time_ms": { + "type": "integer", + "nullable": true + }, + "avg_response_time_ms": { + "type": "integer", + "nullable": true + }, + "max_response_time_ms": { + "type": "integer", + "nullable": true + }, + "last_delivery_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "metrics_window_days": { + "type": "integer", + "description": "Length in days of the window the other summary metrics cover. Always `30`." + } + } + } + } + }, + "examples": { + "Created": { + "value": { + "uuid": "11111111-2222-3333-4444-555555555555", + "label": "Production session webhooks", + "url": "https://yourapp.com/webhooks/didit", + "enabled": true, + "webhook_version": "v3", + "secret_shared_key": "tno2NTeRC1ZK3sBOYlBJDyEzBVENl3pQ1AcZyAW0xnQ", + "secret_last4": "0xnQ", + "previous_secret_expires_at": null, + "subscribed_events": [ + "status.updated", + "data.updated" + ], + "created_by_email": null, + "created_by_name": null, + "created_at": "2026-05-17T10:00:00Z", + "updated_at": "2026-05-17T10:00:00Z", + "health_status": "healthy", + "failing_streak": 0, + "failing_since": null, + "last_success_at": null, + "environment": "live", + "summary": { + "total_deliveries": 0, + "failed_deliveries": 0, + "error_rate_percentage": 0, + "min_response_time_ms": null, + "avg_response_time_ms": null, + "max_response_time_ms": null, + "last_delivery_at": null, + "metrics_window_days": 30 + } + } + } + } + } + } + }, + "400": { + "description": "Validation failed — usually a duplicate URL, missing/invalid `subscribed_events`, or unknown event name.", + "content": { + "application/json": { + "examples": { + "Duplicate URL": { + "value": { + "url": [ + "This webhook destination already exists for the application." + ] + } + }, + "Empty events list": { + "value": { + "subscribed_events": [ + "At least one subscribed event is required." + ] + } + }, + "Unknown event": { + "value": { + "subscribed_events": [ + "Unsupported webhook events: foo.bar" + ] + } + } + } + } + } + }, + "403": { + "description": "API key missing, malformed, expired, or scoped to a different application. Didit returns 403 (never 401) for all authentication failures on this endpoint.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry.", + "content": { + "application/json": { + "examples": { + "Throttled": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/webhook/destinations/{destination_uuid}/": { + "get": { + "summary": "Get webhook destination", + "description": "Retrieve one webhook destination with its `secret_shared_key` signing secret, its health (`health_status`, `failing_streak`, `failing_since`, `last_success_at`) and a delivery `summary` for the last 30 days. API-key callers always receive the secret in full; Console users without `read:webhooks` see an empty string.", + "operationId": "get_webhook_destination", + "x-didit-sdk": { + "resource": "webhooks.destinations", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "tags": [ + "Webhook" + ], + "parameters": [ + { + "name": "destination_uuid", + "in": "path", + "required": true, + "description": "UUID of the destination (the `uuid` returned from create / list).", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "11111111-2222-3333-4444-555555555555" + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nuuid = '11111111-2222-3333-4444-555555555555'\nresp = requests.get(\n f'https://verification.didit.me/v3/webhook/destinations/{uuid}/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nprint(resp.json())" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const uuid = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(`https://verification.didit.me/v3/webhook/destinations/${uuid}/`, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Lookup failed: ${resp.status}`);\nconst destination = await resp.json();\nconsole.log(destination);" + } + ], + "responses": { + "200": { + "description": "Webhook destination details.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "label": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "enabled": { + "type": "boolean" + }, + "webhook_version": { + "type": "string", + "enum": [ + "v1", + "v2", + "v3" + ] + }, + "secret_shared_key": { + "type": "string", + "description": "HMAC signing secret (43-character URL-safe token). Always returned in full to API-key callers; Console users need `read:webhooks`, otherwise an empty string." + }, + "secret_last4": { + "type": "string", + "description": "Last four characters of the destination's current signing secret, so you can tell secrets apart. Not enough to verify a signature." + }, + "previous_secret_expires_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Set for 24 hours after the destination's signing secret is rotated in the Business Console. Until this time, every delivery to the destination also carries `X-Signature-Previous`, `X-Signature-V2-Previous` and `X-Signature-Simple-Previous`, made with the replaced secret (see [Signature verification](/integration/webhooks#signature-verification)). `null` when no rotation happened in the last 24 hours." + }, + "subscribed_events": { + "$ref": "#/components/schemas/WebhookSubscribedEvents" + }, + "created_by_email": { + "type": "string", + "nullable": true, + "description": "Email of the Business Console member who created the destination. `null` for a destination created with an API key." + }, + "created_by_name": { + "type": "string", + "nullable": true, + "description": "Name of the Business Console member who created the destination. `null` for a destination created with an API key." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "health_status": { + "type": "string", + "enum": [ + "healthy", + "degraded", + "failing" + ], + "description": "Delivery health. `failing`: 3 or more consecutive events failed their first delivery attempt since the destination last answered successfully (see `failing_streak`). `degraded`: not failing, but a delivery attempt failed in the last 24 hours and no later attempt has delivered that event; a retry or resend that delivers it clears this. `healthy`: neither. Independent of `enabled`. A failed attempt is one answered with HTTP 400 or above, or not answered at all." + }, + "failing_streak": { + "type": "integer", + "description": "How many consecutive events failed their first delivery attempt since the destination last answered successfully, within the last 30 days. Retries are not counted, and any successful attempt, including a retry or a resend, ends the streak. `0` when no event has failed since the last success." + }, + "failing_since": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the first failed attempt of the current `failing_streak` was made. `null` when `failing_streak` is `0`." + }, + "last_success_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the destination last answered a delivery with an HTTP status under 400, within the last 30 days. `null` if it has not in that window." + }, + "environment": { + "type": "string", + "enum": [ + "live", + "sandbox" + ], + "description": "Environment of the application that owns the destination." + }, + "summary": { + "type": "object", + "description": "Delivery metrics for the last `metrics_window_days` (30) days. `last_delivery_at` is the most recent attempt, whenever it was made.", + "properties": { + "total_deliveries": { + "type": "integer" + }, + "failed_deliveries": { + "type": "integer" + }, + "error_rate_percentage": { + "type": "integer" + }, + "min_response_time_ms": { + "type": "integer", + "nullable": true + }, + "avg_response_time_ms": { + "type": "integer", + "nullable": true + }, + "max_response_time_ms": { + "type": "integer", + "nullable": true + }, + "last_delivery_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "metrics_window_days": { + "type": "integer", + "description": "Length in days of the window the other summary metrics cover. Always `30`." + } + } + } + } + }, + "examples": { + "Healthy destination": { + "value": { + "uuid": "11111111-2222-3333-4444-555555555555", + "label": "Production session webhooks", + "url": "https://yourapp.com/webhooks/didit", + "enabled": true, + "webhook_version": "v3", + "secret_shared_key": "tno2NTeRC1ZK3sBOYlBJDyEzBVENl3pQ1AcZyAW0xnQ", + "secret_last4": "0xnQ", + "previous_secret_expires_at": null, + "subscribed_events": [ + "status.updated", + "data.updated" + ], + "created_by_email": null, + "created_by_name": null, + "created_at": "2026-01-04T08:00:00Z", + "updated_at": "2026-04-22T14:09:11Z", + "health_status": "healthy", + "failing_streak": 0, + "failing_since": null, + "last_success_at": "2026-05-17T09:21:18Z", + "environment": "live", + "summary": { + "total_deliveries": 12453, + "failed_deliveries": 14, + "error_rate_percentage": 0, + "min_response_time_ms": 41, + "avg_response_time_ms": 187, + "max_response_time_ms": 2103, + "last_delivery_at": "2026-05-17T09:21:18Z", + "metrics_window_days": 30 + } + } + } + } + } + } + }, + "403": { + "description": "API key missing, malformed, expired, or scoped to a different application. Didit returns 403 (never 401) for all authentication failures on this endpoint.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No destination with this UUID exists, or it belongs to a different application, or it has been deleted.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Webhook destination not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry.", + "content": { + "application/json": { + "examples": { + "Throttled": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "patch": { + "summary": "Update webhook destination", + "description": "Update a webhook destination. **`subscribed_events` is REPLACED wholesale, not merged**: fetch first, append, resend. Secret is not rotated. A `uuid` in the body is ignored: the path names the destination.", + "operationId": "update_webhook_destination", + "x-didit-sdk": { + "resource": "webhooks.destinations", + "method": "update", + "auth": "api_key", + "server": true + }, + "tags": [ + "Webhook" + ], + "parameters": [ + { + "name": "destination_uuid", + "in": "path", + "required": true, + "description": "UUID of the destination to update.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "11111111-2222-3333-4444-555555555555" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "New human-readable name." + }, + "url": { + "type": "string", + "format": "uri", + "description": "New target URL. Must be unique within the application." + }, + "enabled": { + "type": "boolean", + "description": "Set to `false` to pause deliveries to this destination without deleting it." + }, + "webhook_version": { + "type": "string", + "enum": [ + "v1", + "v2", + "v3" + ], + "description": "New payload schema version for future deliveries." + }, + "subscribed_events": { + "$ref": "#/components/schemas/WebhookSubscribedEvents" + } + } + }, + "examples": { + "Pause destination": { + "value": { + "enabled": false + } + }, + "Add transaction events": { + "value": { + "subscribed_events": [ + "status.updated", + "data.updated", + "transaction.created", + "transaction.status.updated" + ] + } + }, + "Move target URL": { + "value": { + "label": "Production session webhooks v2", + "url": "https://api.yourapp.com/webhooks/didit/v2" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH 'https://verification.didit.me/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"subscribed_events\": [\n \"status.updated\",\n \"data.updated\",\n \"transaction.created\"\n ]\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nuuid = '11111111-2222-3333-4444-555555555555'\nresp = requests.patch(\n f'https://verification.didit.me/v3/webhook/destinations/{uuid}/',\n headers={\n 'x-api-key': os.environ['DIDIT_API_KEY'],\n 'Content-Type': 'application/json',\n },\n json={'enabled': False},\n timeout=10,\n)\nresp.raise_for_status()\nprint('Paused destination:', resp.json()['label'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const uuid = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(`https://verification.didit.me/v3/webhook/destinations/${uuid}/`, {\n method: 'PATCH',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ enabled: false }),\n});\nif (!resp.ok) throw new Error(await resp.text());\nconsole.log(await resp.json());" + } + ], + "responses": { + "200": { + "description": "Updated webhook destination. Body has the same shape as `GET /v3/webhook/destinations/{destination_uuid}/`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "label": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "enabled": { + "type": "boolean" + }, + "webhook_version": { + "type": "string", + "enum": [ + "v1", + "v2", + "v3" + ] + }, + "secret_shared_key": { + "type": "string" + }, + "secret_last4": { + "type": "string", + "description": "Last four characters of the destination's current signing secret, so you can tell secrets apart. Not enough to verify a signature." + }, + "previous_secret_expires_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Set for 24 hours after the destination's signing secret is rotated in the Business Console. Until this time, every delivery to the destination also carries `X-Signature-Previous`, `X-Signature-V2-Previous` and `X-Signature-Simple-Previous`, made with the replaced secret (see [Signature verification](/integration/webhooks#signature-verification)). `null` when no rotation happened in the last 24 hours." + }, + "subscribed_events": { + "$ref": "#/components/schemas/WebhookSubscribedEvents" + }, + "created_by_email": { + "type": "string", + "nullable": true, + "description": "Email of the Business Console member who created the destination. `null` for a destination created with an API key." + }, + "created_by_name": { + "type": "string", + "nullable": true, + "description": "Name of the Business Console member who created the destination. `null` for a destination created with an API key." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "health_status": { + "type": "string", + "enum": [ + "healthy", + "degraded", + "failing" + ], + "description": "Delivery health. `failing`: 3 or more consecutive events failed their first delivery attempt since the destination last answered successfully (see `failing_streak`). `degraded`: not failing, but a delivery attempt failed in the last 24 hours and no later attempt has delivered that event; a retry or resend that delivers it clears this. `healthy`: neither. Independent of `enabled`. A failed attempt is one answered with HTTP 400 or above, or not answered at all." + }, + "failing_streak": { + "type": "integer", + "description": "How many consecutive events failed their first delivery attempt since the destination last answered successfully, within the last 30 days. Retries are not counted, and any successful attempt, including a retry or a resend, ends the streak. `0` when no event has failed since the last success." + }, + "failing_since": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the first failed attempt of the current `failing_streak` was made. `null` when `failing_streak` is `0`." + }, + "last_success_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the destination last answered a delivery with an HTTP status under 400, within the last 30 days. `null` if it has not in that window." + }, + "environment": { + "type": "string", + "enum": [ + "live", + "sandbox" + ], + "description": "Environment of the application that owns the destination." + }, + "summary": { + "type": "object", + "properties": { + "total_deliveries": { + "type": "integer" + }, + "failed_deliveries": { + "type": "integer" + }, + "error_rate_percentage": { + "type": "integer" + }, + "min_response_time_ms": { + "type": "integer", + "nullable": true + }, + "avg_response_time_ms": { + "type": "integer", + "nullable": true + }, + "max_response_time_ms": { + "type": "integer", + "nullable": true + }, + "last_delivery_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "metrics_window_days": { + "type": "integer", + "description": "Length in days of the window the other summary metrics cover. Always `30`." + } + }, + "description": "Delivery metrics for the last `metrics_window_days` (30) days. `last_delivery_at` is the most recent attempt, whenever it was made." + } + } + }, + "examples": { + "Paused": { + "value": { + "uuid": "11111111-2222-3333-4444-555555555555", + "label": "Production session webhooks", + "url": "https://yourapp.com/webhooks/didit", + "enabled": false, + "webhook_version": "v3", + "secret_shared_key": "tno2NTeRC1ZK3sBOYlBJDyEzBVENl3pQ1AcZyAW0xnQ", + "secret_last4": "0xnQ", + "previous_secret_expires_at": null, + "subscribed_events": [ + "status.updated", + "data.updated" + ], + "created_by_email": null, + "created_by_name": null, + "created_at": "2026-01-04T08:00:00Z", + "updated_at": "2026-05-17T11:02:55Z", + "health_status": "healthy", + "failing_streak": 0, + "failing_since": null, + "last_success_at": "2026-05-17T09:21:18Z", + "environment": "live", + "summary": { + "total_deliveries": 12453, + "failed_deliveries": 14, + "error_rate_percentage": 0, + "min_response_time_ms": 41, + "avg_response_time_ms": 187, + "max_response_time_ms": 2103, + "last_delivery_at": "2026-05-17T09:21:18Z", + "metrics_window_days": 30 + } + } + } + } + } + } + }, + "400": { + "description": "Validation failed (duplicate URL, empty/unknown events list, malformed body).", + "content": { + "application/json": { + "examples": { + "Duplicate URL": { + "value": { + "url": [ + "This webhook destination already exists for the application." + ] + } + }, + "Empty events": { + "value": { + "subscribed_events": [ + "At least one subscribed event is required." + ] + } + } + } + } + } + }, + "403": { + "description": "API key missing, malformed, expired, or scoped to a different application. Didit returns 403 (never 401) for all authentication failures on this endpoint.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "Destination with this UUID does not exist (or is deleted, or belongs to another application).", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Webhook destination not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry.", + "content": { + "application/json": { + "examples": { + "Throttled": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "delete": { + "summary": "Delete webhook destination", + "description": "Permanently delete a webhook destination, its signing secret and its entire delivery log. This cannot be undone. No new webhook is sent to the destination's URL; other destinations subscribed to the same events keep receiving them. Afterwards the destination no longer appears in `GET /v3/webhook/destinations/`, and every route under `/v3/webhook/destinations/{destination_uuid}/`, including its deliveries, a delivery's detail and its resend, answers 404. The application's legacy `webhook_url` is set to the URL of the oldest remaining enabled destination, or cleared when no enabled destination is left. Calling DELETE again returns 404.", + "operationId": "delete_webhook_destination", + "x-didit-sdk": { + "resource": "webhooks.destinations", + "method": "delete", + "auth": "api_key", + "server": true + }, + "tags": [ + "Webhook" + ], + "parameters": [ + { + "name": "destination_uuid", + "in": "path", + "required": true, + "description": "UUID of the destination to delete.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "11111111-2222-3333-4444-555555555555" + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X DELETE 'https://verification.didit.me/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nuuid = '11111111-2222-3333-4444-555555555555'\nresp = requests.delete(\n f'https://verification.didit.me/v3/webhook/destinations/{uuid}/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nassert resp.status_code == 204" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const uuid = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(`https://verification.didit.me/v3/webhook/destinations/${uuid}/`, {\n method: 'DELETE',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (resp.status !== 204) throw new Error(`Delete failed: ${resp.status}`);" + } + ], + "responses": { + "204": { + "description": "Destination, signing secret and delivery log deleted permanently. No response body." + }, + "403": { + "description": "API key missing, malformed, expired, or scoped to a different application. Didit returns 403 (never 401) for all authentication failures on this endpoint.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No destination with this UUID exists for the application (already deleted or never existed).", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Webhook destination not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry.", + "content": { + "application/json": { + "examples": { + "Throttled": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/wallet-screening/": { + "post": { + "summary": "Screen a wallet address", + "description": "Run on-demand AML screening for a single crypto wallet/address without creating a transaction. Resolves your application's configured blockchain analytics provider, screens the address synchronously, and returns the normalised risk result. LIGHTNING is available only with an Elliptic or Chainalysis BYOK connection added through Marketplace. Nothing is written to the transactions table. One AML monitoring usage is billed per successful screening (sandbox applications are not billed).", + "operationId": "screenWallet", + "x-didit-sdk": { + "resource": "wallet_screening", + "method": "screen", + "auth": "api_key", + "server": true + }, + "tags": [ + "Transactions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "wallet_address", + "blockchain" + ], + "properties": { + "wallet_address": { + "type": "string", + "description": "The crypto address or network identifier to screen. Must match the selected network (e.g. a `0x`-prefixed 40-hex-char address for EVM chains). Alias: `address`.", + "example": "0x94f1b9b64e2932f6a2db338f616844400cd58e8a" + }, + "blockchain": { + "type": "string", + "description": "Network identifier. Managed screening supports BTC, ETH, LTC, XRP, BCH, DOGE, TRX, SOL, MATIC, BNB, BASE (Base), OP (Optimism), and ARB (Arbitrum One). USDT and USDC are legacy Ethereum aliases, not multi-network searches. Connected-provider coverage varies. LIGHTNING requires a supported connected provider. CELO (Celo), MODE (Mode) and PLUME (Plume) are screened through a connected TRM Labs account; managed screening answers 400 for them. Alias: `currency`.", + "enum": [ + "BTC", + "LIGHTNING", + "ETH", + "LTC", + "XRP", + "BCH", + "DOGE", + "TRX", + "SOL", + "MATIC", + "BNB", + "USDT", + "USDC", + "BASE", + "OP", + "ARB", + "CELO", + "MODE", + "PLUME" + ], + "example": "ETH" + }, + "direction": { + "type": "string", + "description": "Optional. Whether the address is being screened as an inbound (deposit) or outbound (withdrawal) counterparty. Accepts `inbound`, `outbound`, `deposit`, or `withdrawal`. Defaults to a neutral pre-transfer screen when omitted. This is passed to the provider as context only; it does not change the returned `risk_score`, `severity`, or the `source_of_funds`/`destination_of_funds` breakdowns, which are always both returned regardless of `direction`. Changing or omitting `direction` therefore does not require re-screening an address.", + "enum": [ + "inbound", + "outbound", + "deposit", + "withdrawal" + ] + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/wallet-screening/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"wallet_address\": \"0x94f1b9b64e2932f6a2db338f616844400cd58e8a\",\n \"blockchain\": \"ETH\"\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/wallet-screening/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={\n 'wallet_address': '0x94f1b9b64e2932f6a2db338f616844400cd58e8a',\n 'blockchain': 'ETH',\n },\n timeout=20,\n)\nresp.raise_for_status()\nresult = resp.json()\nprint(result['risk_score'], result['severity'], result['sanctions_hit'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const result = await fetch('https://verification.didit.me/v3/wallet-screening/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n wallet_address: '0x94f1b9b64e2932f6a2db338f616844400cd58e8a',\n blockchain: 'ETH',\n }),\n}).then((r) => r.json());\nconsole.log(result.risk_score, result.severity, result.sanctions_hit);" + } + ], + "responses": { + "200": { + "description": "The normalised wallet screening result.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WalletScreeningResult" + }, + "example": { + "provider": "merklescience", + "screening_type": "WALLET_SCREENING", + "risk_score": 95, + "severity": "CRITICAL", + "status": "SCREENED", + "summary": "Merkle Science risk: Critical (5/5). SANCTIONS HIT. Dominant risk: sanctioned. Driven by: the address is attributed to Sanctions / OFAC (Example Sanctions)", + "wallet_address": "0x94f1b9b64e2932f6a2db338f616844400cd58e8a", + "transaction_hash": null, + "blockchain": "ETH", + "block_timestamp": null, + "total_value_usd": null, + "fee_usd": null, + "sender_rows": [], + "receiver_rows": [], + "transaction_flows": [], + "transaction_flows_status": null, + "transaction_flows_error": null, + "source_of_funds": [ + { + "category": "sanctioned", + "percentage": 100.0, + "amount_usd": 9813695.0, + "entity_name": "Example Sanctions", + "entity_type": "Sanctions", + "entity_subtype": "OFAC", + "hops": null, + "description": "Sanctions / OFAC", + "is_direct": true, + "exposure_direction": "incoming", + "exposure_type": "direct", + "provider_entity_id": "118885", + "provider_entity_type_id": "30", + "provider_entity_subtype_id": "125", + "country": "", + "raw_amount": null, + "raw_currency": null + } + ], + "destination_of_funds": [ + { + "category": "defi", + "percentage": 84.824453, + "amount_usd": 7783414.38, + "entity_name": "Example DeFi 7", + "entity_type": "DeFi", + "entity_subtype": "Bridges", + "hops": null, + "description": "DeFi / Bridges", + "is_direct": false, + "exposure_direction": "outgoing", + "exposure_type": "indirect", + "provider_entity_id": "118459", + "provider_entity_type_id": "33", + "provider_entity_subtype_id": "143", + "country": "", + "raw_amount": null, + "raw_currency": null + }, + { + "category": "defi", + "percentage": 15.175547, + "amount_usd": 1392494.37, + "entity_name": "Example DeFi 2", + "entity_type": "DeFi", + "entity_subtype": "Yield Aggregator", + "hops": null, + "description": "DeFi / Yield Aggregator", + "is_direct": false, + "exposure_direction": "outgoing", + "exposure_type": "indirect", + "provider_entity_id": "90192", + "provider_entity_type_id": "33", + "provider_entity_subtype_id": "142", + "country": "", + "raw_amount": null, + "raw_currency": null + } + ], + "counterparty_connections": [ + { + "entity_name": "Example Sanctions", + "entity_type": "Sanctions", + "entity_subtype": "OFAC", + "risk_level": "critical", + "categories": [ + "sanctioned" + ], + "received_usd": null, + "sent_usd": null, + "received_hops": null, + "sent_hops": null, + "percentage": 0.0, + "is_direct": true, + "country": null, + "provider_entity_id": null + }, + { + "entity_name": "Example Sanctions", + "entity_type": "Sanctions", + "entity_subtype": "OFAC", + "risk_level": "critical", + "categories": [ + "sanctioned" + ], + "received_usd": null, + "sent_usd": null, + "received_hops": null, + "sent_hops": null, + "percentage": 0.0, + "is_direct": true, + "country": null, + "provider_entity_id": null + } + ], + "network_nodes": [], + "network_edges": [], + "sanctions_hit": true, + "pep_counterparty": null, + "dominant_risk_category": "sanctioned", + "risk_factors": [ + { + "source": "attribution", + "category": "sanctioned", + "label": "Sanctions / OFAC", + "entity_name": "Example Sanctions", + "direction": "attribution", + "exposure_type": "attribution", + "amount_usd": null, + "percentage": 0.0, + "is_high_risk": true, + "description": "the address is attributed to Sanctions / OFAC (Example Sanctions)" + }, + { + "source": "exposure", + "category": "sanctioned", + "label": "Sanctions / OFAC", + "entity_name": "Example Sanctions", + "direction": "incoming", + "exposure_type": "direct", + "amount_usd": 9813695.0, + "percentage": 51.679304, + "is_high_risk": true, + "description": "51.6793% direct incoming exposure to Sanctions / OFAC (Example Sanctions)" + }, + { + "source": "exposure", + "category": "defi", + "label": "DeFi / Bridges", + "entity_name": "Example DeFi 7", + "direction": "outgoing", + "exposure_type": "indirect", + "amount_usd": 7783414.38, + "percentage": 40.987766, + "is_high_risk": false, + "description": "40.9878% indirect outgoing exposure to DeFi / Bridges (Example DeFi 7)" + }, + { + "source": "exposure", + "category": "defi", + "label": "DeFi / Yield Aggregator", + "entity_name": "Example DeFi 2", + "direction": "outgoing", + "exposure_type": "indirect", + "amount_usd": 1392494.37, + "percentage": 7.33293, + "is_high_risk": false, + "description": "7.33293% indirect outgoing exposure to DeFi / Yield Aggregator (Example DeFi 2)" + } + ], + "raw_response": { + "...": "provider-specific details" + }, + "report_signature": "" + } + } + } + }, + "400": { + "description": "Validation error — malformed wallet address, missing `wallet_address`, unsupported `blockchain`, or LIGHTNING without an Elliptic or Chainalysis BYOK Marketplace connection." + }, + "401": { + "description": "Missing or invalid `x-api-key`." + }, + "409": { + "description": "No AML provider is configured for the application, or the configured provider does not support wallet screening yet. Configure transaction monitoring first." + }, + "502": { + "description": "The AML provider could not complete the screening. Retry later." + } + } + } + }, + "/v3/wallet-screening/pdf/": { + "post": { + "summary": "Render a wallet-screening result as a PDF", + "description": "Stateless companion to `POST /v3/wallet-screening/`: repost the exact JSON body that endpoint returned — including the `report_signature` it added — and get back a Didit-branded PDF of the same result. No provider is called and nothing is billed or persisted; this only re-renders a result Didit already produced. The signature ties the PDF to an unmodified result: editing `risk_score`, `severity`, `sanctions_hit`, or any other signed field before reposting fails with `400`.", + "operationId": "screenWalletPdf", + "x-didit-sdk": { + "resource": "wallet_screening", + "method": "screen_pdf", + "auth": "api_key", + "server": true + }, + "tags": [ + "Transactions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "description": "The unmodified JSON object returned by `POST /v3/wallet-screening/`, including `report_signature`.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/WalletScreeningResult" + } + ], + "required": [ + "report_signature" + ], + "description": "Same shape as the `POST /v3/wallet-screening/` response — pass that response body back verbatim, including `report_signature`." + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "# $RESULT is the saved JSON body from POST /v3/wallet-screening/\ncurl -X POST 'https://verification.didit.me/v3/wallet-screening/pdf/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d \"$RESULT\" \\\n -o wallet_screening_report.pdf" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\n# `result` is the dict returned by POST /v3/wallet-screening/, unmodified\nresponse = requests.post(\n 'https://verification.didit.me/v3/wallet-screening/pdf/',\n headers={'x-api-key': 'YOUR_API_KEY'},\n json=result,\n timeout=30,\n)\nresponse.raise_for_status()\nwith open('wallet_screening_report.pdf', 'wb') as fh:\n fh.write(response.content)" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "// `result` is the object returned by POST /v3/wallet-screening/, unmodified\nconst response = await fetch('https://verification.didit.me/v3/wallet-screening/pdf/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(result),\n});\nif (!response.ok) throw new Error(`PDF generation failed: HTTP ${response.status}`);\nconst pdfBuffer = Buffer.from(await response.arrayBuffer());" + } + ], + "responses": { + "200": { + "description": "The rendered PDF document, returned directly as binary `application/pdf` — there is no JSON wrapper.", + "headers": { + "Content-Disposition": { + "description": "`attachment; filename=wallet_screening_{address}.pdf`, derived from the `wallet_address` (or `transaction_hash`) in the reposted result.", + "schema": { + "type": "string", + "example": "attachment; filename=wallet_screening_0x28c6c06298d514db0899.pdf" + } + } + }, + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "The body is not a JSON object matching the wallet-screening result shape, or `report_signature` is missing or does not match the reposted fields (edited or truncated before repost).", + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "examples": { + "Not an object": { + "value": { + "non_field_errors": [ + "Expected a JSON object matching a wallet-screening result." + ] + } + }, + "Missing or invalid signature": { + "value": { + "report_signature": [ + "This wallet-screening result is missing or fails its report signature - repost the exact JSON returned by the wallet-screening endpoint, unmodified." + ] + } + } + } + } + } + }, + "401": { + "description": "Missing or invalid `x-api-key`." + } + } + } + }, + "/v3/transactions/": { + "get": { + "summary": "List transactions", + "description": "Paginated list of monitored transactions, newest first (`txn_date` desc). `count` is capped at 100; use `total_transactions` for the true row count. This endpoint does not support filtering or search parameters.", + "operationId": "listTransactions", + "x-didit-sdk": { + "resource": "transactions", + "method": "list", + "auth": "api_key", + "server": true + }, + "tags": [ + "Transactions" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50, + "minimum": 1 + }, + "description": "Page size. Defaults to 50 when omitted. There is no enforced maximum, but keep pages small for latency." + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + }, + "description": "Zero-based offset of the first record to return. Prefer following the `next` URL from the previous page." + }, + { + "name": "travel_rule_status", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Return only transactions whose Travel Rule transfer is in this exact status (for example `ON_HOLD` or `AWAITING_COUNTERPARTY`). `count` follows this filter; `total_transactions` still counts every transaction in the application." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/transactions/?limit=50' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/transactions/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n params={'limit': 50},\n timeout=10,\n)\nresp.raise_for_status()\npage = resp.json()\nfor tx in page['results']:\n print(\n tx['transaction_number'],\n tx['status'],\n tx['severity'],\n f\"{tx['amount']} {tx['currency']}\",\n tx.get('subject_name') or tx.get('subject_vendor_data'),\n )" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const url = new URL('https://verification.didit.me/v3/transactions/');\nurl.searchParams.set('limit', '50');\nconst page = await fetch(url, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n}).then((r) => r.json());\nfor (const tx of page.results) {\n console.log(tx.transaction_number, tx.status, tx.severity, tx.amount, tx.currency);\n}" + } + ], + "responses": { + "200": { + "description": "Paginated list of transaction summaries, newest first.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "description": "Capped count of matching transactions (exact up to 100, capped thereafter). Use `total_transactions` for the true count when paginating." + }, + "next": { + "type": "string", + "nullable": true, + "description": "Absolute URL of the next page, or `null` on the last page." + }, + "previous": { + "type": "string", + "nullable": true, + "description": "Absolute URL of the previous page, or `null` on the first page." + }, + "total_transactions": { + "type": "integer", + "description": "Exact total count of (non-deleted) transactions for the application. Present on paginated responses." + }, + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Stable Didit transaction identifier. Use as `{transaction_id}` in detail/notes/related calls." + }, + "transaction_number": { + "type": "integer", + "description": "Application-scoped sequential transaction number shown in Console (e.g. `4123`)." + }, + "txn_id": { + "type": "string", + "description": "Customer-provided identifier from the create call (max 128 chars). Unique per application." + }, + "transaction_type": { + "type": "string", + "description": "Top-level category as stored: `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet`, `gamblingLimitChange`, `gamblingBonusChange`, `auditTrailEvent`. Note multi-word values are echoed back in camelCase even when submitted in snake_case." + }, + "action_type": { + "type": "string", + "description": "Sub-type within the category (e.g. `deposit`, `withdrawal`, `transfer`)." }, "status": { "type": "string", "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Expired" + "APPROVED", + "IN_REVIEW", + "DECLINED", + "AWAITING_USER" ], - "description": "Feature-level status of this ID verification step. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`." + "description": "Current monitoring verdict." }, - "document_type": { + "direction": { + "type": "string", + "enum": [ + "INBOUND", + "OUTBOUND" + ], + "description": "Direction relative to the subject. Stored uppercase regardless of the casing submitted." + }, + "amount": { + "type": "string", + "description": "Transaction amount as a decimal string with trailing zeros stripped (e.g. `\"1500\"`, `\"0.5\"`, `\"0.123456789012345678\"`). Up to 18 decimal places." + }, + "currency": { + "type": "string", + "description": "Currency code of `amount`. Any ISO 4217 alphabetic code is accepted (e.g. `EUR`, `USD`, `NGN`), as is a crypto asset symbol (e.g. `BTC`). Prefer a currently-circulating code. Not every accepted code has an exchange rate: when none is available for the code (for example the ISO accounting unit `XSU`, or a crypto symbol Didit does not price), automatic conversion fails and `preferred_currency_amount` is returned null unless the request supplied a pre-converted `amount_in_default_currency`." + }, + "preferred_currency_amount": { + "type": "string", + "nullable": true, + "description": "`amount` converted to the application's preferred currency, when available." + }, + "preferred_currency_code": { + "type": "string", + "nullable": true, + "description": "Preferred currency code used for `preferred_currency_amount` - the application's base currency, any currently-circulating ISO 4217 code. A code with no available exchange rate leaves `preferred_currency_amount` null unless the request supplied a pre-converted `amount_in_default_currency`." + }, + "payment_details": { + "type": "string", + "nullable": true, + "description": "Free-text payment reference or memo from the submission." + }, + "score": { + "type": "integer", + "description": "Risk score (typically 0–100). Higher = riskier." + }, + "severity": { + "type": "string", + "nullable": true, + "enum": [ + "UNKNOWN", + "LOW", + "MEDIUM", + "HIGH", + "CRITICAL" + ], + "description": "Categorical risk severity set by rules/providers (`UNKNOWN`, `LOW`, `MEDIUM`, `HIGH`, `CRITICAL`). `null` until something sets it." + }, + "subject_name": { + "type": "string", + "nullable": true, + "description": "Full name of the `APPLICANT` party." + }, + "subject_vendor_data": { + "type": "string", + "nullable": true, + "description": "Your internal user/business identifier for the applicant." + }, + "subject_country": { + "type": "string", + "nullable": true, + "description": "Country code of the applicant, from the submitted `address.country` (typically ISO 3166-1 alpha-3)." + }, + "counterparty_name": { + "type": "string", + "nullable": true, + "description": "Full name of the `COUNTERPARTY` party, if any." + }, + "counterparty_vendor_data": { + "type": "string", + "nullable": true, + "description": "Your internal identifier for the counterparty." + }, + "counterparty_country": { + "type": "string", + "nullable": true, + "description": "Country code of the counterparty, from the submitted `address.country`." + }, + "tags": { + "type": "array", + "description": "Tags attached to the transaction (manually or by rules).", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "source": { + "type": "string", + "enum": [ + "didit", + "custom" + ], + "description": "Whether the tag is a Didit preset or a custom tag." + } + } + } + }, + "txn_date": { + "type": "string", + "format": "date-time", + "description": "When the transaction occurred (from `transaction_at` at create time)." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When Didit recorded the submission." + }, + "decision_reason_code": { + "type": "string", + "nullable": true, + "description": "Machine-readable reason for the current `status` (e.g. `WALLET_HIGH_RISK`)." + }, + "decision_reason_label": { + "type": "string", + "nullable": true, + "description": "Human-readable label for `decision_reason_code`." + }, + "assigned_to_name": { + "type": "string", + "nullable": true, + "description": "Display name of the Console reviewer assigned to this transaction." + }, + "travel_rule_status": { + "type": "string", + "nullable": true, + "description": "Status of the transaction's Travel Rule transfer (for example `COMPLETED`), or `null` when it has none." + }, + "travel_rule": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/TransactionTravelRule" + } + ], + "description": "The transaction's Travel Rule transfer, the same block as `travel_rule` on `GET /v3/transactions/{transaction_id}/`, or `null` when the transaction has none." + } + } + } + } + } + }, + "examples": { + "Single transaction": { + "value": { + "count": 1, + "next": null, + "previous": null, + "total_transactions": 1, + "results": [ + { + "uuid": "abcdef12-3456-4890-abcd-ef1234567890", + "transaction_number": 4123, + "txn_id": "your-txn-2026-05-17-001", + "transaction_type": "finance", + "action_type": "withdrawal", + "status": "IN_REVIEW", + "direction": "OUTBOUND", + "amount": "1500", + "currency": "EUR", + "preferred_currency_amount": "1620.45", + "preferred_currency_code": "USD", + "payment_details": "Withdrawal to crypto wallet", + "txn_date": "2026-05-17T08:42:00Z", + "created_at": "2026-05-17T08:42:11Z", + "score": 78, + "severity": "HIGH", + "decision_reason_code": "WALLET_HIGH_RISK", + "decision_reason_label": "Destination wallet flagged as high risk", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "name": "Manual review", + "color": "#F59E0B", + "description": null, + "source": "custom" + } + ], + "subject_name": "Maria Garcia", + "subject_vendor_data": "user-12345", + "counterparty_name": null, + "counterparty_vendor_data": null, + "counterparty_country": null, + "subject_country": null, + "assigned_to_name": "Alex Reviewer", + "travel_rule_status": null, + "travel_rule": null + } + ] + } + } + } + } + } + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the key cannot list transactions for this application. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Throttled": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "post": { + "summary": "Create transaction", + "description": "Submit a transaction for synchronous AML/risk monitoring. The transaction is created `APPROVED`, then your rules (and crypto screening, when applicable) run inline and may flip the returned `status` to `IN_REVIEW` or `DECLINED`. **`transaction_id` is your idempotency key** (max 128 chars, unique per app); reusing it returns 400. A `transaction.created` webhook is dispatched on success for live applications. Sandbox applications are not billed and do not emit `transaction.created` or `transaction.status.updated` webhooks. Sandbox Travel Rule exchanges do emit `travel_rule.status.updated` webhooks with `environment: sandbox`. A Travel Rule transfer is stored and returned before any counterparty is contacted: `travel_rule.status` comes back `AWAITING_COUNTERPARTY` (or `NOT_APPLICABLE` below your threshold, `UNCONFIRMED_OWNERSHIP` for a declared self-hosted wallet, `COMPLETED` when the other wallet is your own verified wallet) and `travel_rule.protocol` is null. Didit then chooses the rail and sends the exchange, and every later status change of the transfer, including `COUNTERPARTY_VASP_NOT_FOUND` when no rail reaches the counterparty, arrives as a `travel_rule.status.updated` webhook. Your rules run again when the rail or the status changes.", + "operationId": "createTransaction", + "tags": [ + "Transactions" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/transactions/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"transaction_id\": \"your-txn-2026-05-17-001\",\n \"transaction_category\": \"finance\",\n \"transaction_details\": {\n \"direction\": \"outbound\",\n \"amount\": 1500,\n \"currency\": \"EUR\",\n \"action_type\": \"withdrawal\"\n },\n \"subject\": {\n \"entity_type\": \"individual\",\n \"vendor_data\": \"user-12345\",\n \"full_name\": \"Maria Garcia\"\n }\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/transactions/',\n headers={\n 'x-api-key': os.environ['DIDIT_API_KEY'],\n 'Content-Type': 'application/json',\n },\n json={\n 'transaction_id': 'your-txn-2026-05-17-001',\n 'transaction_category': 'finance',\n 'transaction_details': {\n 'direction': 'outbound',\n 'amount': 1500,\n 'currency': 'EUR',\n 'action_type': 'withdrawal',\n },\n 'subject': {\n 'entity_type': 'individual',\n 'vendor_data': 'user-12345',\n 'full_name': 'Maria Garcia',\n },\n },\n timeout=15,\n)\nresp.raise_for_status()\ntx = resp.json()\nprint(tx['txn_id'], '->', tx['status'], tx['severity'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/transactions/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n transaction_id: 'your-txn-2026-05-17-001',\n transaction_category: 'finance',\n transaction_details: {\n direction: 'outbound',\n amount: 1500,\n currency: 'EUR',\n action_type: 'withdrawal',\n },\n subject: {\n entity_type: 'individual',\n vendor_data: 'user-12345',\n full_name: 'Maria Garcia',\n },\n }),\n});\nif (!resp.ok) throw new Error(await resp.text());\nconst tx = await resp.json();\nconsole.log(tx.txn_id, tx.status, tx.severity);" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "transaction_id": { + "type": "string", + "description": "Your unique identifier for this transaction (max 128 characters).", + "example": "your-txn-2026-05-17-001" + }, + "transaction_at": { + "type": "string", + "format": "date-time", + "description": "When the transaction occurred. Defaults to now if omitted." + }, + "time_zone": { + "type": "string", + "description": "IANA time zone identifier, e.g. `Europe/Madrid`." + }, + "transaction_category": { + "type": "string", + "description": "Category of the transaction. CamelCase aliases (`travelRule`, `userPlatformEvent`, `gamblingBet`, …) are also accepted; the stored/echoed value is camelCase for multi-word categories.", + "example": "finance", + "enum": [ + "finance", + "kyc", + "travel_rule", + "user_event", + "audit_trail_event", + "gambling_bet", + "gambling_limit_change", + "gambling_bonus_change", + "travelRule" + ] + }, + "transaction_details": { + "type": "object", + "description": "Core financial details of the transaction.", + "required": [ + "direction", + "amount", + "currency" + ], + "properties": { + "direction": { + "type": "string", + "description": "Direction of the transaction relative to the subject. Case-insensitive; `in`/`out` are accepted as aliases. Echoed back uppercase (`INBOUND`/`OUTBOUND`).", + "example": "outbound", + "enum": [ + "inbound", + "outbound", + "in", + "out" + ] + }, + "amount": { + "type": "number", + "description": "Transaction amount (up to 18 decimal places). Send it as a decimal string (e.g. `\"0.123456789012345678\"`) when exact precision matters: a JSON number is parsed as a binary float first, so digits beyond about 15 significant digits are rounded before the amount is stored.", + "example": 1500 + }, + "currency": { + "type": "string", + "description": "Currency code. Any ISO 4217 alphabetic code is accepted (e.g. `EUR`, `USD`, `NGN`), as is a crypto asset symbol (e.g. `BTC`). Prefer a currently-circulating code. Not every accepted code has an exchange rate: when none is available for the code (for example the ISO accounting unit `XSU`, or a crypto symbol Didit does not price), automatic conversion fails and `preferred_currency_amount` is returned null unless the request supplied a pre-converted `amount_in_default_currency`.", + "example": "EUR" + }, + "currency_kind": { + "type": "string", + "description": "Whether the currency is `fiat` or `crypto`." + }, + "amount_in_default_currency": { + "type": "number", + "nullable": true, + "description": "Pre-converted amount in the default currency. If omitted, automatic FX conversion is attempted." + }, + "default_currency": { + "type": "string", + "description": "The default currency code for the converted amount. Any currently-circulating ISO 4217 code." + }, + "payment_details": { + "type": "string", + "nullable": true, + "description": "Free-text payment reference or memo." + }, + "payment_reference_id": { + "type": "string", + "nullable": true, + "description": "External payment system reference. For crypto AML transaction-hash screening, send the blockchain transaction hash and also provide `subject.payment_method.account_id` with the related address." + }, + "action_type": { + "type": "string", + "nullable": true, + "description": "Sub-type of the transaction, e.g. `deposit`, `withdrawal`, `transfer`." + }, + "crypto_params": { + "type": "object", + "description": "Crypto network selection, separate from currency (the asset symbol). Recording a custom token symbol does not guarantee transaction-hash coverage for its contract.", + "properties": { + "crypto_chain": { "type": "string", - "description": "Type of identity document" - }, - "document_subtype": { + "description": "Actual network used for screening. Managed screening supports BTC, ETH, LTC, XRP, BCH, DOGE, TRX, SOL, MATIC, BNB, BASE (Base), OP (Optimism) and ARB (Arbitrum One). Always specify this for multi-network tokens. When it is missing for USDT, USDC or ETH sent with a 0x transaction hash, Didit looks the hash up on Ethereum, BNB Chain, Polygon, Base, Arbitrum and Optimism and screens it on the network where it is found; if no network has it, the transaction is not screened and custom_values.aml_screening_status is FAILED with the reason in custom_values.aml_screening_error. When it is missing for USDT or USDC screened by wallet (no transaction hash), a Tron address is screened on Tron, a Solana address on Solana, and a 0x address on the first of those six networks where it has sent a transaction or holds the token, or on Ethereum when it is active on none of them. Connected-provider coverage varies; LIGHTNING requires a supported connected provider.", + "example": "BASE" + } + } + } + } + }, + "subject": { + "type": "object", + "description": "The subject (applicant) of the transaction.", + "required": [ + "vendor_data", + "full_name" + ], + "properties": { + "entity_type": { + "type": "string", + "description": "Entity type: `individual` or `company`." + }, + "vendor_data": { + "type": "string", + "description": "Your internal user/business identifier.", + "example": "user-12345" + }, + "full_name": { + "type": "string", + "description": "Full name of the subject.", + "example": "Maria Garcia" + }, + "first_name": { + "type": "string", + "description": "First name of the subject." + }, + "last_name": { + "type": "string", + "description": "Last name of the subject." + }, + "date_of_birth": { + "type": "string", + "format": "date", + "description": "Date of birth (YYYY-MM-DD)." + }, + "address": { + "type": "object", + "description": "Address object.", + "properties": { + "country": { "type": "string", - "nullable": true, - "description": "Accepted document subtype code from the workflow document settings, including the document region when applicable. Examples: `ID_CARD_GENERIC`, `QUEENSLAND_DRIVER_LICENSE_GENERIC`." + "description": "Country code (ISO 3166-1 alpha-3 recommended). Copied to the party's `country_code`." }, - "document_number": { - "type": "string", - "description": "Document identification number" + "town": { + "type": "string" }, - "personal_number": { - "type": "string", - "description": "Personal identification number" + "state": { + "type": "string" }, - "portrait_image": { - "type": "string", - "format": "uri", - "description": "URL of the portrait image" + "street": { + "type": "string" }, - "front_image": { - "type": "string", - "format": "uri", - "description": "URL of the front document image" + "post_code": { + "type": "string" + } + } + }, + "institution_details": { + "type": "object", + "description": "Institution details (for institutional subjects).", + "properties": { + "name": { + "type": "string" }, - "front_video": { - "type": "string", - "format": "uri", - "description": "URL of the front document video" + "code": { + "type": "string" }, - "back_image": { + "address": { + "type": "object" + } + } + }, + "device_context": { + "type": "object", + "description": "Device context captured at transaction time. browser_family and platform must be sent as explicit top-level keys — they are not derived from user_agent. The camelCase alias device is also accepted on the request.", + "properties": { + "fingerprint": { "type": "string", - "format": "uri", - "description": "URL of the back document image" + "description": "Unique device fingerprint." }, - "back_video": { + "persistent_device_id": { "type": "string", - "format": "uri", - "description": "URL of the back document video" + "description": "Stable cross-session device identifier (v2 fingerprint schema)." }, - "full_front_image": { + "user_agent": { "type": "string", - "format": "uri", - "description": "URL of the full front document image" + "description": "Browser or app user agent string. CamelCase alias: userAgent." }, - "full_back_image": { + "browser_family": { "type": "string", - "format": "uri", - "description": "URL of the full back document image" + "description": "Browser family (e.g. Chrome, Firefox, Safari). Must be sent explicitly — it is not derived from user_agent. CamelCase alias: browserFamily." }, - "front_image_camera_front": { + "browser_version": { "type": "string", - "format": "uri", - "description": "URL of the front document image captured from the front-facing camera" + "description": "Browser version string. Must be sent explicitly. CamelCase alias: browserVersion." }, - "back_image_camera_front": { + "platform": { "type": "string", - "format": "uri", - "description": "URL of the back document image captured from the front-facing camera" + "description": "Platform identifier (web, ios, android, macos, windows, linux). Must be sent explicitly — it is not derived from user_agent." }, - "date_of_birth": { + "os_family": { "type": "string", - "format": "date", - "description": "Date of birth from document" - }, - "age": { - "type": "integer", - "description": "Calculated age from date of birth" + "description": "Operating system family (e.g. iOS, Android, Windows). CamelCase alias: osFamily." }, - "expiration_date": { + "os_version": { "type": "string", - "format": "date", - "description": "Document expiration date" + "description": "Operating system version string. CamelCase alias: osVersion." }, - "date_of_issue": { + "accept_language": { "type": "string", - "format": "date", - "description": "Document issue date" + "description": "Accept-Language header value. CamelCase alias: acceptLang." }, - "issuing_state": { + "session_id": { "type": "string", - "description": "ISO code of issuing state" + "description": "Session identifier of the user current session. Stored in metadata. CamelCase alias: sessionId." }, - "issuing_state_name": { - "type": "string", - "description": "Full name of issuing state" + "session_age_ms": { + "type": "number", + "description": "Age of the user session in milliseconds at transaction time. CamelCase alias: sessionAgeMs." }, - "first_name": { + "network_context": { + "type": "object", + "description": "Network context with IP address and geo-location. A top-level ipInfo block is also folded into this object.", + "properties": { + "ip_address": { + "type": "string", + "description": "Client IP address. CamelCase alias: ip." + }, + "ip_country_code": { + "type": "string", + "description": "Geo-resolved country of the IP (ISO 3166-1 alpha-3). CamelCase alias: countryCode3." + }, + "latitude": { + "type": "number", + "description": "Geographic latitude of the IP. CamelCase alias: lat." + }, + "longitude": { + "type": "number", + "description": "Geographic longitude of the IP. CamelCase alias: lon." + } + } + } + } + }, + "payment_method": { + "type": "object", + "description": "Payment method used by the subject.", + "properties": { + "method_type": { "type": "string", - "description": "First name from document" + "description": "Type of payment method: `bank_card`, `bank_account`, `crypto_wallet`, `ewallet`, `unhosted_wallet`, `other`." }, - "last_name": { + "account_id": { "type": "string", - "description": "Last name from document" + "description": "Account identifier (e.g. IBAN, wallet address). For crypto screening, this should contain the wallet address for this participant. Inbound transaction-hash screening requires the service/customer deposit address on the subject. Outbound screening uses the destination wallet on the counterparty. On a crypto transfer on bitcoin or Lightning it may be a Lightning payment request (BOLT11 invoice, BOLT12 offer, LNURL) or a Lightning Address: Didit stores the payee it names as the account (\"\" when it names none) and what the request says on the Travel Rule transfer's `chain_details.lightning`. At most 255 characters once stored. On a crypto transfer on bitcoin or Lightning it may be a Lightning payment request (BOLT11 invoice, BOLT12 offer, LNURL) or a Lightning Address: Didit stores the payee it names as the account (\"\" when it names none) and what the request says on the Travel Rule transfer's `chain_details.lightning`. At most 255 characters once stored." }, - "full_name": { + "memo": { "type": "string", - "description": "Full name from document" + "maxLength": 256, + "description": "The destination tag or memo that is part of the wallet on a memo network (XRP, XLM, EOS, ATOM, HBAR). On one shared exchange address each memo is its own payment method. XRP: a destination tag, a whole number from 0 to 4294967295 (a JSON number is accepted). XLM: a text memo of up to 28 bytes, a 64-bit id or a 32-byte hash in hex. EOS: up to 256 bytes. ATOM: up to 256 characters. HBAR: up to 100 bytes. Bitcoin, EVM, Solana, Tron, Litecoin, Bitcoin Cash, Dogecoin and Lightning wallets take no memo: one there is a `400`. Alias: `destinationTag` or `destination_tag`." }, - "gender": { + "issuing_country": { "type": "string", - "description": "Gender from document" - }, - "address": { + "description": "ISO 3166-1 alpha-3 country code of the issuer." + } + } + } + } + }, + "counterparty": { + "type": "object", + "description": "The counterparty of the transaction (optional).", + "properties": { + "entity_type": { + "type": "string", + "description": "Entity type: `individual` or `company`." + }, + "vendor_data": { + "type": "string", + "description": "Your internal identifier for the counterparty." + }, + "full_name": { + "type": "string", + "description": "Full name of the counterparty." + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "date_of_birth": { + "type": "string", + "format": "date" + }, + "address": { + "type": "object", + "description": "Address object with `country` (ISO 3166-1 alpha-3)." + }, + "institution_details": { + "type": "object" + }, + "device_context": { + "type": "object", + "description": "Device context for the counterparty. browser_family and platform must be sent as explicit top-level keys — they are not derived from user_agent.", + "properties": { + "fingerprint": { "type": "string", - "description": "Raw address from document" + "description": "Unique device fingerprint." }, - "formatted_address": { + "persistent_device_id": { "type": "string", - "description": "Formatted full address" + "description": "Stable cross-session device identifier (v2 fingerprint schema)." }, - "place_of_birth": { + "user_agent": { "type": "string", - "description": "Place of birth from document" + "description": "Browser or app user agent string. CamelCase alias: userAgent." }, - "marital_status": { + "browser_family": { "type": "string", - "description": "Marital status from document" + "description": "Browser family (e.g. Chrome, Firefox, Safari). Must be sent explicitly — it is not derived from user_agent. CamelCase alias: browserFamily." }, - "nationality": { + "browser_version": { "type": "string", - "description": "ISO nationality code" - }, - "extra_fields": { - "type": "object", - "description": "Additional document fields. Includes document-specific attributes (e.g. `dl_categories`, `blood_group`, `sponsor`, `employer`, `dl_class_code_b_from`, `dl_class_code_b_to`). Driver license class validity dates are returned as `dl_class_code__from` and `dl_class_code__to`, and per-class restriction/information codes (e.g. column 12 of UK licences) as `dl_class_code__notes`. When the document carries names, address, or place of birth in multiple scripts, this object also includes alternate-script versions of those fields. The workflow setting `preferred_characters` (`latin` or `non_latin`) controls which script populates the top-level fields (`first_name`, `last_name`, `full_name`, `address`, `place_of_birth`); the opposite script lands here as `first_name_non_latin` / `last_name_non_latin` / `full_name_non_latin` / `middle_name_non_latin` / `address_non_latin` / `place_of_birth_non_latin` (when `preferred_characters` is `latin`) or with a `_latin` suffix (when `preferred_characters` is `non_latin`). Keys appear only when the OCR actually extracts a real alternate-script value — a field whose alternate value is in the same script as the preferred set (for example, a Cyrillic-labelled field whose OCR value came back in Latin homoglyphs) is skipped. When `preferred_characters` is `latin` but the document only carries non-Latin text, Didit transliterates the extracted values and exposes `first_name_latin` / `last_name_latin` / etc. as a fallback.", - "additionalProperties": true, - "properties": { - "dl_categories": { - "type": "array", - "items": { - "type": "string" - } - }, - "blood_group": { - "type": "string", - "nullable": true - }, - "first_name_non_latin": { - "type": "string", - "description": "First name in the non-Latin script when `preferred_characters=latin` and a non-Latin version is present on the document (e.g. `Айна` for a Kyrgyz ID)." - }, - "last_name_non_latin": { - "type": "string", - "description": "Surname in the non-Latin script when `preferred_characters=latin` and a non-Latin version is present on the document." - }, - "middle_name_non_latin": { - "type": "string", - "description": "Middle name / patronymic in the non-Latin script when `preferred_characters=latin` and a non-Latin version is present on the document." - }, - "full_name_non_latin": { - "type": "string", - "description": "Composed full name in the non-Latin script. Reconstructed from the individual components when both first and last names are available." - }, - "address_non_latin": { - "type": "string", - "description": "Address in the non-Latin script when `preferred_characters=latin` and a non-Latin version is present on the document." - }, - "place_of_birth_non_latin": { - "type": "string", - "description": "Place of birth in the non-Latin script when `preferred_characters=latin` and a non-Latin version is present on the document." - }, - "first_name_latin": { - "type": "string", - "description": "First name in Latin script. Populated when `preferred_characters=non_latin` and a Latin version is present on the document, or — as a transliteration fallback — when `preferred_characters=latin` but the document only carries non-Latin text." - }, - "last_name_latin": { - "type": "string", - "description": "Surname in Latin script. Same rules as `first_name_latin`." - }, - "middle_name_latin": { - "type": "string", - "description": "Middle name / patronymic in Latin script. Same rules as `first_name_latin`." - }, - "full_name_latin": { - "type": "string", - "description": "Composed full name in Latin script. Always reconstructed from the individual components when both first and last names are available." - }, - "address_latin": { - "type": "string", - "description": "Address in Latin script. Same rules as `first_name_latin`." - }, - "place_of_birth_latin": { - "type": "string", - "description": "Place of birth in Latin script. Same rules as `first_name_latin`." - } - } - }, - "mrz": { - "type": "object", - "nullable": true, - "description": "Parsed Machine-Readable Zone (MRZ) fields extracted from the document. `null` if no MRZ was detected.", - "properties": { - "surname": { - "type": "string", - "example": "MARTINEZ`.", + "additionalProperties": true, + "example": { + "merchant_id": "test", + "bank_name": "test_2" + } + }, + "travel_rule_details": { + "type": "object", + "description": "Travel Rule data. When Travel Rule is enabled and transaction_category is travel_rule, Didit runs the exchange itself: it ignores status, protocol, required and obligations_count, and uses originator_data, beneficiary_data and metadata. Otherwise Didit stores these values as a passive Travel Rule record. While Travel Rule is on, an outbound transfer at or above your threshold must carry what its regime requires (EU TFR or FATF Recommendation 16), checked on the sides you send: a missing field answers 400 under `travel_rule_details.originator_data` or `travel_rule_details.beneficiary_data`, for example \"EU TFR requires the originator name.\"", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "description": "Required whenever you send travel_rule_details. One of the Travel Rule transfer statuses (for example AWAITING_COUNTERPARTY, COMPLETED, NOT_APPLICABLE); lowercase and camelCase spellings and the legacy values UNKNOWN, COMPLIANT, PENDING_ACTION, PENDING_COUNTERPARTY, FAILED and EXEMPT are also accepted and mapped. Any other value returns 400." + }, + "protocol": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "obligations_count": { + "type": "integer" + }, + "originator_data": { + "type": "object", + "description": "Originator party, as flat fields or one IVMS 101 person. For your user with an approved KYC verification, Didit replaces the fields the verification holds with the verified values (see `metadata.originator_provenance`). It must be an object: anything else is a `400`. On a memo network a `memo` key names the originator wallet's memo or tag." + }, + "beneficiary_data": { + "type": "object", + "description": "Beneficiary party, as flat fields or one IVMS 101 person. An empty `wallet_address` is filled from `counterparty.payment_method.account_id`. It must be an object: anything else is a `400`. On a memo network `memo` names the beneficiary wallet's memo or tag; `wallet_address` and `memo` win over the counterparty's payment method." + }, + "metadata": { + "type": "object" + }, + "utxo": { + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleUtxo" + } + ], + "description": "Optional, UTXO chains only (BTC, LTC, BCH, DOGE). The transaction's inputs and outputs, from which Didit infers the originator wallet and the beneficiary output when you do not declare them." + }, + "originator": { + "type": "object", + "description": "An IVMS 101 originator block: `originatorPersons` (one `naturalPerson` or `legalPerson`; a legal person makes the party a company) and `accountNumber` (wallet addresses). Used for `originator_data` when that object is empty. A malformed block answers 400 keyed by its path, for example `originator.originatorPersons`." + }, + "beneficiary": { + "type": "object", + "description": "An IVMS 101 beneficiary block: `beneficiaryPersons` (one `naturalPerson` or `legalPerson`; a legal person makes the party a company) and `accountNumber` (wallet addresses). Used for `beneficiary_data` when that object is empty. A malformed block answers 400 keyed by its path, for example `beneficiary.beneficiaryPersons`." + }, + "ivms101": { + "type": "object", + "description": "The `originator` and `beneficiary` blocks wrapped in one IVMS 101 object (`IVMS101` is accepted too). Top-level blocks win over the wrapper's." + } + } + }, + "network_snapshot": { + "type": "object", + "description": "Pre-computed network graph snapshot.", + "properties": { + "nodes": { + "type": "array", + "items": { + "type": "object" + } + }, + "edges": { + "type": "array", + "items": { + "type": "object" + } + }, + "metrics": { + "type": "object" + } + } + }, + "include_crypto_screening": { + "type": "boolean", + "nullable": true, + "description": "Per-transaction override for crypto blockchain analytics screening. When `true`, crypto screening is performed regardless of the application default. When `false`, crypto screening is skipped even if enabled in the application settings. When omitted or `null`, the application-level default configured in the Console is used. Crypto screening requires `transaction_details.direction`, `currency_kind: \"crypto\"`, a chain/currency, and the wallet address selected by the direction: inbound pre-transfer screening uses `counterparty.payment_method.account_id`, inbound transaction-hash screening uses `subject.payment_method.account_id`, and outbound screening uses `counterparty.payment_method.account_id`. When `transaction_details.payment_reference_id` contains a blockchain transaction hash, Didit performs transaction screening and returns transaction summary enrichment when available." + } + }, + "required": [ + "transaction_id", + "transaction_category", + "transaction_details", + "subject" + ] + }, + "examples": { + "Minimal fiat withdrawal": { + "summary": "Smallest acceptable payload for a fiat outbound transfer", + "value": { + "transaction_id": "your-txn-2026-05-17-001", + "transaction_category": "finance", + "transaction_details": { + "direction": "outbound", + "amount": 1500, + "currency": "EUR", + "action_type": "withdrawal" + }, + "subject": { + "entity_type": "individual", + "vendor_data": "user-12345", + "full_name": "Maria Garcia" + } + } + }, + "Crypto outbound with screening": { + "summary": "Outbound crypto withdrawal to an unhosted wallet, opt-in screening", + "value": { + "transaction_id": "your-txn-2026-05-17-002", + "transaction_category": "finance", + "include_crypto_screening": true, + "transaction_details": { + "direction": "outbound", + "amount": 0.5, + "currency": "BTC", + "currency_kind": "crypto", + "amount_in_default_currency": 32500, + "default_currency": "USD", + "action_type": "withdrawal" + }, + "subject": { + "entity_type": "individual", + "vendor_data": "user-12345", + "full_name": "Maria Garcia", + "payment_method": { + "method_type": "crypto_wallet", + "account_id": "bc1qexampleexchangewalletaddressxyz", + "issuing_country": "ESP" + } + }, + "counterparty": { + "entity_type": "individual", + "payment_method": { + "method_type": "unhosted_wallet", + "account_id": "bc1qexampleexternalwalletaddressabc" + } + } + } + }, + "Travel Rule transfer": { + "summary": "Outbound crypto transfer with Travel Rule originator/beneficiary data", + "value": { + "transaction_id": "your-txn-2026-05-17-003", + "transaction_category": "travel_rule", + "transaction_details": { + "direction": "outbound", + "amount": 25000, + "currency": "USDT", + "currency_kind": "crypto", + "action_type": "transfer" + }, + "subject": { + "entity_type": "individual", + "vendor_data": "user-12345", + "full_name": "Maria Garcia", + "date_of_birth": "1992-03-14", + "address": { + "country": "ESP", + "town": "Madrid", + "street": "Calle Mayor 1", + "post_code": "28013" + } + }, + "counterparty": { + "entity_type": "individual", + "full_name": "John Beneficiary", + "institution_details": { + "name": "Beneficiary VASP, Inc.", + "code": "BVASP-US" + } + }, + "travel_rule_details": { + "status": "AWAITING_COUNTERPARTY", + "protocol": "IVMS101", + "required": true, + "originator_data": { + "name": "Maria Garcia" + }, + "beneficiary_data": { + "name": "John Beneficiary" + } + } + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Transaction submitted and monitored. The body is the full transaction detail (same shape as `GET /v3/transactions/{transaction_id}/`), reflecting any rule outcomes already applied.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionDetail" + }, + "examples": { + "Approved with no rule hits": { + "value": { + "uuid": "abcdef12-3456-4890-abcd-ef1234567890", + "environment": "live", + "transaction_number": 4123, + "txn_id": "your-txn-2026-05-17-001", + "txn_date": "2026-05-17T08:42:00Z", + "zone_id": "Europe/Madrid", + "transaction_type": "finance", + "action_type": "withdrawal", + "direction": "OUTBOUND", + "status": "APPROVED", + "source": "LIVE", + "amount": "1500", + "currency": "EUR", + "currency_type": "fiat", + "amount_in_default_currency": "1620.45", + "default_currency_code": "USD", + "preferred_currency_amount": "1620.45", + "preferred_currency_code": "USD", + "payment_details": "Withdrawal to crypto wallet", + "payment_txn_id": null, + "score": 0, + "severity": null, + "decision_reason_code": null, + "decision_reason_label": null, + "vendor_data": "user-12345", + "metadata": { + "subject": { + "entity_type": "individual", + "vendor_data": "user-12345", + "full_name": "Maria Garcia", + "address": {}, + "institution_details": {}, + "device_context": {} + }, + "counterparty": {} + }, + "props": {}, + "tags": [], + "parties": [ + { + "uuid": "b1111111-2222-4333-8444-555555555555", + "role": "APPLICANT", + "entity_type": "individual", + "kind": "USER", + "vendor_data": "user-12345", + "full_name": "Maria Garcia", + "first_name": null, + "last_name": null, + "country_code": null, + "dob": null, + "address": {}, + "institution_info": {}, + "device": {}, + "external_party_snapshot": null + } + ], + "payment_methods": [], + "documents": [], + "activities": [ + { + "uuid": "d4e5f6a7-8901-4bcd-9ef0-123456789abc", + "activity_type": "TRANSACTION_CREATED", + "source": "TRANSACTION", + "status": "APPROVED", + "title": "withdrawal", + "description": "Transaction recorded.", + "metadata": {}, + "occurred_at": "2026-05-17T08:42:11Z" + } + ], + "alerts": [], + "rule_runs": [], + "provider_results": [], + "travel_rule": null, + "network_snapshot": null, + "remediation": null, + "action_required": null, + "cost_breakdown": null + } + }, + "Flagged for review": { + "value": { + "uuid": "abcdef12-3456-4890-abcd-ef1234567891", + "environment": "live", + "transaction_number": 4124, + "txn_id": "your-txn-2026-05-17-002", + "txn_date": "2026-05-17T08:42:00Z", + "zone_id": "Europe/Madrid", + "transaction_type": "finance", + "action_type": "withdrawal", + "direction": "OUTBOUND", + "status": "IN_REVIEW", + "source": "LIVE", + "amount": "0.5", + "currency": "BTC", + "currency_type": "crypto", + "amount_in_default_currency": "32500", + "default_currency_code": "USD", + "preferred_currency_amount": "32500", + "preferred_currency_code": "USD", + "payment_details": "Withdrawal to crypto wallet", + "payment_txn_id": null, + "score": 78, + "severity": "HIGH", + "decision_reason_code": "WALLET_HIGH_RISK", + "decision_reason_label": "Destination wallet flagged as high risk", + "vendor_data": "user-12345", + "metadata": { + "subject": { + "entity_type": "individual", + "vendor_data": "user-12345", + "full_name": "Maria Garcia", + "address": {}, + "institution_details": {}, + "device_context": {} + }, + "counterparty": {} + }, + "props": { + "order_id": "ord-9988" + }, + "tags": [], + "parties": [ + { + "uuid": "b1111111-2222-4333-8444-555555555555", + "role": "APPLICANT", + "entity_type": "individual", + "kind": "USER", + "vendor_data": "user-12345", + "full_name": "Maria Garcia", + "first_name": null, + "last_name": null, + "country_code": null, + "dob": null, + "address": {}, + "institution_info": {}, + "device": {}, + "external_party_snapshot": null + } + ], + "payment_methods": [], + "documents": [], + "activities": [], + "alerts": [ + { + "uuid": "a7b8c9d0-1234-4abc-9def-567890abcdef", + "title": "Destination wallet flagged as high risk", + "description": "Provider risk score exceeded the configured threshold.", + "severity": "HIGH", + "status": "OPEN", + "source": "RULE", + "metadata": {}, + "created_at": "2026-05-17T08:42:12Z", + "due_at": null + } + ], + "rule_runs": [ + { + "uuid": "e5f6a7b8-9012-4cde-af01-23456789abcd", + "rule": "f6a7b8c9-0123-4def-b012-3456789abcde", + "rule_title": "High-risk wallet screening", + "rule_description": null, + "rule_category": "crypto", + "matched": true, + "is_test": false, + "mode": "ACTIVE", + "severity": "HIGH", + "score_delta": 78, + "status_target": "IN_REVIEW", + "action_summary": "score +78; change_status:IN_REVIEW", + "metadata": {}, + "created_at": "2026-05-17T08:42:12Z" + } + ], + "provider_results": [], + "travel_rule": null, + "network_snapshot": null, + "remediation": null, + "action_required": null, + "cost_breakdown": null + } + } + } + } + } + }, + "400": { + "description": "Validation failed (missing required fields, duplicate `transaction_id`, unknown enum value, invalid currency, etc.).", + "content": { + "application/json": { + "examples": { + "Missing required field": { + "value": { + "transaction_details": { + "amount": [ + "This field is required." + ] + } + } + }, + "Duplicate transaction_id": { + "value": { + "transaction_id": [ + "A transaction with this transaction_id already exists." + ] + } + }, + "Invalid category": { + "value": { + "non_field_errors": [ + "Unsupported transaction category." + ] + } + }, + "Missing subject identity": { + "value": { + "subject": [ + "subject.vendor_data and subject.full_name are required." + ] + } + }, + "Blocklisted participant": { + "value": { + "vendor_data": [ + "The individual 'user-12345' is in the blocklist and cannot participate in transactions." + ] + } + } + } + } + } + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the key cannot submit transactions for this application. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "409": { + "description": "Another request with the same `transaction_id` created the transaction while this one was being processed, so only one of two simultaneous submissions succeeds. Read the existing transaction instead of resubmitting.", + "content": { + "application/json": { + "examples": { + "Concurrent duplicate": { + "value": { + "detail": "A transaction with this transaction_id already exists." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Throttled": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-didit-sdk": { + "resource": "transactions", + "method": "create", + "auth": "api_key", + "server": true + } + } + }, + "/v3/transactions/{transaction_id}/": { + "get": { + "summary": "Get transaction", + "description": "Retrieve the full monitoring record for one transaction: parties, score/severity, rule runs, alerts, Travel Rule outcome, remediation session.", + "operationId": "getTransaction", + "x-didit-sdk": { + "resource": "transactions", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "tags": [ + "Transactions" + ], + "parameters": [ + { + "name": "transaction_id", + "in": "path", + "required": true, + "description": "Didit-stable transaction UUID (the `uuid` returned from create / list, **not** your `txn_id`).", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "abcdef12-3456-7890-abcd-ef1234567890" + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/transactions/abcdef12-3456-7890-abcd-ef1234567890/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nuuid = 'abcdef12-3456-7890-abcd-ef1234567890'\nresp = requests.get(\n f'https://verification.didit.me/v3/transactions/{uuid}/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\ntx = resp.json()\nfor alert in tx['alerts']:\n print(alert.get('code'), alert.get('severity'))" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const uuid = 'abcdef12-3456-7890-abcd-ef1234567890';\nconst resp = await fetch(`https://verification.didit.me/v3/transactions/${uuid}/`, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Lookup failed: ${resp.status}`);\nconst tx = await resp.json();\nconsole.log(tx.status, tx.severity, tx.alerts.length, 'alerts');" + } + ], + "responses": { + "200": { + "description": "Full transaction detail.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionDetail" + }, + "examples": { + "In review with an alert": { + "value": { + "uuid": "abcdef12-3456-4890-abcd-ef1234567890", + "environment": "live", + "transaction_number": 4123, + "txn_id": "your-txn-2026-05-17-001", + "txn_date": "2026-05-17T08:42:00Z", + "zone_id": "Europe/Madrid", + "transaction_type": "finance", + "action_type": "withdrawal", + "direction": "OUTBOUND", + "status": "IN_REVIEW", + "source": "LIVE", + "amount": "1500", + "currency": "EUR", + "currency_type": "fiat", + "amount_in_default_currency": "1620.45", + "default_currency_code": "USD", + "preferred_currency_amount": "1620.45", + "preferred_currency_code": "USD", + "payment_details": "Withdrawal to crypto wallet", + "payment_txn_id": null, + "score": 78, + "severity": "HIGH", + "decision_reason_code": "WALLET_HIGH_RISK", + "decision_reason_label": "Destination wallet flagged as high risk", + "vendor_data": "user-12345", + "metadata": { + "subject": { + "entity_type": "individual", + "vendor_data": "user-12345", + "full_name": "Maria Garcia", + "address": {}, + "institution_details": {}, + "device_context": {} + }, + "counterparty": {} + }, + "props": { + "order_id": "ord-9988" + }, + "tags": [], + "parties": [ + { + "uuid": "b1111111-2222-4333-8444-555555555555", + "role": "APPLICANT", + "entity_type": "individual", + "kind": "USER", + "vendor_data": "user-12345", + "full_name": "Maria Garcia", + "first_name": null, + "last_name": null, + "country_code": null, + "dob": null, + "address": {}, + "institution_info": {}, + "device": {}, + "external_party_snapshot": null + } + ], + "payment_methods": [], + "documents": [], + "activities": [], + "alerts": [ + { + "uuid": "a7b8c9d0-1234-4abc-9def-567890abcdef", + "title": "Destination wallet flagged as high risk", + "description": "Provider risk score exceeded the configured threshold.", + "severity": "HIGH", + "status": "OPEN", + "source": "RULE", + "metadata": {}, + "created_at": "2026-05-17T08:42:12Z", + "due_at": null + } + ], + "rule_runs": [], + "provider_results": [], + "travel_rule": null, + "network_snapshot": null, + "remediation": null, + "action_required": null, + "cost_breakdown": null + } + } + } + } + } + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the key cannot read this application's transactions. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No transaction with this UUID exists for the application (deleted, never created, belongs to another application, or the path segment is not a valid UUID).", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Throttled": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/transactions/sdk-token/": { + "post": { + "summary": "Mint an SDK transaction token", + "description": "Mint a short-lived scoped token your app passes to the Didit SDKs so they can submit transactions directly from the end-user's device via `POST /v1/transactions/`. The token is bound to one `vendor_data` (your user id): every transaction submitted with it has its subject identity enforced server-side from that binding, so a tampered client cannot submit on behalf of another user. Mint tokens from your backend - never ship the API key itself in an app. camelCase aliases (`vendorData`, `ttlSeconds`, `maxUses`) are also accepted.", + "operationId": "createTransactionSdkToken", + "x-didit-sdk": { + "resource": "transactions", + "method": "create_sdk_token", + "auth": "api_key", + "server": true + }, + "tags": [ + "Transactions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "vendor_data": { + "type": "string", + "description": "Your internal identifier for the end user this token is scoped to. Enforced as the subject identity of every transaction submitted with the token.", + "example": "user-042" + }, + "ttl_seconds": { + "type": "integer", + "default": 900, + "maximum": 86400, + "description": "Token lifetime in seconds. Defaults to 900 (15 minutes); maximum 86400 (24 hours)." + }, + "max_uses": { + "type": "integer", + "nullable": true, + "description": "Maximum number of successful submissions allowed with this token. Omit or null for unlimited uses within the TTL." + } + }, + "required": [ + "vendor_data" + ] + }, + "example": { + "vendor_data": "user-042", + "ttl_seconds": 900, + "max_uses": 3 + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/transactions/sdk-token/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"vendor_data\": \"user-042\", \"ttl_seconds\": 900, \"max_uses\": 3}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/transactions/sdk-token/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={'vendor_data': 'user-042', 'ttl_seconds': 900, 'max_uses': 3},\n timeout=10,\n)\nresp.raise_for_status()\ntoken = resp.json()\nprint(token['sdk_token'], token['expires_at'])" + } + ], + "responses": { + "201": { + "description": "The minted token. Hand `sdk_token` to your app; it authenticates the device-facing `/v1/transactions/` endpoints via the `X-Transaction-Token` header until `expires_at`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sdk_token": { + "type": "string", + "description": "The scoped transaction token to pass to the SDK." + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "When the token stops being accepted." + } + } + }, + "example": { + "sdk_token": "q7Zt...urlsafe", + "expires_at": "2026-07-07T12:15:00Z" + } + } + } + }, + "400": { + "description": "Validation failed: missing `vendor_data`, or `ttl_seconds` outside the allowed range.", + "content": { + "application/json": { + "examples": { + "Missing vendor_data": { + "value": { + "vendor_data": [ + "This field is required." + ] + } + } + } + } + } + }, + "403": { + "description": "Missing, invalid, or revoked API key.", + "content": { + "application/json": { + "examples": { + "Invalid API Key": { + "value": { + "detail": "Invalid API Key." + } + } + } + } + } + } + } + } + }, + "/v1/transactions/": { + "post": { + "summary": "Submit a transaction from an SDK", + "description": "Device-facing endpoint the Didit SDKs call to submit a transaction directly from the end-user's device. Authentication is a scoped token from `POST /v3/transactions/sdk-token/` sent in the `X-Transaction-Token` header instead of an API key, and the endpoint is CORS-enabled so browsers can call it cross-origin. The payload uses the camelCase wire aliases shown below (the SDK `submitTransaction` methods build it for you). Three server-side guarantees: the application is always taken from the token; the subject identity (`externalUserId`) is always enforced from the token's `vendor_data`, so spoofed payload values are ignored; and device intelligence (the SDK device fingerprint plus server-derived IP and user agent) is attached to the subject automatically.", + "operationId": "submitTransactionFromSdk", + "x-didit-sdk": { + "resource": "transactions", + "method": "submit_from_device", + "auth": "device_token", + "server": false + }, + "tags": [ + "Transactions" + ], + "security": [ + { + "TransactionTokenAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "txnId": { + "type": "string", + "description": "Your unique identifier for this transaction (maps to `transaction_id`, max 128 characters).", + "example": "wd-2026-07-07-0042" + }, + "txnDate": { + "type": "string", + "format": "date-time", + "description": "When the transaction occurred (maps to `transaction_at`). Defaults to now if omitted." + }, + "zoneId": { + "type": "string", + "description": "IANA time zone identifier (maps to `time_zone`), e.g. `Europe/Madrid`." + }, + "type": { + "type": "string", + "description": "Transaction category (maps to `transaction_category`), e.g. `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet`.", + "example": "travelRule" + }, + "info": { + "type": "object", + "description": "Core financial details of the transaction (maps to `transaction_details`).", + "required": [ + "direction", + "amount", + "currency" + ], + "properties": { + "direction": { + "type": "string", + "description": "Direction relative to the subject. Case-insensitive; `in`/`out` are accepted as aliases. Echoed back uppercase (`INBOUND`/`OUTBOUND`).", + "example": "out" + }, + "amount": { + "type": "number", + "description": "Transaction amount (up to 18 decimal places). A decimal string is also accepted and is the only way to preserve more than about 15 significant digits: a JSON number is parsed as a binary float first, so `0.123456789012345678` is stored as `0.12345678901234568`.", + "example": 0.25 + }, + "currency": { + "type": "string", + "description": "Currency code. Any ISO 4217 alphabetic code is accepted (e.g. `EUR`, `USD`, `NGN`), as is a crypto asset symbol (e.g. `BTC`). Prefer a currently-circulating code. Not every accepted code has an exchange rate: when none is available for the code (for example the ISO accounting unit `XSU`, or a crypto symbol Didit does not price), automatic conversion fails and `preferred_currency_amount` is returned null unless the request supplied a pre-converted `amountInDefaultCurrency`.", + "example": "ETH" + }, + "currencyType": { + "type": "string", + "description": "Whether the currency is `fiat` or `crypto` (maps to `currency_kind`)." + }, + "amountInDefaultCurrency": { + "type": "number", + "nullable": true, + "description": "Pre-converted amount in the default currency (maps to `amount_in_default_currency`). If omitted, automatic FX conversion is attempted." + }, + "defaultCurrencyCode": { + "type": "string", + "description": "The default currency code for the converted amount (maps to `default_currency`). Any currently-circulating ISO 4217 code." + }, + "paymentDetails": { + "type": "string", + "nullable": true, + "description": "Free-text payment reference or memo (maps to `payment_details`)." + }, + "paymentTxnId": { + "type": "string", + "nullable": true, + "description": "External payment reference (maps to `payment_reference_id`); the on-chain transaction hash for crypto transactions." + }, + "type": { + "type": "string", + "nullable": true, + "description": "Sub-type of the transaction (maps to `action_type`), e.g. `deposit`, `withdrawal`, `transfer`." + }, + "cryptoParams": { + "type": "object", + "description": "Crypto-specific parameters, e.g. `{\"crypto_chain\": \"ETH\"}`." + } + } + }, + "subject": { + "type": "object", + "description": "The subject (applicant) of the transaction. `externalUserId` is ignored here: the subject identity is always enforced from the token's `vendor_data`.", + "required": [ + "fullName" + ], + "properties": { + "type": { + "type": "string", + "description": "Entity type (maps to `entity_type`): `individual` (default) or `company`." + }, + "externalUserId": { + "type": "string", + "description": "Ignored on the subject - enforced server-side from the token's `vendor_data`." + }, + "fullName": { + "type": "string", + "example": "Jane Doe" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "dob": { + "type": "string", + "format": "date", + "description": "Date of birth (YYYY-MM-DD; maps to `date_of_birth`)." + }, + "address": { + "type": "object", + "description": "Address object with `country` (ISO 3166-1 alpha-3), `town`, `state`, `street`, `post_code`." + }, + "institutionInfo": { + "type": "object", + "description": "Institution details for institutional participants (maps to `institution_details`)." + }, + "device": { + "type": "object", + "description": "Optional explicit device context. Merged with the automatically captured device intelligence; server-derived fields win on conflict." + }, + "paymentMethod": { + "type": "object", + "description": "Payment method used by the subject (maps to `payment_method`).", + "properties": { + "type": { + "type": "string", + "description": "Payment method kind (maps to `method_type`), e.g. `bank_card`, `bank_account`, `crypto_wallet`, `ewallet`, `unhosted_wallet`, `other`." }, - "matches": { - "type": "array", - "description": "Matching documents found during duplicate/blocklist detection (up to 5 matches)", - "items": { - "type": "object", - "properties": { - "session_id": { - "type": "string", - "format": "uuid", - "description": "Session ID of the matching document" - }, - "session_number": { - "type": "integer", - "description": "Session number of the matching document" - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Vendor data associated with the matching session" - }, - "verification_date": { - "type": "string", - "format": "date-time", - "description": "Date when the matching session was verified" - }, - "user_details": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Full name from the matching document" - }, - "document_type": { - "type": "string", - "description": "Type of the matching document" - }, - "document_number": { - "type": "string", - "description": "Document number of the matching document" - } - } - }, - "status": { - "type": "string", - "description": "Status of the matching session" - }, - "is_blocklisted": { - "type": "boolean", - "description": "Whether the matching document is blocklisted" - }, - "api_service": { - "type": "string", - "nullable": true, - "description": "API service type of the matching session" - }, - "front_image_url": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "URL of the front document image from the matching document" - } - } - } + "accountId": { + "type": "string", + "description": "Account identifier (maps to `account_id`): IBAN, card fingerprint, or wallet address." }, - "document_liveness": { - "type": "object", - "nullable": true, - "description": "Document-liveness scores per fraud type, reported whether or not a warning fired, so an approved document can still be audited. `null` when the check did not run.", - "properties": { - "portrait_replace": { - "type": "object", - "nullable": true, - "properties": { - "score": { - "type": "number", - "nullable": true, - "description": "Likelihood that the document is genuine for this fraud type, 0-100 (higher = more likely genuine). The lowest of the two sides - the value the decision used. `null` when neither side produced a usable score." - }, - "front": { - "type": "number", - "nullable": true, - "description": "Score for the front side; `null` when not captured or when the score was dropped." - }, - "back": { - "type": "number", - "nullable": true, - "description": "Score for the back side; `null` when not captured or when the score was dropped." - }, - "decline_threshold": { - "type": "number", - "nullable": true, - "description": "The decline threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable." - }, - "review_threshold": { - "type": "number", - "nullable": true, - "description": "The review threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable." - }, - "bucket": { - "type": "string", - "nullable": true, - "enum": [ - "approve", - "review", - "decline", - null - ], - "description": "The fraud-type bucket saved at evaluation time. Null when there is no usable score, the check was not applied, or historical evaluation evidence is unavailable. Later workflow edits do not change it." - } - } - }, - "printed_copy": { - "type": "object", - "nullable": true, - "properties": { - "score": { - "type": "number", - "nullable": true, - "description": "Likelihood that the document is genuine for this fraud type, 0-100 (higher = more likely genuine). The lowest of the two sides - the value the decision used. `null` when neither side produced a usable score." - }, - "front": { - "type": "number", - "nullable": true, - "description": "Score for the front side; `null` when not captured or when the score was dropped." - }, - "back": { - "type": "number", - "nullable": true, - "description": "Score for the back side; `null` when not captured or when the score was dropped." - }, - "decline_threshold": { - "type": "number", - "nullable": true, - "description": "The decline threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable." - }, - "review_threshold": { - "type": "number", - "nullable": true, - "description": "The review threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable." - }, - "bucket": { - "type": "string", - "nullable": true, - "enum": [ - "approve", - "review", - "decline", - null - ], - "description": "The fraud-type bucket saved at evaluation time. Null when there is no usable score, the check was not applied, or historical evaluation evidence is unavailable. Later workflow edits do not change it." - } - } - }, - "screen_replay": { - "type": "object", - "nullable": true, - "properties": { - "score": { - "type": "number", - "nullable": true, - "description": "Likelihood that the document is genuine for this fraud type, 0-100 (higher = more likely genuine). The lowest of the two sides - the value the decision used. `null` when neither side produced a usable score." - }, - "front": { - "type": "number", - "nullable": true, - "description": "Score for the front side; `null` when not captured or when the score was dropped." - }, - "back": { - "type": "number", - "nullable": true, - "description": "Score for the back side; `null` when not captured or when the score was dropped." - }, - "decline_threshold": { - "type": "number", - "nullable": true, - "description": "The decline threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable." - }, - "review_threshold": { - "type": "number", - "nullable": true, - "description": "The review threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable." - }, - "bucket": { - "type": "string", - "nullable": true, - "enum": [ - "approve", - "review", - "decline", - null - ], - "description": "The fraud-type bucket saved at evaluation time. Null when there is no usable score, the check was not applied, or historical evaluation evidence is unavailable. Later workflow edits do not change it." - } - } - } + "issuingCountry": { + "type": "string", + "description": "ISO 3166-1 alpha-3 country code of the issuer (maps to `issuing_country`)." + } + } + } + } + }, + "counterparty": { + "type": "object", + "description": "The counterparty of the transaction (optional). Same shape as `subject`; `externalUserId` is meaningful here.", + "properties": { + "type": { + "type": "string", + "description": "Entity type (maps to `entity_type`): `individual` or `company`." + }, + "externalUserId": { + "type": "string", + "description": "Your internal identifier for the counterparty (maps to `vendor_data`)." + }, + "fullName": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "dob": { + "type": "string", + "format": "date" + }, + "address": { + "type": "object" + }, + "institutionInfo": { + "type": "object" + }, + "device": { + "type": "object" + }, + "paymentMethod": { + "type": "object", + "description": "Payment method used by the counterparty (maps to `payment_method`). On travelRule transactions, `type: \"unhosted_wallet\"` declares the counterparty wallet as self-hosted: the transfer skips counterparty-VASP routing, is created directly in `UNCONFIRMED_OWNERSHIP`, and (when `auto_wallet_verification` is enabled) a wallet-ownership widget session is auto-minted and returned in `action_required` so the SDK can launch it.", + "properties": { + "type": { + "type": "string", + "description": "Payment method kind (maps to `method_type`), e.g. `bank_card`, `bank_account`, `crypto_wallet`, `ewallet`, `unhosted_wallet`, `other`. Use `unhosted_wallet` to declare a self-hosted wallet for the Travel Rule." + }, + "accountId": { + "type": "string", + "description": "Account identifier (maps to `account_id`): IBAN, card fingerprint, or wallet address." + }, + "issuingCountry": { + "type": "string" + } + } + } + } + }, + "props": { + "type": "object", + "additionalProperties": true, + "description": "Custom key-value pairs (maps to `custom_properties`). Each key can be referenced in rule conditions as `custom_values.`." + }, + "travelRule": { + "type": "object", + "description": "Travel Rule details (maps to `travel_rule_details`). When Travel Rule is enabled for the application, Didit ignores any `status` you send and drives the exchange itself.", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "obligationsCount": { + "type": "integer" + }, + "originatorData": { + "type": "object" + }, + "beneficiaryData": { + "type": "object" + }, + "metadata": { + "type": "object" + } + } + }, + "includeCryptoScreening": { + "type": "boolean", + "nullable": true, + "description": "Per-transaction override for crypto blockchain analytics screening (maps to `include_crypto_screening`)." + }, + "fingerprint_v2": { + "type": "object", + "description": "The `didit-fp-v2` device fingerprint payload. Injected automatically by the SDKs; only relevant when calling the wire API directly." + } + }, + "required": [ + "txnId", + "type", + "info", + "subject" + ] + }, + "example": { + "txnId": "wd-2026-07-07-0042", + "type": "travelRule", + "info": { + "direction": "out", + "amount": "0.25", + "currency": "ETH", + "currencyType": "crypto", + "cryptoParams": { + "crypto_chain": "ETH" + } + }, + "subject": { + "fullName": "Jane Doe" + }, + "counterparty": { + "fullName": "Ana Diaz", + "paymentMethod": { + "type": "unhosted_wallet", + "accountId": "0xBeneficiaryWallet01" + } + }, + "travelRule": { + "status": "AWAITING_COUNTERPARTY", + "beneficiaryData": { + "wallet_address": "0xBeneficiaryWallet01", + "name": "Ana Diaz" + } + }, + "includeCryptoScreening": true + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v1/transactions/ \\\n -H 'X-Transaction-Token: SDK_TOKEN' -H 'Content-Type: application/json' \\\n -d '{\"txnId\": \"wd-2026-07-07-0042\", \"type\": \"travelRule\", \"info\": {\"direction\": \"out\", \"amount\": \"0.25\", \"currency\": \"ETH\", \"currencyType\": \"crypto\"}, \"subject\": {\"fullName\": \"Jane Doe\"}, \"counterparty\": {\"fullName\": \"Ana Diaz\", \"paymentMethod\": {\"type\": \"unhosted_wallet\", \"accountId\": \"0xBeneficiaryWallet01\"}}, \"travelRule\": {\"status\": \"AWAITING_COUNTERPARTY\", \"beneficiaryData\": {\"wallet_address\": \"0xBeneficiaryWallet01\", \"name\": \"Ana Diaz\"}}, \"includeCryptoScreening\": true}'" + }, + { + "lang": "javascript", + "label": "Web SDK", + "source": "import { DiditSdk } from '@didit-protocol/sdk-web';\n\nconst result = await DiditSdk.shared.submitTransaction({\n transactionToken: sdkToken,\n transaction: {\n txnId: 'wd-2026-07-07-0042',\n category: 'travelRule',\n details: { direction: 'out', amount: '0.25', currency: 'ETH', currencyType: 'crypto' },\n subject: { fullName: 'Jane Doe' },\n counterparty: { fullName: 'Ana Diaz', paymentMethod: { type: 'unhosted_wallet', accountId: '0xBeneficiaryWallet01' } },\n travelRule: { status: 'AWAITING_COUNTERPARTY', beneficiaryData: { wallet_address: '0xBeneficiaryWallet01', name: 'Ana Diaz' } },\n includeCryptoScreening: true\n },\n onActionCompleted: (refreshed) => console.log(refreshed.status)\n});" + } + ], + "responses": { + "201": { + "description": "Transaction submitted and monitored. Same detail shape as `GET /v3/transactions/{transaction_id}/`, including the `action_required` block when the end user must complete a follow-up action (the SDKs auto-launch it by default).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionDetail" + }, + "examples": { + "Wallet ownership required": { + "value": { + "uuid": "22222222-3333-4444-5555-666666666666", + "txn_id": "wd-2026-07-07-0042", + "transaction_type": "travelRule", + "direction": "OUTBOUND", + "status": "APPROVED", + "amount": "0.25", + "currency": "ETH", + "travel_rule": { + "uuid": "33333333-4444-5555-6666-777777777777", + "status": "UNCONFIRMED_OWNERSHIP", + "required": true + }, + "action_required": { + "type": "wallet_ownership", + "url": "https://verify.didit.me/wallet-ownership/3s9x...urlsafe", + "widget_session_id": "77777777-8888-9999-aaaa-bbbbbbbbbbbb", + "expires_at": "2026-07-07T13:00:00Z" + } + } + } + } + } + } + }, + "400": { + "description": "Validation failed (missing required fields, duplicate `txnId`, unknown category).", + "content": { + "application/json": { + "examples": { + "Missing fields": { + "value": { + "transaction_details": { + "currency": [ + "This field is required." + ] + } + } + }, + "Duplicate txnId": { + "value": { + "transaction_id": [ + "A transaction with this transaction_id already exists." + ] + } + } + } + } + } + }, + "401": { + "description": "The transaction token was rejected: missing, unknown, revoked, over its `max_uses`, or expired. Expired-token responses always contain the word `expired` in the detail, which is how the SDKs distinguish `expired_token` from `invalid_token` errors.", + "content": { + "application/json": { + "examples": { + "Invalid token": { + "value": { + "detail": "Invalid transaction token." + } + }, + "Expired token": { + "value": { + "detail": "Transaction token expired." + } + } + } + } + } + }, + "409": { + "description": "Another request with the same `txnId` created the transaction while this one was being processed, so only one of two simultaneous submissions succeeds. Read the existing transaction instead of resubmitting.", + "content": { + "application/json": { + "examples": { + "Concurrent duplicate": { + "value": { + "detail": "A transaction with this transaction_id already exists." + } + } + } + } + } + } + } + } + }, + "/v1/transactions/{transaction_id}/": { + "get": { + "summary": "Get a transaction from an SDK", + "description": "Device-facing read endpoint, authenticated with the same `X-Transaction-Token` used to submit. The SDKs poll it after an auto-launched action flow completes or its window closes, so the final state never depends on a single notification. Returns the same transaction detail as the submit response, including the current `action_required` block (`null` once the action is resolved).", + "operationId": "getTransactionFromSdk", + "x-didit-sdk": { + "resource": "transactions", + "method": "retrieve_from_device", + "auth": "device_token", + "server": false + }, + "tags": [ + "Transactions" + ], + "parameters": [ + { + "name": "transaction_id", + "in": "path", + "required": true, + "description": "Didit-stable transaction UUID (the `uuid` from the submit response, **not** your `txnId`).", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "22222222-3333-4444-5555-666666666666" + } + ], + "security": [ + { + "TransactionTokenAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v1/transactions/22222222-3333-4444-5555-666666666666/' \\\n -H 'X-Transaction-Token: SDK_TOKEN'" + } + ], + "responses": { + "200": { + "description": "The full transaction detail, including `action_required` (`null` once the pending action is resolved).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionDetail" + } + } + } + }, + "401": { + "description": "The transaction token was rejected: missing, unknown, revoked, over its `max_uses`, or expired.", + "content": { + "application/json": { + "examples": { + "Expired token": { + "value": { + "detail": "Transaction token expired." + } + } + } + } + } + }, + "404": { + "description": "No transaction with this UUID exists for the token's application, or the path segment is not a valid UUID.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Not found." + } + } + } + } + } + } + } + } + }, + "/v3/businesses/": { + "get": { + "summary": "List businesses", + "description": "List [businesses](/entities/businesses) Didit assembles from KYB sessions sharing the same `vendor_data` (free-form string, NOT a UUID), plus businesses created via the API. Paginated, ordered by `last_session_at` desc. This endpoint does not support filtering or search parameters — fetch pages and filter client-side.", + "operationId": "list_businesses", + "x-didit-sdk": { + "resource": "businesses", + "method": "list", + "auth": "api_key", + "server": true + }, + "tags": [ + "Businesses" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50, + "minimum": 1 + }, + "description": "Page size. Defaults to 50 when omitted. There is no enforced maximum, but keep pages small for latency." + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + }, + "description": "Zero-based offset of the first record to return. Prefer following the `next` URL from the previous page over computing this by hand." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/businesses/?limit=50' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/businesses/',\n headers={'x-api-key': 'YOUR_API_KEY'},\n params={'limit': 50},\n)\nresp.raise_for_status()\npage = resp.json()\nfor biz in page['results']:\n print(biz['vendor_data'], biz['status'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const url = new URL('https://verification.didit.me/v3/businesses/');\nurl.searchParams.set('limit', '50');\n\nconst page = await fetch(url, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n}).then((r) => r.json());\nconsole.log(page.count, page.results.length);" + } + ], + "responses": { + "200": { + "description": "Paginated list of businesses with status, session counts, and feature breakdown.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "description": "Number of matching rows — exact up to 100, capped at 100 beyond that for performance. Do not use it to detect the last page; rely on `next` being `null` instead." + }, + "next": { + "type": "string", + "nullable": true, + "description": "Absolute URL of the next page, or `null` on the last page." + }, + "previous": { + "type": "string", + "nullable": true, + "description": "Absolute URL of the previous page, or `null` on the first page." + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BusinessListItem" + } + } + } + }, + "examples": { + "Example": { + "value": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "didit_internal_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", + "vendor_data": "company-123", + "display_name": null, + "legal_name": "Acme Trading Ltd", + "registration_number": "12345678", + "country_code": "GB", + "effective_name": "Acme Trading Ltd", + "status": "ACTIVE", + "session_count": 2, + "approved_count": 1, + "declined_count": 0, + "in_review_count": 1, + "features": { + "KYB": "Approved" + }, + "features_list": [ + { + "feature": "KYB", + "status": "Approved" } - } + ], + "last_session_at": "2026-03-15T10:30:00Z", + "last_activity_at": "2026-03-15T10:30:00Z", + "first_session_at": "2026-01-10T08:00:00Z", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "name": "VIP", + "color": "#2567FF" + } + ], + "created_at": "2026-01-10T08:00:00Z" + } + ] + } + } + } + } + } + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the key cannot list businesses for this application. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Rate limited": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/customization/": { + "get": { + "summary": "Get branding customization", + "description": "Returns the white-label branding for the application resolved from the API key: colors, fonts, border radii, logo URLs and UI/UX options. Custom domain and white-label email settings are managed only in the Business Console and are not part of this response.", + "operationId": "get_customization", + "x-didit-sdk": { + "resource": "customization", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "tags": [ + "Customization" + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/customization/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/customization/',\n headers={'x-api-key': 'YOUR_API_KEY'},\n)\nresp.raise_for_status()\nbranding = resp.json()\nprint(branding['font_family'], branding['color_primary'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const branding = await fetch(\n 'https://verification.didit.me/v3/customization/',\n { headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n).then((r) => r.json());\nconsole.log(branding.font_family, branding.color_primary);" + } + ], + "responses": { + "200": { + "description": "Current branding customization for the application.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrandingCustomization" + }, + "examples": { + "Default branding": { + "value": { + "color_primary": "#000000", + "color_secondary": "#9DA1A1", + "color_background": "#FFFFFF", + "color_panel": "#2567FF", + "color_panel_10": "#1A1A1A", + "color_on_panel_1": "#9DA1A1", + "color_on_panel_2": "#F4F4F6", + "color_on_background": "#000000", + "color_button_1": "#2567FF", + "color_button_2": "#F8F8F8", + "color_button_text_1": "#FFFFFF", + "color_button_text_2": "#000000", + "color_pill_text": "#111827", + "border_radius_panel": 10, + "border_radius_buttons": 12, + "font_family": "Inter", + "font_weight": "regular", + "font_url": "https://fonts.googleapis.com/css2?family=Inter", + "logo_square": null, + "logo_rectangular": null, + "logo_rectangular_dark": null, + "favicon": null, + "app_public_name": "Acme Verify", + "privacy_policy_url": "https://acme.com/privacy", + "skip_welcome_screen": false, + "disable_login_with_didit": false, + "hide_progress_bar": false, + "callback_seconds": 3 + } + } + } + } + } + }, + "403": { + "description": "API key missing, malformed, expired, or scoped to a different application. Didit returns 403 (never 401) for all authentication failures on this endpoint.", + "content": { + "application/json": { + "examples": { + "Invalid token": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. Retry with exponential backoff." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "patch": { + "summary": "Update branding customization", + "description": "Partial update of the application's white-label branding. Send `application/json` to change colors, fonts, radii and UI options, or `multipart/form-data` to upload logo images. Only the fields you include are changed. Custom domain and white-label email cannot be set through this endpoint — use the Business Console for those.", + "operationId": "update_customization", + "x-didit-sdk": { + "resource": "customization", + "method": "update", + "auth": "api_key", + "server": true + }, + "tags": [ + "Customization" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrandingCustomizationUpdate" + }, + "examples": { + "Update colors and font": { + "summary": "Change branding colors and font", + "value": { + "color_primary": "#123456", + "color_button_1": "#2567FF", + "font_family": "AR One Sans", + "border_radius_buttons": 16 + } + }, + "Update UI options": { + "summary": "Change public name and flow options", + "value": { + "app_public_name": "Acme Verify", + "privacy_policy_url": "https://acme.com/privacy", + "skip_welcome_screen": true, + "hide_progress_bar": true, + "callback_seconds": 5 + } + }, + "Remove a logo": { + "summary": "Delete the current rectangular logo", + "value": { + "delete_logo_rectangular": true + } + }, + "Remove the dark-mode logo": { + "summary": "Delete the current dark-mode logo", + "value": { + "delete_logo_rectangular_dark": true + } + } + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "description": "Use multipart/form-data only to upload logo images. Branding fields above may also be sent as form fields in the same request.", + "properties": { + "image_square": { + "type": "string", + "format": "binary", + "description": "Square logo (1:1 aspect ratio). PNG, JPG or WebP, at least 512 px on the long side, at most **1 MB**." + }, + "image_rectangular": { + "type": "string", + "format": "binary", + "description": "Rectangular logo (2:1 aspect ratio). PNG, JPG or WebP, at least 512 px on the long side, at most **1 MB**." + }, + "image_rectangular_dark": { + "type": "string", + "format": "binary", + "description": "Rectangular logo for dark mode (2:1 aspect ratio), shown on a dark background. PNG, JPG or WebP, at least 512 px on the long side, at most **1 MB**. Returned as `logo_rectangular_dark`." + }, + "image_favicon": { + "type": "string", + "format": "binary", + "description": "Favicon image. Max **500 KB**." + } + } + }, + "example": { + "image_rectangular": "(binary PNG logo, 2:1)", + "image_rectangular_dark": "(binary PNG logo for dark mode, 2:1)", + "image_favicon": "(binary favicon)" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH 'https://verification.didit.me/v3/customization/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"color_primary\": \"#123456\", \"font_family\": \"AR One Sans\"}'" + }, + { + "lang": "curl", + "label": "curl (logo upload)", + "source": "curl -X PATCH 'https://verification.didit.me/v3/customization/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'image_rectangular=@logo.png' \\\n -F 'image_rectangular_dark=@logo-dark.png' \\\n -F 'image_favicon=@favicon.ico'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.patch(\n 'https://verification.didit.me/v3/customization/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'color_primary': '#123456', 'font_family': 'AR One Sans'},\n)\nresp.raise_for_status()\nprint(resp.json()['color_primary'])\n\n# Upload a logo (multipart):\nwith open('logo.png', 'rb') as logo:\n requests.patch(\n 'https://verification.didit.me/v3/customization/',\n headers={'x-api-key': 'YOUR_API_KEY'},\n files={'image_rectangular': logo},\n )" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/customization/', {\n method: 'PATCH',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ color_primary: '#123456', font_family: 'AR One Sans' }),\n});\nconst branding = await resp.json();\nconsole.log(branding.color_primary);" + } + ], + "responses": { + "200": { + "description": "Branding updated. The FULL branding object is returned (all fields, not just the ones you changed).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrandingCustomization" + }, + "examples": { + "Updated": { + "value": { + "color_primary": "#123456", + "color_secondary": "#9DA1A1", + "color_background": "#FFFFFF", + "color_panel": "#2567FF", + "color_panel_10": "#1A1A1A", + "color_on_panel_1": "#9DA1A1", + "color_on_panel_2": "#F4F4F6", + "color_on_background": "#000000", + "color_button_1": "#2567FF", + "color_button_2": "#F8F8F8", + "color_button_text_1": "#FFFFFF", + "color_button_text_2": "#000000", + "color_pill_text": "#111827", + "border_radius_panel": 10, + "border_radius_buttons": 16, + "font_family": "AR One Sans", + "font_weight": "400", + "font_url": "https://fonts.googleapis.com/css2?family=AR+One+Sans", + "logo_square": null, + "logo_rectangular": "https://cdn.didit.me/white_label/logo_rectangular/...", + "logo_rectangular_dark": null, + "favicon": null, + "app_public_name": "Acme Verify", + "privacy_policy_url": "https://acme.com/privacy", + "skip_welcome_screen": true, + "disable_login_with_didit": false, + "hide_progress_bar": false, + "callback_seconds": 3 + } + } + } + } + } + }, + "400": { + "description": "Invalid body. For example: a malformed hex color, an unknown font family or weight, a Business-Console-only field (`domain`, `from_email`, `verify_domain`, `force_remove_domain`, `verify_email_domain`, `force_remove_email_domain`, and their read-only companions), or a logo upload that is not a PNG, JPG or WebP image, is under 512 px on its long side, is over 1 MB, or has the wrong aspect ratio. Also returned when setting `disable_login_with_didit` to `false` while a custom domain is configured.", + "content": { + "application/json": { + "examples": { + "Bad color": { + "value": { + "color_primary": [ + "Invalid color format. Use '#RRGGBB' or '#RRGGBBAA' format." + ] + } + }, + "Bad logo": { + "value": { + "image_rectangular": [ + "Image aspect ratio must be 2:1. Got 1024x1024 image dimensions" + ] + } + }, + "Unsupported logo format": { + "summary": "Not a PNG, JPG or WebP image (an SVG, for example)", + "value": { + "image_rectangular_dark": [ + "Upload a PNG, JPG or WebP image." + ] + } + }, + "Logo too small": { + "summary": "Under 512 px on the long side", + "value": { + "image_rectangular": [ + "Image must be at least 512 px on its long side. Got 400x200 image dimensions" + ] + } + }, + "Logo too large": { + "summary": "Over 1 MB", + "value": { + "image_square": [ + "File size should not exceed 1 MB" + ] + } + }, + "Console-only field": { + "value": { + "domain": [ + "Manage this field in the Business Console." + ] + } + }, + "Bad font weight": { + "value": { + "font_weight": [ + "Font weight 950 is not available for Inter. Available weights are: 100, 200, 300, 400, 500, 600, 700, 800, 900" + ] + } + } + } + } + } + }, + "403": { + "description": "API key missing, malformed, expired, or scoped to a different application. Didit returns 403 (never 401) for all authentication failures on this endpoint.", + "content": { + "application/json": { + "examples": { + "Invalid token": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. Retry with exponential backoff." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/businesses/{vendor_data}/": { + "get": { + "summary": "Get business", + "description": "Fetch one [business](/entities/businesses) by `vendor_data` (exact match). Adds `metadata` and `updated_at` to the list view. Deleted businesses return 404.", + "operationId": "get_business", + "x-didit-sdk": { + "resource": "businesses", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "tags": [ + "Businesses" + ], + "parameters": [ + { + "name": "vendor_data", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Your unique identifier for the business — a free-form string (NOT a UUID), matched exactly as sent.", + "example": "company-123" + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/businesses/company-123/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/businesses/company-123/',\n headers={'x-api-key': 'YOUR_API_KEY'},\n)\nresp.raise_for_status()\nbiz = resp.json()\nprint(biz['didit_internal_id'], biz['status'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const vendorData = encodeURIComponent('company-123');\nconst biz = await fetch(\n `https://verification.didit.me/v3/businesses/${vendorData}/`,\n { headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n).then((r) => r.json());\nconsole.log(biz.didit_internal_id, biz.status);" + } + ], + "responses": { + "200": { + "description": "Full business detail including metadata and aggregated session data.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessDetailItem" + }, + "examples": { + "Active business": { + "value": { + "didit_internal_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", + "vendor_data": "company-123", + "display_name": null, + "legal_name": "Acme Trading Ltd", + "registration_number": "12345678", + "country_code": "GB", + "effective_name": "Acme Trading Ltd", + "status": "ACTIVE", + "session_count": 2, + "approved_count": 1, + "declined_count": 0, + "in_review_count": 1, + "features": { + "KYB": "Approved" + }, + "features_list": [ + { + "feature": "KYB", + "status": "Approved" + } + ], + "last_session_at": "2026-03-15T10:30:00Z", + "last_activity_at": "2026-03-15T10:30:00Z", + "first_session_at": "2026-01-10T08:00:00Z", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "name": "VIP", + "color": "#2567FF" + } + ], + "created_at": "2026-01-10T08:00:00Z", + "metadata": { + "industry": "fintech" + }, + "updated_at": "2026-03-15T10:30:00Z" + } + } + } + } + } + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the key cannot read this application's businesses. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No (non-deleted) business with the supplied `vendor_data` exists for this application.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Rate limited": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "patch": { + "summary": "Update business", + "description": "Partial update of a business: `display_name`, `legal_name`, `registration_number`, `country_code`, `status`, `metadata`. `metadata` fully replaces. Setting `status` to `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. Every edit is recorded in the company's activity: a status change with the optional `reason`, and a change to `display_name`, `legal_name`, `registration_number`, `country_code` or `metadata` as a profile update.", + "operationId": "update_business", + "x-didit-sdk": { + "resource": "businesses", + "method": "update", + "auth": "api_key", + "server": true + }, + "tags": [ + "Businesses" + ], + "parameters": [ + { + "name": "vendor_data", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Your unique identifier for the business (free-form string, NOT a UUID).", + "example": "company-123" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Partial-update body. Provide only the fields you want to change.", + "properties": { + "display_name": { + "type": "string", + "nullable": true, + "description": "Friendly display name shown in the console (takes precedence over `legal_name` for UI display)." + }, + "legal_name": { + "type": "string", + "nullable": true, + "description": "Official legal name from registry or manual entry." + }, + "registration_number": { + "type": "string", + "nullable": true, + "description": "Company registration or incorporation number." + }, + "country_code": { + "type": "string", + "nullable": true, + "description": "Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `US`)." + }, + "status": { + "type": "string", + "enum": [ + "ACTIVE", + "FLAGGED", + "BLOCKED" + ], + "description": "Business lifecycle status. Setting `BLOCKED` automatically adds the `vendor_data` to the system blocklist; flipping away from `BLOCKED` removes that blocklist entry." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Arbitrary JSON object you attach to the business. **Fully replaces** the existing metadata on update — merge client-side if you only want to add keys." + }, + "reason": { + "type": "string", + "nullable": true, + "maxLength": 1000, + "description": "Why the status changed. Recorded on the status-change entry of the company's **Activity** in the Business Console, only when the status actually changes. Webhooks do not carry it." + } + } + }, + "examples": { + "Update name": { + "summary": "Update display name", + "value": { + "display_name": "Acme Corp" + } + }, + "Flag for review": { + "summary": "Flip status", + "value": { + "status": "FLAGGED" + } + }, + "Add metadata": { + "summary": "Replace metadata JSON", + "value": { + "metadata": { + "industry": "fintech", + "risk_level": "low" + } + } + }, + "Fix registry data": { + "summary": "Correct extracted fields", + "value": { + "legal_name": "Acme Trading International Ltd", + "registration_number": "12345678", + "country_code": "GB" + } + }, + "Flag with reason": { + "summary": "Flag the business and record why", + "value": { + "status": "FLAGGED", + "reason": "Ownership structure under review" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH 'https://verification.didit.me/v3/businesses/company-123/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"display_name\": \"Acme Corp\", \"status\": \"FLAGGED\"}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.patch(\n 'https://verification.didit.me/v3/businesses/company-123/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'display_name': 'Acme Corp', 'status': 'FLAGGED'},\n)\nresp.raise_for_status()\nprint(resp.json()['status'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/businesses/company-123/', {\n method: 'PATCH',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ display_name: 'Acme Corp', status: 'FLAGGED' }),\n});\nconst biz = await resp.json();\nconsole.log(biz.status);" + } + ], + "responses": { + "200": { + "description": "Business updated. The full updated business record is returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessDetailItem" + }, + "examples": { + "Updated": { + "value": { + "didit_internal_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", + "vendor_data": "company-123", + "display_name": "Acme Corp", + "legal_name": "Acme Trading Ltd", + "registration_number": "12345678", + "country_code": "GB", + "effective_name": "Acme Corp", + "status": "FLAGGED", + "session_count": 2, + "approved_count": 1, + "declined_count": 0, + "in_review_count": 1, + "features": { + "KYB": "Approved" + }, + "features_list": [ + { + "feature": "KYB", + "status": "Approved" + } + ], + "last_session_at": "2026-03-15T10:30:00Z", + "last_activity_at": "2026-03-15T10:30:00Z", + "first_session_at": "2026-01-10T08:00:00Z", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "name": "VIP", + "color": "#2567FF" } + ], + "created_at": "2026-01-10T08:00:00Z", + "metadata": { + "industry": "fintech", + "risk_level": "low" + }, + "updated_at": "2026-03-15T10:30:00Z" + } + } + } + } + } + }, + "400": { + "description": "Invalid body: typically an unknown `status`, malformed `metadata` or a `reason` over 1000 characters.", + "content": { + "application/json": { + "examples": { + "Bad status": { + "value": { + "status": [ + "Invalid status. Valid options are: ACTIVE, FLAGGED, BLOCKED" + ] + } + }, + "Bad metadata": { + "value": { + "metadata": [ + "Metadata must be a JSON object." + ] + } + }, + "Reason too long": { + "value": { + "reason": [ + "Ensure this field has no more than 1000 characters." + ] + } + } + } + } + } + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the key cannot update businesses for this application. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No (non-deleted) business with the supplied `vendor_data` exists for this application.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Rate limited": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/businesses/{vendor_data}/update-status/": { + "patch": { + "summary": "Update business status", + "description": "Flip only the lifecycle `status` of a business: `ACTIVE`, `FLAGGED` or `BLOCKED` (not session statuses). `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. Each change is recorded in the company's activity, with the optional `reason`. Sending the current status changes nothing and records no `reason`. The `business.status.updated` webhook does not carry `reason`.", + "operationId": "update_business_status", + "x-didit-sdk": { + "resource": "businesses", + "method": "update_status", + "auth": "api_key", + "server": true + }, + "tags": [ + "Businesses" + ], + "parameters": [ + { + "name": "vendor_data", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Your unique identifier for the business (free-form string, NOT a UUID).", + "example": "company-123" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/EntityStatusUpdateRequest" + }, + { + "type": "object", + "properties": { + "reason": { + "type": "string", + "nullable": true, + "maxLength": 1000, + "description": "Why the status changed. Recorded on the status-change entry of the company's **Activity** in the Business Console, only when the status actually changes. Webhooks do not carry it." } + } + } + ] + }, + "examples": { + "Flag": { + "summary": "Flag the business for review", + "value": { + "status": "FLAGGED" + } + }, + "Block": { + "summary": "Block the business (adds to system blocklist)", + "value": { + "status": "BLOCKED" + } + }, + "Reactivate": { + "summary": "Reactivate the business", + "value": { + "status": "ACTIVE" + } + }, + "Block with reason": { + "summary": "Block the business and record why", + "value": { + "status": "BLOCKED", + "reason": "Registry shows the company dissolved" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH 'https://verification.didit.me/v3/businesses/company-123/update-status/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"status\": \"BLOCKED\"}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.patch(\n 'https://verification.didit.me/v3/businesses/company-123/update-status/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'status': 'BLOCKED'},\n)\nresp.raise_for_status()\nprint(resp.json()['status'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/businesses/company-123/update-status/', {\n method: 'PATCH',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ status: 'BLOCKED' }),\n});\nconst biz = await resp.json();\nconsole.log(biz.status);" + } + ], + "responses": { + "200": { + "description": "Business status updated. Full business record returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessDetailItem" + }, + "examples": { + "Blocked": { + "value": { + "didit_internal_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", + "vendor_data": "company-123", + "display_name": null, + "legal_name": "Acme Trading Ltd", + "registration_number": "12345678", + "country_code": "GB", + "effective_name": "Acme Trading Ltd", + "status": "BLOCKED", + "session_count": 2, + "approved_count": 1, + "declined_count": 0, + "in_review_count": 1, + "features": { + "KYB": "Approved" + }, + "features_list": [ + { + "feature": "KYB", + "status": "Approved" + } + ], + "last_session_at": "2026-03-15T10:30:00Z", + "last_activity_at": "2026-03-15T10:30:00Z", + "first_session_at": "2026-01-10T08:00:00Z", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "name": "VIP", + "color": "#2567FF" + } + ], + "created_at": "2026-01-10T08:00:00Z", + "metadata": { + "industry": "fintech" + }, + "updated_at": "2026-03-15T10:30:00Z" + } + } + } + } + } + }, + "400": { + "description": "Missing or invalid `status`, or a `reason` over 1000 characters.", + "content": { + "application/json": { + "examples": { + "Missing status": { + "value": { + "status": [ + "This field is required." + ] + } + }, + "Bad status": { + "value": { + "status": [ + "Invalid status. Valid options are: ACTIVE, FLAGGED, BLOCKED" + ] + } + }, + "Reason too long": { + "value": { + "reason": [ + "Ensure this field has no more than 1000 characters." + ] + } + } + } + } + } + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the key cannot update businesses for this application. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No (non-deleted) business with the supplied `vendor_data` exists for this application.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Rate limited": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/businesses/delete/": { + "post": { + "summary": "Batch delete businesses", + "description": "**Permanently delete** businesses — this cannot be undone. For blocking prefer [Update Business Status](#patch-/v3/businesses/-vendor_data-/update-status/) with `BLOCKED`.", + "operationId": "batch_delete_businesses", + "x-didit-sdk": { + "resource": "businesses", + "method": "batch_delete", + "auth": "api_key", + "server": true + }, + "tags": [ + "Businesses" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Provide `vendor_data_list`, `didit_internal_id_list`, or `delete_all: true`. Precedence when several are sent: `delete_all` > `vendor_data_list` > `didit_internal_id_list` (only the highest-precedence selector is used).", + "properties": { + "vendor_data_list": { + "type": "array", + "items": { + "type": "string" }, - "nfc_skip_reason": { + "description": "Your own identifiers to delete, matched exactly as sent." + }, + "didit_internal_id_list": { + "type": "array", + "items": { "type": "string", - "nullable": true, - "enum": [ - "USER_SKIPPED", - "DOCUMENT_WITHOUT_CHIP", - "CHIP_CERTIFICATE_UNAVAILABLE", - "DEVICE_WITHOUT_NFC", - "INTEGRATION_WITHOUT_NFC_ACCESS", - "MRZ_KEY_UNAVAILABLE" - ], - "description": "Why NFC chip verification did not run, populated whenever the workflow includes an NFC step. `null` while NFC is pending or when the chip was actually read. Values: `USER_SKIPPED` - the user chose to skip on an NFC-capable device (allowed by `allow_nfc_skip`); `DOCUMENT_WITHOUT_CHIP` - the document has no readable NFC chip (not an ICAO 9303 electronic document); `CHIP_CERTIFICATE_UNAVAILABLE` - the issuing country's signing certificate (CSCA) covering the document's issue period is unavailable, so the chip signature cannot be verified; `DEVICE_WITHOUT_NFC` - the device reported no NFC reader; `INTEGRATION_WITHOUT_NFC_ACCESS` - the verification ran without a native SDK (e.g. in a web browser), which has no access to the phone's NFC reader; `MRZ_KEY_UNAVAILABLE` - the chip access key could not be derived from the document's MRZ." + "format": "uuid" }, - "nfc_verifications": { - "type": "array", - "nullable": true, - "description": "**Array of NFC / ePassport reports** produced by NFC steps in the workflow graph. **This is always a JSON array, never a singular `nfc` object** — read `nfc_verifications[0]` and iterate if your workflow can run NFC more than once. `null` until at least one NFC step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `chip_data`, `authenticity` (SOD + DG integrity), `certificate_summary`, and `warnings[]`. When the NFC step was bypassed before it could ever be offered (chipless document, missing certificate, no NFC capability reported, unreadable MRZ key), this stays `null` and `nfc_skip_reason` explains why; a skip at the step itself (user tapped skip, or the reader turned out unavailable at scan time) still produces a report with `is_nfc_skipped: true`.", - "items": { - "type": "object", - "description": "One NFC report — the full report for a single ePassport chip read.", - "properties": { - "node_id": { - "type": "string", - "description": "Workflow graph node id of the NFC step that produced this report. Use it to correlate the report with the workflow definition." - }, - "status": { - "type": "string", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Resub Requested" - ], - "description": "Feature-level status of this NFC step. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`." - }, - "is_nfc_skipped": { - "type": "boolean", - "description": "`true` when this NFC step was explicitly skipped instead of read; `skip_reason` says why." - }, - "skip_reason": { - "type": "string", - "nullable": true, - "enum": [ - "USER_SKIPPED", - "DOCUMENT_WITHOUT_CHIP", - "CHIP_CERTIFICATE_UNAVAILABLE", - "DEVICE_WITHOUT_NFC", - "INTEGRATION_WITHOUT_NFC_ACCESS", - "MRZ_KEY_UNAVAILABLE" - ], - "description": "Why this NFC step was skipped. Only set when `is_nfc_skipped` is `true`. Same taxonomy as the session-level `nfc_skip_reason`, which additionally covers the cases where NFC never produced a report at all." - }, - "portrait_image": { - "type": "string", - "format": "uri" - }, - "signature_image": { - "type": "string", - "format": "uri" - }, - "chip_data": { - "type": "object", - "properties": { - "document_type": { - "type": "string" - }, - "issuing_country": { - "type": "string" - }, - "document_number": { - "type": "string" - }, - "expiration_date": { - "type": "string", - "format": "date" - }, - "first_name": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "birth_date": { - "type": "string", - "format": "date" - }, - "gender": { - "type": "string" - }, - "nationality": { - "type": "string" - }, - "address": { - "type": "string" - }, - "place_of_birth": { - "type": "string" - } - } - }, - "authenticity": { - "type": "object", - "properties": { - "sod_integrity": { - "type": "boolean" - }, - "dg_integrity": { - "type": "boolean" - } - } - }, - "certificate_summary": { - "type": "object", - "properties": { - "issuer": { - "type": "string" - }, - "subject": { - "type": "string" - }, - "serial_number": { - "type": "string" - }, - "not_valid_after": { - "type": "string", - "format": "date-time" - }, - "not_valid_before": { - "type": "string", - "format": "date-time" - } - } - }, - "warnings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "enum": [ - "ID_VERIFICATION", - "NFC", - "LIVENESS", - "FACEMATCH", - "PROOF_OF_ADDRESS", - "PHONE", - "EMAIL", - "AML", - "LOCATION", - "DATABASE_VALIDATION", - "QUESTIONNAIRE", - "DOCUMENT_AI", - "KYB_REGISTRY", - "KYB_DOCUMENTS", - "KYB_KEY_PEOPLE" - ], - "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload." - }, - "risk": { - "type": "string" - }, - "additional_data": { - "type": "object", - "nullable": true - }, - "log_type": { - "type": "string" - }, - "short_description": { - "type": "string" - }, - "long_description": { - "type": "string" - }, - "node_id": { - "type": "string", - "description": "The workflow graph node ID that generated this warning" - } - } - } - } - } - } + "description": "Didit's internal UUIDs (`didit_internal_id`) to delete." + }, + "delete_all": { + "type": "boolean", + "default": false, + "description": "If `true`, deletes every business in the application. Cannot be combined with the list selectors." + } + } + }, + "examples": { + "Specific businesses": { + "summary": "Delete by vendor_data", + "value": { + "vendor_data_list": [ + "company-123", + "company-456" + ] + } + }, + "Specific internal IDs": { + "summary": "Delete by didit_internal_id", + "value": { + "didit_internal_id_list": [ + "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "b2c3d4e5-f6a7-8901-bcde-f12345678901" + ] + } + }, + "All businesses": { + "summary": "Wipe every business in this application", + "value": { + "delete_all": true + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/businesses/delete/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"vendor_data_list\": [\"company-123\", \"company-456\"]}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/businesses/delete/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'vendor_data_list': ['company-123', 'company-456']},\n)\nresp.raise_for_status()\nprint('deleted', resp.json()['deleted'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/businesses/delete/', {\n method: 'POST',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ vendor_data_list: ['company-123', 'company-456'] }),\n});\nconst body = await resp.json();\nconsole.log('deleted', body.deleted);" + } + ], + "responses": { + "200": { + "description": "Businesses deleted. Returns the number of rows actually removed.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "deleted": { + "type": "integer", + "description": "Number of businesses deleted (excludes vendor_data values that didn't match anything)." + } + } + }, + "examples": { + "Deleted some": { + "value": { + "deleted": 2 + } + }, + "Nothing matched": { + "value": { + "deleted": 0 + } + } + } + } + } + }, + "400": { + "description": "No selector supplied (all of `vendor_data_list`, `didit_internal_id_list`, and `delete_all` missing or empty). The error body is a JSON array.", + "content": { + "application/json": { + "examples": { + "Missing selector": { + "value": [ + "Provide vendor_data_list, didit_internal_id_list, or set delete_all=true." + ] + } + } + } + } + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the key cannot delete businesses for this application. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Rate limited": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/users/aml-monitoring/": { + "post": { + "summary": "Enable or disable AML ongoing monitoring for users", + "description": "Turns AML ongoing monitoring on or off for the selected users, with the same semantics as the Business Console. Exactly one monitored AML screening is kept per user, so a profile that verified several times is monitored and billed once; disabling turns monitoring off across all of the profile's screenings. Users without any AML screening are screened first (billed as a regular AML screening) when they have the full name, date of birth and country needed; otherwise they are reported as `skipped_no_identity_data`.\n\nSet `estimate_only: true` to preview counts and costs without writing anything. Selections of 20 profiles or fewer resolve inline and return `200` with the completed job; larger selections return `202` and run in the background - poll [Get AML monitoring job](#get-/v3/aml-monitoring/jobs/-job_id-/) with the returned `job_id`. Enabling is refused with `403` when the organization balance does not cover the estimate. Identifiers that match nothing are listed in `unmatched` and never fail the request: a selection that matches nothing completes as an empty job (`total` 0), the same convention as the batch delete endpoints.", + "operationId": "toggle_users_aml_monitoring", + "x-didit-sdk": { + "resource": "users", + "method": "set_aml_monitoring", + "auth": "api_key", + "server": true + }, + "tags": [ + "AML Ongoing Monitoring" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "is_enabled" + ], + "description": "Select users with `vendor_data_list`, `didit_internal_id_list`, or both (the two lists are OR'd). At most 1000 identifiers per request across both lists.", + "properties": { + "is_enabled": { + "type": "boolean", + "description": "true to enable monitoring, false to disable it." + }, + "estimate_only": { + "type": "boolean", + "default": false, + "description": "When true, returns the cost estimate and performs no writes - no screening, no billing, no job." + }, + "vendor_data_list": { + "type": "array", + "items": { + "type": "string" }, - "liveness_checks": { - "type": "array", - "nullable": true, - "description": "**Array of liveness reports** produced by liveness steps in the workflow graph. Always a JSON array — never a singular `liveness` object. `null` until at least one liveness step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `method`, `score`, `reference_image`, `video_url`, optional `age_estimation`, cross-session `matches[]`, and `warnings[]`.", - "items": { - "type": "object", - "description": "One liveness report — the full report for a single liveness pass.", - "properties": { - "node_id": { - "type": "string", - "description": "Workflow graph node id of the liveness step that produced this report." - }, - "status": { - "type": "string", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Resub Requested" - ], - "description": "Feature-level status of this liveness step. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`." - }, - "method": { - "type": "string", - "description": "Method used for liveness detection" - }, - "score": { - "type": "number", - "description": "Confidence score of the liveness check" - }, - "reference_image": { - "type": "string", - "format": "uri", - "description": "URL of the reference image used" - }, - "video_url": { - "type": "string", - "format": "uri", - "description": "URL of the liveness check video" - }, - "age_estimation": { - "type": "number", - "description": "Estimated age of the person" - }, - "matches": { - "type": "array", - "description": "Similar faces found by the duplicate-face search (top matches, blocklisted/allowlisted entries first). Each entry's `source` tells you where the matching face came from: `session` (another verification session), `imported` (a face uploaded against a vendor user), `list_entry` (an org-managed allow/block list face), or `retained_template` (an image-free biometric template stored on a User, listed under `/v3/biometric-templates/`). `session_id`, `session_number`, `status` and `api_service` are `null` for non-session sources. `imported` matches still populate `vendor_data`, `verification_date` and — when the vendor user has a name on file — `user_details` (with `full_name` only) from the vendor-user record; `retained_template` matches populate `vendor_user_id`, `vendor_data`, `biometric_template_id` and `verification_date`, while their image and identity fields are `null`; `list_entry` matches return all of these as `null`.", - "items": { - "type": "object", - "properties": { - "session_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Session ID of the matching face. `null` when `source` is `imported`, `list_entry`, or `retained_template`." - }, - "session_number": { - "type": "integer", - "nullable": true - }, - "similarity_percentage": { - "type": "number", - "description": "Face similarity between the two images, clamped to 0–100." - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "`vendor_data` of the matching session or linked User for imported and retained-template faces." - }, - "vendor_user_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Linked User id for `imported` and `retained_template` matches and for session matches linked to a User. `null` for list entries and sessions without a User." - }, - "biometric_template_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Id of the retained biometric template. Set only when `source` is `retained_template`; `null` otherwise." - }, - "verification_date": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the matching face was captured, or when a retained template was created. `null` for list entries." - }, - "user_details": { - "type": "object", - "nullable": true, - "description": "Identity data from the matching session or imported User profile. Always `null` for `retained_template` and `list_entry` matches.", - "properties": { - "full_name": { - "type": "string", - "nullable": true - }, - "document_type": { - "type": "string", - "nullable": true - }, - "document_number": { - "type": "string", - "nullable": true - } - } - }, - "match_image_url": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "Reference image for image-backed matches. Always `null` for `retained_template` matches because retained templates contain no image." - }, - "status": { - "type": "string", - "nullable": true, - "description": "Lifecycle status of the matching session. `null` for non-session sources." - }, - "is_blocklisted": { - "type": "boolean", - "description": "Whether the matching face is on a blocklist." - }, - "is_allowlisted": { - "type": "boolean", - "description": "Whether the matching face is on an allowlist." - }, - "api_service": { - "type": "string", - "nullable": true, - "description": "Standalone API service of the matching session (e.g. `PASSIVE_LIVENESS`, `FACE_MATCH`); `null` for workflow sessions and non-session sources." - }, - "source": { - "type": "string", - "enum": [ - "session", - "imported", - "list_entry", - "retained_template" - ], - "description": "Origin of the matching face: `session`, `imported` (User face upload), `list_entry` (allow/block list), or `retained_template` (an image-free biometric template stored on a User, listed under `/v3/biometric-templates/`)." - } - } - } - }, - "warnings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "enum": [ - "ID_VERIFICATION", - "NFC", - "LIVENESS", - "FACEMATCH", - "PROOF_OF_ADDRESS", - "PHONE", - "EMAIL", - "AML", - "LOCATION", - "DATABASE_VALIDATION", - "QUESTIONNAIRE", - "DOCUMENT_AI", - "KYB_REGISTRY", - "KYB_DOCUMENTS", - "KYB_KEY_PEOPLE" - ], - "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload." - }, - "risk": { - "type": "string" - }, - "additional_data": { - "type": "object", - "nullable": true - }, - "log_type": { - "type": "string" - }, - "short_description": { - "type": "string" - }, - "long_description": { - "type": "string" - }, - "node_id": { - "type": "string", - "description": "The workflow graph node ID that generated this warning" - } - } - } - }, - "face_quality": { - "type": "number", - "nullable": true, - "description": "Face image quality score (0-100%). Only available for passive liveness." - }, - "face_luminance": { - "type": "number", - "nullable": true, - "description": "Face luminance/brightness score (0-100%). Only available for passive liveness." - } - } - } + "description": "Your own identifiers of the users to toggle, matched exactly as sent." + }, + "didit_internal_id_list": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Didit internal ids of the users to toggle - the `didit_internal_id` returned by the create, get and list endpoints." + } + } + }, + "examples": { + "Estimate first": { + "summary": "Preview the cost without writing", + "value": { + "is_enabled": true, + "estimate_only": true, + "vendor_data_list": [ + "user-123", + "user-456" + ] + } + }, + "Enable": { + "summary": "Enable monitoring for two users", + "value": { + "is_enabled": true, + "vendor_data_list": [ + "user-123", + "user-456" + ] + } + }, + "Disable": { + "summary": "Disable monitoring by Didit internal id", + "value": { + "is_enabled": false, + "didit_internal_id_list": [ + "3f1c0d3e-8f0a-4c2e-9a3d-9b1f5f4a2c11" + ] + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/users/aml-monitoring/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"is_enabled\": true, \"vendor_data_list\": [\"user-123\", \"user-456\"]}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/users/aml-monitoring/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'is_enabled': True, 'vendor_data_list': ['user-123', 'user-456']},\n)\nresp.raise_for_status()\njob = resp.json()\nprint(job['status'], job['outcomes'])\nfor row in job['results']:\n print(row['vendor_data'], row['outcome'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/users/aml-monitoring/', {\n method: 'POST',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ is_enabled: true, vendor_data_list: ['user-123', 'user-456'] }),\n});\nconst job = await resp.json();\nconsole.log(job.status, job.outcomes);\nfor (const row of job.results) console.log(row.vendor_data, row.outcome);" + } + ], + "responses": { + "200": { + "description": "The estimate (when `estimate_only` is true) or the completed job for a selection processed inline.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AMLMonitoringToggleResult" + } + } + } + }, + "202": { + "description": "Larger selections run in the background. Poll the job endpoint with `job_id`; `results` fills in as profiles are processed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AMLMonitoringJob" + } + } + } + }, + "400": { + "description": "Validation error: `is_enabled` missing, no selector, or more than 1000 identifiers across both lists." + }, + "401": { + "description": "Missing or invalid API key." + }, + "403": { + "description": "Insufficient balance to enable monitoring for this selection. The response carries the `estimate` so you can top up the right amount.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" }, - "face_matches": { - "type": "array", - "nullable": true, - "description": "**Array of face-match reports** produced by face-match steps in the workflow graph. Always a JSON array — never a singular `face_match` object. `null` until at least one face-match step has produced data. Each item is the full report, including `node_id`, feature-level `status`, similarity `score`, `source_image`, `target_image`, and `warnings[]`.", - "items": { - "type": "object", - "description": "One face-match report — the full report.", - "properties": { - "node_id": { - "type": "string", - "description": "Workflow graph node id of the face-match step that produced this report." - }, - "status": { - "type": "string", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Resub Requested" - ], - "description": "Feature-level status of this face-match step. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`." - }, - "score": { - "type": "number", - "description": "Confidence score of the face match" - }, - "source_image_session_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Session id under which the source (reference) portrait is stored. In biometric-authentication flows this is the session's own id (the submitted portrait is stored under the new session). `null` when the source image is the same session's document portrait." - }, - "source_image": { - "type": "string", - "format": "uri", - "description": "URL of the source image used for comparison" - }, - "target_image": { - "type": "string", - "format": "uri", - "description": "URL of the target image used for comparison" - }, - "warnings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "enum": [ - "ID_VERIFICATION", - "NFC", - "LIVENESS", - "FACEMATCH", - "PROOF_OF_ADDRESS", - "PHONE", - "EMAIL", - "AML", - "LOCATION", - "DATABASE_VALIDATION", - "QUESTIONNAIRE", - "DOCUMENT_AI", - "KYB_REGISTRY", - "KYB_DOCUMENTS", - "KYB_KEY_PEOPLE" - ], - "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload." - }, - "risk": { - "type": "string" - }, - "additional_data": { - "type": "object", - "nullable": true - }, - "log_type": { - "type": "string" - }, - "short_description": { - "type": "string" - }, - "long_description": { - "type": "string" - }, - "node_id": { - "type": "string", - "description": "The workflow graph node ID that generated this warning" - } - } - }, - "description": "Face match verification warnings" - } + "estimate": { + "type": "object", + "description": "Cost preview. Returned directly when estimate_only is true (nothing is written).", + "properties": { + "users_total": { + "type": "integer", + "description": "Profiles matched by the selection." + }, + "already_enabled": { + "type": "integer", + "description": "Profiles already monitored (nothing to do when enabling; these are what disabling turns off)." + }, + "with_existing_aml": { + "type": "integer", + "description": "Profiles whose existing AML screening will be switched to monitored (no new screening)." + }, + "needing_screening": { + "type": "integer", + "description": "Profiles without any AML screening that will be screened first (billed as a regular AML screening)." + }, + "skipped_no_identity_data": { + "type": "integer", + "description": "Profiles that cannot be screened because identity data is missing." + }, + "skipped_blocked": { + "type": "integer", + "description": "Blocked users that will be skipped (always 0 for businesses)." + }, + "estimated_screening_cost_usd": { + "type": "number", + "example": 0.2, + "description": "One-time cost of the new screenings." + }, + "estimated_annual_monitoring_cost_usd": { + "type": "number", + "example": 0.07, + "description": "Yearly monitoring cost of the profiles that will become monitored." + }, + "balance_sufficient": { + "type": "boolean", + "description": "Whether the organization balance covers the screening plus the first year of monitoring." } } + } + } + } + } + } + } + } + } + }, + "/v3/businesses/aml-monitoring/": { + "post": { + "summary": "Enable or disable AML ongoing monitoring for businesses", + "description": "Turns AML ongoing monitoring on or off for the selected businesses, with the same semantics as the Business Console. Exactly one monitored AML screening is kept per business, so a profile that verified several times is monitored and billed once; disabling turns monitoring off across all of the profile's screenings. Businesses without any AML screening are screened first (billed as a regular AML screening) when they have the legal or display name needed; otherwise they are reported as `skipped_no_identity_data`.\n\nSet `estimate_only: true` to preview counts and costs without writing anything. Selections of 20 profiles or fewer resolve inline and return `200` with the completed job; larger selections return `202` and run in the background - poll [Get AML monitoring job](#get-/v3/aml-monitoring/jobs/-job_id-/) with the returned `job_id`. Enabling is refused with `403` when the organization balance does not cover the estimate. Identifiers that match nothing are listed in `unmatched` and never fail the request: a selection that matches nothing completes as an empty job (`total` 0), the same convention as the batch delete endpoints.", + "operationId": "toggle_businesses_aml_monitoring", + "x-didit-sdk": { + "resource": "businesses", + "method": "set_aml_monitoring", + "auth": "api_key", + "server": true + }, + "tags": [ + "AML Ongoing Monitoring" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "is_enabled" + ], + "description": "Select businesses with `vendor_data_list`, `didit_internal_id_list`, or both (the two lists are OR'd). At most 1000 identifiers per request across both lists.", + "properties": { + "is_enabled": { + "type": "boolean", + "description": "true to enable monitoring, false to disable it." + }, + "estimate_only": { + "type": "boolean", + "default": false, + "description": "When true, returns the cost estimate and performs no writes - no screening, no billing, no job." + }, + "vendor_data_list": { + "type": "array", + "items": { + "type": "string" }, - "phone_verifications": { - "type": "array", - "nullable": true, - "description": "**Array of phone verification reports** — always a JSON array, never a singular `phone` object. `null` until at least one phone step has produced data. Each item is the full report, including `node_id`, feature-level `status`, the phone number and country metadata, `lifecycle[]`, `matches[]`, and `warnings[]`.", - "items": { - "type": "object", - "properties": { - "node_id": { - "type": "string", - "description": "Unique identifier for this phone step in the workflow graph." - }, - "status": { - "type": "string", - "description": "Feature-level status of this phone verification step. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`.", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Expired" - ] - }, - "phone_number_prefix": { - "type": "string", - "description": "Country calling code prefix" - }, - "phone_number": { - "type": "string", - "description": "Phone number without country prefix" - }, - "full_number": { - "type": "string", - "description": "Complete phone number with country prefix" - }, - "country_code": { - "type": "string", - "description": "Two-letter country code (ISO 3166-1 alpha-2)" - }, - "country_name": { - "type": "string", - "description": "Full country name" - }, - "carrier": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the phone carrier" - }, - "type": { - "type": "string", - "description": "Type of phone line (mobile, landline, etc)" - } - } - }, - "is_disposable": { - "type": "boolean", - "description": "Whether the phone number is from a disposable service" - }, - "is_virtual": { - "type": "boolean", - "description": "Whether the phone number is virtual" - }, - "verification_method": { - "type": "string", - "description": "Method used to verify the phone number" - }, - "verification_attempts": { - "type": "integer", - "description": "Number of verification attempts made" - }, - "verified_at": { - "type": "string", - "format": "date-time", - "description": "Timestamp when the phone was verified" - }, - "lifecycle": { - "type": "array", - "description": "Chronological list of events in the phone verification lifecycle.", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "timestamp": { - "type": "string", - "format": "date-time" - }, - "details": { - "type": "object", - "nullable": true, - "description": "Event-specific payload. Send events carry `status`, `reason`, `channel`, `actual_channel`; delivery events carry `channel`, `status`; code-check events carry `code_tried`, `status`; final status events carry `reason` — or `null` when there is no reason to report (e.g. `PHONE_VERIFICATION_APPROVED`).", - "properties": { - "status": { - "type": "string" - }, - "reason": { - "type": "string", - "nullable": true - }, - "channel": { - "type": "string", - "nullable": true - }, - "actual_channel": { - "type": "string", - "nullable": true - }, - "code_tried": { - "type": "string" - } - } - }, - "fee": { - "type": "number" - } - } - } - }, - "warnings": { - "type": "array", - "description": "List of warnings related to phone verification", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "enum": [ - "ID_VERIFICATION", - "NFC", - "LIVENESS", - "FACEMATCH", - "PROOF_OF_ADDRESS", - "PHONE", - "EMAIL", - "AML", - "LOCATION", - "DATABASE_VALIDATION", - "QUESTIONNAIRE", - "DOCUMENT_AI", - "KYB_REGISTRY", - "KYB_DOCUMENTS", - "KYB_KEY_PEOPLE" - ], - "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload." - }, - "risk": { - "type": "string" - }, - "additional_data": { - "type": "object", - "nullable": true - }, - "log_type": { - "type": "string" - }, - "short_description": { - "type": "string" - }, - "long_description": { - "type": "string" - }, - "node_id": { - "type": "string", - "description": "The workflow graph node ID that generated this warning" - } - } - } - }, - "matches": { - "type": "array", - "description": "Matches found for this phone number. Each match has a `source`: `session` (another verification session in your application matched on the same number, returned with `session_id`/`session_number`/`vendor_data`/`verification_date`) or `list_entry` (the number is on an org-managed allow/block list — see `/management-api/lists/overview`). Cross-session matches use different `vendor_data` and are capped at 5.", - "items": { - "type": "object", - "properties": { - "session_id": { - "type": "string", - "format": "uuid", - "nullable": true - }, - "session_number": { - "type": "integer", - "nullable": true - }, - "vendor_data": { - "type": "string", - "nullable": true - }, - "verification_date": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "phone_number": { - "type": "string" - }, - "status": { - "type": "string", - "nullable": true - }, - "is_blocklisted": { - "type": "boolean" - }, - "api_service": { - "type": "string", - "nullable": true - }, - "source": { - "type": "string", - "enum": [ - "session", - "list_entry" - ], - "description": "Where this match came from. `session` means another verification session in your application matched on this value. `list_entry` means the value was hit against an org-managed list (allowlist/blocklist) — see the management API lists overview at `/management-api/lists/overview` for how to manage those lists." - } - } - } - } - } - } + "description": "Your own identifiers of the businesses to toggle, matched exactly as sent." + }, + "didit_internal_id_list": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" }, - "email_verifications": { - "type": "array", - "nullable": true, - "description": "**Array of email verification reports** — always a JSON array, never a singular `email` object. `null` until at least one email step has produced data. Each item is the full report, including `node_id`, feature-level `status`, the email address, breach data, `lifecycle[]`, `matches[]`, and `warnings[]`.", - "items": { - "type": "object", - "properties": { - "node_id": { - "type": "string", - "description": "Unique identifier for this email step in the workflow graph." - }, - "status": { - "type": "string", - "description": "Feature-level status of this email verification step. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`.", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Expired" - ] - }, - "email": { - "type": "string", - "description": "The email address being verified" - }, - "is_breached": { - "type": "boolean", - "description": "Indicates if the email was found in known data breaches" - }, - "breaches": { - "type": "array", - "description": "List of known breaches this email was found in", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the breach" - }, - "domain": { - "type": "string", - "description": "Domain affected by the breach" - }, - "logo_path": { - "type": "string", - "description": "URL to the logo of the breached service" - }, - "breach_date": { - "type": "string", - "format": "date", - "description": "Date when the breach occurred" - }, - "description": { - "type": "string", - "description": "Description of the breach" - }, - "is_verified": { - "type": "boolean", - "description": "Whether the breach is verified" - }, - "data_classes": { - "type": "array", - "description": "Types of data exposed in the breach", - "items": { - "type": "string" - } - }, - "breach_emails_count": { - "type": "integer", - "description": "Number of email addresses affected in the breach" - } - } - } - }, - "is_disposable": { - "type": "boolean", - "description": "Indicates if the email is from a disposable provider" - }, - "is_undeliverable": { - "type": "boolean", - "description": "Indicates if the email address is undeliverable" - }, - "verification_attempts": { - "type": "integer", - "description": "Number of verification attempts for this email" - }, - "verified_at": { - "type": "string", - "format": "date-time", - "description": "Timestamp when the email was verified" - }, - "warnings": { - "type": "array", - "description": "List of warnings related to email verification", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "enum": [ - "ID_VERIFICATION", - "NFC", - "LIVENESS", - "FACEMATCH", - "PROOF_OF_ADDRESS", - "PHONE", - "EMAIL", - "AML", - "LOCATION", - "DATABASE_VALIDATION", - "QUESTIONNAIRE", - "DOCUMENT_AI", - "KYB_REGISTRY", - "KYB_DOCUMENTS", - "KYB_KEY_PEOPLE" - ], - "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload." - }, - "risk": { - "type": "string" - }, - "additional_data": { - "type": "object", - "nullable": true - }, - "log_type": { - "type": "string" - }, - "short_description": { - "type": "string" - }, - "long_description": { - "type": "string" - }, - "node_id": { - "type": "string", - "description": "The workflow graph node ID that generated this warning" - } - } - } - }, - "lifecycle": { - "type": "array", - "description": "Chronological list of events in the email verification lifecycle.", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "timestamp": { - "type": "string", - "format": "date-time" - }, - "details": { - "type": "object", - "nullable": true, - "description": "Event-specific payload. Send events carry `status`, `reason`; code-check events carry `code_tried`, `status`; final status events carry `reason` — or `null` when there is no reason to report (e.g. `EMAIL_VERIFICATION_APPROVED`).", - "properties": { - "status": { - "type": "string" - }, - "reason": { - "type": "string", - "nullable": true - }, - "code_tried": { - "type": "string" - } - } - }, - "fee": { - "type": "number" - } - } - } - }, - "matches": { - "type": "array", - "description": "Matches found for this email address. Each match has a `source`: `session` (another verification session in your application matched on the same email, returned with `session_id`/`session_number`/`vendor_data`/`verification_date`) or `list_entry` (the email is on an org-managed allow/block list — see `/management-api/lists/overview`). Cross-session matches use different `vendor_data` and are capped at 5.", - "items": { - "type": "object", - "properties": { - "session_id": { - "type": "string", - "format": "uuid", - "nullable": true - }, - "session_number": { - "type": "integer", - "nullable": true - }, - "vendor_data": { - "type": "string", - "nullable": true - }, - "verification_date": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "email": { - "type": "string", - "format": "email" - }, - "status": { - "type": "string", - "nullable": true - }, - "is_blocklisted": { - "type": "boolean" - }, - "api_service": { - "type": "string", - "nullable": true - }, - "source": { - "type": "string", - "enum": [ - "session", - "list_entry" - ], - "description": "Where this match came from. `session` means another verification session in your application matched on this value. `list_entry` means the value was hit against an org-managed list (allowlist/blocklist) — see the management API lists overview at `/management-api/lists/overview` for how to manage those lists." - } - } - } - } + "description": "Didit internal ids of the businesses to toggle - the `didit_internal_id` returned by the create, get and list endpoints." + } + } + }, + "examples": { + "Estimate first": { + "summary": "Preview the cost without writing", + "value": { + "is_enabled": true, + "estimate_only": true, + "vendor_data_list": [ + "company-123", + "company-456" + ] + } + }, + "Enable": { + "summary": "Enable monitoring for two businesses", + "value": { + "is_enabled": true, + "vendor_data_list": [ + "company-123", + "company-456" + ] + } + }, + "Disable": { + "summary": "Disable monitoring by Didit internal id", + "value": { + "is_enabled": false, + "didit_internal_id_list": [ + "3f1c0d3e-8f0a-4c2e-9a3d-9b1f5f4a2c11" + ] + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/businesses/aml-monitoring/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"is_enabled\": true, \"vendor_data_list\": [\"company-123\", \"company-456\"]}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/businesses/aml-monitoring/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'is_enabled': True, 'vendor_data_list': ['company-123', 'company-456']},\n)\nresp.raise_for_status()\njob = resp.json()\nprint(job['status'], job['outcomes'])\nfor row in job['results']:\n print(row['vendor_data'], row['outcome'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/businesses/aml-monitoring/', {\n method: 'POST',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ is_enabled: true, vendor_data_list: ['company-123', 'company-456'] }),\n});\nconst job = await resp.json();\nconsole.log(job.status, job.outcomes);\nfor (const row of job.results) console.log(row.vendor_data, row.outcome);" + } + ], + "responses": { + "200": { + "description": "The estimate (when `estimate_only` is true) or the completed job for a selection processed inline.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AMLMonitoringToggleResult" + } + } + } + }, + "202": { + "description": "Larger selections run in the background. Poll the job endpoint with `job_id`; `results` fills in as profiles are processed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AMLMonitoringJob" + } + } + } + }, + "400": { + "description": "Validation error: `is_enabled` missing, no selector, or more than 1000 identifiers across both lists." + }, + "401": { + "description": "Missing or invalid API key." + }, + "403": { + "description": "Insufficient balance to enable monitoring for this selection. The response carries the `estimate` so you can top up the right amount.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "estimate": { + "type": "object", + "description": "Cost preview. Returned directly when estimate_only is true (nothing is written).", + "properties": { + "users_total": { + "type": "integer", + "description": "Profiles matched by the selection." + }, + "already_enabled": { + "type": "integer", + "description": "Profiles already monitored (nothing to do when enabling; these are what disabling turns off)." + }, + "with_existing_aml": { + "type": "integer", + "description": "Profiles whose existing AML screening will be switched to monitored (no new screening)." + }, + "needing_screening": { + "type": "integer", + "description": "Profiles without any AML screening that will be screened first (billed as a regular AML screening)." + }, + "skipped_no_identity_data": { + "type": "integer", + "description": "Profiles that cannot be screened because identity data is missing." + }, + "skipped_blocked": { + "type": "integer", + "description": "Blocked users that will be skipped (always 0 for businesses)." + }, + "estimated_screening_cost_usd": { + "type": "number", + "example": 0.2, + "description": "One-time cost of the new screenings." + }, + "estimated_annual_monitoring_cost_usd": { + "type": "number", + "example": 0.07, + "description": "Yearly monitoring cost of the profiles that will become monitored." + }, + "balance_sufficient": { + "type": "boolean", + "description": "Whether the organization balance covers the screening plus the first year of monitoring." } } + } + } + } + } + } + } + } + } + }, + "/v3/aml-monitoring/jobs/{job_id}/": { + "get": { + "summary": "Get AML ongoing monitoring job", + "description": "Status, outcome counters and per-record results of a monitoring toggle job started from `POST /v3/users/aml-monitoring/` or `POST /v3/businesses/aml-monitoring/`. Poll every couple of seconds while `status` is `PENDING` or `PROCESSING`; `results` is complete once it is `COMPLETED`.", + "operationId": "get_aml_monitoring_job", + "x-didit-sdk": { + "resource": "aml_monitoring", + "method": "retrieve_job", + "auth": "api_key", + "server": true + }, + "tags": [ + "AML Ongoing Monitoring" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The `job_id` returned by the toggle endpoint." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl 'https://verification.didit.me/v3/aml-monitoring/jobs/JOB_ID/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import time\nimport requests\n\nurl = 'https://verification.didit.me/v3/aml-monitoring/jobs/JOB_ID/'\nwhile True:\n job = requests.get(url, headers={'x-api-key': 'YOUR_API_KEY'}).json()\n if job['status'] in ('COMPLETED', 'FAILED'):\n break\n time.sleep(2)\nprint(job['outcomes'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const url = 'https://verification.didit.me/v3/aml-monitoring/jobs/JOB_ID/';\nlet job;\ndo {\n await new Promise((r) => setTimeout(r, 2000));\n job = await (await fetch(url, { headers: { 'x-api-key': process.env.DIDIT_API_KEY } })).json();\n} while (job.status === 'PENDING' || job.status === 'PROCESSING');\nconsole.log(job.outcomes);" + } + ], + "responses": { + "200": { + "description": "The job.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "job_id": { + "type": "string", + "format": "uuid" }, - "poa_verifications": { - "type": "array", - "nullable": true, - "description": "**Array of Proof of Address (POA) reports** — always a JSON array, never a singular `poa` object. `null` until at least one POA step has produced data. Each item is the full report, including `node_id`, feature-level `status`, the document file URL, issuer, dates, parsed address, and `warnings[]`.", - "items": { - "type": "object", - "properties": { - "node_id": { - "type": "string", - "description": "Unique identifier for this POA step in the workflow graph." - }, - "status": { - "type": "string", - "description": "Feature-level status of this Proof of Address step. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`.", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Resub Requested" - ] - }, - "document_file": { - "type": "string", - "format": "uri", - "description": "URL of the proof of address document" - }, - "issuing_state": { - "type": "string", - "description": "Two-letter country code of the issuing state" - }, - "document_type": { - "type": "string", - "description": "Type of proof of address document" - }, - "document_subtype": { - "type": "string", - "nullable": true, - "description": "More specific classification of the POA document within its `document_type`, when one was detected. `null` otherwise." - }, - "document_language": { - "type": "string", - "description": "Language code of the document" - }, - "document_metadata": { - "type": "object", - "properties": { - "file_size": { - "type": "integer", - "description": "Size of the document" - }, - "content_type": { - "type": "string", - "description": "Content type of the document" - }, - "creation_date": { - "type": "string", - "format": "date", - "description": "Date when the document was created" - }, - "modified_date": { - "type": "string", - "format": "date", - "description": "Date when the document was modified" - }, - "overlay_manipulation": { - "type": "object", - "nullable": true, - "description": "PDF forensic analysis for suspected overlay-text manipulation. Null when no overlay manipulation evidence was found or the document was not a PDF.", - "properties": { - "detected": { - "type": "boolean", - "description": "Whether overlay-text manipulation evidence was detected." - }, - "analyzed": { - "type": "boolean", - "description": "Whether the PDF could be analyzed." - }, - "signals": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Forensic signals that fired, such as duplicate_font_subset or glyph_fragmentation." - }, - "manipulated_regions": { - "type": "array", - "description": "Suspected manipulated areas in PDF page coordinates.", - "items": { - "type": "object", - "properties": { - "page": { - "type": "integer" - }, - "x": { - "type": "number" - }, - "y": { - "type": "number" - }, - "width": { - "type": "number" - }, - "height": { - "type": "number" - }, - "page_width": { - "type": "number" - }, - "page_height": { - "type": "number" - } - } - } - } - } - }, - "creator": { - "type": "string", - "nullable": true, - "description": "Creator application recorded in the document metadata." - }, - "producer": { - "type": "string", - "nullable": true, - "description": "Producer software recorded in the PDF metadata." - }, - "software": { - "type": "string", - "nullable": true, - "description": "Editing/processing software recorded in the file metadata." - }, - "encryption": { - "type": "string", - "nullable": true, - "description": "Encryption scheme of the document, when encrypted." - }, - "is_signed": { - "type": "boolean", - "nullable": true, - "description": "Whether the PDF carries a digital signature." - }, - "is_tampered": { - "type": "boolean", - "nullable": true, - "description": "Whether signature validation indicates the file was modified after signing." - }, - "signature_info": { - "type": "object", - "nullable": true, - "description": "Details of the digital signature(s), when present." - }, - "exif_original_date": { - "type": "string", - "nullable": true, - "description": "EXIF original capture date, for image uploads." - }, - "exif_digitized_date": { - "type": "string", - "nullable": true, - "description": "EXIF digitization date, for image uploads." - }, - "processed_by_known_editor": { - "type": "boolean", - "nullable": true, - "description": "Whether the file metadata indicates processing by a known document editor." - }, - "has_different_creation_mod_date": { - "type": "boolean", - "nullable": true, - "description": "Whether the creation and modification dates differ — a common manipulation signal." - } - }, - "description": "File-forensics metadata extracted from the uploaded POA document. Always contains the keys below (values are `null` when not applicable, e.g. EXIF dates for PDFs). `null` when no metadata could be extracted.", - "nullable": true - }, - "issuer": { - "type": "string", - "description": "Name of the document issuer" - }, - "issue_date": { - "type": "string", - "format": "date", - "description": "Date when the document was issued" - }, - "expiration_date": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Expiration date of the document, if available" - }, - "poa_address": { - "type": "string", - "description": "Raw address from the proof of address document" - }, - "poa_formatted_address": { - "type": "string", - "description": "Formatted address from the proof of address document" - }, - "poa_parsed_address": { - "type": "object", - "properties": { - "address_type": { - "type": "string", - "description": "Type of address (Street, Avenue, etc)" - }, - "street_1": { - "type": "string", - "description": "Primary street address" - }, - "street_2": { - "type": "string", - "nullable": true, - "description": "Secondary street address" - }, - "city": { - "type": "string", - "description": "City name" - }, - "region": { - "type": "string", - "description": "Region or state name" - }, - "country": { - "type": "string", - "description": "Two-letter country code" - }, - "postal_code": { - "type": "string", - "description": "Postal code" - }, - "document_location": { - "type": "object", - "properties": { - "latitude": { - "type": "number", - "description": "Latitude coordinate" - }, - "longitude": { - "type": "number", - "description": "Longitude coordinate" - } - } - } - } - }, - "name_on_document": { - "type": "string", - "nullable": true, - "description": "Account-holder / addressee name extracted from the POA document, used for name matching. `null` when no name could be extracted." - }, - "name_match_score_expected_details": { - "type": "number", - "nullable": true, - "description": "Similarity score (0-100) between `name_on_document` and the expected name supplied at session creation. `null` when either side is missing." - }, - "name_match_score_id_verification": { - "type": "number", - "nullable": true, - "description": "Similarity score (0-100) between `name_on_document` and the name extracted by the ID verification step. `null` when either side is missing." - }, - "expected_details_address": { - "type": "string", - "nullable": true, - "description": "Expected raw address for verification" - }, - "expected_details_formatted_address": { - "type": "string", - "nullable": true, - "description": "Expected formatted address for verification" - }, - "expected_details_parsed_address": { - "type": "object", - "description": "Expected parsed address details for verification" - }, - "extra_fields": { - "type": "object", - "description": "Additional banking and contact details extracted from the document.", - "properties": { - "bank_account_number": { - "type": "string", - "nullable": true - }, - "bank_iban": { - "type": "string", - "nullable": true - }, - "bank_sort_code": { - "type": "string", - "nullable": true - }, - "bank_routing_number": { - "type": "string", - "nullable": true - }, - "bank_swift_bic": { - "type": "string", - "nullable": true - }, - "bank_branch_name": { - "type": "string", - "nullable": true - }, - "bank_branch_address": { - "type": "string", - "nullable": true - }, - "document_phone_number": { - "type": "string", - "nullable": true - }, - "additional_names": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "extra_files": { - "type": "array", - "items": { - "type": "string", - "format": "uri" - }, - "description": "Additional proof of address files" - }, - "warnings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "enum": [ - "ID_VERIFICATION", - "NFC", - "LIVENESS", - "FACEMATCH", - "PROOF_OF_ADDRESS", - "PHONE", - "EMAIL", - "AML", - "LOCATION", - "DATABASE_VALIDATION", - "QUESTIONNAIRE", - "DOCUMENT_AI", - "KYB_REGISTRY", - "KYB_DOCUMENTS", - "KYB_KEY_PEOPLE" - ], - "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload." - }, - "risk": { - "type": "string" - }, - "additional_data": { - "type": "object", - "nullable": true - }, - "log_type": { - "type": "string" - }, - "short_description": { - "type": "string" - }, - "long_description": { - "type": "string" - }, - "node_id": { - "type": "string", - "description": "The workflow graph node ID that generated this warning" - } - } - } - }, - "detected_codes": { - "type": "array", - "description": "QR codes and barcodes detected across every processed page of the document. Empty when none are found. A located but undecodable QR code has `parsed_payload: null`.", - "items": { - "$ref": "#/components/schemas/DocumentAIDetectedCode" - } - } + "status": { + "type": "string", + "enum": [ + "PENDING", + "PROCESSING", + "COMPLETED", + "FAILED" + ] + }, + "is_enabled": { + "type": "boolean", + "description": "true when the job enables monitoring, false when it disables it." + }, + "target_type": { + "type": "string", + "enum": [ + "user", + "business" + ], + "description": "Which profile family the job toggles." + }, + "total": { + "type": "integer", + "description": "Profiles the job processes." + }, + "processed": { + "type": "integer", + "description": "Profiles processed so far." + }, + "outcomes": { + "type": "object", + "description": "Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.", + "properties": { + "enabled": { + "type": "integer", + "description": "Monitoring enabled on an existing AML screening." + }, + "screened_and_enabled": { + "type": "integer", + "description": "A new AML screening was created (billed) and monitoring enabled on it." + }, + "already_enabled": { + "type": "integer", + "description": "Monitoring was already active; nothing changed and nothing was billed." + }, + "disabled": { + "type": "integer", + "description": "Monitoring turned off across every AML screening of the profile." + }, + "skipped_no_identity_data": { + "type": "integer", + "description": "Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen." + }, + "skipped_blocked": { + "type": "integer", + "description": "Skipped: the user is blocked." + }, + "skipped_sandbox": { + "type": "integer", + "description": "Skipped: sandbox application, no screening is created." + }, + "failed_provider": { + "type": "integer", + "description": "The screening could not be completed; retry later." + }, + "failed_insufficient_balance": { + "type": "integer", + "description": "Balance ran out while the job was running." } } }, - "document_ai_documents": { + "results": { "type": "array", - "nullable": true, - "description": "**Document AI reports grouped by workflow node** — always a JSON array, never a singular object. `null` until at least one Document AI step has produced data. Present only when `session_kind = user`. Each item is a node group with an aggregate `status`, the `node_id`, an `items[]` array of the per-document results (extracted data keyed by the configured field key), and `warnings`.", + "description": "One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API; a job started from the Business Console reports counters only.", "items": { "type": "object", - "description": "Document AI results grouped by workflow node.", "properties": { - "status": { + "didit_internal_id": { "type": "string", - "description": "Aggregate Document AI status for this node, rolled up from its `items[]`.", - "enum": [ - "Approved", - "Declined", - "In Review", - "Not Finished" - ] + "format": "uuid", + "description": "Didit internal id of the profile." }, - "node_id": { + "vendor_data": { "type": "string", "nullable": true, - "description": "Unique identifier for this Document AI step in the workflow graph." - }, - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "node_id": { - "type": "string", - "nullable": true, - "description": "Workflow graph node that produced this document." - }, - "document_key": { - "type": "string", - "description": "Key of the configured Document AI document this file corresponds to." - }, - "original_filename": { - "type": "string", - "nullable": true, - "description": "Filename as uploaded by the user." - }, - "file_size": { - "type": "integer", - "nullable": true, - "description": "File size in bytes." - }, - "file_url": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "Short-lived presigned link to the uploaded file." - }, - "status": { - "type": "string", - "enum": [ - "Approved", - "Declined", - "In Review", - "Not Finished" - ], - "description": "Per-document Document AI status." - }, - "extracted_data": { - "type": "object", - "additionalProperties": true, - "nullable": true, - "description": "Values extracted by Document AI, keyed by the configured field `key`. Each value is a string, number, or `null`." - }, - "detected_codes": { - "type": "array", - "description": "QR codes and barcodes detected across every processed page. Empty when none are found. A located but undecodable QR code has `parsed_payload: null`.", - "items": { - "$ref": "#/components/schemas/DocumentAIDetectedCode" - } - }, - "fields": { - "type": "array", - "description": "The configured extraction fields for this document.", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string", - "description": "Stable field key used as the key in `extracted_data`." - }, - "name": { - "type": "string", - "description": "Human-readable field name." - }, - "instruction": { - "type": "string", - "description": "Extraction instruction shown to the model." - }, - "type": { - "type": "string", - "enum": [ - "text", - "number", - "date" - ], - "description": "Expected value type of the extracted field." - }, - "required": { - "type": "boolean", - "description": "Whether this field is required." - } - } - } - }, - "document_metadata": { - "type": "object", - "nullable": true, - "description": "File-forensics metadata extracted from the uploaded document (e.g. `is_tampered`). `null` when nothing could be extracted." - }, - "cross_check_result": { - "type": "object", - "nullable": true, - "description": "Cross-reference of the extracted document fields against other session data. `null` when no cross-check was performed." - }, - "created_at": { - "type": "string", - "format": "date-time" - } - } - } + "description": "Your own identifier for the profile." }, - "warnings": { - "type": "array", - "nullable": true, - "items": { - "type": "object" - }, - "description": "Warning logs raised for this Document AI node." + "outcome": { + "type": "string", + "enum": [ + "enabled", + "screened_and_enabled", + "already_enabled", + "disabled", + "skipped_no_identity_data", + "skipped_blocked", + "skipped_sandbox", + "failed_provider", + "failed_insufficient_balance" + ] } } } }, - "questionnaire_responses": { - "type": "array", - "nullable": true, - "description": "**Array of questionnaire response reports** — always a JSON array, never a singular `questionnaire` object. `null` until at least one questionnaire step has produced data. Each item is the full report, including `node_id`, the questionnaire metadata, sections with form elements, and the user's answers.", - "items": { - "type": "object", - "properties": { - "node_id": { - "type": "string", - "description": "Unique identifier for this questionnaire step in the workflow graph." - }, - "questionnaire_id": { - "type": "string", - "description": "Unique identifier for the questionnaire" - }, - "title": { - "type": "string", - "description": "Title of the questionnaire" - }, - "description": { - "type": "string", - "description": "Description of the questionnaire" - }, - "languages": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Supported languages" - }, - "default_language": { - "type": "string", - "description": "Default language" - }, - "is_active": { - "type": "boolean", - "description": "Whether the questionnaire is active" - }, - "is_simple_questionnaire": { - "type": "boolean", - "description": "Whether the questionnaire is a simple linear form (no branching graph)." - }, - "questionnaire_group_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Stable identifier of the questionnaire group across versions." - }, - "version": { - "type": "integer", - "nullable": true, - "description": "Version number of the questionnaire that was answered." - }, - "published_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "Timestamp at which this questionnaire version was published." - }, - "sections": { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string", - "nullable": true, - "description": "Section title" - }, - "description": { - "type": "string", - "nullable": true, - "description": "Section description" - }, - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "description": "Unique identifier for the item" - }, - "value": { - "type": "string", - "nullable": true, - "description": "Stable node identifier of this form element within the questionnaire graph." - }, - "element_type": { - "type": "string", - "description": "Type of the questionnaire element" - }, - "is_required": { - "type": "boolean", - "description": "Whether the item is required" - }, - "title": { - "type": "string", - "description": "Title of the item" - }, - "description": { - "type": "string", - "nullable": true, - "description": "Description of the item" - }, - "placeholder": { - "type": "string", - "nullable": true, - "description": "Placeholder text" - }, - "choices": { - "type": "array", - "nullable": true, - "items": { - "type": "object", - "properties": { - "label": { - "type": "string" - }, - "value": { - "type": "string" - }, - "requires_text_input": { - "type": "boolean", - "description": "Whether this choice requires additional text input", - "nullable": true - } - }, - "required": [ - "label", - "value" - ] - }, - "description": "Choices for dropdown or single choice elements" - }, - "max_files": { - "type": "integer", - "description": "Maximum number of files allowed", - "nullable": true - }, - "answer": { - "type": "object", - "description": "Answer provided for the item", - "properties": { - "value": { - "type": "string", - "nullable": true - }, - "text": { - "type": "string", - "nullable": true - }, - "files": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - } - } - }, - "required_if": { - "type": "object", - "nullable": true, - "description": "Conditional-requirement rule, when the element is only required for certain answers." - }, - "repeatable_config": { - "type": "object", - "nullable": true, - "description": "Configuration for repeatable element groups, when applicable." - } - }, - "required": [ - "uuid", - "value", - "element_type", - "is_required", - "title" - ] - } - } - } - } - }, - "status": { - "type": "string", - "description": "Status of the questionnaire" - } - } - } + "error": { + "type": "string", + "nullable": true, + "description": "Generic failure message when status is FAILED; the detailed cause is kept on the platform side." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "completed_at": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + } + } + } + }, + "401": { + "description": "Missing or invalid API key." + }, + "404": { + "description": "No such job for this application." + } + } + } + }, + "/v3/users/{vendor_data}/update-status/": { + "patch": { + "summary": "Update user status", + "description": "Flip only the lifecycle `status` of a user: `ACTIVE`, `FLAGGED` or `BLOCKED` (not session statuses). `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. Each change is recorded as a `STATUS_CHANGED` activity on the user, with the optional `reason` as its `comment`. Sending the current status changes nothing and records no `reason`. The `user.status.updated` webhook does not carry `reason`.", + "operationId": "update_user_status", + "x-didit-sdk": { + "resource": "users", + "method": "update_status", + "auth": "api_key", + "server": true + }, + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "vendor_data", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Your unique identifier for the user." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/EntityStatusUpdateRequest" + }, + { + "type": "object", + "properties": { + "reason": { + "type": "string", + "nullable": true, + "maxLength": 1000, + "description": "Why the status changed. Recorded on the status-change entry of the user's activity (`comments[].comment`, and **Activity** in the Business Console), only when the status actually changes. Webhooks do not carry it." + } + } + } + ] + }, + "examples": { + "Activate": { + "summary": "Reactivate the user", + "value": { + "status": "ACTIVE" + } + }, + "Flag": { + "summary": "Flag the user for review", + "value": { + "status": "FLAGGED" + } + }, + "Block": { + "summary": "Block the user (adds to system blocklist)", + "value": { + "status": "BLOCKED" + } + }, + "Block with reason": { + "summary": "Block the user and record why", + "value": { + "status": "BLOCKED", + "reason": "Chargeback fraud pattern confirmed" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH 'https://verification.didit.me/v3/users/user-abc-123/update-status/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"status\": \"BLOCKED\"}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.patch(\n 'https://verification.didit.me/v3/users/user-abc-123/update-status/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'status': 'BLOCKED'},\n)\nresp.raise_for_status()\nprint(resp.json()['status'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/users/user-abc-123/update-status/', {\n method: 'PATCH',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ status: 'BLOCKED' }),\n});\nconst user = await resp.json();\nconsole.log(user.status);" + } + ], + "responses": { + "200": { + "description": "User status updated. Full user record returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDetailItem" + }, + "examples": { + "Blocked": { + "value": { + "didit_internal_id": "f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418", + "vendor_data": "user-abc-123", + "display_name": null, + "full_name": "John Michael Doe", + "date_of_birth": "1990-05-15", + "effective_name": "John Michael Doe", + "status": "BLOCKED", + "metadata": { + "tier": "premium", + "source": "website" + }, + "portrait_image_url": "https:///...", + "session_count": 3, + "approved_count": 2, + "declined_count": 0, + "in_review_count": 1, + "issuing_states": [ + "USA" + ], + "approved_emails": [ + "john@example.com" + ], + "approved_phones": [ + "+14155551234" + ], + "features": { + "ID_VERIFICATION": "Approved", + "LIVENESS": "Approved", + "FACE_MATCH": "Approved", + "AML": "Approved" + }, + "features_list": [ + { + "feature": "ID_VERIFICATION", + "status": "Approved" + }, + { + "feature": "LIVENESS", + "status": "Approved" + }, + { + "feature": "FACE_MATCH", + "status": "Approved" + }, + { + "feature": "AML", + "status": "Approved" + } + ], + "last_session_at": "2025-06-15T10:30:00Z", + "last_activity_at": "2025-06-15T10:30:00Z", + "first_session_at": "2025-06-01T08:00:00Z", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "tag": { + "uuid": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b", + "name": "VIP", + "color": "#2567FF", + "description": null, + "source": "custom", + "created_at": "2025-05-01T09:00:00Z", + "updated_at": "2025-05-01T09:00:00Z" + }, + "added_by_email": "analyst@acme.com", + "added_by_name": "Jane Analyst", + "created_at": "2025-06-02T11:00:00Z" + } + ], + "comments": [ + { + "uuid": "c1111111-2222-4333-8444-555555555555", + "comment_type": "STATUS_CHANGED", + "comment": "Chargeback fraud pattern confirmed", + "actor_email": null, + "actor_name": null, + "previous_status": "ACTIVE", + "new_status": "BLOCKED", + "previous_value": null, + "new_value": null, + "changed_fields": [], + "metadata": null, + "mentioned_emails": [], + "created_at": "2025-06-15T10:30:00Z" + } + ], + "created_at": "2025-06-01T08:00:00Z", + "updated_at": "2025-06-15T10:30:00Z" + } + } + } + } + } + }, + "400": { + "description": "Missing or invalid `status`, or a `reason` over 1000 characters.", + "content": { + "application/json": { + "examples": { + "Missing status": { + "value": { + "status": [ + "This field is required." + ] + } + }, + "Bad status": { + "value": { + "status": [ + "Invalid status. Valid options are: ACTIVE, FLAGGED, BLOCKED" + ] + } + }, + "Reason too long": { + "value": { + "reason": [ + "Ensure this field has no more than 1000 characters." + ] + } + } + } + } + } + }, + "401": { + "description": "No credentials supplied. Requests to `/v3/users/*` paths without an `x-api-key` header (or `Authorization: Bearer` token) are rejected by the authentication middleware before reaching the API.", + "content": { + "application/json": { + "examples": { + "Missing credentials": { + "value": { + "detail": "You must be authenticated with a valid access token to access this endpoint." + } + } + } + } + } + }, + "403": { + "description": "Invalid or revoked API key, or the key cannot update users for this application. Note: on this endpoint an *invalid* key returns `403` (not `401`).", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No (non-deleted) user with the supplied `vendor_data` exists for this application.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Rate limited": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/businesses/create/": { + "post": { + "summary": "Create business", + "description": "Pre-create a [business](/entities/businesses) *without* a KYB session. All fields are optional, but you should always send `vendor_data` — without it the record cannot be linked to sessions or transactions. When provided, `vendor_data` must be unique among non-deleted businesses (exact match; conflicts return 400). Not idempotent.", + "operationId": "create_business", + "x-didit-sdk": { + "resource": "businesses", + "method": "create", + "auth": "api_key", + "server": true + }, + "tags": [ + "Businesses" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your unique identifier for this business (free-form string, NOT a UUID). Optional but strongly recommended. When provided, must be unique among non-deleted businesses for the application; matched exactly as sent." + }, + "display_name": { + "type": "string", + "nullable": true, + "maxLength": 255, + "description": "Friendly display name shown in the console (takes precedence over `legal_name` for UI display)." + }, + "legal_name": { + "type": "string", + "nullable": true, + "maxLength": 255, + "description": "Official legal name of the company." + }, + "registration_number": { + "type": "string", + "nullable": true, + "maxLength": 100, + "description": "Company registration or incorporation number." + }, + "country_code": { + "type": "string", + "nullable": true, + "maxLength": 10, + "description": "Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `US`)." + }, + "status": { + "type": "string", + "enum": [ + "ACTIVE", + "FLAGGED", + "BLOCKED" + ], + "default": "ACTIVE", + "description": "Initial lifecycle status. Defaults to `ACTIVE`." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Arbitrary JSON object you attach to the business. Defaults to `{}`." + } + } + }, + "examples": { + "Basic": { + "summary": "Recommended minimum", + "value": { + "vendor_data": "company-456", + "legal_name": "New Corp Ltd", + "country_code": "US" + } + }, + "Full": { + "summary": "All fields", + "value": { + "vendor_data": "company-789", + "display_name": "New Corp", + "legal_name": "New Corp International Ltd", + "registration_number": "98765432", + "country_code": "DE", + "status": "ACTIVE", + "metadata": { + "industry": "fintech", + "tier": "enterprise" + } + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/businesses/create/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"vendor_data\": \"company-456\", \"legal_name\": \"New Corp Ltd\", \"country_code\": \"US\"}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/businesses/create/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={\n 'vendor_data': 'company-456',\n 'legal_name': 'New Corp Ltd',\n 'country_code': 'US',\n },\n)\nresp.raise_for_status()\nbiz = resp.json()\nprint(biz['didit_internal_id'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/businesses/create/', {\n method: 'POST',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n vendor_data: 'company-456',\n legal_name: 'New Corp Ltd',\n country_code: 'US',\n }),\n});\nconst biz = await resp.json();\nconsole.log(biz.didit_internal_id);" + } + ], + "responses": { + "201": { + "description": "Business created. Full business record returned (same shape as Get Business).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessDetailItem" + }, + "examples": { + "Created": { + "value": { + "didit_internal_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", + "vendor_data": "company-456", + "display_name": null, + "legal_name": "New Corp Ltd", + "registration_number": null, + "country_code": "US", + "effective_name": "New Corp Ltd", + "status": "ACTIVE", + "session_count": 0, + "approved_count": 0, + "declined_count": 0, + "in_review_count": 0, + "features": {}, + "features_list": [], + "last_session_at": null, + "last_activity_at": "2026-03-15T10:30:00Z", + "first_session_at": null, + "tags": [], + "created_at": "2026-03-15T10:30:00Z", + "metadata": {}, + "updated_at": "2026-03-15T10:30:00Z" + } + } + } + } + } + }, + "400": { + "description": "Invalid body — typically a duplicate `vendor_data`, an unknown `status`, or malformed `metadata`.", + "content": { + "application/json": { + "examples": { + "Duplicate": { + "value": { + "vendor_data": [ + "A business with this vendor_data already exists." + ] + } + }, + "Bad status": { + "value": { + "status": [ + "Invalid status. Valid options are: ACTIVE, FLAGGED, BLOCKED" + ] + } + }, + "Bad metadata": { + "value": { + "metadata": [ + "Metadata must be a JSON object." + ] + } + } + } + } + } + }, + "403": { + "description": "Missing, invalid, or revoked API key, or the key cannot create businesses for this application. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Rate limited": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/users/create/": { + "post": { + "summary": "Create user", + "description": "Create a [user](/entities/users) by `vendor_data`, before or without a verification session. `vendor_data` must be unique among the application's users (a duplicate returns `400`). Not idempotent.\n\n**Contact details:** `email` and `phone_number` are stored in `metadata.contact`, and `country` in `metadata.address`. When you send `first_name` or `last_name` without `full_name`, Didit joins them into `full_name`.\n\n**Verification link:** with `send_verification_link: true`, Didit also creates a verification session for the person on `workflow_id` and emails them the link at `email`. The session runs the same checks as [Create Session](/sessions-api/create-session), such as the blocklist and your credit balance. The person and the session are created together or not at all: if any check fails, nothing is written and no email is sent. The response's `verification_link` describes the session, and is `null` when you don't ask for a link.\n\n**API key access:** a scoped API key needs `users` write. With `send_verification_link: true` it also needs `sessions` write, or the call answers `403` before anything is written. Full-access keys are unaffected. See [API keys](/console/api-keys).", + "operationId": "create_user", + "x-didit-sdk": { + "resource": "users", + "method": "create", + "auth": "api_key", + "server": true + }, + "tags": [ + "Users" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "vendor_data" + ], + "properties": { + "vendor_data": { + "type": "string", + "description": "Your unique identifier for this user (free-form string, NOT a UUID). Must be unique among the application's users; matched exactly as sent." + }, + "full_name": { + "type": "string", + "nullable": true, + "maxLength": 512, + "description": "Full legal name of the user." + }, + "display_name": { + "type": "string", + "nullable": true, + "description": "Friendly display name shown in the console (takes precedence over `full_name` for UI display).", + "maxLength": 255 + }, + "date_of_birth": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Date of birth in `YYYY-MM-DD` format." + }, + "status": { + "type": "string", + "enum": [ + "ACTIVE", + "FLAGGED", + "BLOCKED" + ], + "default": "ACTIVE", + "description": "Initial lifecycle status. Defaults to `ACTIVE`." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Arbitrary JSON object you attach to the user. Defaults to `{}`." + }, + "approved_emails": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Pre-trusted email addresses for this user, e.g. `[\"john@example.com\"]`." + }, + "approved_phones": { + "type": "array", + "items": { + "type": "string" }, - "aml_screenings": { - "type": "array", - "nullable": true, - "description": "**Array of AML screening reports** — always a JSON array, never a singular `aml` object. `null` until at least one AML step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `total_hits`, `entity_type`, `hits[]`, `score`, `screened_data`, `is_ongoing_monitoring_enabled`, and `warnings[]`.", - "items": { - "type": "object", - "properties": { - "node_id": { - "type": "string", - "description": "Unique identifier for this AML step in the workflow graph." - }, - "status": { - "type": "string", - "description": "Feature-level status of this AML screening. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`.", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Resub Requested" - ] - }, - "total_hits": { - "type": "integer", - "description": "Total number of matches found" - }, - "entity_type": { - "type": "string", - "description": "Type of entity screened: person or company" - }, - "hits": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique identifier of the match" - }, - "url": { - "type": "string", - "format": "uri", - "description": "Source URL for the match" - }, - "match": { - "type": "boolean", - "description": "Whether this is a confirmed match" - }, - "score": { - "type": "number", - "format": "float", - "nullable": true, - "minimum": 0, - "maximum": 1, - "description": "Deprecated weighted match score in the 0-1 range (e.g. `0.73`). Use `match_score` (0-100) instead.", - "deprecated": true - }, - "target": { - "type": "boolean", - "nullable": true, - "description": "Whether the matched entity is the direct target of the listing (as opposed to a related party). `null` when the source does not specify it." - }, - "caption": { - "type": "string", - "description": "Caption or title of the match" - }, - "datasets": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "PEP", - "PEP Level 1", - "PEP Level 2", - "PEP Level 3", - "PEP Level 4", - "Sanctions", - "Adverse Media", - "Warnings and Regulatory Enforcement", - "Fitness and Probity", - "SIP", - "SIE", - "Insolvency" - ] - }, - "description": "List of dataset categories where the match was found. Indicates whether the entity appeared in PEP lists (with level), sanctions databases, adverse media, warnings/regulatory enforcement, or other compliance datasets." - }, - "features": { - "type": "object", - "nullable": true, - "description": "Additional features of the match" - }, - "rca_name": { - "type": "string", - "description": "RCA name if applicable" - }, - "last_seen": { - "type": "string", - "format": "date-time", - "description": "Date when match was last seen" - }, - "risk_view": { - "type": "object", - "properties": { - "crimes": { - "type": "object", - "properties": { - "score": { - "type": "integer" - }, - "weightage": { - "type": "integer" - }, - "risk_level": { - "type": "string" - }, - "risk_scores": { - "type": "object" - } - } - }, - "countries": { - "type": "object", - "properties": { - "score": { - "type": "integer" - }, - "weightage": { - "type": "integer" - }, - "risk_level": { - "type": "string" - }, - "risk_scores": { - "type": "object" - } - } - }, - "categories": { - "type": "object", - "properties": { - "score": { - "type": "integer" - }, - "weightage": { - "type": "integer" - }, - "risk_level": { - "type": "string" - }, - "risk_scores": { - "type": "object" - } - } - }, - "custom_list": { - "type": "object" - } - } - }, - "first_seen": { - "type": "string", - "format": "date-time", - "description": "Date when match was first seen" - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "array", - "items": { - "type": "string" - } - }, - "alias": { - "type": "array", - "items": { - "type": "string" - } - }, - "notes": { - "type": "array", - "items": { - "type": "string" - } - }, - "title": { - "type": "string", - "nullable": true - }, - "gender": { - "type": "array", - "items": { - "type": "string" - } - }, - "height": { - "type": "string", - "nullable": true - }, - "topics": { - "type": "string", - "nullable": true - }, - "weight": { - "type": "string", - "nullable": true - }, - "address": { - "type": "string", - "nullable": true - }, - "country": { - "type": "string", - "nullable": true - }, - "website": { - "type": "string", - "nullable": true - }, - "eyeColor": { - "type": "string", - "nullable": true - }, - "keywords": { - "type": "string", - "nullable": true - }, - "lastName": { - "type": "array", - "items": { - "type": "string" - } - }, - "position": { - "type": "string", - "nullable": true - }, - "religion": { - "type": "string", - "nullable": true - }, - "birthDate": { - "type": "array", - "items": { - "type": "string" - } - }, - "education": { - "type": "array", - "items": { - "type": "string" - } - }, - "ethnicity": { - "type": "string", - "nullable": true - }, - "firstName": { - "type": "array", - "items": { - "type": "string" - } - }, - "hairColor": { - "type": "string", - "nullable": true - }, - "weakAlias": { - "type": "string", - "nullable": true - }, - "birthPlace": { - "type": "array", - "items": { - "type": "string" - } - }, - "modifiedAt": { - "type": "string", - "nullable": true - }, - "wikidataId": { - "type": "string", - "nullable": true - }, - "citizenship": { - "type": "string", - "nullable": true - }, - "nationality": { - "type": "string", - "nullable": true - } - } - }, - "match_score": { - "type": "number", - "description": "Weighted identity confidence score (0-100) calculated from name similarity, date of birth, and country matching. Used to determine if hit is a False Positive or Possible Match.", - "format": "float", - "nullable": true - }, - "risk_score": { - "type": "number", - "description": "Entity risk score (0-100). Used to determine final AML status (Approved/In Review/Declined) based on the highest risk_score among non-False-Positive hits." - }, - "review_status": { - "type": "string", - "description": "Review status for this hit. Can be 'Unreviewed', 'Confirmed Match', 'False Positive', or 'Inconclusive'.", - "enum": [ - "Unreviewed", - "Confirmed Match", - "False Positive", - "Inconclusive" - ] - }, - "score_breakdown": { - "type": "object", - "description": "Detailed breakdown of how the match_score was calculated.", - "properties": { - "name_score": { - "type": "integer", - "description": "Raw name similarity percentage (0-100)." - }, - "name_weight": { - "type": "integer", - "description": "Original weight for name from config (0-100)." - }, - "name_weight_normalized": { - "type": "number", - "description": "Normalized weight for name after re-weighting for missing data (0-100)." - }, - "name_contribution": { - "type": "number", - "description": "Points contributed by name to the total score." - }, - "dob_score": { - "type": "integer", - "description": "DOB match score (-100 to 100)." - }, - "dob_weight": { - "type": "integer", - "description": "Original weight for DOB from config (0-100)." - }, - "dob_weight_normalized": { - "type": "number", - "description": "Normalized weight for DOB after re-weighting for missing data (0-100)." - }, - "dob_contribution": { - "type": "number", - "description": "Points contributed by DOB to the total score." - }, - "country_score": { - "type": "integer", - "description": "Country match score (-100 to 100)." - }, - "country_weight": { - "type": "integer", - "description": "Original weight for country from config (0-100)." - }, - "country_weight_normalized": { - "type": "number", - "description": "Normalized weight for country after re-weighting for missing data (0-100)." - }, - "country_contribution": { - "type": "number", - "description": "Points contributed by country to the total score." - }, - "document_number_match_type": { - "type": "string", - "description": "Result of document number 'Golden Key' evaluation.", - "enum": [ - "MATCH", - "NEUTRAL", - "HARD_MISMATCH" - ] - }, - "document_number_effect": { - "type": "string", - "description": "Human-readable description of the document number effect on the score." - }, - "total_score": { - "type": "integer", - "description": "Final weighted match score (0-100)." - } - } - }, - "pep_matches": { - "type": "array", - "items": { - "type": "object", - "properties": { - "aliases": { - "type": "array", - "items": { - "type": "string" - } - }, - "education": { - "type": "array", - "items": { - "type": "string" - } - }, - "list_name": { - "type": "string" - }, - "publisher": { - "type": "string" - }, - "source_url": { - "type": "string", - "format": "uri" - }, - "description": { - "type": "string" - }, - "matched_name": { - "type": "string" - }, - "pep_position": { - "type": "string" - }, - "date_of_birth": { - "type": "string" - }, - "other_sources": { - "type": "array", - "items": { - "type": "string" - } - }, - "place_of_birth": { - "type": "string" - } - } - } - }, - "linked_entities": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "array", - "items": { - "type": "string" - } - }, - "active": { - "type": "array", - "items": { - "type": "string" - } - }, - "status": { - "type": "array", - "items": { - "type": "string" - } - }, - "details": { - "type": "array", - "items": { - "type": "string" - } - }, - "relation": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "warning_matches": { - "type": "array", - "description": "List of warning and regulatory enforcement matches. Includes entries from categories: Warning, SIP, Warnings and Regulatory Enforcement, Fitness and Probity, SIE, and Insolvency.", - "items": { - "type": "object", - "properties": { - "list_name": { - "type": "string", - "description": "Name of the warning list publisher" - }, - "matched_name": { - "type": "string", - "description": "Name matched in the warning list" - }, - "description": { - "type": "string", - "description": "Description of the warning entry" - }, - "publisher": { - "type": "string", - "description": "Publisher or source of the warning" - }, - "source_url": { - "type": "string", - "format": "uri", - "description": "URL to the source of the warning" - }, - "other_sources": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Additional source URLs" - }, - "countries": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "description": "Countries associated with the warning" - }, - "created_at": { - "type": "string", - "nullable": true, - "description": "Date the warning entry was created" - }, - "updated_at": { - "type": "string", - "nullable": true, - "description": "Date the warning entry was last updated" - }, - "additional_data": { - "type": "object", - "description": "Additional data from the warning source. May contain a `data` object with `case_details` array.", - "properties": { - "data": { - "type": "object", - "properties": { - "case_details": { - "type": "array", - "description": "Details of warning cases associated with this match", - "items": { - "type": "object", - "properties": { - "case_type": { - "type": "string", - "description": "Type of the warning case", - "enum": [ - "Accusation", - "Arrest", - "Bribery", - "Criminal", - "Fraud", - "Illegal Fundraising", - "Non-conviction Terror", - "Proclaimed Offender", - "Regulatory Enforcement", - "Terrorism", - "Terrorism-related", - "Terrorism-related listing" - ] - } - } - } - } - } - } - } - } - } - } - }, - "sanction_matches": { - "type": "array", - "description": "List of sanction list matches found for this entity.", - "items": { - "type": "object", - "properties": { - "list_name": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "description": "Name(s) of the sanction list(s)" - }, - "matched_name": { - "type": "string", - "description": "Name matched in the sanction list" - }, - "description": { - "type": "string", - "description": "Description of the sanction entry" - }, - "reason": { - "type": "string", - "nullable": true, - "description": "Free-text reason why the entity was sanctioned. Joined from the sanction list's reason entries. Examples: 'Executive Order 13224 (Terrorism)', 'Support for actions undermining Ukraine's sovereignty', 'Listed due to national security concerns.'" - }, - "source_url": { - "type": "string", - "format": "uri", - "description": "URL to the source of the sanction" - }, - "other_sources": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Additional source URLs" - }, - "details": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Additional details about the sanction" - }, - "legal_basis": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Legal basis for the sanction" - }, - "listed_on": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "description": "Date(s) when the entity was listed" - }, - "sanction_list": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "description": "Sanction list identifier(s)" - }, - "sanction_program": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Sanction program(s) the entity is listed under" - }, - "sanctioning_authority": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Authority/authorities that imposed the sanction" - }, - "updated_on": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Date(s) when the sanction entry was last updated" - }, - "additional_data": { - "type": "object", - "description": "Additional data from the sanction source not captured in the main fields" - } - } - } - }, - "adverse_media_details": { - "type": "object", - "properties": { - "sentiment": { - "type": "string" - }, - "entity_type": { - "type": "string" - }, - "sentiment_score": { - "type": "integer" - }, - "adverse_keywords": { - "type": "object", - "additionalProperties": { - "type": "integer" - } - } - } - }, - "adverse_media_matches": { - "type": "array", - "items": { - "type": "object", - "properties": { - "country": { - "type": "string" - }, - "summary": { - "type": "string" - }, - "headline": { - "type": "string" - }, - "sentiment": { - "type": "string" - }, - "thumbnail": { - "type": "string", - "format": "uri" - }, - "source_url": { - "type": "string", - "format": "uri" - }, - "author_name": { - "type": "string", - "nullable": true - }, - "other_sources": { - "type": "array", - "items": { - "type": "string" - } - }, - "adverse_keywords": { - "type": "array", - "items": { - "type": "string" - } - }, - "sentiment_score": { - "type": "integer" - }, - "publication_date": { - "type": "string", - "format": "date-time" - } - } - } - }, - "additional_information": { - "type": "object" - } - } - } - }, - "score": { - "type": "number", - "format": "float", - "nullable": true, - "minimum": 0, - "maximum": 100, - "description": "Overall AML screening score — the highest risk score across all hits (float, 0-100). `null` until computed." - }, - "screened_data": { - "type": "object", - "properties": { - "full_name": { - "type": "string" - }, - "nationality": { - "type": "string" - }, - "date_of_birth": { - "type": "string", - "format": "date" - }, - "document_number": { - "type": "string", - "nullable": true - } - } - }, - "is_ongoing_monitoring_enabled": { - "type": "boolean", - "description": "Whether AML ongoing monitoring is enabled for this screening. When `true`, the entity is re-screened periodically and any new hits update the report." - }, - "next_ongoing_monitoring_bill_date": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Date (YYYY-MM-DD) when this ongoing-monitoring subscription will next be billed. Computed as the date of the last ongoing-monitoring billing plus 365 days when `is_ongoing_monitoring_enabled = true`; otherwise `null`. This is a billing date, not a screening date - use `last_ongoing_screening_at` for that." - }, - "last_ongoing_screening_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When ongoing monitoring last completed a re-screening of this record, whether or not the result changed. `null` when the record has never been re-screened; it is never back-filled from the enablement or billing date." - }, - "last_ongoing_screening_datasets_updated_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "The most recent AML dataset update date the provider reported at the time of that screening. `null` when the provider reported no dataset dates; never estimated." - }, - "warnings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "enum": [ - "ID_VERIFICATION", - "NFC", - "LIVENESS", - "FACEMATCH", - "PROOF_OF_ADDRESS", - "PHONE", - "EMAIL", - "AML", - "LOCATION", - "DATABASE_VALIDATION", - "QUESTIONNAIRE", - "DOCUMENT_AI", - "KYB_REGISTRY", - "KYB_DOCUMENTS", - "KYB_KEY_PEOPLE" - ], - "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Within an AML screening this is typically `AML`, but the enum above is authoritative across all feature warnings on the decision payload." - }, - "risk": { - "type": "string" - }, - "additional_data": { - "type": "object", - "nullable": true - }, - "log_type": { - "type": "string" - }, - "short_description": { - "type": "string" - }, - "long_description": { - "type": "string" - }, - "node_id": { - "type": "string", - "description": "The workflow graph node ID that generated this warning" - } - } + "description": "Pre-trusted phone numbers for this user, e.g. `[\"+14155551234\"]`." + }, + "issuing_states": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Pre-recorded issuing countries (ISO 3166-1 alpha-3), e.g. `[\"USA\"]`." + }, + "first_name": { + "type": "string", + "maxLength": 255, + "description": "Given name. When `full_name` is not sent, Didit joins `first_name` and `last_name` with a space into `full_name`. Not returned as a separate field." + }, + "last_name": { + "type": "string", + "maxLength": 255, + "description": "Family name. Joined with `first_name` into `full_name` when `full_name` is not sent. Not returned as a separate field." + }, + "email": { + "type": "string", + "format": "email", + "description": "Contact email, stored in `metadata.contact.email` (it replaces an `email` you send inside `metadata.contact`). Required when `send_verification_link` is `true`: the link is sent to this address. If `metadata.contact` is not a JSON object, the call answers `400` on this field." + }, + "phone_number": { + "type": "string", + "maxLength": 32, + "description": "Contact phone number, stored in `metadata.contact.phone_number`. If `metadata.contact` is not a JSON object, the call answers `400` on this field." + }, + "country": { + "type": "string", + "maxLength": 3, + "description": "Country as an ISO 3166 code, alpha-2 or alpha-3 (`ES` or `ESP`). Stored uppercased in `metadata.address.country`. If `metadata.address` is not a JSON object, the call answers `400` on this field." + }, + "send_verification_link": { + "type": "boolean", + "default": false, + "description": "When `true`, Didit creates a verification session for the person on `workflow_id` and emails them the link at `email`. Requires `email` and `workflow_id`. A scoped API key also needs `sessions` write." + }, + "workflow_id": { + "type": "string", + "format": "uuid", + "description": "The workflow of the verification session. Required when `send_verification_link` is `true`. It must verify a person: a Business Verification (KYB) workflow is refused. Ignored otherwise." + }, + "language": { + "type": "string", + "maxLength": 10, + "description": "Language of the verification email, as a [supported language](/integration/supported-languages) code. English when omitted. Ignored when `send_verification_link` is not `true`." + } + } + }, + "examples": { + "Basic": { + "summary": "Create with basic info", + "value": { + "vendor_data": "user-abc-123", + "full_name": "John Doe" + } + }, + "Full": { + "summary": "All fields", + "value": { + "vendor_data": "user-abc-456", + "full_name": "Jane Smith", + "display_name": "Jane S.", + "date_of_birth": "1990-05-15", + "status": "ACTIVE", + "metadata": { + "tier": "premium", + "source": "api" + }, + "approved_emails": [ + "jane@example.com" + ] + } + }, + "Contact details": { + "summary": "Name, email, phone and country", + "value": { + "vendor_data": "user-abc-789", + "first_name": "Julia", + "last_name": "Martin", + "email": "julia.martin@example.com", + "phone_number": "+34600000000", + "country": "ES" + } + }, + "With verification link": { + "summary": "Create the person and email them a verification link", + "value": { + "vendor_data": "user-abc-789", + "first_name": "Julia", + "last_name": "Martin", + "email": "julia.martin@example.com", + "send_verification_link": true, + "workflow_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "language": "es" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/users/create/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"vendor_data\": \"user-abc-123\", \"full_name\": \"John Doe\"}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/users/create/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'vendor_data': 'user-abc-123', 'full_name': 'John Doe'},\n)\nresp.raise_for_status()\nuser = resp.json()\nprint(user['didit_internal_id'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/users/create/', {\n method: 'POST',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ vendor_data: 'user-abc-123', full_name: 'John Doe' }),\n});\nconst user = await resp.json();\nconsole.log(user.didit_internal_id);" + } + ], + "responses": { + "201": { + "description": "User created. The full user record (same shape as Get User), plus `verification_link`: the verification session Didit emailed when `send_verification_link` was `true`, otherwise `null`.", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/UserDetailItem" + }, + { + "type": "object", + "required": [ + "verification_link" + ], + "properties": { + "verification_link": { + "type": "object", + "nullable": true, + "description": "The verification session Didit created and emailed to the person. Always present: `null` when `send_verification_link` was not `true`.", + "required": [ + "session_id", + "session_number", + "status", + "url", + "workflow_id", + "email" + ], + "properties": { + "session_id": { + "type": "string", + "format": "uuid", + "description": "ID of the verification session." + }, + "session_number": { + "type": "integer", + "description": "Sequential number of the session inside your application." + }, + "status": { + "type": "string", + "description": "Status of the session, `Not Started` for a session that was just created." + }, + "url": { + "type": "string", + "format": "uri", + "description": "The verification link Didit emailed to the person." + }, + "workflow_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "The workflow the session runs." + }, + "email": { + "type": "string", + "format": "email", + "description": "The address the link was sent to." } } } } + } + ] + }, + "examples": { + "Created": { + "value": { + "didit_internal_id": "f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418", + "vendor_data": "user-abc-123", + "display_name": null, + "full_name": "John Doe", + "date_of_birth": null, + "effective_name": "John Doe", + "status": "ACTIVE", + "metadata": {}, + "portrait_image_url": null, + "session_count": 0, + "approved_count": 0, + "declined_count": 0, + "in_review_count": 0, + "issuing_states": [], + "approved_emails": [], + "approved_phones": [], + "features": {}, + "features_list": [], + "last_session_at": null, + "last_activity_at": "2025-06-15T10:30:00Z", + "first_session_at": null, + "tags": [], + "comments": [], + "created_at": "2025-06-15T10:30:00Z", + "updated_at": "2025-06-15T10:30:00Z", + "verification_link": null + } + }, + "Created with verification link": { + "value": { + "didit_internal_id": "9b2f6c1e-3d4a-4e5f-8a7b-1c2d3e4f5a6b", + "vendor_data": "user-abc-789", + "display_name": null, + "full_name": "Julia Martin", + "date_of_birth": null, + "effective_name": "Julia Martin", + "status": "ACTIVE", + "metadata": { + "contact": { + "email": "julia.martin@example.com" + } + }, + "portrait_image_url": null, + "session_count": 1, + "approved_count": 0, + "declined_count": 0, + "in_review_count": 0, + "issuing_states": [], + "approved_emails": [], + "approved_phones": [], + "features": {}, + "features_list": [], + "last_session_at": "2025-06-15T10:30:00Z", + "last_activity_at": "2025-06-15T10:30:00Z", + "first_session_at": "2025-06-15T10:30:00Z", + "tags": [], + "comments": [], + "created_at": "2025-06-15T10:30:00Z", + "updated_at": "2025-06-15T10:30:00Z", + "verification_link": { + "session_id": "3fa85f64-5717-4562-b3fc-2c963f66afa7", + "session_number": 43762, + "status": "Not Started", + "url": "https://verify.didit.me/en/session/3FaJ9wLqX2Mz", + "workflow_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "email": "julia.martin@example.com" + } + } + } + } + } + } + }, + "400": { + "description": "Invalid body, keyed by field: typically a duplicate `vendor_data`, a missing required field or a malformed value. With `send_verification_link: true`, also a missing `email` or `workflow_id`, an unknown or Business Verification (KYB) workflow, an unsupported `language` (keyed `contact_details.email_lang`), a blocklisted `vendor_data`, or too few credits to start the session. Nothing is written.", + "content": { + "application/json": { + "examples": { + "Duplicate": { + "value": { + "vendor_data": [ + "A user with this vendor_data already exists." + ] + } + }, + "Missing vendor_data": { + "value": { + "vendor_data": [ + "This field is required." + ] + } + }, + "Bad metadata": { + "value": { + "metadata": [ + "Metadata must be a JSON object." + ] + } + }, + "Bad status": { + "value": { + "status": [ + "Invalid status. Valid options are: ACTIVE, FLAGGED, BLOCKED" + ] + } + }, + "Bad country": { + "value": { + "country": [ + "Use an ISO 3166 country code, like ES or ESP." + ] + } + }, + "Contact not an object": { + "value": { + "email": [ + "metadata.contact must be a JSON object to store this value." + ] + } + }, + "Address not an object": { + "value": { + "country": [ + "metadata.address must be a JSON object to store this value." + ] + } + }, + "Link without email or workflow": { + "value": { + "email": [ + "An email address is required to send a verification link." + ], + "workflow_id": [ + "Pick the workflow to send a verification link for." + ] + } + }, + "Unknown workflow": { + "value": { + "workflow_id": [ + "Invalid workflow_id." + ] + } + }, + "Business workflow": { + "value": { + "workflow_id": [ + "This workflow verifies a business. Pick a workflow that verifies a person to send them a verification link." + ] + } + }, + "Blocklisted vendor_data": { + "value": { + "vendor_data": "This user or business has been blocked and cannot create new verification sessions." + } + }, + "Insufficient credits": { + "value": { + "detail": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + } + } + } + } + } + }, + "401": { + "description": "No credentials supplied. Requests to `/v3/users/*` paths without an `x-api-key` header (or `Authorization: Bearer` token) are rejected by the authentication middleware before reaching the API.", + "content": { + "application/json": { + "examples": { + "Missing credentials": { + "value": { + "detail": "You must be authenticated with a valid access token to access this endpoint." + } + } + } + } + } + }, + "403": { + "description": "Invalid or revoked API key, or the key cannot create users for this application. A scoped API key needs `users` write, and also `sessions` write when `send_verification_link` is `true`; nothing is written. Note: on this endpoint an *invalid* key returns `403` (not `401`).", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "No sessions write": { + "summary": "Scoped key asks for a verification link without sessions write", + "value": { + "detail": "This API key has no write access to sessions." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Rate limited": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/vendor-users/by-id/{didit_internal_id}/faces/upload/": { + "post": { + "summary": "Upload user face", + "description": "Attach a trusted imported face image to an existing User profile. Use this after creating or retrieving a User by `vendor_data` and reading its `didit_internal_id`. The uploaded face is stored on the User profile and indexed for duplicate detection and face search. It is not added to a face blocklist.", + "operationId": "upload_user_face", + "x-didit-sdk": { + "resource": "users", + "method": "upload_face", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID that owns the application." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID that owns the User profile." + }, + { + "name": "didit_internal_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Didit's internal User UUID, returned as `didit_internal_id` by `GET /v3/users/{vendor_data}/` and `POST /v3/users/create/`." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "image" + ], + "properties": { + "image": { + "type": "string", + "format": "byte", + "description": "Raw base64-encoded JPG or PNG face image. Do not include a `data:image/...;base64,` prefix. Max decoded size: 2 MB." + }, + "comment": { + "type": "string", + "description": "Optional operator-visible note for the uploaded face.", + "default": "" + } + } + }, + "examples": { + "Imported face": { + "summary": "Attach a face imported from a previous provider", + "value": { + "image": "/9j/4AAQSkZJRgABAQEA...", + "comment": "Imported from previous KYC provider" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/vendor-users/by-id/f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418/faces/upload/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"image\": \"/9j/4AAQSkZJRgABAQEA...\", \"comment\": \"Imported from previous KYC provider\"}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/vendor-users/by-id/f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418/faces/upload/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={\n 'image': '/9j/4AAQSkZJRgABAQEA...',\n 'comment': 'Imported from previous KYC provider',\n },\n)\nresp.raise_for_status()\nface = resp.json()\nprint(face['uuid'], face['image_url'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/vendor-users/by-id/f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418/faces/upload/', {\n method: 'POST',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n image: '/9j/4AAQSkZJRgABAQEA...',\n comment: 'Imported from previous KYC provider',\n }),\n});\nconst face = await resp.json();\nconsole.log(face.uuid, face.image_url);" + } + ], + "responses": { + "201": { + "description": "Face uploaded and attached to the User profile.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "uuid", + "image_url", + "comment", + "uploaded_by", + "created_at" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "UUID of the uploaded User face record." + }, + "image_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Signed URL for the uploaded face image." + }, + "comment": { + "type": "string", + "nullable": true, + "description": "Comment supplied when the face was uploaded." + }, + "uploaded_by": { + "type": "string", + "nullable": true, + "description": "Email or actor identifier of the uploader when available." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Upload timestamp." + } + } + }, + "examples": { + "Uploaded": { + "value": { + "uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "image_url": "https://signed-url.example/face.jpg", + "comment": "Imported from previous KYC provider", + "uploaded_by": "api", + "created_at": "2026-05-18T12:00:00Z" + } + } + } + } + } + }, + "400": { + "description": "Invalid request body or image. Common causes: invalid base64, invalid image, no detectable face, image over 2 MB, missing embedding, or the User already has 5 uploaded faces.", + "content": { + "application/json": { + "examples": { + "No face": { + "value": { + "image": "No face detected in the image. Please upload a clear photo with a visible face." + } + }, + "Too many faces": { + "value": { + "detail": "Maximum of 5 faces per user reached." + } + }, + "Too large": { + "value": { + "image": "Image exceeds maximum size of 2 MB." + } + } + } + } + } + }, + "401": { + "description": "Missing or invalid API key.", + "content": { + "application/json": { + "examples": { + "Missing key": { + "value": { + "detail": "Authentication credentials were not provided." + } + } + } + } + } + }, + "403": { + "description": "The credential is valid but cannot write User profiles for this application.", + "content": { + "application/json": { + "examples": { + "Forbidden": { + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "404": { + "description": "No User exists for that `didit_internal_id` in the application.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded; back off and retry.", + "content": { + "application/json": { + "examples": { + "Rate limited": { + "value": { + "detail": "Request was throttled. Expected available in 30 seconds." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/session/{sessionId}/update-data/": { + "patch": { + "summary": "Update KYC data extracted from the ID document on a verification session", + "description": "Correct the identity fields that were extracted (OCR / NFC) from the user's ID document — misread names, dates, document numbers, and so on — after the verification has finished. Typical callers are a compliance analyst working from your own back office, or an automated reconciliation job. Every field in the body is optional: only the keys you send are written, everything else is left untouched (`PATCH` semantics).\n\nThe session must belong to your application and be in `Approved`, `Declined`, `In Review`, or `Kyc Expired` status. Sessions in any other state (e.g. still `In Progress`) return `404` with `\"No Session matches the given query.\"` — distinct from the `\"Not found.\"` returned for an unknown or deleted `sessionId`. The session must also carry an ID-verification (KYC) record: sessions whose workflow never ran an ID-verification step return `404` (`\"The session does not have a KYC.\"`). For graph workflows with multiple ID-verification steps, target a specific step with `node_id` (query parameter or body field); when omitted, the most recent KYC record is patched.\n\nTwo derived fields are recomputed on every call: `full_name` (from `first_name` + `last_name`) and `issuing_state_name` (from `issuing_state`). If the free-text `address` changes, the server may also re-geocode it and refresh `parsed_address` (skipped for sandbox sessions). `extra_fields` and `parsed_address` are merged key-by-key rather than replaced — see the per-field descriptions for the `null`-to-delete semantics.\n\nSubmitting at least one valid field fires the `data.updated` [webhook](/integration/webhooks) (trigger `manual_review`) — even when the submitted values equal the stored ones. When values actually change, a `KYC_DATA_UPDATED` activity entry recording the changed fields with previous and new values is appended to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and the console activity timeline. The session's status and decision are **not** re-evaluated — use [Update Status](#patch-/v3/session/-sessionId-/update-status/) to change the outcome. An empty JSON body (`{}`) is accepted and simply returns the current KYC data without firing the webhook.\n\nAccepts an API key or a console user token; either way the credential needs the `write:sessions` permission. Authentication and permission failures both return `403` — this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. This endpoint has a dedicated rate limit of 10 requests per minute per API key (invalid requests count too); the shared write budget of 300 requests per minute also applies.", + "operationId": "patch_v3_session_update_data", + "x-didit-sdk": { + "resource": "sessions", + "method": "update_data", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "UUID of the User Verification (KYC) session whose extracted document data you are correcting. Business (KYB) session IDs are not valid here — for company data use [Update KYB Company Data](#patch-/v3/session/-sessionId-/kyb/-companyUuid-/update-data/).", + "schema": { + "type": "string", + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } + }, + { + "name": "node_id", + "in": "query", + "required": false, + "description": "Workflow graph node whose KYC record to patch, for graph workflows with multiple ID-verification steps (also accepted as a `node_id` body field). When omitted, the most recent KYC record on the session is patched. If no KYC exists for the given node, the call returns `404` (`\"The session does not have a KYC for node ''.\"`).", + "schema": { + "type": "string", + "example": "feature_ocr" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "All fields optional — send only what you want to change.", + "properties": { + "document_type": { + "type": "string", + "description": "Document type code. Accepts the short codes listed in the enum, plus six long-form aliases normalized server-side before validation: `Passport` → `P`, `Identity Card` → `ID`, `Driver's License` → `DL`, `Residence Permit` → `RP`, `Health Insurance Card` → `HIC`, `Tax Card` → `TC`. Any other value (including `PASSPORT` in caps) returns `400`.", + "enum": [ + "P", + "DL", + "ID", + "RP", + "SSC", + "HIC", + "WP", + "TC", + "VISA", + "PSC", + "BC", + "OTHER" + ], + "nullable": true, + "example": "P" + }, + "document_subtype": { + "type": "string", + "maxLength": 128, + "nullable": true, + "description": "Document subtype from the document registry, including the region prefix when the registry distinguishes regional variants." + }, + "document_number": { + "type": "string", + "maxLength": 255, + "nullable": true, + "description": "Document number as printed on the ID.", + "example": "PAB123456" + }, + "personal_number": { + "type": "string", + "maxLength": 255, + "nullable": true, + "description": "Personal / national identification number where the document carries one (e.g. Spanish DNI on a passport).", + "example": "99999999R" + }, + "date_of_birth": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Date of birth in `YYYY-MM-DD`. Any other format returns `400`.", + "example": "1980-01-01" + }, + "date_of_issue": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Issue date of the document in `YYYY-MM-DD`." + }, + "expiration_date": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Expiration date of the document in `YYYY-MM-DD`." + }, + "issuing_state": { + "type": "string", + "nullable": true, + "description": "Issuing country as an ISO 3166-1 alpha-3 code. Case-insensitive (`esp` is stored as `ESP`); the human-readable `issuing_state_name` shown in [Get Decision](#get-/v3/session/-sessionId-/decision/) is recomputed from this value. Invalid codes return `400`.", + "example": "ESP" + }, + "first_name": { + "type": "string", + "maxLength": 255, + "nullable": true, + "description": "Given name(s). The stored `full_name` is recomputed as `first_name + ' ' + last_name` on every call to this endpoint.", + "example": "Carmen" + }, + "last_name": { + "type": "string", + "maxLength": 255, + "nullable": true, + "description": "Family name(s). Also feeds the recomputed `full_name`.", + "example": "Espanola Espanola" + }, + "gender": { + "type": "string", + "enum": [ + "M", + "F", + "U" + ], + "nullable": true, + "description": "Sex as printed on the document: `M` (male), `F` (female), or `U` (unknown). Any other value — including `X` — returns `400`." + }, + "address": { + "type": "string", + "maxLength": 255, + "nullable": true, + "description": "Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it synchronously before responding — the 200 body already reflects the refreshed `parsed_address`. The re-geocode also requires `issuing_state` to be present on the KYC record." + }, + "place_of_birth": { + "type": "string", + "maxLength": 255, + "nullable": true, + "description": "Place of birth as printed on the document." + }, + "nationality": { + "type": "string", + "maxLength": 32, + "nullable": true, + "description": "Nationality as printed on the document. Free text up to 32 characters — typically an ISO 3166-1 alpha-3 code (e.g. `ESP`), but not validated as one." + }, + "marital_status": { + "type": "string", + "enum": [ + "SINGLE", + "MARRIED", + "DIVORCED", + "WIDOWED", + "UNKNOWN" + ], + "description": "Marital status where the document carries it. Any other value returns `400`." + }, + "extra_fields": { + "type": "object", + "additionalProperties": true, + "nullable": true, + "description": "Per-key overrides for document-specific extra fields (e.g. `blood_group`, `dl_categories`, `mrz_string`). Keys are merged into the existing override map: send a key with a value to set it, or with `null` / `\"\"` to remove that key from the merged view. Send the whole field as `null` to clear all previously applied overrides. Must be a JSON object, otherwise `400`. The response always returns the merged view (OCR-extracted values with your overrides applied)." + }, + "parsed_address": { + "type": "object", + "nullable": true, + "description": "Structured address. Only these nested keys are accepted: `address_type`, `city`, `region`, `street_1`, `street_2`, `postal_code`, `country` — any other key returns `400`. Per-key semantics: a value sets the key, `null` removes it. `formatted_address` is recomputed server-side by joining `street_1`, `street_2`, `city`, `region`, `postal_code`, and `country` and is returned in the response — you cannot set it directly.", + "properties": { + "address_type": { + "type": "string", + "nullable": true, + "description": "Free-form address classification (e.g. `home`)." + }, + "city": { + "type": "string", + "nullable": true, + "example": "Madrid" + }, + "region": { + "type": "string", + "nullable": true, + "description": "State / province / autonomous community." + }, + "street_1": { + "type": "string", + "nullable": true, + "example": "Avda de Madrid 34" + }, + "street_2": { + "type": "string", + "nullable": true + }, + "postal_code": { + "type": "string", + "nullable": true, + "example": "28013" + }, + "country": { + "type": "string", + "nullable": true, + "description": "ISO 3166-1 alpha-2 country code. Lowercase input is normalized to uppercase; alpha-3 codes are rejected with `400`.", + "example": "ES" + } + } + } + } + }, + "examples": { + "Correct name and DOB": { + "summary": "Fix OCR-misread name, date of birth, and structured address", + "value": { + "first_name": "Carmen", + "last_name": "Espanola Espanola", + "date_of_birth": "1980-01-01", + "issuing_state": "ESP", + "marital_status": "MARRIED", + "extra_fields": { + "blood_group": "O+" }, - "ip_analyses": { - "type": "array", - "nullable": true, - "description": "**Array of Device & IP Analysis reports** — always a JSON array, never a singular `ip_analysis` object. `null` until at least one IP-analysis step has produced data. Each item is the full report, including `node_id`, feature-level `status`, IP and geolocation data, device fingerprint, and `warnings[]`. Entries with duplicate `(node_id, ip_address, device_fingerprint)` are deduplicated server-side.", - "items": { - "type": "object", - "properties": { - "node_id": { - "type": "string", - "description": "Unique identifier for this Device & IP Analysis step in the workflow graph." - }, - "status": { - "type": "string", - "description": "Feature-level status of this Device & IP Analysis entry. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`.", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Resub Requested" - ] - }, - "device_brand": { - "type": "string", - "description": "Brand of the device used" - }, - "device_model": { - "type": "string", - "description": "Model of the device used" - }, - "browser_family": { - "type": "string", - "description": "Browser family used" - }, - "os_family": { - "type": "string", - "description": "Operating system family" - }, - "platform": { - "type": "string", - "description": "Platform type (mobile, desktop, etc)" - }, - "device_fingerprint": { - "type": "string", - "nullable": true, - "description": "Unique device fingerprint identifier" - }, - "ip_country": { - "type": "string", - "description": "Country detected from IP" - }, - "ip_country_code": { - "type": "string", - "description": "Two-letter country code from IP" - }, - "ip_state": { - "type": "string", - "description": "State/region detected from IP" - }, - "ip_city": { - "type": "string", - "description": "City detected from IP" - }, - "latitude": { - "type": "number", - "description": "Latitude coordinate of IP location" - }, - "longitude": { - "type": "number", - "description": "Longitude coordinate of IP location" - }, - "ip_address": { - "type": "string", - "description": "IP address" - }, - "isp": { - "type": "string", - "nullable": true, - "description": "Internet Service Provider" - }, - "organization": { - "type": "string", - "nullable": true, - "description": "Organization associated with IP" - }, - "is_vpn_or_tor": { - "type": "boolean", - "description": "Whether IP is from VPN or Tor network" - }, - "is_data_center": { - "type": "boolean", - "description": "Whether IP is from a data center" - }, - "time_zone": { - "type": "string", - "description": "Time zone detected from IP" - }, - "time_zone_offset": { - "type": "string", - "description": "Time zone offset" - }, - "ip": { - "type": "object", - "description": "IP geolocation of this entry plus great-circle distances (km) to the ID document address and the POA document address. Distances are `null` when either side has no resolvable location.", - "properties": { - "location": { - "type": "object", - "nullable": true, - "properties": { - "latitude": { - "type": "number", - "example": 41.2706327 - }, - "longitude": { - "type": "number", - "example": 1.9770097 - } - }, - "description": "Coordinates, or `null` when unavailable." - }, - "distance_from_id_document": { - "type": "number", - "nullable": true, - "example": 23.4 - }, - "distance_from_poa_document": { - "type": "number", - "nullable": true, - "example": 12.3 - } - } - }, - "id_document": { - "type": "object", - "description": "Location extracted from the ID document address plus distances (km) to the IP geolocation and the POA document address. `location` is `null` when no document address could be geocoded.", - "properties": { - "location": { - "type": "object", - "nullable": true, - "properties": { - "latitude": { - "type": "number", - "example": 41.2706327 - }, - "longitude": { - "type": "number", - "example": 1.9770097 - } - }, - "description": "Coordinates, or `null` when unavailable." - }, - "distance_from_ip": { - "type": "number", - "nullable": true, - "example": 23.4 - }, - "distance_from_poa_document": { - "type": "number", - "nullable": true, - "example": 18.7 - } - } - }, - "poa_document": { - "type": "object", - "description": "Location extracted from the Proof of Address document plus distances (km) to the IP geolocation and the ID document address. `location` is `null` when the session has no POA step or the address could not be geocoded.", - "properties": { - "location": { - "type": "object", - "nullable": true, - "properties": { - "latitude": { - "type": "number", - "example": 41.2706327 - }, - "longitude": { - "type": "number", - "example": 1.9770097 - } - }, - "description": "Coordinates, or `null` when unavailable." - }, - "distance_from_ip": { - "type": "number", - "nullable": true, - "example": 12.3 - }, - "distance_from_id_document": { - "type": "number", - "nullable": true, - "example": 18.7 - } - } - }, - "warnings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "enum": [ - "ID_VERIFICATION", - "NFC", - "LIVENESS", - "FACEMATCH", - "PROOF_OF_ADDRESS", - "PHONE", - "EMAIL", - "AML", - "LOCATION", - "DATABASE_VALIDATION", - "QUESTIONNAIRE", - "DOCUMENT_AI", - "KYB_REGISTRY", - "KYB_DOCUMENTS", - "KYB_KEY_PEOPLE" - ], - "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload." - }, - "risk": { - "type": "string" - }, - "additional_data": { - "type": "object", - "nullable": true - }, - "log_type": { - "type": "string" - }, - "short_description": { - "type": "string" - }, - "long_description": { - "type": "string" - }, - "node_id": { - "type": "string", - "description": "The workflow graph node ID that generated this warning" - } - } - } - }, - "matches": { - "type": "array", - "description": "Cross-session matches found for this entry's IP address or device identity. Every match is another verification session in your application (`source` is always `session`) that shares the same value, always belonging to a different vendor user / `vendor_data` than the current session, capped at 5 per collector (`device_fingerprint` sums two independently capped collectors, so up to 10 entries are possible). Every item carries `match_source`, `confidence`, `match_mode`, `device_info` and `location_info`; the keys `recovery_similarity`, `tls_ja4_corroborated` and `recovery_gate_reason` are present only when `match_source` is `recovered_high`.", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "session_id": { - "type": "string", - "format": "uuid", - "description": "Session ID of the matching session." - }, - "session_number": { - "type": "integer", - "description": "Sequential number of the matching session." - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "`vendor_data` of the matching session — always different from the current session's vendor user." - }, - "verification_date": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "Creation date of the matching session (`YYYY-MM-DDTHH:MM:SSZ`)." - }, - "match_type": { - "type": "string", - "enum": [ - "ip_address", - "device_fingerprint" - ], - "description": "Whether the match was on IP address or device fingerprint" - }, - "match_source": { - "type": "string", - "enum": [ - "ip_address", - "persistent_id", - "legacy_fp", - "recovered_high" - ], - "description": "How this match was reached: `ip_address` (shared IP address — network co-location, not a device-identity claim), `persistent_id` (exact stored persistent device identifier), `legacy_fp` (legacy `didit-fp-*` device-fingerprint hash), or `recovered_high` (high-confidence fuzzy fingerprint-recovery hit). Lower-confidence recovery candidates surface as risk warnings only, never in this array." - }, - "matched_value": { - "type": "string", - "nullable": true, - "description": "The value that matched: the shared IP address, the persistent device identifier or legacy fingerprint hash, or — for `recovered_high` — the UUID of the recovered device." - }, - "status": { - "type": "string", - "description": "Lifecycle status of the matching session." - }, - "is_blocklisted": { - "type": "boolean", - "description": "Whether the matched value is on a blocklist. Currently always `false` for device/IP matches." - }, - "api_service": { - "type": "string", - "nullable": true, - "description": "Standalone API service of the matching session; `null` for workflow sessions." - }, - "source": { - "type": "string", - "enum": [ - "session" - ], - "description": "Always `session` for device/IP matches — every match is another verification session in your application." - }, - "device_info": { - "type": "object", - "description": "Device details observed on the matching session.", - "properties": { - "device_brand": { - "type": "string", - "nullable": true - }, - "device_model": { - "type": "string", - "nullable": true - }, - "browser_family": { - "type": "string", - "nullable": true - }, - "os_family": { - "type": "string", - "nullable": true - }, - "platform": { - "type": "string", - "nullable": true - }, - "device_fingerprint": { - "type": "string", - "nullable": true - } - } - }, - "location_info": { - "type": "object", - "description": "Network and geolocation details observed on the matching session.", - "properties": { - "ip_address": { - "type": "string", - "nullable": true - }, - "ip_country": { - "type": "string", - "nullable": true - }, - "ip_country_code": { - "type": "string", - "nullable": true - }, - "ip_state": { - "type": "string", - "nullable": true - }, - "ip_city": { - "type": "string", - "nullable": true - }, - "is_vpn_or_tor": { - "type": "boolean" - }, - "is_data_center": { - "type": "boolean" - } - } - }, - "confidence": { - "type": "number", - "minimum": 0, - "maximum": 1, - "description": "Server-side identification confidence, computed as `1 - P(false positive)`: `1.0` for deterministic matches (`persistent_id`, or hardware-rooted `recovered_high`), `0.5` for `legacy_fp`, `0.0` for `ip_address` (co-location only), and a corroboration-adjusted value below `1.0` for probabilistic recovered matches (higher when the TLS JA4 fingerprint and IP country independently agree)." - }, - "match_mode": { - "type": "string", - "enum": [ - "deterministic", - "probabilistic", - "co_occurrence" - ], - "description": "Evidence class of the match: `deterministic` (exact stored or hardware-rooted identifier that cannot collide between physical devices), `probabilistic` (fuzzy fingerprint evidence), or `co_occurrence` (shared network only, no device-identity claim)." - }, - "recovery_similarity": { - "type": "number", - "description": "Cosine similarity of the fingerprint-recovery match, rounded to 4 decimals. Present only when `match_source` is `recovered_high`." - }, - "tls_ja4_corroborated": { - "type": "boolean", - "description": "Whether the TLS JA4 fingerprints of the two observations agree — a server-observed signal the browser cannot spoof. Present only when `match_source` is `recovered_high`." - }, - "recovery_gate_reason": { - "type": "string", - "description": "Why the recovery match passed the false-positive hard gate: `hardware_root_match` (a hardware-rooted device identifier matched exactly) or, on configurations that admit probe-only evidence, `exact_composite_plus__families` / `very_high_similarity_plus__families`. Present only when `match_source` is `recovered_high`." - } - } - } - } - } - } + "parsed_address": { + "street_1": "Avda de Madrid 34", + "city": "Madrid", + "postal_code": "28013", + "country": "ES" + } + } + }, + "Remove an extra field": { + "summary": "Delete one extra-field key, keep the rest", + "value": { + "extra_fields": { + "blood_group": null + } + } + }, + "Long-form document type": { + "summary": "Long-form alias is normalized to the short code (here P)", + "value": { + "document_type": "Passport" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "KYC data updated. Returns the **full current KYC record** (every updatable field with its stored value, plus the merged `extra_fields` and the current `parsed_address`) — not just the fields you submitted. The `data.updated` webhook fires when at least one valid field was in the payload.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "document_type": { + "type": "string", + "description": "Document type code. Accepts the short codes listed in the enum, plus six long-form aliases normalized server-side before validation: `Passport` → `P`, `Identity Card` → `ID`, `Driver's License` → `DL`, `Residence Permit` → `RP`, `Health Insurance Card` → `HIC`, `Tax Card` → `TC`. Any other value (including `PASSPORT` in caps) returns `400`.", + "enum": [ + "P", + "DL", + "ID", + "RP", + "SSC", + "HIC", + "WP", + "TC", + "VISA", + "PSC", + "BC", + "OTHER" + ], + "nullable": true, + "example": "P" }, - "database_validations": { - "type": "array", + "document_subtype": { + "type": "string", + "maxLength": 128, "nullable": true, - "description": "**Array of Database Validation reports** — always a JSON array, never a singular `database_validation` object. `null` until at least one DB validation step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `screened_data`, the per-service `validations[]` (each with its registry `source_data`), and `warnings[]`. The `screened_data` and `source_data` fields vary by country:\n\n**ARG (Argentina)**: screened_data: {document_number, selfie, gender, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, date_of_birth, date_of_death, tax_id, tax_id_type, face_match_score, sit_1_since, banks, last_position, highest_position, rejected_checks}. Uses RENAPER biometric face-match validation. Gender is auto-inferred from face analysis or given name when not present in the document (e.g., driver licenses)\n\n**BOL (Bolivia)**: screened_data: {document_number, date_of_birth, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, date_of_birth, gender}\n\n**BRA (Brazil)**: screened_data: {tax_number, first_name, last_name, date_of_birth}. source_data always includes {identification_number, first_name, last_name, date_of_birth, lgpd_minor, minor_under_18, minor_under_16} for successful CPF matches. Successful adult lookups set those flags to false. The Receita Federal Consulta CPF service may return HTTP 422 (minor under 18) or 451 (under 16) with LGPD-withheld data; in those cases source_data sets the minor flags to true as appropriate, includes the upstream HTTP status, and field-level validation is no_match. See the Brazil section of the Database Validation Outcome Codes reference for the full code list.\n\n**CHL (Chile)**: screened_data: {personal_number, first_name, last_name, date_of_birth}. source_data: {identification_number, first_name, last_name, date_of_birth, gender}\n\n**COL (Colombia)**: screened_data: {document_number, document_type, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, date_of_birth, document_type}\n\n**CRI (Costa Rica)**: screened_data: {personal_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name}\n\n**DOM (Dominican Republic)**: screened_data: {personal_number}. source_data: {identification_number} (returns valid/invalid only)\n\n**ECU (Ecuador)**: screened_data: {personal_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, street, formatted_address, gender, nationality, education, marital_status, spouse, mother_name, father_name, profession}\n\n**ESP (Spain)**: screened_data: {personal_number, document_type, expiration_date, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, document_type, expiration_date}\n\n**GTM (Guatemala)**: screened_data: {document_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name}\n\n**HND (Honduras)**: screened_data: {document_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name}\n\n**MEX (Mexico)**: screened_data: {personal_number (CURP), first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, date_of_birth, gender, nationality, curp_status, state_of_birth, doc_probatorio, registration_year, registration_state, registration_municipality, num_acta, crip, issuing_state_code, foreign_registry_number, folio_carta, folio_certificado}. Uses the RENAPO civil registry. A separate INE validity-check service is also available for MEX as an additional one-by-one validation.\n\n**PAN (Panama)**: screened_data: {personal_number, selfie}. source_data: {identification_number}. Uses SIB biometric validation\n\n**PER (Peru)**: screened_data: {personal_number (DNI), first_name, last_name}. source_data: {identification_number, first_name, last_name, paternal_name, maternal_name, verification_number, verification_letter}\n\n**PRY (Paraguay)**: screened_data: {document_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name}\n\n**SLV (El Salvador)**: screened_data: {document_number, date_of_birth, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name}\n\n**URY (Uruguay)**: screened_data: {personal_number, date_of_birth, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name}\n\n**VEN (Venezuela)**: screened_data: {document_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, gender, date_of_birth, document_type}", - "items": { - "type": "object", - "properties": { - "node_id": { - "type": "string", - "description": "Unique identifier for this database validation step in the workflow graph." - }, - "issuing_state": { - "type": "string", - "description": "ISO 3166-1 alpha-3 code of the country whose government/registry sources were queried (e.g. `BRA`, `MEX`, `ESP`, `NGA`, `IDN`). Database Validation covers 60+ countries across the Americas, Europe, Africa, the Middle East and Asia-Pacific, plus global multi-country sources — coverage expands regularly, so treat the value as an open set rather than a fixed enum." - }, - "validation_type": { - "type": "string", - "enum": [ - "one_by_one", - "two_by_two", - "not_enabled" - ], - "description": "How the result was corroborated: `one_by_one` (single-service outcome), `two_by_two` (two or more distinct services independently full-matched the identification number), or `not_enabled` (persisted when no service produced a usable match). The value is never `null`. Dedicated biometric 2x2 validation modes are currently available for `ECU` and `PER`." - }, - "screened_data": { - "type": "object", - "description": "The input data used for validation. Fields vary by country — see the Database Validation docs (`/core-technology/database-validation/overview`) for country-specific formats.", - "properties": { - "last_name": { - "type": "string" - }, - "first_name": { - "type": "string" - }, - "tax_number": { - "type": "string", - "description": "Brazil: CPF (11 digits)" - }, - "personal_number": { - "type": "string", - "description": "Chile: RUT, DOM: Cédula (11 digits), ECU: Cédula (10 digits), ESP: DNI/NIE, MEX: CURP (18 chars), PER: DNI (8 digits), URY: CI" - }, - "document_number": { - "type": "string", - "description": "ARG: DNI, BOL: CI, COL: Cédula, GTM: DPI, HND: DNI, PAN: Cédula, PRY: CI, SLV: DUI, VEN: Cédula" - }, - "date_of_birth": { - "type": "string", - "format": "date" - }, - "document_type": { - "type": "string", - "description": "COL: CC/CE/TI/NIT, ESP: ID/PASSPORT" - }, - "expiration_date": { - "type": "string", - "format": "date", - "description": "Required for ESP" - } - } - }, - "validations": { - "type": "array", - "description": "One entry per registry service that returned a validation payload. Each entry identifies the service (`service_id`, `service_name`) and carries the per-field `validation` comparison, the service `outcome_code` / `outcome_detail` when reported, and the normalised registry record in `source_data`.", - "items": { - "type": "object", - "properties": { - "service_id": { - "type": "string", - "description": "Identifier of the registry service that produced this validation result.", - "example": "bra_cpf" - }, - "service_name": { - "type": "string", - "description": "Human-readable name of the registry service.", - "example": "Brazil - CPF status check" - }, - "validation": { - "type": "object", - "description": "The results of the field validations.", - "properties": { - "full_name": { - "type": "string", - "description": "Match result for full name (e.g., 'full_match', 'partial_match', 'no_match')." - }, - "date_of_birth": { - "type": "string", - "description": "Match result for date of birth." - }, - "identification_number": { - "type": "string", - "description": "Match result for identification number." - } - } - }, - "outcome_code": { - "type": "string", - "nullable": true, - "description": "Machine-readable outcome of the registry lookup (e.g. `MATCH`, `NO_MATCH`, `PARTIAL_MATCH`, `INCONCLUSIVE`, source-specific codes). Present when the service reports one." - }, - "outcome_detail": { - "type": "string", - "nullable": true, - "description": "Human-readable explanation of `outcome_code`, when provided by the service." - }, - "source_data": { - "type": "object", - "description": "The data returned from the government database. Common fields: identification_number, first_name, last_name, full_name, date_of_birth, gender. Additional fields by country: BRA always includes lgpd_minor, minor_under_18, and minor_under_16 on successful CPF matches; it also includes the upstream HTTP status when CPF data is withheld under LGPD (HTTP 422/451). ECU adds street, formatted_address, nationality, education, marital_status, spouse, mother_name, father_name, profession. PER adds paternal_name, maternal_name, verification_number, verification_letter. ESP adds document_type, expiration_date. VEN/COL add document_type.", - "additionalProperties": true - } - } - } - }, - "errors": { - "type": "array", - "description": "Errors returned by registry services while validating, each tagged with the `service_id` that produced it. **This key is omitted entirely when there are no errors.**", - "items": { - "type": "object", - "description": "The upstream service's error object spread as-is — its keys vary by service (commonly a message/code and any provider-specific details) — plus `service_id` identifying the service that errored.", - "properties": { - "service_id": { - "type": "string", - "nullable": true, - "description": "Identifier of the registry service that errored." - } - }, - "additionalProperties": true - } - }, - "match_type": { - "type": "string", - "enum": [ - "no_match", - "partial_match", - "full_match" - ], - "nullable": true, - "description": "The aggregate match outcome across the queried services. `null` until the validation has been evaluated." - }, - "status": { - "type": "string", - "description": "Feature-level status of this database validation. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`.", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Resub Requested" - ] - }, - "warnings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "enum": [ - "ID_VERIFICATION", - "NFC", - "LIVENESS", - "FACEMATCH", - "PROOF_OF_ADDRESS", - "PHONE", - "EMAIL", - "AML", - "LOCATION", - "DATABASE_VALIDATION", - "QUESTIONNAIRE", - "DOCUMENT_AI", - "KYB_REGISTRY", - "KYB_DOCUMENTS", - "KYB_KEY_PEOPLE" - ], - "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload." - }, - "risk": { - "type": "string" - }, - "additional_data": { - "type": "object", - "nullable": true - }, - "log_type": { - "type": "string" - }, - "short_description": { - "type": "string" - }, - "long_description": { - "type": "string" - }, - "node_id": { - "type": "string", - "description": "The workflow graph node ID that generated this warning" - } - } - }, - "description": "Warnings related to the database validation." - } - } - } + "description": "Document subtype from the document registry, including the region prefix when the registry distinguishes regional variants." }, - "reviews": { - "type": "array", - "items": { - "type": "object", - "properties": { - "user": { - "type": "string", - "description": "Who performed the review. Usually the console reviewer's identifier (their email), but the value is `\"API Client\"` for status updates made with an API key and `\"Unknown\"` when no actor could be resolved — so do not parse this field as an email address." - }, - "new_status": { - "type": "string" - }, - "comment": { - "type": "string" - }, - "created_at": { - "type": "string", - "format": "date-time" - } - } - } + "document_number": { + "type": "string", + "maxLength": 255, + "nullable": true, + "description": "Document number as printed on the ID.", + "example": "PAB123456" }, - "created_at": { + "personal_number": { "type": "string", - "format": "date-time", - "description": "Timestamp at which the session was created (ISO 8601, UTC)." + "maxLength": 255, + "nullable": true, + "description": "Personal / national identification number where the document carries one (e.g. Spanish DNI on a passport).", + "example": "99999999R" }, - "expires_at": { + "date_of_birth": { "type": "string", - "format": "date-time", + "format": "date", "nullable": true, - "description": "Timestamp at which this session is scheduled to expire. Set on User Verification (KYC) sessions when an expiration was configured at creation time (or once `Approved` status triggers KYC expiry computation). `null` for sessions without a configured expiration, and for kinds that do not use expiry (e.g. Business Verification (KYB) sessions that never received an `expires_at`)." + "description": "Date of birth in `YYYY-MM-DD`. Any other format returns `400`.", + "example": "1980-01-01" }, - "registry_checks": { - "type": "array", + "date_of_issue": { + "type": "string", + "format": "date", "nullable": true, - "description": "Company registry checks. Present only when `session_kind = business`. Each item wraps the full company-registry payload.", - "items": { - "type": "object", - "description": "One company registry check — the full company-registry payload enriched with ownership structure and warnings. Present only when `session_kind = business`.", - "properties": { - "status": { - "type": "string", - "description": "Feature lifecycle status." - }, - "node_id": { - "type": "string", - "nullable": true - }, - "data_resolved": { - "type": "boolean" - }, - "company": { - "type": "object", - "description": "Full registry payload for the company (registry record merged with any user-provided or console-edited data).", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "node_id": { - "type": "string", - "nullable": true - }, - "status": { - "type": "string" - }, - "registry_status": { - "type": "string", - "nullable": true - }, - "data_resolved": { - "type": "boolean" - }, - "company_name": { - "type": "string" - }, - "registration_number": { - "type": "string", - "nullable": true - }, - "country_code": { - "type": "string", - "description": "Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `DE`, `US`)." - }, - "region": { - "type": "string", - "nullable": true, - "description": "ISO 3166-2 subdivision code when applicable (e.g. `CA`, `NY` for US states)." - }, - "company_type": { - "type": "string", - "nullable": true - }, - "incorporation_date": { - "type": "string", - "format": "date", - "nullable": true - }, - "registered_address": { - "type": "string", - "nullable": true - }, - "tax_number": { - "type": "string", - "nullable": true - }, - "risk_level": { - "type": "string", - "enum": [ - "LOW", - "MEDIUM", - "HIGH" - ], - "nullable": true - }, - "verification_status": { - "type": "string", - "nullable": true - }, - "is_from_registry": { - "type": "boolean" - }, - "fetch_status": { - "type": "string", - "nullable": true - }, - "alternative_names": { - "type": "string", - "nullable": true, - "description": "Alternative, trading, former or original-language company names as text, when reported." - }, - "nature_of_business": { - "type": "string", - "nullable": true - }, - "registered_capital": { - "type": "string", - "nullable": true - }, - "registered_capital_amount": { - "type": "string", - "format": "decimal", - "nullable": true, - "description": "Registered/share capital amount as a decimal string (e.g. `\"100.00\"`), parsed from the registry filing. `null` when not available." - }, - "registered_capital_currency": { - "type": "string", - "nullable": true, - "description": "ISO 4217 currency code of `registered_capital_amount` (e.g. `\"GBP\"`). `null` when not available." - }, - "website": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string", - "format": "email", - "nullable": true - }, - "phone": { - "type": "string", - "nullable": true - }, - "legal_entity_identifier": { - "type": "string", - "nullable": true - }, - "location_of_registration": { - "type": "string", - "nullable": true - }, - "vat_number": { - "type": "string", - "nullable": true, - "description": "EU VAT number, when collected during the hosted registry step or edited afterwards." - }, - "vat_validation_status": { - "type": "string", - "enum": [ - "valid", - "invalid", - "format_valid", - "invalid_format", - "could_not_validate", - "not_applicable" - ], - "description": "Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session." - }, - "vat_validated_number": { - "type": "string", - "nullable": true, - "description": "The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran." - }, - "vat_validation_source": { - "type": "string", - "nullable": true, - "enum": [ - "eu_vies", - "pl_vat_white_list", - "no_brreg", - "ch_uid_register", - "br_receita_federal", - "co_rues", - "cr_hacienda", - "ec_sri", - "py_dnit", - "format_rules", - null - ], - "description": "The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran." - }, - "vat_validated_name": { - "type": "string", - "nullable": true, - "description": "Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." - }, - "vat_validated_address": { - "type": "string", - "nullable": true, - "description": "Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." - }, - "vat_checked_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "Timestamp of the check. `null` when no check ran." - }, - "financial_summary": { - "type": "object", - "nullable": true - }, - "officers": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string" - }, - "designation": { - "type": "string", - "nullable": true - }, - "role": { - "type": "string", - "nullable": true - }, - "nationality": { - "type": "string", - "nullable": true - }, - "is_active": { - "type": "boolean" - }, - "kyc_status": { - "type": "string", - "enum": [ - "Approved", - "Declined", - "Pending" - ], - "nullable": true - }, - "kyc_session_url": { - "type": "string", - "format": "uri", - "nullable": true - }, - "first_name": { - "type": "string", - "nullable": true, - "description": "Given name when explicitly reported by the registry; not inferred from the full name." - }, - "last_name": { - "type": "string", - "nullable": true, - "description": "Family name when explicitly reported by the registry." - }, - "address": { - "type": "string", - "nullable": true, - "description": "Officer address reported by the registry." - }, - "appointment_date": { - "type": "string", - "nullable": true, - "description": "Date the officer was appointed.", - "format": "date" - }, - "termination_date": { - "type": "string", - "nullable": true, - "description": "Date the appointment ended, when reported.", - "format": "date" - }, - "date_of_birth": { - "type": "string", - "nullable": true, - "description": "Complete registry-reported birth date. Null when only a birth year or month is known.", - "format": "date" - }, - "date_of_birth_year": { - "type": "integer", - "nullable": true, - "minimum": 1, - "maximum": 9999, - "description": "Registry-reported birth year, including when the full date is unavailable." - }, - "date_of_birth_month": { - "type": "integer", - "nullable": true, - "minimum": 1, - "maximum": 12, - "description": "Registry-reported birth month. Null when only the year is known." - } - } - } - }, - "beneficial_owners": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string" - }, - "first_name": { - "type": "string", - "nullable": true - }, - "last_name": { - "type": "string", - "nullable": true - }, - "entity_type": { - "type": "string", - "enum": [ - "person", - "company" - ] - }, - "roles": { - "type": "array", - "items": { - "type": "string" - } - }, - "ownership_min_shares": { - "type": "string", - "nullable": true, - "description": "Lower bound of the ownership percentage as reported by the registry. Free-form string (e.g. `\"25.00\"`, `\"50\"`); use `effective_ownership_percent` for a parsed numeric value." - }, - "ownership_max_shares": { - "type": "string", - "nullable": true, - "description": "Upper bound of the ownership percentage as reported by the registry. Free-form string; use `effective_ownership_percent` for a parsed numeric value." - }, - "is_active": { - "type": "boolean" - }, - "kyc_status": { - "type": "string", - "enum": [ - "Approved", - "Declined", - "Pending" - ], - "nullable": true - }, - "kyc_session_url": { - "type": "string", - "format": "uri", - "nullable": true - }, - "effective_ownership_percent": { - "type": "number", - "nullable": true - }, - "company_name": { - "type": "string", - "nullable": true - }, - "registration_number": { - "type": "string", - "nullable": true - }, - "country_of_birth": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string", - "nullable": true - }, - "nationality": { - "type": "string", - "nullable": true - }, - "designation": { - "type": "string", - "nullable": true - }, - "voting_min_shares": { - "type": "string", - "nullable": true - }, - "voting_max_shares": { - "type": "string", - "nullable": true - }, - "date_of_birth": { - "type": "string", - "nullable": true, - "description": "Complete birth date when reported.", - "format": "date" - }, - "appointment_date": { - "type": "string", - "nullable": true, - "description": "Appointment date when reported.", - "format": "date" - }, - "shareholding": { - "$ref": "#/components/schemas/KYBRegistryShareholding" - } - } - } - }, - "addresses": { - "type": "array", - "nullable": true, - "description": "Every address the registry states, one canonical shape. Null when it states none.", - "items": { - "$ref": "#/components/schemas/KYBRegistryAddress" - } - }, - "industries": { - "type": "array", - "nullable": true, - "description": "The activities the registry classifies the company under, one canonical shape. Null when it states none.", - "items": { - "$ref": "#/components/schemas/KYBRegistryIndustry" - } - }, - "accounts": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts." - }, - "registry_data": { - "type": "object", - "description": "Raw record returned by the registry source, kept for reviewers. Its shape differs by country and source and is not a stable contract: read the canonical fields (`addresses`, `industries`, `ownership`, `beneficial_owners[].shareholding`) instead." - }, - "user_provided_data": { - "type": "object", - "description": "Data confirmed or edited by the end user." - }, - "confirmed_by_user_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "last_console_edit_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "is_editable": { - "type": "boolean" - }, - "ownership": { - "$ref": "#/components/schemas/KYBRegistryOwnership" - }, - "registry_tier": { - "type": "string", - "nullable": true, - "description": "The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`." - }, - "registry_tier_delivered": { - "type": "string", - "nullable": true, - "description": "The tier the registry delivered and that is billed. Also in `ownership.tier_delivered`." - }, - "registry_ownership_status": { - "type": "string", - "nullable": true, - "description": "`not_requested`, `pending`, `delivered` or `unavailable`. Also in `ownership.status`." - } - } - }, - "ownership_structure": { - "type": "object", - "description": "Raw ownership snapshot kept for reviewers; its shape differs by registry source and is not a stable contract. Read `company.beneficial_owners` and `company.ownership` instead." - }, - "warnings": { - "type": "array", - "items": { - "type": "object" - } - } - } - } + "description": "Issue date of the document in `YYYY-MM-DD`." + }, + "expiration_date": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Expiration date of the document in `YYYY-MM-DD`." + }, + "issuing_state": { + "type": "string", + "nullable": true, + "description": "Issuing country as an ISO 3166-1 alpha-3 code. Case-insensitive (`esp` is stored as `ESP`); the human-readable `issuing_state_name` shown in [Get Decision](#get-/v3/session/-sessionId-/decision/) is recomputed from this value. Invalid codes return `400`.", + "example": "ESP" + }, + "first_name": { + "type": "string", + "maxLength": 255, + "nullable": true, + "description": "Given name(s). The stored `full_name` is recomputed as `first_name + ' ' + last_name` on every call to this endpoint.", + "example": "Carmen" + }, + "last_name": { + "type": "string", + "maxLength": 255, + "nullable": true, + "description": "Family name(s). Also feeds the recomputed `full_name`.", + "example": "Espanola Espanola" + }, + "gender": { + "type": "string", + "enum": [ + "M", + "F", + "U" + ], + "nullable": true, + "description": "Sex as printed on the document: `M` (male), `F` (female), or `U` (unknown). Any other value — including `X` — returns `400`." }, - "key_people_checks": { - "type": "array", + "address": { + "type": "string", + "maxLength": 255, "nullable": true, - "description": "Aggregate key-people checks. Present only when `session_kind = business`. Each item uses the two-bucket shape (`registry`, `submitted`, `ubo_kyc_summary`).", - "items": { - "type": "object", - "description": "Aggregate check over the company's officers + UBOs. Uses the `registry` / `submitted` shape. Present only when session_kind = business.", - "properties": { - "status": { - "type": "string" - }, - "node_id": { - "type": "string", - "nullable": true - }, - "registry": { - "type": "object", - "description": "Parties discovered via the registry check.", - "properties": { - "officers": { - "type": "array", - "description": "Officers discovered via the registry check, each enriched with the status of any child KYC session and AML screening spawned for them.", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string", - "description": "Officer's full name as recorded in the registry." - }, - "designation": { - "type": "string", - "nullable": true, - "description": "Free-text designation from the registry (e.g. `Director`, `Company Secretary`)." - }, - "role": { - "type": "string", - "enum": [ - "director", - "non_executive_director", - "secretary", - "chairman", - "ubo", - "shareholder", - "representative", - "founder", - "legal_advisor", - "authorized_signatory", - "trustee", - "beneficiary", - "settlor", - "protector", - "company_officer", - "investor", - "other" - ], - "description": "Normalized officer role." - }, - "nationality": { - "type": "string", - "nullable": true - }, - "appointment_date": { - "type": "string", - "nullable": true, - "description": "Date the officer was appointed.", - "format": "date" - }, - "termination_date": { - "type": "string", - "nullable": true, - "description": "Date the appointment ended, when reported.", - "format": "date" - }, - "is_active": { - "type": "boolean" - }, - "address": { - "type": "string", - "nullable": true, - "description": "Officer address reported by the registry." - }, - "aml_status": { - "type": "string", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Resub Requested" - ], - "nullable": true, - "description": "Status of the AML screening run for this officer. `null` when no AML screening was run." - }, - "kyc_status": { - "type": "string", - "enum": [ - "Not Started", - "In Progress", - "Approved", - "Declined", - "In Review", - "Expired", - "Abandoned", - "Kyc Expired", - "Resubmitted", - "Awaiting User" - ], - "nullable": true, - "description": "Raw lifecycle status of the child KYC session spawned for this officer. This is the full session status enum (not a reduced Approved/Declined/Pending set). `null` when no child KYC session exists." - }, - "kyc_session_url": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "Shareable URL of the child KYC session. `null` when no child session exists." - }, - "kyc_session_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Session id of the child KYC session. Pass it to this same decision endpoint to fetch the person's full KYC report. `null` when no child session exists." - }, - "verification_workflow_type": { - "type": "string", - "nullable": true, - "description": "Base workflow type of the child session's workflow (e.g. `kyc`). `null` for graph-based workflows or when no child session exists." - }, - "verification_workflow_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Workflow id used by the child session. `null` when no child session exists." - }, - "verification_workflow_label": { - "type": "string", - "nullable": true, - "description": "Human-readable label of the child session's workflow. `null` when unset." - }, - "verification_workflow_features": { - "type": "array", - "nullable": true, - "description": "Per-feature progress of the child verification session, e.g. `[{\"feature\": \"ID_VERIFICATION\", \"status\": \"Approved\"}]`. `null` when the child session has no resolvable feature list.", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "description": "Feature display name (e.g. `ID_VERIFICATION`, `LIVENESS`, `AML`)." - }, - "status": { - "type": "string", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Resub Requested" - ], - "description": "Feature-level status inside the child session." - } - } - } - }, - "didit_internal_id": { - "type": "string", - "nullable": true, - "description": "Stable internal identifier of the verified individual behind the child session — useful for cross-session linking. `null` when no child session exists or the user has not been resolved yet." - }, - "kyc_document_type": { - "type": "string", - "nullable": true, - "description": "Document type presented in the child KYC session (e.g. `P`, `ID`, `DL`). `null` until the child session has an ID verification result." - }, - "kyc_nationality": { - "type": "string", - "nullable": true, - "description": "Nationality extracted from the document in the child KYC session. `null` until available." - }, - "kyc_liveness_passed": { - "type": "boolean", - "nullable": true, - "description": "Whether the child KYC session's liveness check passed. `null` when no liveness check has run." - }, - "kyc_verified_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the child KYC session reached `Approved`. `null` while the child session has any other status." - }, - "kyc_aml_categories": { - "type": "object", - "nullable": true, - "description": "Per-category summary derived from the person's AML hits. `null` when there are no hits.", - "properties": { - "sanctions": { - "type": "string", - "enum": [ - "match", - "clear" - ] - }, - "pep": { - "type": "string", - "enum": [ - "match", - "clear" - ] - }, - "adverse_media": { - "type": "string", - "enum": [ - "match", - "clear" - ] - }, - "warnings": { - "type": "string", - "enum": [ - "match", - "clear" - ] - } - } - }, - "kyc_notification_sent_at": { - "type": "string", - "nullable": true, - "description": "ISO 8601 timestamp of the last verification-invite notification sent for this person's child session. `null` when none was sent." - }, - "first_name": { - "type": "string", - "nullable": true, - "description": "Given name when explicitly reported by the registry; not inferred from the full name." - }, - "last_name": { - "type": "string", - "nullable": true, - "description": "Family name when explicitly reported by the registry." - }, - "date_of_birth": { - "type": "string", - "nullable": true, - "description": "Complete registry-reported birth date. Null when only a birth year or month is known.", - "format": "date" - }, - "date_of_birth_year": { - "type": "integer", - "nullable": true, - "minimum": 1, - "maximum": 9999, - "description": "Registry-reported birth year, including when the full date is unavailable." - }, - "date_of_birth_month": { - "type": "integer", - "nullable": true, - "minimum": 1, - "maximum": 12, - "description": "Registry-reported birth month. Null when only the year is known." - } - } - } - }, - "beneficial_owners": { - "type": "array", - "description": "Beneficial owners discovered via the registry check, each enriched with the status of any child KYC session and AML screening spawned for them.", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string", - "description": "Beneficial owner's full name (or company name for corporate UBOs)." - }, - "first_name": { - "type": "string", - "nullable": true - }, - "last_name": { - "type": "string", - "nullable": true - }, - "date_of_birth": { - "type": "string", - "nullable": true, - "description": "Complete birth date when reported.", - "format": "date" - }, - "country_of_birth": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string", - "nullable": true - }, - "company_name": { - "type": "string", - "nullable": true - }, - "registration_number": { - "type": "string", - "nullable": true - }, - "entity_type": { - "type": "string", - "enum": [ - "person", - "company" - ] - }, - "nationality": { - "type": "string", - "nullable": true - }, - "designation": { - "type": "string", - "nullable": true - }, - "roles": { - "type": "array", - "description": "Role names held by this owner, e.g. `[\"ubo\", \"shareholder\"]`.", - "items": { - "type": "string" - } - }, - "ownership_min_shares": { - "type": "string", - "nullable": true, - "description": "Lower bound of the ownership percentage as reported by the registry. Free-form string (e.g. `\"25\"`, `\"25-50%\"`); use `effective_ownership_percent` for a parsed numeric value." - }, - "ownership_max_shares": { - "type": "string", - "nullable": true, - "description": "Upper bound of the ownership percentage as reported by the registry. Free-form string; use `effective_ownership_percent` for a parsed numeric value." - }, - "is_active": { - "type": "boolean" - }, - "aml_status": { - "type": "string", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Resub Requested" - ], - "nullable": true, - "description": "Status of the AML screening run for this beneficial owner. `null` when no AML screening was run." - }, - "kyc_status": { - "type": "string", - "enum": [ - "Not Started", - "In Progress", - "Approved", - "Declined", - "In Review", - "Expired", - "Abandoned", - "Kyc Expired", - "Resubmitted", - "Awaiting User" - ], - "nullable": true, - "description": "Raw lifecycle status of the child KYC session spawned for this beneficial owner. This is the full session status enum (not a reduced Approved/Declined/Pending set). `null` when no child KYC session exists." - }, - "kyc_session_url": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "Shareable URL of the child KYC session. `null` when no child session exists." - }, - "kyc_session_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Session id of the child KYC session. Pass it to this same decision endpoint to fetch the person's full KYC report. `null` when no child session exists." - }, - "verification_workflow_type": { - "type": "string", - "nullable": true, - "description": "Base workflow type of the child session's workflow (e.g. `kyc`). `null` for graph-based workflows or when no child session exists." - }, - "verification_workflow_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Workflow id used by the child session. `null` when no child session exists." - }, - "verification_workflow_label": { - "type": "string", - "nullable": true, - "description": "Human-readable label of the child session's workflow. `null` when unset." - }, - "verification_workflow_features": { - "type": "array", - "nullable": true, - "description": "Per-feature progress of the child verification session, e.g. `[{\"feature\": \"ID_VERIFICATION\", \"status\": \"Approved\"}]`. `null` when the child session has no resolvable feature list.", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "description": "Feature display name (e.g. `ID_VERIFICATION`, `LIVENESS`, `AML`)." - }, - "status": { - "type": "string", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Resub Requested" - ], - "description": "Feature-level status inside the child session." - } - } - } - }, - "didit_internal_id": { - "type": "string", - "nullable": true, - "description": "Stable internal identifier of the verified individual behind the child session — useful for cross-session linking. `null` when no child session exists or the user has not been resolved yet." - }, - "kyc_document_type": { - "type": "string", - "nullable": true, - "description": "Document type presented in the child KYC session (e.g. `P`, `ID`, `DL`). `null` until the child session has an ID verification result." - }, - "kyc_nationality": { - "type": "string", - "nullable": true, - "description": "Nationality extracted from the document in the child KYC session. `null` until available." - }, - "kyc_liveness_passed": { - "type": "boolean", - "nullable": true, - "description": "Whether the child KYC session's liveness check passed. `null` when no liveness check has run." - }, - "kyc_verified_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the child KYC session reached `Approved`. `null` while the child session has any other status." - }, - "kyc_aml_categories": { - "type": "object", - "nullable": true, - "description": "Per-category summary derived from the person's AML hits. `null` when there are no hits.", - "properties": { - "sanctions": { - "type": "string", - "enum": [ - "match", - "clear" - ] - }, - "pep": { - "type": "string", - "enum": [ - "match", - "clear" - ] - }, - "adverse_media": { - "type": "string", - "enum": [ - "match", - "clear" - ] - }, - "warnings": { - "type": "string", - "enum": [ - "match", - "clear" - ] - } - } - }, - "kyc_notification_sent_at": { - "type": "string", - "nullable": true, - "description": "ISO 8601 timestamp of the last verification-invite notification sent for this person's child session. `null` when none was sent." - }, - "child_beneficial_owners": { - "type": "array", - "description": "For corporate UBOs: the owning entity's own beneficial owners, as raw objects extracted from the registry. Empty array when not applicable.", - "items": { - "type": "object" - } - }, - "effective_ownership_percent": { - "type": "number", - "nullable": true, - "description": "Parsed numeric ownership percentage — the max bound when available, otherwise the min bound. `null` when neither bound could be parsed." - }, - "voting_min_shares": { - "type": "string", - "nullable": true - }, - "voting_max_shares": { - "type": "string", - "nullable": true - }, - "appointment_date": { - "type": "string", - "nullable": true, - "description": "Appointment date when reported.", - "format": "date" - } - } - } - } - } - }, - "submitted": { - "type": "object", - "description": "Parties submitted by the business admin during the Key People flow (BusinessParty rows where source=USER).", - "properties": { - "parties": { - "type": "array", - "items": { - "type": "object", - "description": "Party submitted by the business admin via the Key People flow (source=USER).", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "entity_type": { - "type": "string", - "enum": [ - "person", - "company" - ] - }, - "source": { - "type": "string", - "enum": [ - "USER" - ], - "description": "Always `USER` here — this bucket only contains parties entered or confirmed by the end user in the Key People flow (registry-discovered people appear under `registry`)." - }, - "name": { - "type": "string" - }, - "first_name": { - "type": "string", - "nullable": true - }, - "last_name": { - "type": "string", - "nullable": true - }, - "company_name": { - "type": "string", - "nullable": true - }, - "registration_number": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string", - "format": "email", - "nullable": true - }, - "nationality": { - "type": "string", - "nullable": true, - "description": "Nationality for persons; country of incorporation for company parties." - }, - "phone_number": { - "type": "string", - "nullable": true - }, - "date_of_birth": { - "type": "string", - "format": "date", - "nullable": true - }, - "position": { - "type": "string", - "nullable": true, - "description": "Free-text job title / position (persons only)." - }, - "custom_fields": { - "type": "object", - "description": "Workflow-defined custom fields collected for this party; keys are defined by the workflow's key-people field descriptors." - }, - "is_active": { - "type": "boolean" - }, - "is_skipped": { - "type": "boolean", - "description": "True when the end user skipped verification for this party (only possible when the role allows skipping)." - }, - "requires_verification": { - "type": "boolean", - "description": "True when a child KYC / KYB sub-session is required for this party based on role settings and ownership thresholds." - }, - "roles": { - "type": "array", - "description": "All roles held by this party, each with its own ownership and voting percentages. There is no scalar `role`/`ownership_percent` on the party itself.", - "items": { - "type": "object", - "properties": { - "role": { - "type": "string", - "enum": [ - "director", - "non_executive_director", - "secretary", - "chairman", - "ubo", - "shareholder", - "representative", - "founder", - "legal_advisor", - "authorized_signatory", - "trustee", - "beneficiary", - "settlor", - "protector", - "company_officer", - "investor", - "other" - ] - }, - "ownership_percent": { - "type": "number", - "nullable": true - }, - "voting_percent": { - "type": "number", - "nullable": true - } - } - } - }, - "kyc_session_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Child KYC session id for person parties requiring verification. `null` otherwise." - }, - "kyc_session_status": { - "type": "string", - "enum": [ - "Not Started", - "In Progress", - "Approved", - "Declined", - "In Review", - "Expired", - "Abandoned", - "Kyc Expired", - "Resubmitted", - "Awaiting User" - ], - "nullable": true, - "description": "Lifecycle status of the child KYC session. `null` when no child KYC session exists." - }, - "kyc_session_url": { - "type": "string", - "format": "uri", - "nullable": true - }, - "kyb_sub_session_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Child KYB sub-session id for corporate parties requiring verification. `null` otherwise." - }, - "kyb_sub_session_status": { - "type": "string", - "enum": [ - "Not Started", - "In Progress", - "Approved", - "Declined", - "In Review", - "Expired", - "Abandoned", - "Kyc Expired", - "Resubmitted", - "Awaiting User" - ], - "nullable": true, - "description": "Lifecycle status of the child KYB sub-session. `null` when no sub-session exists." - }, - "kyb_sub_session_url": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "Shareable URL of the child KYB sub-session. `null` when no sub-session exists." - }, - "verification_workflow_type": { - "type": "string", - "nullable": true, - "description": "Base workflow type of the child session's workflow (KYC session preferred, falling back to the KYB sub-session). `null` for graph-based workflows or when no child session exists." - }, - "verification_workflow_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Workflow id used by the child session. `null` when no child session exists." - }, - "verification_workflow_label": { - "type": "string", - "nullable": true, - "description": "Human-readable label of the child session's workflow. `null` when unset." - }, - "verification_workflow_features": { - "type": "array", - "nullable": true, - "description": "Per-feature progress of the child verification session, e.g. `[{\"feature\": \"ID_VERIFICATION\", \"status\": \"Approved\"}]`. `null` when the child session has no resolvable feature list.", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "description": "Feature display name (e.g. `ID_VERIFICATION`, `LIVENESS`, `AML`)." - }, - "status": { - "type": "string", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Resub Requested" - ], - "description": "Feature-level status inside the child session." - } - } - } - }, - "didit_internal_id": { - "type": "string", - "nullable": true, - "description": "Stable internal identifier of the verified individual behind the child session. `null` when not resolved." - } - } - } - } - } - }, - "ubo_kyc_summary": { - "type": "object", - "nullable": true, - "description": "Aggregate UBO KYC progress, or null when no UBOs have linked KYC sessions.", - "properties": { - "total": { - "type": "integer" - }, - "approved": { - "type": "integer" - }, - "flagged": { - "type": "integer", - "description": "UBOs whose KYC is Declined or In Review." - }, - "pending": { - "type": "integer" - } - } - }, - "warnings": { - "type": "array", - "items": { - "type": "object" - } - } - } - } + "description": "Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it in the background of the same request and may overwrite `parsed_address` with the geocoder result." }, - "document_verifications": { - "type": "array", + "place_of_birth": { + "type": "string", + "maxLength": 255, "nullable": true, - "description": "Document verification checks grouped by workflow node. Present only when `session_kind = business`.", - "items": { - "type": "object", - "description": "Documents grouped by workflow node. Present only when session_kind = business.", - "properties": { - "status": { - "type": "string" - }, - "node_id": { - "type": "string", - "nullable": true - }, - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "document_type": { - "type": "string", - "enum": [ - "LEGAL_PRESENCE", - "COMPANY_DETAILS", - "OWNERSHIP_STRUCTURE", - "REPRESENTATIVES_AUTHORIZATION", - "OTHER" - ], - "nullable": true, - "description": "High-level KYB document group the file was classified into. `null` while unclassified." - }, - "document_subtype": { - "type": "string", - "nullable": true, - "description": "Specific OCR-detected subtype (e.g. `CERTIFICATE_OF_INCORPORATION`, `SHAREHOLDER_REGISTRY`); `UNKNOWN` when the OCR could not confidently classify the document." - }, - "document_group": { - "type": "string", - "enum": [ - "LEGAL_PRESENCE", - "COMPANY_DETAILS", - "OWNERSHIP_STRUCTURE", - "REPRESENTATIVES_AUTHORIZATION", - "OTHER" - ], - "nullable": true, - "description": "Alias of `document_type` — the group name used by `groups` and `required_groups`." - }, - "file_url": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "Short-lived presigned link to the uploaded file. `null` for analyst-created document request placeholders (`is_requested = true`) that have not been fulfilled yet." - }, - "original_filename": { - "type": "string", - "nullable": true, - "description": "Filename as uploaded by the user or analyst." - }, - "file_size": { - "type": "integer", - "nullable": true, - "description": "File size in bytes." - }, - "status": { - "type": "string", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Resub Requested" - ], - "description": "Per-document verification status." - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "cross_check_result": { - "type": "object", - "nullable": true, - "description": "Cross-reference of the OCR-extracted document fields against the registry company profile. An object with `critical`, `non_critical` and `people` sections, each mapping a field name (e.g. `company_name`, `registration_number`, `incorporation_date`, `registered_address`) to a comparison record like `{\"document\": \"...\", \"registry\": \"...\", \"match\": true, \"score\": 0.97}`. `null` when the document had no OCR data or there was no registry company to compare against." - }, - "document_metadata": { - "type": "object", - "nullable": true, - "description": "Metadata extracted from the uploaded corporate document. When metadata extraction succeeded, all 16 keys are always present (individual values are `null` when not applicable to the file type); the whole object is `null` when nothing could be extracted. For PDFs, `overlay_manipulation` carries forensic overlay-text analysis.", - "properties": { - "file_size": { - "type": "integer", - "nullable": true, - "description": "Size of the document." - }, - "content_type": { - "type": "string", - "nullable": true, - "description": "Content type of the document." - }, - "creation_date": { - "type": "string", - "nullable": true, - "description": "Date when the document was created." - }, - "modified_date": { - "type": "string", - "nullable": true, - "description": "Date when the document was modified." - }, - "creator": { - "type": "string", - "nullable": true, - "description": "Authoring tool/user recorded in the file metadata (PDF `Creator`)." - }, - "producer": { - "type": "string", - "nullable": true, - "description": "Software that produced the file (PDF `Producer`)." - }, - "software": { - "type": "string", - "nullable": true, - "description": "Editing software recorded in the file metadata." - }, - "encryption": { - "type": "string", - "nullable": true, - "description": "Encryption scheme of the file, when encrypted." - }, - "is_signed": { - "type": "boolean", - "nullable": true, - "description": "Whether the document carries a digital signature." - }, - "is_tampered": { - "type": "boolean", - "nullable": true, - "description": "Whether the digital signature failed validation (post-signing modification)." - }, - "signature_info": { - "type": "object", - "nullable": true, - "description": "Details of the digital signature(s), when present." - }, - "exif_original_date": { - "type": "string", - "nullable": true, - "description": "EXIF original capture date, for image uploads." - }, - "exif_digitized_date": { - "type": "string", - "nullable": true, - "description": "EXIF digitization date, for image uploads." - }, - "processed_by_known_editor": { - "type": "boolean", - "nullable": true, - "description": "Whether metadata indicates the file passed through a known image/PDF editor." - }, - "has_different_creation_mod_date": { - "type": "boolean", - "nullable": true, - "description": "Whether creation and modification dates differ — a possible (but not conclusive) editing signal." - }, - "overlay_manipulation": { - "type": "object", - "nullable": true, - "description": "PDF forensic analysis for suspected overlay-text manipulation. Null when no overlay manipulation evidence was found or the document was not a PDF.", - "properties": { - "detected": { - "type": "boolean", - "description": "Whether overlay-text manipulation evidence was detected." - }, - "analyzed": { - "type": "boolean", - "description": "Whether the PDF could be analyzed." - }, - "signals": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Forensic signals that fired, such as duplicate_font_subset or glyph_fragmentation." - }, - "manipulated_regions": { - "type": "array", - "description": "Suspected manipulated areas in PDF page coordinates.", - "items": { - "type": "object", - "properties": { - "page": { - "type": "integer" - }, - "x": { - "type": "number" - }, - "y": { - "type": "number" - }, - "width": { - "type": "number" - }, - "height": { - "type": "number" - }, - "page_width": { - "type": "number" - }, - "page_height": { - "type": "number" - } - } - } - } - } - } - } - }, - "description": { - "type": "string", - "nullable": true, - "description": "Analyst-provided label or reason for a document request. `null` for regular uploads." - }, - "is_requested": { - "type": "boolean", - "description": "True for analyst-created document request placeholders awaiting an upload." - }, - "ocr_data": { - "type": "object", - "nullable": true - }, - "is_console_upload": { - "type": "boolean", - "description": "True for documents uploaded from the console rather than the end-user flow. Console uploads never change the feature-level aggregate status." - }, - "is_confirmed": { - "type": "boolean", - "description": "Console uploads start `false` and flip `true` once the admin confirms them; end-user uploads are always `true`." - } - } - } - }, - "groups": { - "type": "object", - "description": "Per-group progress counters keyed by group name (e.g. legal_presence, ownership_structure).", - "additionalProperties": { - "type": "object", - "properties": { - "total": { - "type": "integer" - }, - "approved": { - "type": "integer" - }, - "pending": { - "type": "integer" - }, - "declined": { - "type": "integer" - }, - "in_review": { - "type": "integer" - }, - "other": { - "type": "integer" - }, - "missing": { - "type": "integer", - "description": "Requested placeholders with no file uploaded yet." - } - } - } - }, - "required_groups": { - "type": "array", - "items": { - "type": "string" - } - }, - "warnings": { - "type": "array", - "items": { - "type": "object" - } - } - } - } + "description": "Place of birth as printed on the document." }, - "environment": { + "nationality": { + "type": "string", + "maxLength": 32, + "nullable": true, + "description": "Nationality as printed on the document. Free text up to 32 characters — typically an ISO 3166-1 alpha-3 code (e.g. `ESP`), but not validated as one." + }, + "marital_status": { "type": "string", "enum": [ - "live", - "sandbox" + "SINGLE", + "MARRIED", + "DIVORCED", + "WIDOWED", + "UNKNOWN" ], - "description": "Whether the session was created by a live application (`live`) or a sandbox application (`sandbox`). Sandbox sessions are free, fully simulated, and never bill credits." + "description": "Marital status where the document carries it. Any other value returns `400`." }, - "resubmissions": { - "type": "array", - "description": "Returned only with `include=resubmissions`, for user and business sessions. One entry per resubmission request, oldest first. Each entry carries the results the resubmitted steps returned before the request, so earlier submissions stay readable. Deleting the session deletes every submission.", - "items": { - "type": "object", - "properties": { - "number": { - "type": "integer", - "description": "1 for the first resubmission of the session, 2 for the second, and so on." - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "When the resubmission was requested." - }, - "previous_status": { - "type": "string", - "description": "The session status just before the request." - }, - "nodes": { - "type": "array", - "description": "The workflow steps sent back to the user.", - "items": { - "type": "object", - "properties": { - "node_id": { - "type": "string" - }, - "feature": { - "type": "string" - } - } - } - }, - "reasons": { - "type": "object", - "nullable": true, - "additionalProperties": { - "type": "string" - }, - "description": "The reason given for each step, keyed by `node_id`." - }, - "requested_by": { - "type": "object", - "nullable": true, - "description": "Who requested the resubmission.", - "properties": { - "name": { - "type": "string" - }, - "email": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "results": { - "type": "object", - "additionalProperties": true, - "description": "What the resubmitted steps returned before the request, under the same keys and in the same shape as the current results of this response (`id_verifications`, `nfc_verifications`, `liveness_checks`, `face_matches`, `poa_verifications`, `phone_verifications`, `email_verifications`, `aml_screenings`, `ip_analyses`, `database_validations`, `questionnaire_responses`, `document_ai_documents`, `geolocation_checks`; for business sessions also `document_verifications`). Only the keys of the steps that were sent back appear." - } + "extra_fields": { + "type": "object", + "additionalProperties": true, + "nullable": true, + "description": "Merged view of the document's extra fields: the OCR-extracted values with all operator overrides applied. Keys removed via a `null` override are absent from this map." + }, + "parsed_address": { + "type": "object", + "nullable": true, + "description": "Current structured address (with internal bookkeeping keys stripped), or `null` when none exists. Includes the recomputed `formatted_address` alongside the seven editable keys.", + "properties": { + "address_type": { + "type": "string", + "nullable": true, + "description": "Free-form address classification (e.g. `home`)." + }, + "city": { + "type": "string", + "nullable": true, + "example": "Madrid" + }, + "region": { + "type": "string", + "nullable": true, + "description": "State / province / autonomous community." + }, + "street_1": { + "type": "string", + "nullable": true, + "example": "Avda de Madrid 34" + }, + "street_2": { + "type": "string", + "nullable": true + }, + "postal_code": { + "type": "string", + "nullable": true, + "example": "28013" + }, + "country": { + "type": "string", + "nullable": true, + "description": "ISO 3166-1 alpha-2 country code. Lowercase input is normalized to uppercase; alpha-3 codes are rejected with `400`.", + "example": "ES" + }, + "formatted_address": { + "type": "string", + "nullable": true, + "description": "Joined, human-readable form of the structured address. Recomputed server-side after every `parsed_address` edit.", + "example": "Avda de Madrid 34, Madrid, 28013, ES" } } } } + }, + "examples": { + "Updated KYC": { + "summary": "Full KYC record after the correction", + "value": { + "document_type": "P", + "document_subtype": null, + "document_number": "PAB123456", + "personal_number": "99999999R", + "date_of_birth": "1980-01-01", + "date_of_issue": "2021-02-09", + "expiration_date": "2031-02-08", + "issuing_state": "ESP", + "first_name": "Carmen", + "last_name": "Espanola Espanola", + "gender": "F", + "address": "AVDA DE MADRID 34, MADRID, MADRID 28013", + "place_of_birth": "MADRID", + "nationality": "ESP", + "marital_status": "MARRIED", + "extra_fields": { + "mrz_string": "P\": [\"\"]}`; errors on nested `parsed_address` / `extra_fields` keys use an object value: `{\"parsed_address\": {\"\": \"\"}}`.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string", - "example": "Authentication credentials were not provided or are invalid." + "examples": { + "Invalid gender": { + "summary": "gender must be M, F, or U", + "value": { + "gender": [ + "\"X\" is not a valid choice." + ] + } + }, + "Invalid document type": { + "summary": "Not a short code or long-form alias", + "value": { + "document_type": [ + "\"PASSPORT\" is not a valid choice." + ] + } + }, + "Invalid issuing state": { + "summary": "issuing_state is not ISO 3166-1 alpha-3", + "value": { + "issuing_state": [ + "Invalid ISO 3166-1 alpha-3 country code for issuing_state." + ] + } + }, + "Invalid marital status": { + "summary": "marital_status not in the enum", + "value": { + "marital_status": [ + "\"ENGAGED\" is not a valid choice." + ] + } + }, + "Bad date format": { + "summary": "Dates must be YYYY-MM-DD", + "value": { + "date_of_birth": [ + "Date has wrong format. Use one of these formats instead: YYYY-MM-DD." + ] + } + }, + "Unsupported parsed_address key": { + "summary": "Only the seven documented keys are allowed", + "value": { + "parsed_address": { + "zipcode": "Unsupported key. Allowed keys: address_type, city, region, street_1, street_2, postal_code, country" + } + } + }, + "Invalid parsed_address country": { + "summary": "Nested country must be alpha-2, not alpha-3", + "value": { + "parsed_address": { + "country": "Invalid ISO 3166-1 alpha-2 country code." + } + } + }, + "extra_fields not an object": { + "summary": "extra_fields must be a JSON object", + "value": { + "extra_fields": [ + "This field must be a JSON object." + ] } } - }, + } + } + } + }, + "403": { + "description": "Missing/invalid credentials or insufficient permissions. This API returns `403` for authentication failures — never `401`.", + "content": { + "application/json": { "examples": { - "Missing or invalid credentials": { - "summary": "No x-api-key header, or the key failed introspection", + "Invalid Credentials": { + "summary": "Missing or invalid x-api-key", "value": { "detail": "Authentication credentials were not provided or are invalid." } }, - "Insufficient permissions": { - "summary": "Valid credential lacking read:sessions or access to the owning application", + "Missing Privilege": { + "summary": "Credential lacks write:sessions", "value": { "detail": "You do not have permission to perform this action." } @@ -10211,29 +28547,32 @@ } }, "404": { - "description": "Session not found. Returned when no live session matches the supplied `sessionId` as either a User Verification (KYC) or a Business Verification (KYB) session — a session that has been deleted also lands here. Body is the standard `{\"detail\": ...}` envelope.\n\n**Two distinct `detail` strings.** If the `sessionId` is unknown as both a KYC and a KYB session, the pre-auth owner lookup fails and you receive `{\"detail\": \"Not found.\"}` — this happens before credential validation, so it is returned even for unauthenticated calls. If the session exists but cannot be retrieved under the authenticated application (for example a stale or just-deleted record), the view returns `{\"detail\": \"Session not found.\"}`. Both indicate the same outcome — the session is not accessible — so match on the `404` status code rather than the string.", + "description": "Unknown session, session in a non-editable state, or no KYC record to patch. Each case has a distinct `detail` message (see examples). The unknown-session lookup runs before credential checks.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string", - "example": "Not found." - } - } - }, "examples": { - "Unknown session id": { - "summary": "sessionId is unknown to this application", + "Unknown session": { + "summary": "No session with this ID in your application (or it was deleted)", "value": { "detail": "Not found." } }, - "Session not retrievable": { - "summary": "Session exists but is not retrievable for this application", + "Wrong session state": { + "summary": "Session exists but is not Approved / Declined / In Review / Kyc Expired", "value": { - "detail": "Session not found." + "detail": "No Session matches the given query." + } + }, + "No KYC record": { + "summary": "The workflow never ran an ID-verification step", + "value": { + "detail": "The session does not have a KYC." + } + }, + "No KYC for node": { + "summary": "node_id given but no KYC exists for it", + "value": { + "detail": "The session does not have a KYC for node 'feature_missing'." } } } @@ -10241,55 +28580,22 @@ } }, "429": { - "description": "Rate limit exceeded. GET requests are limited to **600 per minute per credential** (API key, Bearer token, or source IP when unauthenticated). Inspect `Retry-After` / `X-RateLimit-Reset` and back off before retrying. If you hit this limit while polling for decisions, switch to the `status.updated` webhook and fetch the decision once per status change instead.", - "headers": { - "Retry-After": { - "description": "Seconds to wait before retrying the request.", - "schema": { - "type": "integer", - "minimum": 1, - "example": 30 - } - }, - "X-RateLimit-Limit": { - "description": "Maximum number of requests allowed in the current window (600 for GET requests).", - "schema": { - "type": "integer", - "example": 600 - } - }, - "X-RateLimit-Remaining": { - "description": "Requests remaining in the current window.", - "schema": { - "type": "integer", - "example": 0 - } - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp (seconds) at which the current window resets.", - "schema": { - "type": "integer", - "example": 1750000000 - } - } - }, + "description": "Dedicated rate limit exceeded — 10 KYC data updates per minute per API key (rejected requests count toward the budget). The shared 300/min write budget can also trigger with its own message. Inspect `Retry-After` and the `X-RateLimit-*` response headers before retrying.", "content": { "application/json": { "schema": { "type": "object", "properties": { "detail": { - "type": "string", - "description": "Human-readable explanation of the rate-limit breach.", - "example": "GET request rate limit exceeded. You can make up to 600 requests per minute." + "type": "string" } } }, "examples": { - "Throttled": { - "summary": "More than 600 GET requests in one minute from the same credential", + "Rate limited": { + "summary": "KYC update budget exhausted", "value": { - "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + "detail": "Session KYC data update rate limit exceeded. You can make up to 10 requests per minute." } } } @@ -10297,82 +28603,164 @@ } } }, - "parameters": [ - { - "name": "sessionId", - "in": "path", - "required": true, - "description": "UUID of the verification session whose decision you want to retrieve. This is the `session_id` returned by `POST /v3/session/`. The same path works for both User Verification (KYC) and Business Verification (KYB) sessions — the server resolves the id against both session types.\n\n**Must be a valid UUID.** The route only matches well-formed UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{\"detail\": ...}` envelope shown below. Validate the UUID format client-side so you can distinguish an input error from a genuine missing-session `404`.", - "schema": { - "type": "string", - "format": "uuid", - "example": "11111111-2222-3333-4444-555555555555" - } - }, - { - "name": "include", - "in": "query", - "required": false, - "description": "Comma-separated list of extra payload sections. Supported values: `events` and `resubmissions`, alone or together (`events,resubmissions`). When `include=events` is passed, the response additionally contains the session event timeline (`events`, including the `WORKFLOW_BRANCH_EVALUATED` / `WORKFLOW_STATUS_NODE_REACHED` routing events described on the Retrieve Session page), the full activity log (`activity_reviews`), blocklist flags (`blocklisted`), a per-session `cost_breakdown`, workflow metadata (`workflow_type`, `workflow_version`, `workflow_version_id`, and `workflow_terminal_node_id` - the last recorded workflow status node that set a session status, or `null` if none has been recorded; historical provenance that is not cleared by manual approvals, declines or resubmissions, and is replaced by a later workflow status node), and session metadata (`api_service` — string or `null`, set only for standalone-API sessions; `session_type` — `API`, `HOSTED`, or `MIGRATED`; `has_device_nfc_support` — boolean). Two fields change relative to the plain shape: the top-level `session_kind` discriminator is **omitted** (the events shape only exists for user sessions), and the `features` array switches from plain strings to `{feature, node_id}` objects — where the email feature is reported as `EMAIL` instead of `EMAIL_VERIFICATION`. This expanded shape is primarily used by the Didit Console; most API integrations should omit the parameter. **KYB limitation:** `include=events` only applies to user (KYC) sessions. Business sessions (`session_kind = \"business\"`) always return the standard business decision shape — `events`, `activity_reviews`, `blocklisted`, `cost_breakdown` and the extra `workflow_*`/session metadata are never returned for business sessions, and their `features` array stays a plain string array. With `include=resubmissions` (user and business sessions alike) the response also contains `resubmissions`: one entry per resubmission request, oldest first, each with `number`, `created_at`, `previous_status`, `nodes`, `reasons`, `requested_by` and `results` (what the resubmitted steps returned before the request, under the same keys and in the same shape as the current results). Each `cost_breakdown` item carries `usage_type`, `price`, `details` and `created_at`: a resubmitted step that runs again is billed as a new item.", - "schema": { - "type": "string", - "enum": [ - "events", - "resubmissions", - "events,resubmissions" - ], - "example": "events" - } - } - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], "x-codeSamples": [ { "lang": "curl", - "label": "cURL", - "source": "curl -X GET 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/decision/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Accept: application/json'" + "label": "curl", + "source": "curl -X PATCH \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-data/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"first_name\": \"Carmen\",\n \"last_name\": \"Espanola Espanola\",\n \"date_of_birth\": \"1980-01-01\",\n \"issuing_state\": \"ESP\",\n \"parsed_address\": {\n \"street_1\": \"Avda de Madrid 34\",\n \"city\": \"Madrid\",\n \"postal_code\": \"28013\",\n \"country\": \"ES\"\n }\n }'" }, { "lang": "python", "label": "Python", - "source": "import requests\n\nsession_id = \"11111111-2222-3333-4444-555555555555\"\nurl = f\"https://verification.didit.me/v3/session/{session_id}/decision/\"\nheaders = {\n 'x-api-key': 'YOUR_API_KEY',\n \"Accept\": \"application/json\",\n}\n\nresponse = requests.get(url, headers=headers, timeout=15)\nresponse.raise_for_status()\ndecision = response.json()\n\nprint(\"session_kind:\", decision[\"session_kind\"])\nprint(\"top-level status:\", decision[\"status\"])\n\n# nfc_verifications is ALWAYS an array — never a singular `nfc` field.\nfor nfc in decision.get(\"nfc_verifications\") or []:\n print(\"nfc node_id:\", nfc[\"node_id\"], \"status:\", nfc[\"status\"])\n\nfor id_check in decision.get(\"id_verifications\") or []:\n print(\"id node_id:\", id_check[\"node_id\"], \"status:\", id_check[\"status\"])" + "source": "import requests\n\nresponse = requests.patch(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-data/\",\n headers={\n \"x-api-key\": \"YOUR_API_KEY\",\n \"Content-Type\": \"application/json\",\n },\n json={\n \"first_name\": \"Carmen\",\n \"last_name\": \"Espanola Espanola\",\n \"date_of_birth\": \"1980-01-01\",\n \"issuing_state\": \"ESP\",\n \"extra_fields\": {\"blood_group\": \"O+\"},\n \"parsed_address\": {\"city\": \"Madrid\", \"postal_code\": \"28013\", \"country\": \"ES\"},\n },\n)\nresponse.raise_for_status()\nkyc = response.json() # full current KYC record\nprint(kyc[\"first_name\"], kyc[\"parsed_address\"][\"formatted_address\"])" }, { "lang": "javascript", "label": "JavaScript", - "source": "const sessionId = '11111111-2222-3333-4444-555555555555';\nconst response = await fetch(\n `https://verification.didit.me/v3/session/${sessionId}/decision/`,\n {\n method: 'GET',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n Accept: 'application/json',\n },\n }\n);\n\nif (!response.ok) {\n throw new Error(`Decision fetch failed: ${response.status}`);\n}\nconst decision = await response.json();\n\nconsole.log('session_kind:', decision.session_kind);\nconsole.log('top-level status:', decision.status);\n\n// nfc_verifications is ALWAYS an array — never a singular `nfc` field.\nfor (const nfc of decision.nfc_verifications ?? []) {\n console.log('nfc node_id:', nfc.node_id, 'status:', nfc.status);\n}\n\nfor (const idCheck of decision.id_verifications ?? []) {\n console.log('id node_id:', idCheck.node_id, 'status:', idCheck.status);\n}" + "source": "const response = await fetch(\n 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-data/',\n {\n method: 'PATCH',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n first_name: 'Carmen',\n last_name: 'Espanola Espanola',\n date_of_birth: '1980-01-01',\n issuing_state: 'ESP',\n parsed_address: { city: 'Madrid', postal_code: '28013', country: 'ES' },\n }),\n },\n);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst kyc = await response.json(); // full current KYC record" } ] } }, - "/v3/session/{sessionId}/delete/": { - "delete": { - "summary": "Delete a verification session (KYC or KYB)", - "description": "Delete a single verification session, User Verification (KYC) or Business Verification (KYB), by its `session_id`. The id is resolved against KYC sessions first, then KYB sessions, so both kinds are deleted through this one URL.\n\n**What is deleted:**\n- The session and every verification attempt it holds, including results that a resubmission replaced: the decision, extracted data, all related feature records (ID verification, face, liveness, face match, AML, registry checks, IP analysis, and so on), its reviews and its activity log.\n- All stored files owned by the session: document front/back photos (full, cropped, and privacy-blurred variants), document-capture videos, portrait crops, NFC chip portrait and signature images, face reference images, liveness videos, face-match source/target images, Proof of Address documents, and any extra uploaded files (KYC); uploaded company documents and extra files (KYB). Previously issued media URLs (`https:///...`) stop resolving.\n- The session's face data. Its face embedding is deleted too, unless you choose to keep an image-free template (see **Face biometric data**).\n- Webhook deliveries still queued for the session, and their retries. Webhooks already delivered are not affected.\n- For a Business Verification (KYB) session: the verifications it created for its key people (KYC) and its corporate owners (KYB), recursively, reported as `deleted_child_verifications`. A verification it only links to, such as an existing approved KYC it reused, is not deleted.\n\nThe session disappears from every read endpoint immediately: `GET /v3/session/{sessionId}/decision/` returns `404` and `GET /v3/sessions/` stops listing it. Ongoing AML monitoring and KYB registry monitoring started by the deleted verifications stop.\n\n**What is not affected:** blocklist entries created from this session (face or document) are **not** removed; manage those with the blocklist endpoints. Share tokens issued for the session are not revoked, but there is nothing left for them to import: an import with one returns `400` with `{\"share_token\": [\"Original session does not exist.\"]}`. Credits already consumed are **not** refunded. The parent User or Business entity is **not** deleted; use `POST /v3/users/delete/` or `POST /v3/businesses/delete/` for those. If the User's profile portrait came from this session, the portrait is removed with it.\n\n**Face biometric data:** by default the session's face embedding is deleted with the session (`face_retention_outcome: deleted`). An application that opted in to biometric-template retention (`face_retention_policy: retain_until_user_deleted` on `PATCH /v3/webhook/`), or a request with `retain_face_embeddings: true`, keeps one separately managed, image-free face biometric template anchored to the session's User (`retained_with_user`) so that duplicate detection, Face Search, and biometric authentication keep working. The template never contains the face image, media, identity fields, the decision, or the deleted session's id; it is biometric data deleted at `biometric_template_expires_at`, or earlier on User deletion, privacy erasure, or an explicit purge through `/v3/biometric-templates/`. A session without a linked User cannot retain a template (`ineligible_no_vendor_user`). Send `deletion_instruction: privacy_erasure` for data-subject requests: it purges every retained template for that User and never retains a new one.\n\n**Irreversible:** deletion is permanent and there is no restore endpoint. Export anything you need (for example a decision PDF) before you call it.\n\n**Side effects:** no webhook is emitted for deletions.\n\n**Response:** `200 OK` once the deletion has completed, with a JSON body reporting the `face_retention_outcome`, the retained template's expiry and, for a KYB session, `deleted_child_verifications`. Earlier versions of this endpoint returned `204 No Content`; update clients that assert on `204`. Keep your `instruction_id` in your own erasure log.\n\n**Idempotency:** not idempotent at the HTTP level: the first call returns `200`; repeating it returns `404` because the session no longer resolves. After a `503` the session is still in place; repeat the same request to finish deleting it.\n\n**Authentication:** send your application's API key in the `x-api-key` header; the session must belong to that application. Console user access tokens (`Authorization: Bearer ...`) may also call this endpoint when they carry the `delete:sessions` permission. Authentication and permission failures both return `403`; this API never returns `401`.\n\n**Rate limit:** shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.\n\nTo bulk-delete KYC sessions by their numeric `session_number`, use `POST /v3/sessions/delete/` instead.", - "operationId": "delete_v3_session_by_id", + "/v3/session/{sessionId}/features/{nodeId}/update-status/": { + "patch": { + "summary": "Update an individual feature (step) status", + "description": "Set the status of a single verification feature (step) within a session. Target the feature by its workflow graph `nodeId`. On non-graph workflows, whose steps have no node, use `default` and also pass `feature_type` to say which step you mean.\n\nWorks for both User Verification (KYC) and Business Verification (KYB) sessions and every step type: ID/OCR, Liveness, Face Match, NFC, AML, Proof of Address, Phone, Email, Database Validation, Questionnaire, and the KYB registry / documents / key-people checks. This endpoint is **not** available for standalone API sessions (`session_type = API`).\n\n`new_status` must be one of `Approved`, `Declined`, or `In Review`, and must differ from the feature's current status.\n\n**When the override applies.** Without `defer_until_flow_finishes` (or with `false`) the override applies at once, as it always did; when the person is still in the flow it is recorded as `forced`. With `defer_until_flow_finishes: true` while the person is still in the flow (the session is `Not Started`, `In Progress` or `Resubmitted` and has not finished), the override is stored as `pending_until_flow_finishes` and applies when the flow finishes, after the flow reaches its own decision. On a session whose flow already ended it applies at once either way. A waiting override is cancelled instead when the person redoes its step before finishing, when a newer override on the same check replaces it, when the session leaves the flow unfinished (abandoned, expired, or decided mid-flow), or when you cancel it with `POST /v3/session/{sessionId}/features/overrides/{overrideId}/cancel/`. List a session's overrides with `GET /v3/session/{sessionId}/features/overrides/`.\n\n**Effect on the session.** A session that already has a decision keeps it: only the check changes. When the session has no decision yet and its workflow is waiting on exactly this step, applying the override completes the step: the workflow moves on, later steps run, and a status node can set the session decision and fire `status.updated`.\n\n**Webhooks and activity.** A `data.updated` webhook fires when the override applies (at once, or when the flow finishes), never while it waits or when it is cancelled. Every override is recorded in the session's activity as `FEATURE_OVERRIDE_PENDING`, `FEATURE_OVERRIDE_APPLIED` or `FEATURE_OVERRIDE_CANCELLED`, with who made it; the request's entry carries your `comment`.\n\nAuthenticate with your API key (`x-api-key`) or a user authorization header carrying the `write:sessions` privilege; a scoped API key needs write access to decisions.", + "operationId": "patch_v3_session_update_feature_status", + "x-didit-sdk": { + "resource": "sessions", + "method": "update_feature_status", + "auth": "api_key", + "server": true + }, "tags": [ "Sessions" ], + "security": [ + { + "ApiKeyAuth": [] + } + ], "parameters": [ { "in": "path", "name": "sessionId", "required": true, - "description": "UUID (`session_id`) of the User Verification (KYC) or Business Verification (KYB) session to delete, as returned when the session was created. Must be a canonical hyphenated UUID — a non-UUID value does not match the route and returns `404`.", + "description": "UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs — the service resolves the ID across both session types.", "schema": { "type": "string", "format": "uuid", "example": "11111111-2222-3333-4444-555555555555" } + }, + { + "in": "path", + "name": "nodeId", + "required": true, + "description": "Workflow graph node ID of the feature to update (for example `feature_ocr_1`). Use the literal value `default` for non-graph (single-step) workflows where the feature has no node ID.", + "schema": { + "type": "string", + "example": "feature_ocr_1" + } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "new_status" + ], + "properties": { + "new_status": { + "type": "string", + "description": "Target status for this feature. Must differ from the current value.", + "enum": [ + "Approved", + "Declined", + "In Review" + ], + "example": "Approved" + }, + "comment": { + "type": "string", + "description": "Optional free-text note recorded on the session's review trail for this feature change.", + "example": "Document manually verified by compliance." + }, + "feature_type": { + "type": "string", + "enum": [ + "ID_VERIFICATION", + "OCR", + "OCR_BACK", + "NFC", + "LIVENESS", + "FACE", + "AGE_ESTIMATION", + "AGE_VERIFICATION", + "FACE_SEARCH", + "FACE_MATCH", + "IP_ANALYSIS", + "AML", + "PHONE", + "PHONE_VERIFICATION", + "EMAIL", + "EMAIL_VERIFICATION", + "POA", + "PROOF_OF_ADDRESS", + "DATABASE_VALIDATION", + "DATA_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + "BANK_VERIFICATION", + "GEOLOCATION" + ], + "description": "Which kind of step to target. Needed on non-graph workflows (`nodeId` = `default`), where every step shares no node; optional otherwise. An unknown value returns `400`.", + "example": "LIVENESS" + }, + "defer_until_flow_finishes": { + "type": "boolean", + "default": false, + "description": "While the person is still in the flow, store the override as `pending_until_flow_finishes` and apply it when the flow finishes, instead of at once. Omitted or `false`, the override applies at once. On a session whose flow already ended it applies at once either way.", + "example": false + } + } + }, + "examples": { + "Apply Now": { + "summary": "Approve the ID step at once", + "value": { + "new_status": "Approved", + "comment": "Document manually verified by compliance." + } + }, + "Wait For The Flow": { + "summary": "Approve the liveness step once the person finishes the flow", + "value": { + "new_status": "Approved", + "comment": "Liveness retried with a reviewer present.", + "defer_until_flow_finishes": true + } + }, + "Non-graph Workflow": { + "summary": "Target the liveness step of a workflow without nodes (nodeId = default)", + "value": { + "new_status": "In Review", + "feature_type": "LIVENESS" + } + } + } + } + } + }, "responses": { "200": { - "description": "Session deleted. The body reports what happened to the session's face biometric data and, for a KYB session, how many verifications it created were deleted with it.", + "description": "The override was recorded. `new_status` is the check's status as stored: the requested one once the override applied, the unchanged one while it waits for the flow to finish. `override` says which happened.", "content": { "application/json": { "schema": { @@ -10380,209 +28768,187 @@ "properties": { "session_id": { "type": "string", - "format": "uuid", - "description": "The deleted session." - }, - "session_number": { - "type": "integer", - "nullable": true, - "description": "The deleted session's number." + "format": "uuid" }, - "face_retention_outcome": { + "node_id": { "type": "string", - "enum": [ - "retained_with_user", - "deleted", - "none", - "ineligible_no_vendor_user" - ], - "description": "What happened to the session's face biometric data. `deleted`: the face embedding was deleted with the session (default). `retained_with_user`: the session is deleted and one image-free biometric template stays anchored to the User; see `biometric_template_uuid`. `none`: the session had no face embedding (for example a KYB session, or a KYC session without a liveness selfie). `ineligible_no_vendor_user`: retention was requested but the session has no linked User, so the embedding was deleted with the session. A biometric-store failure is never reported through this field on this endpoint: it returns `503` and the session is not deleted." + "nullable": true, + "description": "The updated step's workflow node, or `null` for a step without a node (the `default` path in a non-graph workflow).", + "example": "feature_ocr_1" }, - "biometric_template_uuid": { + "feature_type": { "type": "string", - "format": "uuid", - "nullable": true, - "description": "Id of the retained biometric template when `face_retention_outcome` is `retained_with_user`; `null` otherwise. Manage it through `/v3/biometric-templates/{template_uuid}/`." + "description": "Model name of the updated feature (e.g. `KYC`, `Face`, `FaceMatch`, `AML`, `POA`).", + "example": "KYC" }, - "biometric_template_expires_at": { + "new_status": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the retained biometric template is deleted, when `face_retention_outcome` is `retained_with_user`; `null` when no template is retained." + "description": "The check's stored status: the requested one once the override applied, the unchanged one while the override waits for the flow to finish.", + "example": "Approved" }, - "deleted_child_verifications": { - "type": "integer", - "description": "For a Business Verification (KYB) session: how many verifications it created (its key people's KYC sessions and its corporate owners' KYB sessions, recursively) were deleted with it. Always `0` for a KYC session." + "override": { + "$ref": "#/components/schemas/FeatureStatusOverride" } - } + }, + "required": [ + "session_id", + "node_id", + "feature_type", + "new_status", + "override" + ] }, "examples": { - "Deleted (default)": { - "value": { - "session_id": "4c5c7f3a-3b1d-4b7e-9c39-2b4f7e6a1d02", - "session_number": 1024, - "face_retention_outcome": "deleted", - "biometric_template_uuid": null, - "biometric_template_expires_at": null, - "deleted_child_verifications": 0 - } - }, - "Retained with User": { - "value": { - "session_id": "4c5c7f3a-3b1d-4b7e-9c39-2b4f7e6a1d02", - "session_number": 1024, - "face_retention_outcome": "retained_with_user", - "biometric_template_uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", - "biometric_template_expires_at": "2027-10-09T10:15:00Z", - "deleted_child_verifications": 0 + "Applied": { + "summary": "The override applied at once", + "value": { + "session_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "node_id": "feature_ocr_1", + "feature_type": "KYC", + "new_status": "Approved", + "override": { + "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "node_id": "feature_ocr_1", + "feature_type": "", + "feature_model": "KYC", + "previous_status": "In Review", + "new_status": "Approved", + "state": "applied", + "forced": false, + "reason": "Document manually verified by compliance.", + "requested_by": { + "email": "reviewer@example.com", + "name": "Alex Reviewer", + "type": "CONSOLE_USER" + }, + "requested_at": "2026-10-13T09:15:00Z", + "applied_at": "2026-10-13T09:15:00Z", + "cancelled_at": null, + "cancel_reason": null + } } }, - "Ineligible: no User": { - "summary": "Retention requested on a session created without vendor_data", - "value": { - "session_id": "7a1e9c2d-5f4b-4c3a-8e2d-1b0a9f8e7d6c", - "session_number": 1031, - "face_retention_outcome": "ineligible_no_vendor_user", - "biometric_template_uuid": null, - "biometric_template_expires_at": null, - "deleted_child_verifications": 0 + "Waiting For The Flow": { + "summary": "defer_until_flow_finishes while the person is still in the flow", + "value": { + "session_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "node_id": "feature_liveness_1", + "feature_type": "Face", + "new_status": "Declined", + "override": { + "id": "9b2f4c1e-8d3a-4e5f-a6b7-c8d9e0f1a2b3", + "node_id": "feature_liveness_1", + "feature_type": "", + "feature_model": "Face", + "previous_status": "Declined", + "new_status": "Approved", + "state": "pending_until_flow_finishes", + "forced": false, + "reason": "Liveness retried with a reviewer present.", + "requested_by": { + "email": "reviewer@example.com", + "name": "Alex Reviewer", + "type": "CONSOLE_USER" + }, + "requested_at": "2026-10-13T09:15:00Z", + "applied_at": null, + "cancelled_at": null, + "cancel_reason": null + } } - }, - "KYB session": { - "value": { - "session_id": "0f3c9b7e-2d1a-4e5f-8c6b-7a9d8e1f2c3b", - "session_number": 88, - "face_retention_outcome": "none", - "biometric_template_uuid": null, - "biometric_template_expires_at": null, - "deleted_child_verifications": 3 - }, - "summary": "A business verification deleted with the three verifications it created for its key people" } } } } }, "400": { - "description": "Validation error — nothing is deleted.", + "description": "Validation error. Returned when `new_status` is invalid or unchanged, when `feature_type` is unknown, or when the session is a standalone API session.", "content": { "application/json": { "examples": { - "Privacy erasure cannot retain": { + "Same Status": { + "summary": "new_status equals the current status", "value": { - "retain_face_embeddings": [ - "Privacy erasure cannot retain biometric templates." + "new_status": [ + "The new status is the same as the current status." ] } }, - "No finite retention duration": { - "summary": "Retention requested without face_retention_days on the request or the application", + "API Session": { + "summary": "Feature status cannot be changed on standalone API sessions", + "value": [ + "Cannot change feature status for API sessions." + ] + }, + "Unknown feature_type": { + "summary": "feature_type is not a known step type", "value": { - "detail": "A finite biometric-template retention duration is required." + "feature_type": [ + "Unknown feature_type 'SELFIE'." + ] } } } } } }, - "403": { - "description": "Authentication or permission failure. Missing/invalid credentials also return `403` — this API never returns `401`. Exception: when the `sessionId` does not resolve to a live session, the pre-auth owner lookup returns `404` first, even with missing or invalid credentials.", + "401": { + "description": "The scoped API key has expired.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } - }, "examples": { - "Missing or invalid API key": { - "summary": "No x-api-key header, or the key is invalid", - "value": { - "detail": "Authentication credentials were not provided or are invalid." - } - }, - "Insufficient permission": { - "summary": "User token without delete:sessions, or credentials for an application that does not own the session", + "Expired API Key": { + "summary": "Scoped API key past its expiry date", "value": { - "detail": "You do not have permission to perform this action." + "detail": "This API key has expired." } } } } } }, - "404": { - "description": "No live KYC or KYB session with this `session_id` exists — the id is unknown or the session was already deleted (repeating a successful delete lands here).", + "403": { + "description": "Missing/invalid credentials or insufficient permissions.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } - }, "examples": { - "Not Found": { - "summary": "Unknown or already-deleted session", + "Invalid Credentials": { + "summary": "Missing or invalid x-api-key", "value": { - "detail": "Not found." - } - } - } - } - } - }, - "429": { - "description": "Shared write rate limit exceeded (300 POST/PATCH/DELETE requests per minute per API key). Inspect `Retry-After` and the `X-RateLimit-*` response headers before retrying.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" + "detail": "Authentication credentials were not provided or are invalid." } - } - }, - "examples": { - "Rate limited": { - "summary": "Write budget exhausted", + }, + "Missing Privilege": { + "summary": "API key lacks write:sessions", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped Key Without Access": { + "summary": "Scoped API key without write access to decisions", "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + "detail": "This API key has no write access to decisions." } } } } } }, - "503": { - "description": "The deletion could not finish because a storage service, a biometric store, or a monitoring provider was unavailable. The session is still in place; repeat the same request until it returns `200`. Some of its stored files or its face embedding may already be deleted, so do not rely on them after a `503`. Retries converge on a single template.", + "404": { + "description": "The session was not found, or it has no feature with the given `nodeId`.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } - }, "examples": { - "Session deletion unavailable": { - "summary": "A storage service or a monitoring provider was unavailable", + "Session Not Found": { + "summary": "Unknown session_id", "value": { - "detail": "The session could not be deleted because a storage service or a monitoring provider is unavailable. The session is still in place; repeat the request to finish deleting it." + "detail": "Session not found." } }, - "Biometric store unavailable": { - "summary": "A biometric store was unavailable while retaining or purging a template", + "Feature Not Found": { + "summary": "No feature for this node_id", "value": { - "detail": "The biometric-template operation could not finish because an external store is unavailable. The operation is retryable." + "detail": "No feature found with node_id 'feature_ocr_1' in this session." } } } @@ -10590,336 +28956,430 @@ } } }, - "security": [ - { - "ApiKeyAuth": [] - } - ], "x-codeSamples": [ { "lang": "curl", - "label": "curl (default policy)", - "source": "curl -X DELETE \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/delete/ \\\n -H 'x-api-key: YOUR_API_KEY'" - }, - { - "lang": "curl", - "label": "curl (retain template)", - "source": "curl -X DELETE \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/delete/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"retain_face_embeddings\": true, \"face_retention_days\": 365}'" - }, - { - "lang": "curl", - "label": "curl (privacy erasure)", - "source": "curl -X DELETE \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/delete/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"deletion_instruction\": \"privacy_erasure\", \"instruction_id\": \"dsar-2026-0142\"}'" + "label": "curl", + "source": "curl -X PATCH \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/features/feature_ocr_1/update-status/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"new_status\": \"Approved\",\n \"comment\": \"Document manually verified by compliance.\"\n }'" }, { "lang": "python", "label": "Python", - "source": "import requests\n\nresponse = requests.delete(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/delete/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n json={\"retain_face_embeddings\": True, \"face_retention_days\": 365},\n)\nresponse.raise_for_status()\nprint(response.json()[\"face_retention_outcome\"])" + "source": "import requests\n\nresponse = requests.patch(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/features/feature_ocr_1/update-status/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\", \"Content-Type\": \"application/json\"},\n json={\"new_status\": \"Approved\", \"comment\": \"Document manually verified by compliance.\"},\n)\nprint(response.json())" + } + ] + } + }, + "/v3/session/{sessionId}/update-aml-hit-status/": { + "patch": { + "summary": "Update an AML hit review status", + "description": "Set the review status of a **single AML hit** (a potential sanction / PEP / watchlist / adverse-media match) returned by AML screening, without changing the overall session decision. Identify the hit by its `hit_id` (the `id` of an entry in the AML check's `hits` array — see the session decision).\n\n`review_status` is one of `Unreviewed`, `Confirmed Match`, `False Positive`, or `Inconclusive`. Reviewer decisions are preserved across ongoing-monitoring updates, so a hit you mark `False Positive` stays that way when the provider re-screens.\n\nA successful call fires a `data.updated` webhook and appends an activity entry (with actor attribution) to the session's review trail. For graph workflows with multiple AML checks, pass `node_id` to target a specific check.\n\nAuthenticate with your API key (`x-api-key`) or a user authorization header — either must carry the `write:sessions` privilege.", + "operationId": "patch_v3_session_update_aml_hit_status", + "x-didit-sdk": { + "resource": "sessions", + "method": "update_aml_hit_status", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sessionId", + "required": true, + "description": "UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs — the service resolves the ID across both session types.", + "schema": { + "type": "string", + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch(\n 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/delete/',\n {\n method: 'DELETE',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ deletion_instruction: 'privacy_erasure', instruction_id: 'dsar-2026-0142' }),\n },\n);\nconst body = await resp.json();\nconsole.log(body.face_retention_outcome);" + "in": "query", + "name": "node_id", + "required": false, + "description": "Workflow graph node ID. Required only when the session has multiple AML checks (one per node); omit it for single-AML sessions.", + "schema": { + "type": "string", + "example": "feature_aml_1" + } } ], "requestBody": { - "required": false, - "description": "Optional. Omit the body entirely to follow the application's retention policy with an operational deletion.", + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "hit_id", + "review_status" + ], "properties": { - "retain_face_embeddings": { - "type": "boolean", - "nullable": true, - "description": "Override the application's `face_retention_policy` for this deletion. `true` keeps one image-free face biometric template anchored to the session's User after the session is deleted; `false` deletes the face embedding with the session; omit or `null` to follow the application policy. Ignored for KYB sessions and for sessions without a face embedding.", - "example": true - }, - "face_retention_days": { - "type": "integer", - "minimum": 1, - "maximum": 3650, - "description": "Finite retention duration, in days, for a retained template. Required when a template is retained unless the application already sets `face_retention_days`. The template's `expires_at` is the earliest of this duration, the application's general data-retention window, and `face_retention_deadline`.", - "example": 365 - }, - "face_retention_deadline": { + "hit_id": { "type": "string", - "format": "date-time", - "description": "Optional hard expiry for a retained template. Must be in the future.", - "example": "2027-08-28T00:00:00Z" + "description": "The `id` of the hit in the AML check's `hits` array.", + "example": "abc123" }, - "deletion_instruction": { + "review_status": { "type": "string", + "description": "The new review status for the hit.", "enum": [ - "operational_session_delete", - "privacy_erasure" + "Unreviewed", + "Confirmed Match", + "False Positive", + "Inconclusive" ], - "default": "operational_session_delete", - "description": "The class of instruction you are giving. `operational_session_delete` deletes the session and retains a template only when the policy or `retain_face_embeddings` says so. `privacy_erasure` purges every retained biometric template anchored to the session's User before deleting the session, cannot be overridden by the application policy, and cannot be combined with `retain_face_embeddings: true` (`400`).", - "example": "operational_session_delete" + "example": "False Positive" }, - "instruction_id": { + "node_id": { "type": "string", - "maxLength": 255, - "description": "Your durable reference for this deletion instruction (for example an erasure-ticket id). Recorded on the audit trail and on any retained template. Generated by Didit when omitted.", - "example": "dsar-2026-0142" - } - } - }, - "examples": { - "Retain a biometric template": { - "summary": "Delete the session, keep an image-free template for 365 days", - "value": { - "retain_face_embeddings": true, - "face_retention_days": 365, - "instruction_id": "post-approval-cleanup-2026-08" - } - }, - "Force deletion on an opted-in application": { - "summary": "Delete the face embedding regardless of the application policy", - "value": { - "retain_face_embeddings": false - } - }, - "Privacy erasure": { - "summary": "Data-subject request: delete the session and purge every retained template for that person", - "value": { - "deletion_instruction": "privacy_erasure", - "instruction_id": "dsar-2026-0142" + "description": "Optional graph node ID. Use it (here or as a query parameter) when the session has multiple AML checks.", + "example": "feature_aml_1" } } } } } - } - } - }, - "/v3/session/{sessionId}/generate-pdf/": { - "get": { - "summary": "Download a PDF report for a verification session", - "description": "Render and download a compliance-ready PDF report for a finished verification session. The same path serves both User Verification (KYC) and Business Verification (KYB) sessions — the server resolves the `sessionId` against both session types and picks the matching report template.\n\n**Eligible statuses.** The session must already carry a final or reviewable status:\n\n- KYC sessions: `Approved`, `Declined`, `In Review`, or `Kyc Expired`.\n- KYB sessions: `Approved`, `Declined`, or `In Review`.\n\nAny other status (`Not Started`, `In Progress`, `Expired`, `Abandoned`, …) returns `403` with an explanatory `detail` string — see the `403` response below.\n\n**What the report contains.** The PDF mirrors the console session view at the moment of the request:\n\n- **KYC** — rolled-up session status, the warnings list, and one section per executed feature: ID Verification (document images plus extracted fields), NFC, Liveness, Face Match, Email Verification, Phone Verification, Proof of Address, Questionnaire answers, Database Validation, AML Screening, and IP Analysis — followed by session tags, console review activity (comments and status changes), and the session event timeline.\n- **KYB** — registry checks (company data), AML screenings, business document verifications, key-people checks, questionnaire responses, phone/email verifications, IP analyses, and the session event timeline, followed by tags and review activity.\n\n**Branding.** If your application has white-label customization, the report is rendered with your logo and links to your privacy-policy URL; otherwise it carries Didit branding. Reports are rendered in English — there is no language parameter.\n\n**No caching.** Every call re-renders the PDF from the current session data, so a report generated after a manual review reflects the reviewer’s decision. Two calls for the same session can produce byte-different files — archive the downloaded file if you need an immutable copy.\n\n**Latency.** Rendering downloads every stored image (document sides, selfies, extra files) before composing the PDF, so media-heavy sessions can take several seconds. Use a generous client read timeout (60 s recommended) and stream the body to disk.\n\n**Trailing slash.** The canonical route ends with a trailing slash (`…/generate-pdf/`). A request without it is served directly with the same response — there is no `301` redirect, so no client has to follow redirects and `curl` does not need `-L`. Use the slashed URL as in the samples.\n\nIf you need the underlying data as JSON instead of a rendered report, use `GET /v3/session/{sessionId}/decision/`. For every reportable session of one user in a single document, use `GET /v3/users/{vendor_data}/generate-pdf/`.", - "operationId": "get_v3_session_generate_pdf", - "tags": [ - "Sessions" - ], - "parameters": [ - { - "name": "sessionId", - "in": "path", - "required": true, - "description": "UUID of the User Verification (KYC) or Business Verification (KYB) session to render — the `session_id` returned by `POST /v3/session/`. The same path works for both session kinds; use the `Content-Disposition` filename on the response to tell which report type you received.\n\n**Must be a well-formed, lowercase UUID.** The route only matches valid UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{\"detail\": ...}` envelope shown below. The route's UUID converter only matches the canonical lowercase form — uppercase-hex UUIDs are rejected with the HTML 404.", - "schema": { - "type": "string", - "format": "uuid", - "example": "11111111-2222-3333-4444-555555555555" - } - } - ], + }, "responses": { "200": { - "description": "The rendered PDF document, returned directly as binary `application/pdf` — there is no JSON wrapper and no download-URL indirection. The body starts with the `%PDF` magic bytes (PDF 1.7). Save it to a `.pdf` file or stream it through to your caller.", - "headers": { - "Content-Disposition": { - "description": "`attachment; filename=session_{sessionId}.pdf` for KYC sessions, `attachment; filename=business_{sessionId}.pdf` for KYB sessions — the prefix tells you which report template was rendered.", - "schema": { - "type": "string", - "example": "attachment; filename=session_11111111-2222-3333-4444-555555555555.pdf" - } - }, - "Content-Length": { - "description": "Total size of the PDF in bytes; the body is not chunked.", + "description": "Hit review status updated.", + "content": { + "application/json": { "schema": { - "type": "integer", - "example": 25191 + "type": "object", + "required": [ + "message", + "hit_id", + "review_status" + ], + "properties": { + "message": { + "type": "string", + "description": "Always `Hit status updated successfully`." + }, + "hit_id": { + "type": "string", + "description": "The updated hit, with surrounding whitespace removed." + }, + "review_status": { + "type": "string", + "enum": [ + "Unreviewed", + "Confirmed Match", + "False Positive", + "Inconclusive" + ], + "description": "The review status now stored on the hit." + } + } + }, + "example": { + "message": "Hit status updated successfully", + "hit_id": "abc123", + "review_status": "False Positive" } } - }, + } + }, + "400": { + "description": "Validation error — `hit_id` empty or `review_status` not one of the allowed values.", "content": { - "application/pdf": { - "schema": { - "type": "string", - "format": "binary" + "application/json": { + "example": { + "review_status": [ + "\"Maybe\" is not a valid choice." + ] } } } }, "403": { - "description": "Authentication, authorization, or session-status failure. **This endpoint never returns `401`** — Didit's API-key authentication does not emit a `WWW-Authenticate` challenge, so missing or invalid credentials also surface as `403`. Distinguish the failure modes by the `detail` string:\n\n- `\"Authentication credentials were not provided or are invalid.\"` — no `x-api-key` header (and no `Authorization: Bearer` token), or the supplied credential failed introspection (revoked, expired, or malformed).\n- `\"You do not have permission to perform this action.\"` — the credential is valid but cannot read this session: a console-user Bearer token lacks the `read:sessions` permission or cannot access the owning application, or an API key belongs to a different organization than the session.\n- `\"You can only generate a PDF for sessions in review, declined, approved or kyc expired\"` — the KYC session exists but is not in an eligible status yet (for example `Not Started` or `In Progress`). Wait for the `status.updated` webhook before requesting the report.\n- `\"You can only generate a PDF for sessions in review, declined or approved\"` — same condition for KYB sessions.\n\nNote that for a completely unknown `sessionId` the endpoint resolves the owning application before validating credentials, so a missing session returns `404` even when the credential is missing or invalid.", + "description": "Missing/invalid credentials or insufficient permissions. This API returns `403` for authentication failures — never `401`.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string", - "example": "You can only generate a PDF for sessions in review, declined, approved or kyc expired" - } - } - }, "examples": { - "Missing or invalid credentials": { - "summary": "No x-api-key header, or the key failed introspection", + "Invalid Credentials": { + "summary": "Missing or invalid x-api-key", "value": { "detail": "Authentication credentials were not provided or are invalid." } }, - "Insufficient permissions": { - "summary": "Valid credential lacking read:sessions or access to the owning application", + "Missing Privilege": { + "summary": "API key lacks write:sessions", "value": { "detail": "You do not have permission to perform this action." } - }, - "KYC session not finished": { - "summary": "KYC session still Not Started / In Progress", - "value": { - "detail": "You can only generate a PDF for sessions in review, declined, approved or kyc expired" - } - }, - "KYB session not finished": { - "summary": "KYB session still Not Started / In Progress", - "value": { - "detail": "You can only generate a PDF for sessions in review, declined or approved" - } } } } } }, "404": { - "description": "No active session with this `sessionId` exists — it was never created, or it has been deleted. Owner resolution happens before credential validation, so this `404` is returned even for unauthenticated requests. Depending on which lookup misses, the `detail` string is `\"Not found.\"` (the common case) or `\"Session not found.\"`.", + "description": "The session was not found, the session has no AML check (optionally for the given `node_id`), or no hit matches `hit_id`.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string", - "example": "Not found." - } - } - }, "examples": { - "Unknown session": { - "summary": "sessionId does not match any KYC or KYB session", + "No AML Check": { + "summary": "Session has no AML check", "value": { - "detail": "Not found." + "detail": "The session does not have an AML check." } }, - "Recently deleted session": { - "summary": "Session was deleted between lookups", + "Hit Not Found": { + "summary": "Unknown hit_id", "value": { - "detail": "Session not found." + "detail": "Hit with ID 'abc123' not found in the AML check." } } } } } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-aml-hit-status/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"hit_id\": \"abc123\",\n \"review_status\": \"False Positive\"\n }'" }, - "429": { - "description": "Rate limit exceeded. PDF generation has a dedicated limit of **50 requests per minute per credential** (API key, Bearer token, or source IP when unauthenticated), in addition to the global 600/min GET limit. Wait `Retry-After` seconds (also reflected in the `X-RateLimit-*` headers), then retry. Reports are not cached server-side, so download each report once and store the file instead of re-fetching it.", - "headers": { - "Retry-After": { - "description": "Seconds to wait before retrying the request.", - "schema": { - "type": "integer", - "minimum": 1, - "example": 30 - } - }, - "X-RateLimit-Limit": { - "description": "Maximum number of PDF generations allowed in the current window (50).", - "schema": { - "type": "integer", - "example": 50 - } - }, - "X-RateLimit-Remaining": { - "description": "Requests remaining in the current window.", - "schema": { - "type": "integer", - "example": 0 - } - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp (seconds) at which the current window resets.", - "schema": { - "type": "integer", - "example": 1750000000 + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresponse = requests.patch(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-aml-hit-status/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\", \"Content-Type\": \"application/json\"},\n json={\"hit_id\": \"abc123\", \"review_status\": \"False Positive\"},\n)\nprint(response.json())" + } + ] + } + }, + "/v3/session/{sessionId}/bulk-update-aml-hit-status/": { + "patch": { + "summary": "Bulk update AML hit review statuses", + "description": "Update the review status of **multiple AML hits** in one call. Provide `hit_updates`, a list of `{ hit_id, review_status }` pairs. `review_status` is one of `Unreviewed`, `Confirmed Match`, `False Positive`, or `Inconclusive`.\n\nThe update is all-or-nothing: if any `hit_id` is not found in the AML check, the request returns `404` and no hit is changed. A successful call fires a single `data.updated` webhook and records one activity entry covering all changes. For graph workflows with multiple AML checks, pass `node_id` to target a specific check.\n\nAuthenticate with your API key (`x-api-key`) or a user authorization header — either must carry the `write:sessions` privilege.", + "operationId": "patch_v3_session_bulk_update_aml_hit_status", + "x-didit-sdk": { + "resource": "sessions", + "method": "bulk_update_aml_hit_status", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "sessionId", + "required": true, + "description": "UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs — the service resolves the ID across both session types.", + "schema": { + "type": "string", + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } + }, + { + "in": "query", + "name": "node_id", + "required": false, + "description": "Workflow graph node ID. Required only when the session has multiple AML checks (one per node); omit it for single-AML sessions.", + "schema": { + "type": "string", + "example": "feature_aml_1" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "hit_updates" + ], + "properties": { + "hit_updates": { + "type": "array", + "description": "Non-empty list of hit updates.", + "items": { + "type": "object", + "required": [ + "hit_id", + "review_status" + ], + "properties": { + "hit_id": { + "type": "string", + "example": "abc123" + }, + "review_status": { + "type": "string", + "enum": [ + "Unreviewed", + "Confirmed Match", + "False Positive", + "Inconclusive" + ], + "example": "Confirmed Match" + } + } + } + }, + "node_id": { + "type": "string", + "description": "Optional graph node ID. Use it (here or as a query parameter) when the session has multiple AML checks.", + "example": "feature_aml_1" + } } } - }, + } + } + }, + "responses": { + "200": { + "description": "All hits updated.", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "message", + "updated_hits" + ], "properties": { - "detail": { + "message": { "type": "string", - "description": "Human-readable explanation of the rate-limit breach.", - "example": "Session PDF generation rate limit exceeded. You can make up to 50 requests per minute." + "description": "`Updated {n} hit(s) successfully`, where `n` is the number of entries in `hit_updates`." + }, + "updated_hits": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The `hit_id` of every entry in `hit_updates`, in request order, including hits whose status did not change." } } }, + "example": { + "message": "Updated 2 hit(s) successfully", + "updated_hits": [ + "abc123", + "def456" + ] + } + } + } + }, + "400": { + "description": "Validation error — `hit_updates` empty, or an entry is missing `hit_id` / `review_status` or carries an invalid value.", + "content": { + "application/json": { + "example": { + "hit_updates": [ + "hit_updates cannot be empty" + ] + } + } + } + }, + "403": { + "description": "Missing/invalid credentials or insufficient permissions. This API returns `403` for authentication failures — never `401`.", + "content": { + "application/json": { "examples": { - "Throttled": { - "summary": "More than 50 PDF generations in one minute from the same credential", + "Invalid Credentials": { + "summary": "Missing or invalid x-api-key", "value": { - "detail": "Session PDF generation rate limit exceeded. You can make up to 50 requests per minute." + "detail": "Authentication credentials were not provided or are invalid." + } + }, + "Missing Privilege": { + "summary": "API key lacks write:sessions", + "value": { + "detail": "You do not have permission to perform this action." } } } } } + }, + "404": { + "description": "The session was not found, the session has no AML check, or one or more `hit_id`s do not exist (no hit is changed in that case).", + "content": { + "application/json": { + "example": { + "detail": "Hits not found: zzz999" + } + } + } } }, - "security": [ - { - "ApiKeyAuth": [] - } - ], "x-codeSamples": [ { "lang": "curl", - "label": "cURL", - "source": "curl --fail \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/generate-pdf/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n --output report.pdf" + "label": "curl", + "source": "curl -X PATCH \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/bulk-update-aml-hit-status/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"hit_updates\": [\n { \"hit_id\": \"abc123\", \"review_status\": \"Confirmed Match\" },\n { \"hit_id\": \"def456\", \"review_status\": \"False Positive\" }\n ]\n }'" }, { "lang": "python", "label": "Python", - "source": "import requests\n\nresponse = requests.get(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/generate-pdf/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n stream=True,\n timeout=60,\n)\nresponse.raise_for_status()\nwith open(\"report.pdf\", \"wb\") as fh:\n for chunk in response.iter_content(chunk_size=8192):\n fh.write(chunk)" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "import { writeFile } from 'node:fs/promises';\n\nconst response = await fetch(\n 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/generate-pdf/',\n { headers: { 'x-api-key': 'YOUR_API_KEY' } },\n);\nif (!response.ok) throw new Error(`PDF generation failed: HTTP ${response.status}`);\nawait writeFile('report.pdf', Buffer.from(await response.arrayBuffer()));" + "source": "import requests\n\nresponse = requests.patch(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/bulk-update-aml-hit-status/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\", \"Content-Type\": \"application/json\"},\n json={\n \"hit_updates\": [\n {\"hit_id\": \"abc123\", \"review_status\": \"Confirmed Match\"},\n {\"hit_id\": \"def456\", \"review_status\": \"False Positive\"},\n ]\n },\n)\nprint(response.json())" } ] } }, - "/v3/session/{sessionId}/sandbox/arm/": { - "post": { - "summary": "Arm a sandbox scenario on a session", - "description": "Persist a sandbox scenario on a `Not Started` session and expand its bundled magic values (email, phone, or `expected_details` fields) onto the session, so the mocked providers reproduce the scenario's outcome once the flow runs. This is the endpoint the pre-flow scenario picker in Didit's hosted verification UI calls when a tester chooses an outcome before starting capture. Build your own picker against it, or call it directly from a sandbox test harness.\n\nAuthenticate with the session's own `Session-Token` (returned in `session_token` by [Create Session](/sessions-api/create-session)), not your application `x-api-key`: this endpoint is meant to be called from the client, not your backend. A scenario is only accepted while `status` is `Not Started`. Calling it again while still `Not Started` re-arms the session: it swaps in the new scenario's magic values and clears only the values the *previous* scenario wrote, so any value the user already typed by hand (email, first name, etc.) survives the re-arm. Arming a session that has moved past `Not Started`, or that belongs to a live (non-sandbox) application, is rejected.\n\nSee [Sandbox & Test Data](/integration/sandbox-testing) for the full scenario catalog and the other two ways to drive an outcome: `sandbox_scenario` at session create, and typing magic values directly.", - "operationId": "post_v3_session_sandbox_arm", + "/v3/session/{sessionId}/update-poa-data/": { + "patch": { + "summary": "Update Proof of Address data extracted from the POA document on a verification session", + "description": "Correct the fields extracted from the user's Proof of Address document — utility bill, bank statement, or other address evidence — after the verification has finished: issuer, dates, the printed name, the address itself, and banking details. Every field in the body is optional: only the keys you send are written, everything else is left untouched (`PATCH` semantics).\n\nThe session must belong to your application and be in `Approved`, `Declined`, `In Review`, or `Kyc Expired` status. Sessions in any other state (e.g. still `In Progress`) return `404` with `\"No Session matches the given query.\"` — distinct from the `\"Not found.\"` returned for an unknown or deleted `sessionId`. The session must also carry a POA record: sessions whose workflow never ran a Proof of Address step return `404` (`\"The session does not have a POA.\"`). For graph workflows with multiple POA steps, target a specific step with `node_id` (query parameter or body field); when omitted, the most recent POA record is patched.\n\n`extra_fields` is merged key-by-key: nine known keys (`bank_iban`, `additional_names`, …) write through to dedicated POA columns, any other key is kept as a custom extra, and `null` / `\"\"` deletes a key. `poa_parsed_address` accepts only the seven documented keys and recomputes `formatted_address` / `poa_formatted_address` after every edit. If the free-text `poa_address` changes, the server may also re-geocode it and refresh the structured address (skipped for sandbox sessions).\n\nUnlike the KYC variant of this endpoint, the `data.updated` [webhook](/integration/webhooks) (trigger `manual_review`) fires only when at least one stored value **actually changed** — re-sending identical values is a silent no-op. When values change, a `POA_DATA_UPDATED` activity entry recording the changed fields with previous and new values is appended to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and the console activity timeline. The session's status and decision are **not** re-evaluated — use [Update Status](#patch-/v3/session/-sessionId-/update-status/) to change the outcome.\n\nAccepts an API key or a console user token; either way the credential needs the `write:sessions` permission. Authentication and permission failures both return `403` — this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. This endpoint has a dedicated rate limit of 10 requests per minute per API key (invalid requests count too); the shared write budget of 300 requests per minute also applies.\n\nAn empty JSON body `{}` is a no-op: it returns `200` echoing the current POA record without firing the webhook (`has_changes` stays false).", + "operationId": "patch_v3_session_update_poa_data", + "x-didit-sdk": { + "resource": "sessions", + "method": "update_poa_data", + "auth": "api_key", + "server": true + }, "tags": [ "Sessions" ], "security": [ { - "SessionTokenAuth": [] + "ApiKeyAuth": [] } ], "parameters": [ { - "in": "path", "name": "sessionId", + "in": "path", "required": true, - "description": "UUID of the sandbox verification session to arm. Must match the session encoded in the Session-Token used to authenticate.", + "description": "UUID of the User Verification (KYC) session whose attached POA record you are correcting.", + "schema": { + "type": "string", + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } + }, + { + "name": "node_id", + "in": "query", + "required": false, + "description": "Workflow graph node whose POA record to patch, for graph workflows with multiple Proof of Address steps (also accepted as a `node_id` body field). When omitted, the most recent POA record on the session is patched. If no POA exists for the given node, the call returns `404` (`\"The session does not have a POA for node ''.\"`).", "schema": { "type": "string", - "format": "uuid", - "example": "11111111-2222-3333-4444-555555555555" + "example": "feature_poa" } } ], @@ -10929,22 +29389,140 @@ "application/json": { "schema": { "type": "object", - "required": [ - "scenario" - ], + "description": "All fields optional — send only what you want to change.", "properties": { - "scenario": { + "issuing_state": { "type": "string", - "description": "Slug of the scenario to arm. See the catalog at GET /v1/sandbox/scenarios/ or the scenario table in [Sandbox & Test Data](/integration/sandbox-testing).", - "example": "decline_document_expired" + "nullable": true, + "description": "Country that issued the POA document, ISO 3166-1 alpha-3. Case-insensitive (normalized to uppercase); invalid codes return `400`.", + "example": "ESP" + }, + "document_type": { + "type": "string", + "enum": [ + "UTILITY_BILL", + "BANK_STATEMENT", + "GOVERNMENT_ISSUED_DOCUMENT", + "OTHER_POA_DOCUMENT", + "UNKNOWN" + ], + "description": "POA document category. Values are uppercase — lowercase variants like `utility_bill` return `400`.", + "example": "UTILITY_BILL" + }, + "document_language": { + "type": "string", + "maxLength": 50, + "nullable": true, + "description": "Language of the document, typically an ISO 639-1 code (e.g. `es`). Free text — not validated as a language code.", + "example": "es" + }, + "issuer": { + "type": "string", + "maxLength": 255, + "nullable": true, + "description": "Organization that issued the document (utility company, bank, government agency).", + "example": "Energy Provider S.A." + }, + "issue_date": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Issue date of the POA document in `YYYY-MM-DD`. Any other format returns `400`.", + "example": "2026-04-12" + }, + "poa_address": { + "type": "string", + "nullable": true, + "description": "Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it in the background of the same request and may overwrite `poa_parsed_address` / `poa_formatted_address` with the geocoder result. The re-geocode also requires `issuing_state` to be present on the POA record.", + "example": "Avda de Madrid 34, 28013 Madrid" + }, + "name_on_document": { + "type": "string", + "maxLength": 255, + "nullable": true, + "description": "Recipient name as printed on the document.", + "example": "Carmen Espanola" + }, + "extra_fields": { + "type": "object", + "additionalProperties": true, + "nullable": true, + "description": "Document-specific extras, merged key-by-key. Nine keys map to dedicated POA columns: `bank_account_number`, `bank_iban`, `bank_sort_code`, `bank_routing_number`, `bank_swift_bic`, `bank_branch_name`, `bank_branch_address`, `document_phone_number`, and `additional_names` (must be a list of strings or `null`, otherwise `400`). Any other key is stored as a custom extra field. Per-key semantics: a value sets the key; `null` or `\"\"` deletes it. Must be a JSON object, otherwise `400`." + }, + "poa_parsed_address": { + "type": "object", + "nullable": true, + "description": "Structured address. Only these nested keys are accepted: `address_type`, `city`, `region`, `street_1`, `street_2`, `postal_code`, `country` — any other key returns `400`. Per-key semantics: a value sets the key, `null` removes it. `formatted_address` (and the top-level `poa_formatted_address`) is recomputed server-side by joining `street_1`, `street_2`, `city`, `region`, `postal_code`, and `country` — you cannot set it directly.", + "properties": { + "address_type": { + "type": "string", + "nullable": true, + "description": "Free-form address classification (e.g. `home`)." + }, + "city": { + "type": "string", + "nullable": true, + "example": "Madrid" + }, + "region": { + "type": "string", + "nullable": true, + "description": "State / province / autonomous community." + }, + "street_1": { + "type": "string", + "nullable": true, + "example": "Avda de Madrid 34" + }, + "street_2": { + "type": "string", + "nullable": true + }, + "postal_code": { + "type": "string", + "nullable": true, + "example": "28013" + }, + "country": { + "type": "string", + "nullable": true, + "description": "ISO 3166-1 alpha-2 country code. Lowercase input is normalized to uppercase; alpha-3 codes are rejected with `400`.", + "example": "ES" + } + } } } }, "examples": { - "Arm": { - "summary": "Arm the AML PEP-hit scenario", + "Correct issuer and address": { + "summary": "Fix document classification, issuer, and the structured address", "value": { - "scenario": "decline_aml_hit" + "issuing_state": "ESP", + "document_type": "BANK_STATEMENT", + "issuer": "Banco Example S.A.", + "issue_date": "2026-05-02", + "name_on_document": "Carmen Espanola", + "poa_address": "Avda de Madrid 34, 28013 Madrid", + "extra_fields": { + "bank_iban": "ES9121000418450200051332", + "additional_names": [ + "Carmen E. Espanola" + ] + }, + "poa_parsed_address": { + "street_1": "Avda de Madrid 34", + "city": "Madrid", + "postal_code": "28013", + "country": "ES" + } + } + }, + "Remove a banking detail": { + "summary": "Delete one extra-field key, keep the rest", + "value": { + "extra_fields": { + "bank_iban": null + } } } } @@ -10953,33 +29531,158 @@ }, "responses": { "200": { - "description": "Scenario armed. Its magic values are now on the session. `status` reflects the session's current status, which this call never changes: it is always `Not Started` on success.", + "description": "POA data updated. Returns the **full current POA record** (every updatable field with its stored value, plus the complete `extra_fields` map, `poa_parsed_address`, and `poa_formatted_address`) — not just the fields you submitted. The `data.updated` webhook fires only when at least one value actually changed.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "session_id": { + "issuing_state": { "type": "string", - "format": "uuid" + "nullable": true, + "description": "Country that issued the POA document, ISO 3166-1 alpha-3. Case-insensitive (normalized to uppercase); invalid codes return `400`.", + "example": "ESP" }, - "scenario": { + "document_type": { "type": "string", - "description": "The armed scenario slug (echoes the request)." + "enum": [ + "UTILITY_BILL", + "BANK_STATEMENT", + "GOVERNMENT_ISSUED_DOCUMENT", + "OTHER_POA_DOCUMENT", + "UNKNOWN" + ], + "description": "POA document category. Values are uppercase — lowercase variants like `utility_bill` return `400`.", + "example": "UTILITY_BILL" }, - "status": { + "document_language": { "type": "string", - "example": "Not Started" + "maxLength": 50, + "nullable": true, + "description": "Language of the document, typically an ISO 639-1 code (e.g. `es`). Free text — not validated as a language code.", + "example": "es" + }, + "issuer": { + "type": "string", + "maxLength": 255, + "nullable": true, + "description": "Organization that issued the document (utility company, bank, government agency).", + "example": "Energy Provider S.A." + }, + "issue_date": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Issue date of the POA document in `YYYY-MM-DD`. Any other format returns `400`.", + "example": "2026-04-12" + }, + "poa_address": { + "type": "string", + "nullable": true, + "description": "Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it in the background of the same request and may overwrite `poa_parsed_address` / `poa_formatted_address` with the geocoder result.", + "example": "Avda de Madrid 34, 28013 Madrid" + }, + "name_on_document": { + "type": "string", + "maxLength": 255, + "nullable": true, + "description": "Recipient name as printed on the document.", + "example": "Carmen Espanola" + }, + "extra_fields": { + "type": "object", + "additionalProperties": true, + "nullable": true, + "description": "Always present in the response: the nine known keys (with `null` for unset ones, `additional_names` defaulting to `[]`) plus any custom keys previously stored." + }, + "poa_parsed_address": { + "type": "object", + "nullable": true, + "description": "Current structured address, or `null` when none exists. Includes the recomputed `formatted_address` alongside the seven editable keys. Returned as stored — after a server-side geocode it can contain bookkeeping keys (e.g. `raw_results`) alongside the editable keys.", + "properties": { + "address_type": { + "type": "string", + "nullable": true, + "description": "Free-form address classification (e.g. `home`)." + }, + "city": { + "type": "string", + "nullable": true, + "example": "Madrid" + }, + "region": { + "type": "string", + "nullable": true, + "description": "State / province / autonomous community." + }, + "street_1": { + "type": "string", + "nullable": true, + "example": "Avda de Madrid 34" + }, + "street_2": { + "type": "string", + "nullable": true + }, + "postal_code": { + "type": "string", + "nullable": true, + "example": "28013" + }, + "country": { + "type": "string", + "nullable": true, + "description": "ISO 3166-1 alpha-2 country code. Lowercase input is normalized to uppercase; alpha-3 codes are rejected with `400`.", + "example": "ES" + }, + "formatted_address": { + "type": "string", + "nullable": true, + "description": "Joined, human-readable form of the structured address. Recomputed server-side after every `poa_parsed_address` edit.", + "example": "Avda de Madrid 34, Madrid, 28013, ES" + } + } + }, + "poa_formatted_address": { + "type": "string", + "nullable": true, + "description": "Top-level copy of the recomputed `formatted_address`, kept in sync when the structured address changes.", + "example": "Avda de Madrid 34, Madrid, 28013, ES" } } }, "examples": { - "Armed": { - "summary": "Scenario armed on a Not Started session", + "Updated POA": { + "summary": "Full POA record after the correction", "value": { - "session_id": "11111111-2222-3333-4444-555555555555", - "scenario": "decline_aml_hit", - "status": "Not Started" + "issuing_state": "ESP", + "document_type": "BANK_STATEMENT", + "document_language": "es", + "issuer": "Banco Example S.A.", + "issue_date": "2026-05-02", + "poa_address": "Avda de Madrid 34, 28013 Madrid", + "name_on_document": "Carmen Espanola", + "extra_fields": { + "bank_account_number": null, + "bank_iban": "ES9121000418450200051332", + "bank_sort_code": null, + "bank_routing_number": null, + "bank_swift_bic": null, + "bank_branch_name": null, + "bank_branch_address": null, + "document_phone_number": null, + "additional_names": [ + "Carmen E. Espanola" + ] + }, + "poa_parsed_address": { + "city": "Madrid", + "street_1": "Avda de Madrid 34", + "postal_code": "28013", + "country": "ES", + "formatted_address": "Avda de Madrid 34, Madrid, 28013, ES" + }, + "poa_formatted_address": "Avda de Madrid 34, Madrid, 28013, ES" } } } @@ -10987,38 +29690,126 @@ } }, "400": { - "description": "Validation error: an unknown scenario slug, or the session has already progressed past `Not Started`.", + "description": "Validation error. Field errors arrive as `{\"\": [\"\"]}`; errors on nested `poa_parsed_address` / `extra_fields` keys use an object value: `{\"extra_fields\": {\"\": \"\"}}`.", "content": { "application/json": { "examples": { - "Unknown Scenario": { - "summary": "scenario is not in the catalog", + "Invalid document type": { + "summary": "Enum is uppercase — lowercase variants are rejected", "value": { - "scenario": [ - "Unknown sandbox scenario: 'not-a-real-scenario'." + "document_type": [ + "\"utility_bill\" is not a valid choice." ] } }, - "Already Started": { - "summary": "Session moved past Not Started", - "value": [ - "Scenario can only be armed before the verification starts." - ] + "Invalid issuing state": { + "summary": "issuing_state is not ISO 3166-1 alpha-3", + "value": { + "issuing_state": [ + "Invalid ISO 3166-1 alpha-3 country code for issuing_state." + ] + } + }, + "additional_names not a list": { + "summary": "additional_names accepts a list of strings or null", + "value": { + "extra_fields": { + "additional_names": "Must be a list or null." + } + } + }, + "Unsupported poa_parsed_address key": { + "summary": "Only the seven documented keys are allowed", + "value": { + "poa_parsed_address": { + "zipcode": "Unsupported key. Allowed keys: address_type, city, region, street_1, street_2, postal_code, country" + } + } + }, + "Bad date format": { + "summary": "Dates must be YYYY-MM-DD", + "value": { + "issue_date": [ + "Date has wrong format. Use one of these formats instead: YYYY-MM-DD." + ] + } + } + } + } + } + }, + "403": { + "description": "Missing/invalid credentials or insufficient permissions. This API returns `403` for authentication failures — never `401`.", + "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": "Credential lacks write:sessions", + "value": { + "detail": "You do not have permission to perform this action." + } } } } } }, "404": { - "description": "The Session-Token does not resolve to this `session_id`, or the session belongs to a live (non-sandbox) application.", + "description": "Unknown session, session in a non-editable state, or no POA record to patch. Each case has a distinct `detail` message (see examples). The unknown-session lookup runs before credential checks.", "content": { "application/json": { "examples": { - "Not Found": { - "summary": "Live session, or session_id / token mismatch", + "Unknown session": { + "summary": "No session with this ID in your application (or it was deleted)", "value": { "detail": "Not found." } + }, + "Wrong session state": { + "summary": "Session exists but is not Approved / Declined / In Review / Kyc Expired", + "value": { + "detail": "No Session matches the given query." + } + }, + "No POA record": { + "summary": "The workflow never ran a Proof of Address step", + "value": { + "detail": "The session does not have a POA." + } + }, + "No POA for node": { + "summary": "node_id given but no POA exists for it", + "value": { + "detail": "The session does not have a POA for node 'feature_missing'." + } + } + } + } + } + }, + "429": { + "description": "Dedicated rate limit exceeded — 10 POA data updates per minute per API key (rejected requests count toward the budget). The shared 300/min write budget can also trigger with its own message. Inspect `Retry-After` and the `X-RateLimit-*` response headers before retrying.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Rate limited": { + "summary": "POA update budget exhausted", + "value": { + "detail": "Session POA data update rate limit exceeded. You can make up to 10 requests per minute." + } } } } @@ -11029,26 +29820,32 @@ { "lang": "curl", "label": "curl", - "source": "curl -X POST \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/sandbox/arm/ \\\n -H 'Session-Token: YOUR_SESSION_TOKEN' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"scenario\": \"decline_aml_hit\"\n }'" + "source": "curl -X PATCH \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-poa-data/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"issuing_state\": \"ESP\",\n \"document_type\": \"BANK_STATEMENT\",\n \"issuer\": \"Banco Example S.A.\",\n \"issue_date\": \"2026-05-02\",\n \"name_on_document\": \"Carmen Espanola\",\n \"poa_address\": \"Avda de Madrid 34, 28013 Madrid\",\n \"extra_fields\": {\n \"bank_iban\": \"ES9121000418450200051332\"\n },\n \"poa_parsed_address\": {\n \"city\": \"Madrid\",\n \"postal_code\": \"28013\",\n \"country\": \"ES\"\n }\n }'" }, { "lang": "python", "label": "Python", - "source": "import requests\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/sandbox/arm/\",\n headers={\n \"Session-Token\": \"YOUR_SESSION_TOKEN\",\n \"Content-Type\": \"application/json\",\n },\n json={\"scenario\": \"decline_aml_hit\"},\n)\nresponse.raise_for_status()\nprint(response.json()) # {'session_id': '...', 'scenario': 'decline_aml_hit', 'status': 'Not Started'}" + "source": "import requests\n\nresponse = requests.patch(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-poa-data/\",\n headers={\n \"x-api-key\": \"YOUR_API_KEY\",\n \"Content-Type\": \"application/json\",\n },\n json={\n \"issuing_state\": \"ESP\",\n \"document_type\": \"BANK_STATEMENT\",\n \"issuer\": \"Banco Example S.A.\",\n \"issue_date\": \"2026-05-02\",\n \"name_on_document\": \"Carmen Espanola\",\n \"poa_address\": \"Avda de Madrid 34, 28013 Madrid\",\n \"extra_fields\": {\"bank_iban\": \"ES9121000418450200051332\"},\n \"poa_parsed_address\": {\"city\": \"Madrid\", \"postal_code\": \"28013\", \"country\": \"ES\"},\n },\n)\nresponse.raise_for_status()\npoa = response.json() # full current POA record\nprint(poa[\"issuer\"], poa[\"poa_formatted_address\"])" }, { "lang": "javascript", "label": "JavaScript", - "source": "const response = await fetch(\n 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/sandbox/arm/',\n {\n method: 'POST',\n headers: {\n 'Session-Token': sessionToken,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ scenario: 'decline_aml_hit' }),\n },\n);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst { session_id, scenario, status } = await response.json();" + "source": "const response = await fetch(\n 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-poa-data/',\n {\n method: 'PATCH',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n issuing_state: 'ESP',\n document_type: 'BANK_STATEMENT',\n issuer: 'Banco Example S.A.',\n issue_date: '2026-05-02',\n name_on_document: 'Carmen Espanola',\n poa_address: 'Avda de Madrid 34, 28013 Madrid',\n poa_parsed_address: { city: 'Madrid', postal_code: '28013', country: 'ES' },\n }),\n },\n);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst poa = await response.json(); // full current POA record" } ] } }, - "/v3/session/{sessionId}/update-status/": { + "/v3/session/{sessionId}/kyb/{companyUuid}/update-data/": { "patch": { - "summary": "Approve, decline, or request resubmission for a verification session", - "description": "Manually override the final decision of a KYC or KYB verification session. Set `new_status` to `Approved` or `Declined` to record a manual decision, or to `Resubmitted` to clear specific verification steps and let the user redo them through the same verification URL. The endpoint accepts both user (KYC) and business (KYB) session IDs.\n\nCall it once a session has reached a reviewable state: the current status must be `Approved`, `Declined`, `In Review`, `Kyc Expired`, `Abandoned`, or `Resubmitted`. Sessions that are still `Not Started`, `In Progress`, or `Awaiting User`, or that ended as `Expired`, return `400`. The new status must differ from the current one, and your API key's organization needs the `write:sessions` permission. The eligible-state check runs before body validation, so an ineligible session returns the `Wrong Current Status` error even when the payload is also invalid.\n\nFor `Resubmitted`, pass `nodes_to_resubmit` to choose which workflow steps the user must redo, or omit it to auto-select existing feature attempts in a resubmittable state (`Declined`, `In Review`, `Not Finished`, `Expired`). Never-attempted features are not auto-selected — list them explicitly in `nodes_to_resubmit`, or the request returns `400` when no recorded attempt needs resubmission. Captured data for the selected steps is deleted, the steps are reordered to match the workflow graph, the session's expiration window restarts, and the original verification URL becomes usable again. Backend-only steps (`AML`, `DATABASE_VALIDATION`, `IP_ANALYSIS`) at the head of the list run immediately without user interaction; if every resubmitted step is backend-only, the session re-finalizes within the same request. `KYB_REGISTRY` and `KYB_KEY_PEOPLE` cannot be resubmitted directly — resubmit the relevant child KYC sessions instead and the parent business session recomputes automatically.\n\nEvery successful call fires the `status.updated` [webhook](/integration/webhooks) once the change commits and appends an activity entry — with actor attribution and your `comment` — to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and in the console activity timeline. The user-profile aggregates linked to the session's `vendor_data` update as well. Declining disables ongoing AML monitoring for the session; re-approving re-enables it (from `Resubmitted`, `Declined`, or `Kyc Expired`) when the workflow has monitoring turned on. Approving or declining a child KYC that belongs to a KYB key-people check recomputes the parent business session's status. Set `send_email: true` (with `email_address`) to notify the user: a status notice for `Approved`/`Declined`, or a resubmission email containing the verification link and per-step reasons for `Resubmitted`.\n\nThe operation is intentionally not repeatable with the same value: re-sending the current status returns `400` (`The new status is the same as the current status.`), so accidental double-submissions are harmless. Authentication and permission failures both return `403` — this API never responds `401`.", - "operationId": "patch_v3_session_update_status", + "summary": "Update registry-extracted company data on a Business Verification (KYB) session", + "description": "Correct the registry-extracted company data on a Business Verification (KYB) session — company name, registration number, addresses, status, contact details — identified by `companyUuid`. Typical callers are a compliance analyst fixing a registry misread, or your back office syncing corrected company data. Every field in the body is optional (`PATCH` semantics); an empty JSON body (`{}`) is a no-op that simply echoes the current company record.\n\nEdits are written in two places at once: fields with a canonical company column (name, registration number, country, incorporation date, tax number, type, statuses, capital fields (`registered_capital`, `registered_capital_amount`, `registered_capital_currency`), alternative names, nature of business, website, email, phone, LEI, location, and `legal_address` → `registered_address`) update that column, and **every** submitted key is also merged into the `registry_data` snapshot (dates as ISO strings) so the \"extracted from registry\" view reflects the correction. Fields without a column — `region`, `registration_date`, `dissolution_date`, `control_scheme` — land in `registry_data` only. `user_provided_data` (what the end user confirmed in the hosted flow) is never touched, and `last_console_edit_at` is stamped on every non-empty PATCH.\n\nUnlike the KYC/POA update endpoints, there is **no session-status gate**: the company can be edited at any point in the business session's lifecycle. The company must belong to the session in the path (`404` with `\"KYB company does not belong to the given session.\"` otherwise) and to your application. Every non-empty PATCH fires the `data.updated` [webhook](/integration/webhooks) on the business session (trigger `console_registry_edit`) — even when the submitted values equal the stored ones. The session's status and decision are **not** re-evaluated, and no review/audit entry is recorded.\n\nAccepts an API key or a console user token; either way the credential needs the `write:businesses` permission. Authentication and permission failures both return `403` — this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. The shared write budget of 300 requests per minute per API key applies.\n\nFor User Verification sessions, the KYC counterpart is `PATCH /v3/session/{sessionId}/update-data/`; to change the session decision itself use `PATCH /v3/session/{sessionId}/update-status/`.", + "operationId": "patch_v3_session_kyb_company_update_data", + "x-didit-sdk": { + "resource": "sessions", + "method": "update_kyb_company_data", + "auth": "api_key", + "server": true + }, "tags": [ "Sessions" ], @@ -11059,14 +29856,25 @@ ], "parameters": [ { - "in": "path", "name": "sessionId", + "in": "path", "required": true, - "description": "UUID of the verification session to update. Accepts both user (KYC) and business (KYB) session IDs — the service resolves the ID across both session types.", + "description": "UUID of the parent Business Verification (KYB) session.", "schema": { "type": "string", "format": "uuid", - "example": "11111111-2222-3333-4444-555555555555" + "example": "33333333-4444-5555-6666-777777777777" + } + }, + { + "name": "companyUuid", + "in": "path", + "required": true, + "description": "UUID of the company record to update (`registry_checks[].company.uuid` on the business session decision). Must belong to the session referenced by `sessionId`.", + "schema": { + "type": "string", + "format": "uuid", + "example": "88888888-9999-aaaa-bbbb-cccccccccccc" } } ], @@ -11076,140 +29884,191 @@ "application/json": { "schema": { "type": "object", - "required": [ - "new_status" - ], + "description": "All fields optional — send only what you want to change. Provided fields update the canonical company columns (where one exists) and are merged into the `registry_data` snapshot.", "properties": { - "new_status": { + "company_name": { + "type": "string", + "maxLength": 255, + "description": "Legal name of the company. Written to the canonical column and the registry snapshot.", + "example": "Docs Probe Corp" + }, + "registration_number": { + "type": "string", + "maxLength": 100, + "description": "Official registration number with the company registry.", + "example": "DPC-001" + }, + "country_code": { + "type": "string", + "maxLength": 2, + "description": "Country of incorporation, ISO 3166-1 alpha-2. Case-insensitive (`us` is stored as `US`). Alpha-3 codes fail the 2-character limit with `400`.", + "example": "US" + }, + "region": { + "type": "string", + "maxLength": 10, + "nullable": true, + "description": "Subdivision / region tag where the registry has one (e.g. US state). Stored in the registry snapshot only — it has no canonical column." + }, + "incorporation_date": { + "type": "string", + "format": "date", + "description": "Incorporation date in `YYYY-MM-DD`. Optional like every other field — the `PATCH` is partial. Any other date format returns `400`.", + "example": "2015-04-12" + }, + "registration_date": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Registration date where the registry distinguishes it from incorporation. Stored in the registry snapshot only." + }, + "dissolution_date": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Dissolution date for companies that are no longer active. Stored in the registry snapshot only." + }, + "tax_number": { + "type": "string", + "maxLength": 100, + "description": "Tax identification number." + }, + "legal_address": { + "type": "string", + "description": "Legal / registered address. Aliased server-side to the canonical `registered_address` column — the response returns it under `registered_address`, while the registry snapshot keeps the `legal_address` key you sent.", + "example": "1 Example Plaza, Wilmington, DE 19801" + }, + "company_type": { + "type": "string", + "maxLength": 100, + "description": "Legal entity type (e.g. `Limited Liability Company`, `PLC`, `GmbH`).", + "example": "Limited Liability Company" + }, + "registry_status": { + "type": "string", + "enum": [ + "active", + "dissolved", + "deregistered", + "see full details", + "authorised", + "appointed representative", + "unauthorised", + "inactive", + "no longer authorised", + "closed", + "struck off" + ], + "nullable": true, + "description": "Registry-reported company status. Lowercase, some values contain spaces (e.g. `struck off`, `no longer authorised`). Any other value returns `400`.", + "example": "active" + }, + "verification_status": { "type": "string", - "description": "Target status. `Approved` and `Declined` record a final manual decision (each can also overturn the other). `Resubmitted` clears the selected steps and sends the session back to the user. Any other value returns `400`.", "enum": [ - "Approved", - "Declined", - "Resubmitted" + "verified", + "failed", + "unknown" ], - "example": "Approved" + "nullable": true, + "description": "Outcome of the registry verification. Any other value returns `400`.", + "example": "verified" }, - "comment": { + "alternative_names": { "type": "string", - "description": "Free-text reason for the change, stored on the session's review trail and returned in the `reviews` array of [Get Decision](#get-/v3/session/-sessionId-/decision/). For example `Duplicated user`.", - "example": "All checks passed manual review" + "description": "Trade names, former legal names, DBA names (free text)." }, - "nodes_to_resubmit": { - "type": "array", - "description": "Workflow steps the user must redo. Only acted on when `new_status` is `Resubmitted`. For `Approved`/`Declined` the entries are still schema-validated (an invalid `feature` value returns `400`) but schema-valid entries are semantically ignored. If omitted, the server auto-selects existing OCR, Liveness, Face Match, POA, Phone, Email, AML, Database Validation, and Questionnaire attempts whose status is `Declined`, `In Review`, `Not Finished`, or `Expired` (NFC, IP analysis, age estimation, face search, and KYB document attempts are never auto-selected; non-face-match face attempts are selected as `LIVENESS`) — features the user never attempted (no recorded attempt) are NOT selected, so a session with zero attempts returns `400` (\"No features found that need resubmission\") even though nothing was approved; pass `nodes_to_resubmit` explicitly in that case. Steps are executed in workflow-graph order regardless of the order you send them. `KYB_REGISTRY`, `KYB_KEY_PEOPLE`, and the `KYB` alias are rejected with `400` — those parent checks recompute from their child KYC sessions.", - "items": { - "type": "object", - "required": [ - "node_id", - "feature" - ], - "properties": { - "node_id": { - "type": "string", - "description": "Node identifier as it appears in the session's workflow definition (for example `feature_ocr`, `feature_liveness`).", - "example": "feature_ocr" - }, - "feature": { - "type": "string", - "description": "Feature type of the node. The aliases `ID_VERIFICATION`, `POA`, `PHONE`, and `EMAIL` are normalized server-side to `OCR`, `PROOF_OF_ADDRESS`, `PHONE_VERIFICATION`, and `EMAIL_VERIFICATION`. `KYB_REGISTRY`, `KYB_KEY_PEOPLE`, and `KYB` pass schema validation but are always rejected with the business-rule `400` shown in the examples.", - "enum": [ - "OCR", - "OCR_BACK", - "NFC", - "AML", - "FACE", - "LIVENESS", - "FACE_MATCH", - "IP_ANALYSIS", - "AGE_ESTIMATION", - "PROOF_OF_ADDRESS", - "PHONE_VERIFICATION", - "EMAIL_VERIFICATION", - "FACE_SEARCH", - "DATABASE_VALIDATION", - "QUESTIONNAIRE", - "DOCUMENT_AI", - "KYB_DOCUMENTS", - "ID_VERIFICATION", - "POA", - "PHONE", - "EMAIL", - "KYB_REGISTRY", - "KYB_KEY_PEOPLE", - "KYB" - ], - "example": "OCR" - } - } - } + "nature_of_business": { + "type": "string", + "description": "Short description of the company's activity.", + "example": "Software development" }, - "send_email": { - "type": "boolean", - "description": "Whether to email the user about the change. Requires `email_address`. For `Approved`/`Declined` the user receives a status notice; for `Resubmitted` the email includes the verification link and the per-step resubmission reasons.", - "default": false, - "example": false + "registered_capital": { + "type": "string", + "maxLength": 100, + "description": "Human-readable registered capital (e.g. `USD 50000`). Use `registered_capital_amount` + `registered_capital_currency` for structured values, or this field for free-form text.", + "example": "USD 50000" }, - "email_address": { + "registered_capital_amount": { + "type": "number", + "format": "double", + "nullable": true, + "description": "Numeric registered-capital amount (up to 20 digits, 2 decimal places). Sets the `registered_capital_amount` column and is mirrored into `registry_data` as a decimal string; the response echoes it as a decimal string (e.g. `\"1500000.50\"`)." + }, + "registered_capital_currency": { + "type": "string", + "maxLength": 3, + "nullable": true, + "description": "ISO 4217 currency code for `registered_capital_amount`. Case-insensitive (`usd` is stored as `USD`); values that are not exactly 3 letters return `400` with the ISO 4217 message. Sets the canonical `registered_capital_currency` column and is mirrored into `registry_data`.", + "example": "USD" + }, + "website": { + "type": "string", + "maxLength": 500, + "description": "Company website. Bare domains are accepted and normalized with an `https://` prefix (`docsprobe.example.com` is stored as `https://docsprobe.example.com`); values that still fail URL validation return `400`.", + "example": "https://docsprobe.example.com" + }, + "email": { "type": "string", "format": "email", - "description": "Recipient for the notification email. **Required when `send_email` is `true`** — omitting it returns `400`.", - "example": "user@example.com" + "description": "Contact email on file with the registry.", + "example": "legal@docsprobe.example.com" }, - "email_language": { + "phone": { "type": "string", - "description": "Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`).", - "default": "en", - "example": "en" + "maxLength": 50, + "description": "Contact phone. Must match `+`, digits, spaces, dashes, dots, or parentheses, with 7-15 digits total — otherwise `400`.", + "example": "+1 302 555 0143" }, - "email_preview_token": { + "legal_entity_identifier": { "type": "string", - "writeOnly": true, - "minLength": 1, - "maxLength": 512, - "description": "Optional signed receipt from Preview Resubmission Email. When supplied with send_email=true and new_status=Resubmitted, the recipient, language, checks, current session state, sender and message must still match the preview. A changed or expired receipt returns 409 before changing the session or sending email. Existing integrations may omit this field." + "maxLength": 50, + "description": "LEI, where the company has one." + }, + "location_of_registration": { + "type": "string", + "maxLength": 255, + "description": "City / subdivision of the registry of record." + }, + "vat_number": { + "type": "string", + "maxLength": 64, + "description": "EU VAT number. Merged into the `registry_data` snapshot only — editing it here does not re-run VAT and tax ID validation, so `vat_validation_status` and the other `vat_*` result fields are unchanged." + }, + "control_scheme": { + "type": "string", + "description": "Control / governance scheme when the registry exposes it. Stored in the registry snapshot only." } } }, "examples": { - "Approve Session": { - "summary": "Approve a session after manual review", - "value": { - "new_status": "Approved", - "comment": "All checks passed manual review" - } - }, - "Decline Session": { - "summary": "Decline a session", + "Correct registry data": { + "summary": "Fix the company's registry-extracted fields", "value": { - "new_status": "Declined", - "comment": "Suspected fraud" + "company_name": "Docs Probe Corp", + "registration_number": "DPC-001", + "country_code": "US", + "incorporation_date": "2015-04-12", + "company_type": "Limited Liability Company", + "legal_address": "1 Example Plaza, Wilmington, DE 19801", + "registry_status": "active", + "verification_status": "verified", + "website": "docsprobe.example.com", + "email": "legal@docsprobe.example.com", + "phone": "+1 302 555 0143", + "registered_capital": "USD 50000", + "nature_of_business": "Software development" } }, - "Resubmit Specific Steps": { - "summary": "Request resubmission of chosen steps, with email", + "Single field": { + "summary": "Only the keys you send are written", "value": { - "new_status": "Resubmitted", - "nodes_to_resubmit": [ - { - "node_id": "feature_ocr", - "feature": "OCR" - }, - { - "node_id": "feature_liveness", - "feature": "LIVENESS" - } - ], - "send_email": true, - "email_address": "user@example.com", - "email_language": "en" + "registry_status": "dissolved" } }, - "Resubmit Auto-select": { - "summary": "Request resubmission (auto-select non-approved steps)", - "value": { - "new_status": "Resubmitted" - } + "No-op": { + "summary": "Empty body echoes the current record without firing the webhook", + "value": {} } } } @@ -11217,24 +30076,320 @@ }, "responses": { "200": { - "description": "Status updated. The response contains only the `session_id` — fetch the updated session via [Get Decision](#get-/v3/session/-sessionId-/decision/). The `status.updated` webhook fires once the change commits.", + "description": "Company data updated (or echoed unchanged for an empty body). Returns the full KYB company record — the same shape as `registry_checks[].company` on the business session decision. Note how `legal_address` comes back as `registered_address`. `registered_capital_amount`/`registered_capital_currency` populate their canonical columns and are mirrored into `registry_data`.", "content": { "application/json": { "schema": { "type": "object", + "description": "Full KYB company record — the same shape as the `company` object inside `registry_checks[]` on the business session decision.", "properties": { - "session_id": { + "uuid": { "type": "string", "format": "uuid", - "description": "UUID of the updated session." + "description": "Company record identifier (the `companyUuid` path parameter)." + }, + "node_id": { + "type": "string", + "nullable": true, + "description": "Workflow graph node this company record belongs to, when the workflow is graph-based." + }, + "status": { + "type": "string", + "description": "Feature lifecycle status of the registry check (e.g. `Not Finished`, `Approved`, `Declined`, `In Review`)." + }, + "registry_status": { + "type": "string", + "nullable": true, + "description": "Registry-reported company status (e.g. `active`, `dissolved`, `struck off`)." + }, + "data_resolved": { + "type": "boolean", + "description": "`true` when the company data is complete — always `true` for manual-entry companies; for registry-sourced companies, `true` once the registry fetch resolved." + }, + "company_name": { + "type": "string", + "nullable": true + }, + "registration_number": { + "type": "string", + "nullable": true + }, + "country_code": { + "type": "string", + "nullable": true, + "description": "ISO 3166-1 alpha-2 country of incorporation." + }, + "region": { + "type": "string", + "nullable": true + }, + "company_type": { + "type": "string", + "nullable": true + }, + "incorporation_date": { + "type": "string", + "format": "date", + "nullable": true + }, + "registered_address": { + "type": "string", + "nullable": true, + "description": "Canonical registered address — updated when you send `legal_address`." + }, + "tax_number": { + "type": "string", + "nullable": true + }, + "risk_level": { + "type": "string", + "nullable": true + }, + "verification_status": { + "type": "string", + "nullable": true, + "description": "`verified`, `failed`, or `unknown`." + }, + "is_from_registry": { + "type": "boolean", + "description": "`true` when the company was pre-filled from a registry search rather than entered manually." + }, + "fetch_status": { + "type": "string", + "nullable": true, + "description": "Registry fetch lifecycle for registry-sourced companies (`pending` / `resolved`); `null` for manual entries." + }, + "alternative_names": { + "type": "string", + "nullable": true, + "description": "Alternative, trading, former or original-language company names as text, when reported." + }, + "nature_of_business": { + "type": "string", + "nullable": true + }, + "registered_capital": { + "type": "string", + "nullable": true + }, + "registered_capital_amount": { + "type": "string", + "nullable": true, + "description": "Canonical column. Returned as a decimal string (e.g. `\"2750000.00\"`)." + }, + "registered_capital_currency": { + "type": "string", + "nullable": true, + "description": "Canonical column. Set by the registry flow or by console/API edits via this endpoint." + }, + "website": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "nullable": true + }, + "phone": { + "type": "string", + "nullable": true + }, + "legal_entity_identifier": { + "type": "string", + "nullable": true + }, + "location_of_registration": { + "type": "string", + "nullable": true + }, + "vat_number": { + "type": "string", + "nullable": true, + "description": "EU VAT number, when collected during the hosted registry step or edited afterwards." + }, + "vat_validation_status": { + "type": "string", + "enum": [ + "valid", + "invalid", + "format_valid", + "invalid_format", + "could_not_validate", + "not_applicable" + ], + "description": "Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session." + }, + "vat_validated_number": { + "type": "string", + "nullable": true, + "description": "The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran." + }, + "vat_validation_source": { + "type": "string", + "nullable": true, + "enum": [ + "eu_vies", + "pl_vat_white_list", + "no_brreg", + "ch_uid_register", + "br_receita_federal", + "co_rues", + "cr_hacienda", + "ec_sri", + "py_dnit", + "format_rules", + null + ], + "description": "The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran." + }, + "vat_validated_name": { + "type": "string", + "nullable": true, + "description": "Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." + }, + "vat_validated_address": { + "type": "string", + "nullable": true, + "description": "Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." + }, + "vat_checked_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Timestamp of the check. `null` when no check ran." + }, + "financial_summary": { + "type": "object", + "nullable": true, + "additionalProperties": true + }, + "officers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + }, + "description": "Directors / officers attached to the company, including per-person KYC progress." + }, + "beneficial_owners": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + }, + "description": "Beneficial owners attached to the company, including per-person KYC progress." + }, + "addresses": { + "type": "array", + "nullable": true, + "items": { + "type": "object", + "additionalProperties": true + }, + "description": "Registry address detail, when the registry returned any." + }, + "industries": { + "type": "array", + "nullable": true, + "items": { + "type": "object", + "additionalProperties": true + }, + "description": "Registry industry classification, when available." + }, + "accounts": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts." + }, + "registry_data": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Raw registry payload snapshot. Every key you PATCH is merged into this map (dates serialized as ISO strings), so the \"extracted from registry\" view reflects operator corrections." + }, + "user_provided_data": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "What the end user typed during the hosted registry-confirmation step. Never modified by this endpoint." + }, + "confirmed_by_user_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the end user confirmed the company data in the hosted flow." + }, + "last_console_edit_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Stamped on every successful non-empty PATCH, so UIs can surface \"edited by operator\" context." + }, + "is_editable": { + "type": "boolean", + "description": "`true` while the end user can still edit the registry-sourced data in the hosted flow (registry-sourced and not yet confirmed)." } } }, "examples": { - "Updated": { - "summary": "Returned for Approved, Declined, and Resubmitted alike", + "Updated company": { + "summary": "Full company record after the correction", "value": { - "session_id": "3472fb7c-8f7c-4d1a-9cf4-cf3d74ce1a60" + "uuid": "6b9479f5-6769-46c8-baf1-df63bbbc5f8c", + "node_id": null, + "status": "Not Finished", + "registry_status": "active", + "data_resolved": true, + "company_name": "Docs Probe Corp", + "registration_number": "DPC-001", + "country_code": "US", + "region": null, + "company_type": "Limited Liability Company", + "incorporation_date": "2015-04-12", + "registered_address": "1 Example Plaza, Wilmington, DE 19801", + "tax_number": null, + "risk_level": null, + "verification_status": "verified", + "is_from_registry": false, + "fetch_status": null, + "alternative_names": null, + "nature_of_business": "Software development", + "registered_capital": "USD 50000", + "registered_capital_amount": null, + "registered_capital_currency": "USD", + "website": "https://docsprobe.example.com", + "email": "legal@docsprobe.example.com", + "phone": "+1 302 555 0143", + "legal_entity_identifier": null, + "location_of_registration": null, + "financial_summary": null, + "officers": [], + "beneficial_owners": [], + "addresses": null, + "industries": null, + "accounts": null, + "registry_data": { + "company_name": "Docs Probe Corp", + "registration_number": "DPC-001", + "country_code": "US", + "incorporation_date": "2015-04-12", + "legal_address": "1 Example Plaza, Wilmington, DE 19801", + "company_type": "Limited Liability Company", + "registry_status": "active", + "verification_status": "verified", + "nature_of_business": "Software development", + "registered_capital": "USD 50000", + "registered_capital_currency": "USD", + "website": "https://docsprobe.example.com", + "email": "legal@docsprobe.example.com", + "phone": "+1 302 555 0143" + }, + "user_provided_data": null, + "confirmed_by_user_at": null, + "last_console_edit_at": "2026-06-12T00:02:34.276061Z", + "is_editable": false } } } @@ -11242,75 +30397,71 @@ } }, "400": { - "description": "Validation error. Field errors arrive as `{\"\": [\"\"]}` (some business-rule rejections use a string value instead: `{\"nodes_to_resubmit\": \"\"}`), cross-state errors as `{\"detail\": \"\"}`, and the same-status error as a bare JSON array.", + "description": "Validation error. Field errors arrive as `{\"\": [\"\"]}`. Max-length checks run before format checks, so `country_code: \"USA\"` reports the 2-character limit rather than the ISO message.", "content": { "application/json": { "examples": { - "Wrong Current Status": { - "summary": "Session is not in an updatable state", + "Alpha-3 country code": { + "summary": "country_code must be alpha-2 — length check fires first", "value": { - "detail": "Only sessions with status Approved, Declined, In Review, Kyc Expired, Abandoned, Resubmitted can be updated. Current status: In Progress" + "country_code": [ + "Ensure this field has no more than 2 characters." + ] } }, - "Invalid Status Value": { - "summary": "new_status is a real session status but not one of Approved/Declined/Resubmitted", + "Non-alphabetic country code": { + "summary": "2 characters but not letters", "value": { - "new_status": [ - "You can only update the status to 'Approved', 'Declined', or 'Resubmitted'." + "country_code": [ + "country_code must be a 2-letter ISO 3166-1 alpha-2 code." ] } }, - "Missing new_status": { - "summary": "Required field omitted", + "Invalid currency": { + "summary": "3 characters but not an ISO 4217 code", "value": { - "new_status": [ - "This field is required." + "registered_capital_currency": [ + "Currency must be a 3-letter ISO 4217 code (e.g. USD, EUR, GBP)." ] } }, - "Same Status": { - "summary": "New status equals the current status", - "value": [ - "The new status is the same as the current status." - ] - }, - "Email Address Required": { - "summary": "send_email true without email_address", + "Invalid phone": { + "summary": "Phone fails the format / digit-count check", "value": { - "email_address": [ - "Email address is required when send_email is true." + "phone": [ + "Invalid phone number format." ] } }, - "Non-resubmittable Feature": { - "summary": "Resubmit targets a KYB parent check", + "Invalid registry status": { + "summary": "registry_status not in the enum", "value": { - "nodes_to_resubmit": "The following features cannot be resubmitted directly: KYB_REGISTRY. Resubmit the relevant child KYC sessions instead; the parent will recompute automatically." + "registry_status": [ + "\"liquidated\" is not a valid choice." + ] } }, - "Nothing To Resubmit": { - "summary": "Auto-select found no non-approved features", + "Invalid email": { + "summary": "Malformed email address", "value": { - "nodes_to_resubmit": "No features found that need resubmission. All features are already approved." + "email": [ + "Enter a valid email address." + ] } }, - "Invalid Feature Choice": { - "summary": "Unknown feature in nodes_to_resubmit", + "Invalid website": { + "summary": "Value fails URL validation even after https:// normalization", "value": { - "nodes_to_resubmit": [ - { - "feature": [ - "\"BOGUS\" is not a valid choice. Valid choices are: OCR, OCR_BACK, NFC, AML, FACE, LIVENESS, FACE_MATCH, IP_ANALYSIS, AGE_ESTIMATION, PROOF_OF_ADDRESS, PHONE_VERIFICATION, EMAIL_VERIFICATION, FACE_SEARCH, DATABASE_VALIDATION, QUESTIONNAIRE, KYB_REGISTRY, KYB_DOCUMENTS, KYB_KEY_PEOPLE, ID_VERIFICATION, POA, PHONE, EMAIL, KYB" - ] - } + "website": [ + "Enter a valid URL." ] } }, - "Invalid Status Value (not a status at all)": { - "summary": "new_status is not a valid StatusChoices value", + "Bad date format": { + "summary": "Dates must be YYYY-MM-DD", "value": { - "new_status": [ - "\"Bogus\" is not a valid choice." + "incorporation_date": [ + "Date has wrong format. Use one of these formats instead: YYYY-MM-DD." ] } } @@ -11330,7 +30481,7 @@ } }, "Missing Privilege": { - "summary": "API key lacks write:sessions", + "summary": "Credential lacks write:businesses", "value": { "detail": "You do not have permission to perform this action." } @@ -11340,1542 +30491,3040 @@ } }, "404": { - "description": "No KYC or KYB session with this `session_id` exists in your application (deleted sessions also return `404`).", + "description": "Unknown session, unknown company, or a company that belongs to a different session. Each case has a distinct `detail` message (see examples). The unknown-session lookup runs before credential checks.", "content": { "application/json": { "examples": { - "Not Found": { - "summary": "Unknown session_id", + "Unknown session": { + "summary": "No session with this ID in your application (or it was deleted)", "value": { "detail": "Not found." } + }, + "Unknown company": { + "summary": "No company with this UUID in your application", + "value": { + "detail": "No KYBCompany matches the given query." + } + }, + "Company / session mismatch": { + "summary": "companyUuid exists but belongs to another session", + "value": { + "detail": "KYB company does not belong to the given session." + } } } } } }, "429": { - "description": "A resubmit targeting only backend-only features (AML, DATABASE_VALIDATION, IP_ANALYSIS) was repeated within the 30-second cooldown window; an immediate repeat cannot change the outcome. Back off and retry after the interval indicated in `Retry-After`.", + "description": "Shared write rate limit exceeded (300 POST/PATCH/DELETE requests per minute per API key). Inspect `Retry-After` and the `X-RateLimit-*` response headers before retrying.", "content": { "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, "examples": { - "Backend-only resubmit throttled": { + "Rate limited": { + "summary": "Write budget exhausted", "value": { - "detail": "This session was already resubmitted for backend-only features within the last 30 seconds; repeating it cannot change the outcome. Wait before resubmitting again." + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." } } } } } - }, - "409": { - "description": "The email preview changed or expired. Generate a new preview before confirming; no session changes or email delivery occur." } }, "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X PATCH \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-status/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"new_status\": \"Approved\",\n \"comment\": \"All checks passed manual review\"\n }'" + "source": "curl -X PATCH \\\n https://verification.didit.me/v3/session/33333333-4444-5555-6666-777777777777/kyb/88888888-9999-aaaa-bbbb-cccccccccccc/update-data/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"company_name\": \"Docs Probe Corp\",\n \"registration_number\": \"DPC-001\",\n \"country_code\": \"US\",\n \"incorporation_date\": \"2015-04-12\",\n \"company_type\": \"Limited Liability Company\",\n \"legal_address\": \"1 Example Plaza, Wilmington, DE 19801\",\n \"registry_status\": \"active\"\n }'" }, { "lang": "python", "label": "Python", - "source": "import requests\n\nresponse = requests.patch(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-status/\",\n headers={\n \"x-api-key\": \"YOUR_API_KEY\",\n \"Content-Type\": \"application/json\",\n },\n json={\n \"new_status\": \"Resubmitted\",\n \"nodes_to_resubmit\": [\n {\"node_id\": \"feature_ocr\", \"feature\": \"OCR\"},\n {\"node_id\": \"feature_liveness\", \"feature\": \"LIVENESS\"},\n ],\n \"send_email\": True,\n \"email_address\": \"user@example.com\",\n \"email_language\": \"en\",\n },\n)\nresponse.raise_for_status()\nprint(response.json()) # {'session_id': '11111111-2222-3333-4444-555555555555'}" + "source": "import requests\n\nresponse = requests.patch(\n \"https://verification.didit.me/v3/session/33333333-4444-5555-6666-777777777777/kyb/\"\n \"88888888-9999-aaaa-bbbb-cccccccccccc/update-data/\",\n headers={\n \"x-api-key\": \"YOUR_API_KEY\",\n \"Content-Type\": \"application/json\",\n },\n json={\n \"company_name\": \"Docs Probe Corp\",\n \"registration_number\": \"DPC-001\",\n \"country_code\": \"US\",\n \"incorporation_date\": \"2015-04-12\",\n \"legal_address\": \"1 Example Plaza, Wilmington, DE 19801\",\n \"registry_status\": \"active\",\n },\n)\nresponse.raise_for_status()\ncompany = response.json() # full company record\nprint(company[\"company_name\"], company[\"registered_address\"])" }, { "lang": "javascript", "label": "JavaScript", - "source": "const response = await fetch(\n 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-status/',\n {\n method: 'PATCH',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n new_status: 'Declined',\n comment: 'Suspected fraud',\n }),\n },\n);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst { session_id } = await response.json();" + "source": "const response = await fetch(\n 'https://verification.didit.me/v3/session/33333333-4444-5555-6666-777777777777/kyb/' +\n '88888888-9999-aaaa-bbbb-cccccccccccc/update-data/',\n {\n method: 'PATCH',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n company_name: 'Docs Probe Corp',\n registration_number: 'DPC-001',\n country_code: 'US',\n incorporation_date: '2015-04-12',\n legal_address: '1 Example Plaza, Wilmington, DE 19801',\n registry_status: 'active',\n }),\n },\n);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst company = await response.json(); // full company record" } ] } }, - "/v3/id-verification/": { - "post": { - "summary": "ID Verification (document OCR + fraud checks)", - "description": "OCR an identity document and run document fraud checks in one call — returns the extracted holder fields, the parsed MRZ, an `Approved`/`Declined` `status`, and a `warnings` list explaining every issue found. Supports 14,000+ document types from 220+ countries and territories.\n\n**How it works.** Send the document's `front_image` (required) and, for two-sided documents, the `back_image`. Upload uncropped photos with all four corners of the document visible — the service detects, aligns, and crops the document itself, so do not pre-crop. The document is classified automatically (you never declare the country or document type), the visual zone is read with OCR, and the MRZ and any barcodes are decoded. Cross-checks then validate dates, number formats, and MRZ check digits, and compare the visual zone against the MRZ. With `perform_document_liveness=true`, the images are additionally screened for screen replays, printed copies, and portrait manipulation.\n\n**Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Fraud and hard-failure risks always decline: `DOCUMENT_EXPIRED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, `PUBLIC_DOCUMENT_IMAGE_DETECTED`, plus extraction failures (`NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`, `DOCUMENT_NUMBER_NOT_DETECTED`). (`PORTRAIT_IMAGE_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `INVALID_DATE`, and `MRZ_NOT_DETECTED` exist in workflow sessions but are never produced by this standalone endpoint.) Three risk groups are configurable per request via `invalid_mrz_action`, `inconsistent_data_action`, and `expiration_date_not_detected_action` (`DECLINE` or `NO_ACTION`). All other warnings (e.g. `POSSIBLE_DUPLICATED_USER`) are informational and never decline on their own. A document that cannot be processed at all returns `400` with `{\"error\": \"COULD_NOT_RECOGNIZE_DOCUMENT\"}`; a readable but problematic document returns `200` with `status: \"Declined\"` — always inspect `id_verification.status` and `id_verification.warnings`, not just the HTTP code.\n\n**Billing.** Each `200` response consumes one ID Verification API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing.\n\n**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the cropped document/portrait images are stored and returned as short-lived media URLs (`https:///ocr/...`), and a `status.updated` webhook is emitted to your configured webhook endpoints. When `false`, nothing is stored, `request_id` is a one-off correlation UUID, and `portrait_image`/`front_image`/`back_image` are returned inline as base64-encoded JPEG strings.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (a fictional \"Sandbox User\" USA identity card), no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", - "operationId": "post_v3id-verification", + "/v3/session/{sessionId}/kyb/parties/{partyUuid}/": { + "patch": { + "summary": "Skip (or re-require) a Key People party on a Business Verification (KYB) session", + "description": "Exempt a Key People party from verification, or put it back in scope. This is the operator escape hatch for a party that can never finish its own check - most often because the individual KYC session handed to a UBO or director was deleted, which freezes that child's status and leaves the KYB parked in `Awaiting User` with nothing left that could move it.\n\nSetting `is_skipped: true` drops the party out of the Key People aggregation and immediately recomputes the `KYBKeyPeopleCheck` and the parent session status, so the business session can settle and then be approved or declined. Sending `is_skipped: false` puts the party back in scope and the check goes back to waiting on it.\n\nUnlike the applicant-facing skip in the hosted Key People screen, this endpoint is **not** gated by the role's `allow_skip` setting - that flag limits what the applicant may decide, not what your analyst may override. `requires_verification` is deliberately left untouched, so an overridden party reads as `requires_verification: true` **and** `is_skipped: true` and the record still shows what the rules asked for.\n\nThe party must belong to the session in the path (`404` with `\"Party does not belong to the given session.\"` otherwise) and to your application. A PATCH that actually changes the flag fires the `data.updated` [webhook](/integration/webhooks) on the business session with trigger `console_key_people_skip`; a PATCH that sets the flag to its current value is a no-op that echoes the party back.\n\nAccepts an API key or a console user token; either way the credential needs the `write:businesses` permission.", + "operationId": "patch_v3_session_kyb_party_update", + "x-didit-sdk": { + "resource": "sessions", + "method": "update_kyb_party", + "auth": "api_key", + "server": true + }, "tags": [ - "Standalone APIs" + "Sessions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "UUID of the parent Business Verification (KYB) session.", + "schema": { + "type": "string", + "format": "uuid", + "example": "33333333-4444-5555-6666-777777777777" + } + }, + { + "name": "partyUuid", + "in": "path", + "required": true, + "description": "UUID of the Key People party to update (`key_people_checks[].submitted.parties[].uuid` on the business session decision). Must belong to the session referenced by `sessionId`.", + "schema": { + "type": "string", + "format": "uuid", + "example": "0a3f1e2c-7c2f-4a1b-9d55-2f8d2a5d6b41" + } + } ], - "parameters": [], "requestBody": { "required": true, "content": { - "multipart/form-data": { + "application/json": { "schema": { "type": "object", "required": [ - "front_image" + "is_skipped" ], "properties": { - "front_image": { - "type": "string", - "format": "binary", - "description": "Front side of the identity document. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`, `pdf`. Maximum upload size: **10 MB** (larger files are rejected with `400`). PDFs are rendered to an image before OCR; encrypted PDFs require `front_image_password`. Images are automatically compressed to ~0.5 MB and EXIF orientation is applied. Upload the full, uncropped capture with all four corners of the document visible." - }, - "front_image_password": { - "type": "string", - "description": "Password to decrypt `front_image` when it is an encrypted PDF. Sending an encrypted PDF without (or with a wrong) password returns `400`.", - "writeOnly": true - }, - "back_image": { - "type": "string", - "format": "binary", - "description": "Back side of the document — send it whenever the document has one (many ID cards carry the MRZ or a barcode on the back; omitting it simply leaves the MRZ and back-side fields empty in the response — this endpoint does not raise a missing-MRZ warning). Same format and size limits as `front_image`. When omitted, `back_image` and back-side fields are `null` in the response." - }, - "back_image_password": { - "type": "string", - "description": "Password to decrypt `back_image` when it is an encrypted PDF.", - "writeOnly": true - }, - "perform_document_liveness": { - "type": "boolean", - "default": true, - "description": "Screens both images for presentation fraud: screen replays (`SCREEN_CAPTURE_DETECTED`), printed copies (`PRINTED_COPY_DETECTED`), and portrait manipulation (`PORTRAIT_MANIPULATION_DETECTED`). Any of these auto-declines. Enabled by default; set to `false` to skip fraud screening and reduce latency.", - "example": true - }, - "minimum_age": { - "type": "integer", - "nullable": true, - "default": null, - "minimum": 1, - "maximum": 120, - "description": "Accepted and validated (1–120) but **currently not applied** by this endpoint — it never produces a `MINIMUM_AGE_NOT_MET` warning from this field. Enforce age rules from the returned `date_of_birth`/`age`, or use a verification-session workflow with age restrictions." - }, - "expiration_date_not_detected_action": { - "type": "string", - "enum": [ - "NO_ACTION", - "DECLINE" - ], - "default": "NO_ACTION", - "description": "What to do when no expiration date can be read (`EXPIRATION_DATE_NOT_DETECTED`). Default `NO_ACTION` because many identity documents print no expiry date. Note: a *detected and past* expiry date always declines (`DOCUMENT_EXPIRED`), regardless of this option." - }, - "invalid_mrz_action": { - "type": "string", - "enum": [ - "NO_ACTION", - "DECLINE" - ], - "default": "DECLINE", - "description": "What to do when the extracted MRZ fails check-digit validation (`MRZ_VALIDATION_FAILED`). A missing MRZ raises no warning on this endpoint. Only relevant for documents that carry an MRZ. `REVIEW` is not supported on this endpoint and returns `400`." - }, - "inconsistent_data_action": { - "type": "string", - "enum": [ - "NO_ACTION", - "DECLINE" - ], - "default": "DECLINE", - "description": "What to do when extracted data is internally inconsistent: `DATA_INCONSISTENT`, `MRZ_AND_DATA_EXTRACTED_FROM_OCR_NOT_SAME` (visual zone disagrees with the MRZ), or `DOCUMENT_NAME_DIFFERENT_FROM_OTHER_APPROVED_DOCUMENTS`." - }, - "critical_field_occlusion_action": { - "type": "string", - "enum": [ - "NO_ACTION", - "DECLINE" - ], - "default": "DECLINE", - "description": "What to do when a critical printed field was physically covered on the capture (`CRITICAL_FIELD_OCCLUSION_DETECTED`) - an identity number, a name, the date of birth, or the expiry date. Declines by default, like this endpoint's other document-integrity risks: covering the numbers a report is reconciled against is what a redacted document looks like. The value read for a covered field is dropped from the extraction either way, so the matching `*_NOT_DETECTED` warning usually accompanies it. `REVIEW` is not supported on this endpoint and returns `400`." - }, - "document_or_personal_number_format_mismatch_action": { - "type": "string", - "enum": [ - "NO_ACTION", - "DECLINE" - ], - "default": "NO_ACTION", - "description": "What to do when a number does not match the format its issuing authority uses for that document type (`DOCUMENT_NUMBER_FORMAT_MISMATCH`, `PERSONAL_NUMBER_FORMAT_MISMATCH`). Default `NO_ACTION`, which reports the warning without changing the status. `REVIEW` is not supported on this endpoint and returns `400`." - }, - "preferred_characters": { - "type": "string", - "enum": [ - "latin", - "non_latin" - ], - "default": "latin", - "description": "Preferred script for name/address fields on documents that print both Latin and non-Latin text (Arabic, Cyrillic, CJK, …). `latin` returns transliterated/Latin values; `non_latin` prefers the native script." - }, - "save_api_request": { + "is_skipped": { "type": "boolean", - "default": true, - "description": "When `true` (default), persists the call as an API-type session — visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with document images returned as media URLs. When `false`, nothing is stored, `request_id` is a transient UUID, and images are returned inline as base64 JPEG strings.", + "description": "`true` exempts the party from verification so the Key People check can settle without it; `false` puts it back in scope.", "example": true - }, - "vendor_data": { - "type": "string", - "description": "Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later.", - "example": "user-123" - }, - "metadata": { - "type": "object", - "additionalProperties": true, - "description": "Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={\"flow\":\"onboarding\"}`) — it is parsed into an object.", - "example": { - "flow": "onboarding" - } } } - }, - "example": { - "front_image": "(binary JPEG/PNG/PDF of the document front)", - "back_image": "(binary JPEG/PNG/PDF of the document back)", - "perform_document_liveness": true, - "save_api_request": true, - "vendor_data": "user-123", - "metadata": { - "flow": "onboarding" - } } } } }, "responses": { "200": { - "description": "Document processed. `id_verification.status` is `Approved` or `Declined`; every detected issue is itemized in `id_verification.warnings`. A problematic document still returns `200` with `status: \"Declined\"` — inspect the body, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id and image fields are short-lived media URLs; with `save_api_request=false` they are inline base64 JPEG strings.", + "description": "Party updated (or echoed unchanged when the flag already had this value). Returns the party - the same shape as an entry of `key_people_checks[].submitted.parties[]` on the business session decision.", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Key People party record - the same shape as an entry of `key_people_checks[].submitted.parties[]` on the business session decision.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Party identifier." + }, + "entity_type": { + "type": "string", + "enum": [ + "person", + "company" + ], + "description": "Whether the party is an individual or a company." + }, + "name": { + "type": "string", + "description": "Display name of the party." + }, + "is_skipped": { + "type": "boolean", + "description": "Whether the party is exempt from verification." + }, + "requires_verification": { + "type": "boolean", + "description": "What the role and threshold rules asked for. Left untouched by this endpoint." + }, + "kyc_session_status": { + "type": "string", + "nullable": true, + "description": "Status of the linked individual KYC session, frozen at deletion time when the session was deleted." + }, + "kyc_session_url": { + "type": "string", + "nullable": true, + "description": "Link to the linked individual KYC session. `null` when there is none or when it was deleted." + }, + "kyc_session_deleted": { + "type": "boolean", + "description": "The linked individual KYC session was deleted, so it can never settle." + }, + "kyb_sub_session_deleted": { + "type": "boolean", + "description": "Corporate-party counterpart of `kyc_session_deleted`." + } + } + } + } + } + }, + "400": { + "description": "Validation error on the request body. `is_skipped` is the only field this endpoint reads, and it is required. Field errors arrive as `{\"\": [\"\"]}`. Body validation runs *after* the party lookup, so a malformed `is_skipped` sent to a party that does not exist reports `404`, not `400`.", "content": { "application/json": { "examples": { - "Approved": { - "summary": "Two-sided ID card read cleanly (saved — images as media URLs)", + "Missing is_skipped": { + "summary": "Empty body, or a body that omits `is_skipped`", "value": { - "request_id": "11d219ed-d59c-4b1d-8d65-9b933f12d5b8", - "id_verification": { - "status": "Approved", - "document_type": "Identity Card", - "document_subtype": "ID_CARD_GENERIC", - "document_number": "CBX164224", - "personal_number": "20446581H", - "portrait_image": "https:///ocr/11d219ed-d59c-4b1d-8d65-9b933f12d5b8-portrait_image-8c2f.jpg?signature=...", - "front_image": "https:///ocr/11d219ed-d59c-4b1d-8d65-9b933f12d5b8-front_image-4a1e.jpg?signature=...", - "back_image": "https:///ocr/11d219ed-d59c-4b1d-8d65-9b933f12d5b8-back_image-77b0.jpg?signature=...", - "front_image_camera_front": null, - "back_image_camera_front": null, - "front_image_camera_front_face_match_score": null, - "back_image_camera_front_face_match_score": null, - "front_image_quality_score": { - "focus_score": 78.0, - "brightness_score": 83.2, - "brightness_issue": "ok", - "is_document_fully_visible": true, - "resolution_score": 40.7, - "overall_score": 70.2 - }, - "back_image_quality_score": { - "focus_score": 100.0, - "brightness_score": 95.6, - "brightness_issue": "ok", - "is_document_fully_visible": true, - "resolution_score": 43.0, - "overall_score": 84.4 - }, - "date_of_birth": "1980-01-13", - "age": 46, - "expiration_date": "2032-03-31", - "date_of_issue": "2022-03-31", - "issuing_state": "ESP", - "issuing_state_name": "Spain", - "first_name": "Julio Francisco", - "last_name": "Fores Sena", - "full_name": "Julio Francisco Fores Sena", - "gender": "M", - "address": "Brda. Urb. El Cardonal 0052 52 Po3 B,Taco,San Cristobal De La Laguna,Santa Cruz De Tenerife", - "formatted_address": "Av. el Cardonal, 52, b, 38108 La Laguna, Santa Cruz de Tenerife, Spain", - "place_of_birth": "Valencia, Valencia", - "marital_status": "UNKNOWN", - "nationality": "ESP", - "extra_fields": { - "first_surname": "Fores", - "second_surname": "Sena" - }, - "mrz": { - "surname": "FORES SENA", - "name": "JULIO FRANCISCO", - "country": "ESP", - "nationality": "ESP", - "birth_date": "800113", - "expiry_date": "320331", - "sex": "M", - "document_type": "ID", - "document_number": "CBX164224", - "optional_data": "20446581H", - "optional_data_2": "", - "birth_date_hash": "9", - "expiry_date_hash": "8", - "document_number_hash": "3", - "final_hash": "3", - "personal_number": "20446581H", - "warnings": [], - "errors": [], - "mrz_type": "TD1", - "mrz_string": "IDESPCBX164224320446581H<<<<<<\n8001139M3203318ESP<<<<<<<<<<<3\nFORES/ocr/f61681b8-9422-4378-ac3e-04c6a247fb45-portrait_image-1d9a.jpg?signature=...", - "front_image": "https:///ocr/f61681b8-9422-4378-ac3e-04c6a247fb45-front_image-5c2b.jpg?signature=...", - "back_image": null, - "front_image_camera_front": null, - "back_image_camera_front": null, - "front_image_camera_front_face_match_score": null, - "back_image_camera_front_face_match_score": null, - "front_image_quality_score": { - "focus_score": 100.0, - "brightness_score": 95.7, - "brightness_issue": "ok", - "is_document_fully_visible": true, - "resolution_score": 22.2, - "overall_score": 79.3 - }, - "back_image_quality_score": null, - "date_of_birth": "1997-01-30", - "age": 29, - "expiration_date": "2025-11-26", - "date_of_issue": null, - "issuing_state": "ESP", - "issuing_state_name": "Spain", - "first_name": "Lucia", - "last_name": "Marin Castro", - "full_name": "Lucia Marin Castro", - "gender": "F", - "address": null, - "formatted_address": null, - "place_of_birth": null, - "marital_status": "UNKNOWN", - "nationality": "ESP", - "extra_fields": { - "first_surname": "Marin", - "second_surname": "Castro" - }, - "mrz": {}, - "parsed_address": null, - "warnings": [ - { - "risk": "DOCUMENT_EXPIRED", - "feature": "ID_VERIFICATION", - "additional_data": null, - "log_type": "error", - "short_description": "Document expired", - "long_description": "The document's expiration date has passed, rendering it no longer valid for use." - } - ], - "barcodes": [] - }, - "vendor_data": "user-123", - "metadata": null, - "created_at": "2026-06-12T02:25:48.117204+00:00" + "detail": "You do not have permission to perform this action." } } - }, + } + } + } + }, + "404": { + "description": "Unknown party, or a party that belongs to a different session. Each case has a distinct `detail` message (see examples). Credential and permission checks run before the party lookup, so an unauthorised call reports `403` rather than `404`. There is no separate session lookup: an unknown `sessionId` paired with a party of yours reports the mismatch message.", + "content": { + "application/json": { + "examples": { + "Unknown party": { + "summary": "No party with this UUID in your application", + "value": { + "detail": "Not found." + } + }, + "Party / session mismatch": { + "summary": "partyUuid exists but hangs off another session", + "value": { + "detail": "Party does not belong to the given session." + } + } + } + } + } + }, + "429": { + "description": "Shared write rate limit exceeded (300 POST/PATCH/DELETE requests per minute per API key). Inspect `Retry-After` and the `X-RateLimit-*` response headers before retrying.", + "content": { + "application/json": { "schema": { "type": "object", "properties": { - "request_id": { - "type": "string", - "format": "uuid", - "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." - }, - "id_verification": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "Approved", - "Declined" - ], - "description": "`Approved` when no warning resolves to a decline; `Declined` otherwise — the `warnings` list explains why." - }, - "document_type": { - "type": "string", - "nullable": true, - "description": "Detected document type display name: `Identity Card`, `Passport`, `Driver's License`, `Residence Permit`, `Health Insurance Card`, or `Tax Card`. `null` when the document could not be classified.", - "example": "Identity Card" - }, - "document_subtype": { - "type": "string", - "nullable": true, - "description": "Finer-grained subtype when available (e.g. `ID_CARD_GENERIC`, `EPASSPORT`).", - "example": "ID_CARD_GENERIC" - }, - "document_number": { - "type": "string", - "nullable": true, - "example": "CBX164224" - }, - "personal_number": { - "type": "string", - "nullable": true, - "description": "Secondary personal/national number printed on some documents.", - "example": "20446581H" - }, - "portrait_image": { - "type": "string", - "nullable": true, - "description": "Cropped portrait photo from the document. A short-lived media URL (`https:///ocr/...`) when `save_api_request=true` (default); an inline base64-encoded JPEG string when `save_api_request=false`. `null` when no portrait was detected." - }, - "front_image": { - "type": "string", - "nullable": true, - "description": "Aligned crop of the document front. Media URL when saved, inline base64 JPEG otherwise." - }, - "back_image": { - "type": "string", - "nullable": true, - "description": "Aligned crop of the document back. `null` when no `back_image` was uploaded." - }, - "front_image_camera_front": { - "type": "string", - "nullable": true, - "description": "Always `null` on this endpoint (populated only in camera-capture verification sessions)." - }, - "back_image_camera_front": { - "type": "string", - "nullable": true, - "description": "Always `null` on this endpoint." - }, - "front_image_camera_front_face_match_score": { - "type": "number", - "nullable": true, - "description": "Always `null` on this endpoint." - }, - "back_image_camera_front_face_match_score": { - "type": "number", - "nullable": true, - "description": "Always `null` on this endpoint." - }, - "front_image_quality_score": { - "type": "object", - "nullable": true, - "description": "Capture-quality diagnostics for the uploaded image (`null` when the side was not provided or scoring failed). All scores are 0–100.", - "properties": { - "focus_score": { - "type": "number", - "format": "float", - "description": "Sharpness of the document crop (higher is sharper)." - }, - "brightness_score": { - "type": "number", - "format": "float", - "description": "Exposure quality (higher is better)." - }, - "brightness_issue": { - "type": "string", - "nullable": true, - "description": "`ok` when exposure is fine, otherwise the detected issue (e.g. too dark / too bright)." - }, - "is_document_fully_visible": { - "type": "boolean", - "nullable": true, - "description": "Whether all four corners of the document are inside the frame." - }, - "resolution_score": { - "type": "number", - "format": "float", - "description": "Effective resolution of the document area." - }, - "overall_score": { - "type": "number", - "format": "float", - "description": "Aggregate quality score." - } - } - }, - "back_image_quality_score": { - "type": "object", - "nullable": true, - "description": "Capture-quality diagnostics for the uploaded image (`null` when the side was not provided or scoring failed). All scores are 0–100.", - "properties": { - "focus_score": { - "type": "number", - "format": "float", - "description": "Sharpness of the document crop (higher is sharper)." - }, - "brightness_score": { - "type": "number", - "format": "float", - "description": "Exposure quality (higher is better)." - }, - "brightness_issue": { - "type": "string", - "nullable": true, - "description": "`ok` when exposure is fine, otherwise the detected issue (e.g. too dark / too bright)." - }, - "is_document_fully_visible": { - "type": "boolean", - "nullable": true, - "description": "Whether all four corners of the document are inside the frame." - }, - "resolution_score": { - "type": "number", - "format": "float", - "description": "Effective resolution of the document area." - }, - "overall_score": { - "type": "number", - "format": "float", - "description": "Aggregate quality score." - } - } - }, - "date_of_birth": { - "type": "string", - "format": "date", - "nullable": true, - "example": "1980-01-13" - }, - "age": { - "type": "integer", - "nullable": true, - "description": "Holder age in years, computed from `date_of_birth`.", - "example": 46 - }, - "expiration_date": { - "type": "string", - "format": "date", - "nullable": true, - "example": "2032-03-31" - }, - "date_of_issue": { - "type": "string", - "format": "date", - "nullable": true, - "example": "2022-03-31" - }, - "issuing_state": { - "type": "string", - "nullable": true, - "description": "Issuing country (ISO 3166-1 alpha-3).", - "example": "ESP" - }, - "issuing_state_name": { - "type": "string", - "nullable": true, - "example": "Spain" - }, - "first_name": { - "type": "string", - "nullable": true, - "example": "Julio Francisco" - }, - "last_name": { - "type": "string", - "nullable": true, - "example": "Fores Sena" - }, - "full_name": { - "type": "string", - "nullable": true, - "example": "Julio Francisco Fores Sena" - }, - "gender": { - "type": "string", - "nullable": true, - "description": "`M`, `F`, or `null` when not printed/detected.", - "example": "M" - }, - "address": { - "type": "string", - "nullable": true, - "description": "Address exactly as printed on the document." - }, - "formatted_address": { - "type": "string", - "nullable": true, - "description": "Geocoded, normalized version of `address`." - }, - "place_of_birth": { - "type": "string", - "nullable": true, - "example": "Valencia, Valencia" - }, - "marital_status": { - "type": "string", - "nullable": true, - "description": "`UNKNOWN` unless the document prints marital status.", - "example": "UNKNOWN" - }, - "nationality": { - "type": "string", - "nullable": true, - "description": "Holder nationality (ISO 3166-1 alpha-3).", - "example": "ESP" - }, - "extra_fields": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Document-specific extras that have no dedicated column (e.g. `first_surname`/`second_surname` on Spanish IDs, license classes on driving licenses)." - }, - "mrz": { - "type": "object", - "description": "Parsed machine-readable zone. An **empty object** (`{}`) when the document has no MRZ or it could not be read — also check the `warnings` list in that case. Dates inside the MRZ use the raw `YYMMDD` format.", - "properties": { - "surname": { - "type": "string", - "example": "FORES SENA" - }, - "name": { - "type": "string", - "example": "JULIO FRANCISCO" - }, - "country": { - "type": "string", - "description": "Issuing country (ISO 3166-1 alpha-3).", - "example": "ESP" - }, - "nationality": { - "type": "string", - "description": "Holder nationality (ISO 3166-1 alpha-3).", - "example": "ESP" - }, - "birth_date": { - "type": "string", - "description": "Raw MRZ date of birth (`YYMMDD`).", - "example": "800113" - }, - "expiry_date": { - "type": "string", - "description": "Raw MRZ expiry date (`YYMMDD`).", - "example": "320331" - }, - "sex": { - "type": "string", - "example": "M" - }, - "document_type": { - "type": "string", - "description": "MRZ document-type code (e.g. `ID`, `P`).", - "example": "ID" - }, - "document_number": { - "type": "string", - "example": "CBX164224" - }, - "optional_data": { - "type": "string", - "nullable": true, - "example": "20446581H" - }, - "optional_data_2": { - "type": "string", - "nullable": true, - "example": "" - }, - "birth_date_hash": { - "type": "string", - "description": "MRZ check digit for the birth date.", - "example": "9" - }, - "expiry_date_hash": { - "type": "string", - "description": "MRZ check digit for the expiry date.", - "example": "8" - }, - "document_number_hash": { - "type": "string", - "description": "MRZ check digit for the document number.", - "example": "3" - }, - "final_hash": { - "type": "string", - "description": "Composite MRZ check digit.", - "example": "3" - }, - "personal_number": { - "type": "string", - "nullable": true, - "example": "20446581H" - }, - "warnings": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Non-fatal MRZ parsing warnings." - }, - "errors": { - "type": "array", - "items": { - "type": "string" - }, - "description": "MRZ check-digit/parsing errors. Non-empty errors surface as an `MRZ_VALIDATION_FAILED` warning." - }, - "mrz_type": { - "type": "string", - "description": "MRZ layout: `TD1` (ID cards, 3 lines), `TD2`, or `TD3` (passports, 2 lines).", - "example": "TD1" - }, - "mrz_string": { - "type": "string", - "description": "Raw MRZ text as read, lines separated by `\\n`." - }, - "mrz_key": { - "type": "string", - "description": "Composite key (document number + check digits) used for cross-checks." - } - } + "detail": { + "type": "string" + } + } + }, + "examples": { + "Rate limited": { + "summary": "Write budget exhausted", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + } + } + } + } + } + } + }, + "/v3/travel-rule/settings/": { + "get": { + "summary": "Get Travel Rule settings", + "description": "Read the application's Travel Rule settings and VASP profile. Auto-creates a default (disabled) settings row and an empty profile on first read.", + "operationId": "getTravelRuleSettings", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "retrieve_settings", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET https://verification.didit.me/v3/travel-rule/settings/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Current Travel Rule settings and VASP profile.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleSettingsDetail" + }, + "examples": { + "Disabled (default)": { + "value": { + "is_enabled": false, + "jurisdiction": "EU", + "name_matching_strictness": "DEFAULT", + "confirmation_timeout_hours": 48, + "timeout_outcome": "HOLD", + "threshold_amount": "0.00", + "inbound_auto_accept": true, + "allow_self_declaration": true, + "allow_screenshot_proof": true, + "auto_wallet_verification": true, + "vasp_attribution_enabled": true, + "legal_name": "", + "lei": "", + "compliance_email": "", + "is_discoverable": true, + "counterparty_screening_enabled": false, + "sunrise_policy": "", + "sunrise_jurisdictions": [ + "CO", + "KH", + "VN" + ], + "non_responsive_policy": "", + "travel_address": "", + "disabled_networks": [], + "networks": { + "gtr": { + "rail": "GTR", + "mode": "unavailable", + "platform_available": false, + "connected": false }, - "parsed_address": { - "type": "object", - "nullable": true, - "description": "Structured, geocoded breakdown of `address`. `null` when the document carries no address or it could not be parsed.", - "properties": { - "street_1": { - "type": "string", - "nullable": true, - "example": "Avenida el Cardonal 52" - }, - "street_2": { - "type": "string", - "nullable": true, - "example": "b" - }, - "city": { - "type": "string", - "nullable": true, - "example": "La Laguna" - }, - "region": { - "type": "string", - "nullable": true, - "example": "Canarias" - }, - "country": { - "type": "string", - "nullable": true, - "description": "ISO 3166-1 alpha-2 country code.", - "example": "ES" - }, - "postal_code": { - "type": "string", - "nullable": true, - "example": "38108" - }, - "address_type": { - "type": "string", - "nullable": true, - "example": "Avenida" - }, - "formatted_address": { - "type": "string", - "nullable": true, - "description": "Canonical geocoded address string.", - "example": "Av. el Cardonal, 52, b, 38108 La Laguna, Santa Cruz de Tenerife, Spain" - }, - "raw_results": { - "type": "object", - "additionalProperties": true, - "description": "Raw geocoding payload (`address_components`, `geometry.location`, `place_id`, …) for advanced consumers." - }, - "document_location": { - "type": "object", - "nullable": true, - "properties": { - "latitude": { - "type": "number" - }, - "longitude": { - "type": "number" - } - }, - "description": "Geocoded coordinates of the document address." - }, - "label": { - "type": "string", - "nullable": true, - "description": "Which document field the address was read from.", - "example": "Spain Identity Card Address" - }, - "is_verified": { - "type": "boolean", - "description": "Whether geocoding confirmed the address exists." - }, - "category": { - "type": "string", - "nullable": true, - "description": "Address category (e.g. `Residential`, `Commercial`).", - "example": "Residential" - } - } + "trust": { + "rail": "TRUST", + "mode": "unavailable", + "platform_available": false, + "connected": false }, - "warnings": { - "type": "array", - "description": "Empty on a clean approval. Every entry explains one detected risk; entries with `log_type: \"error\"` set `status` to `Declined`.", - "items": { - "type": "object", - "properties": { - "risk": { - "type": "string", - "description": "Machine-readable risk code. Always-decline risks: `DOCUMENT_EXPIRED`, `MINIMUM_AGE_NOT_MET`, `PORTRAIT_IMAGE_NOT_DETECTED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, `PUBLIC_DOCUMENT_IMAGE_DETECTED`, `INVALID_DATE`, `COULD_NOT_RECOGNIZE_DOCUMENT` (400-only, never a warning), `DOCUMENT_NUMBER_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`. Governed by request options: `MRZ_NOT_DETECTED` / `MRZ_VALIDATION_FAILED` (`invalid_mrz_action`), `DATA_INCONSISTENT` / `MRZ_AND_DATA_EXTRACTED_FROM_OCR_NOT_SAME` / `DOCUMENT_NAME_DIFFERENT_FROM_OTHER_APPROVED_DOCUMENTS` (`inconsistent_data_action`), `EXPIRATION_DATE_NOT_DETECTED` (`expiration_date_not_detected_action`). Other risks (e.g. `POSSIBLE_DUPLICATED_USER`) are informational.", - "example": "DOCUMENT_EXPIRED" - }, - "feature": { - "type": "string", - "enum": [ - "ID_VERIFICATION" - ], - "description": "Feature that raised the warning. Always `ID_VERIFICATION` on this endpoint." - }, - "additional_data": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Extra context for some risks (e.g. `POSSIBLE_DUPLICATED_USER` includes `duplicated_session_id`); `null` for most." - }, - "log_type": { - "type": "string", - "enum": [ - "error", - "warning", - "information" - ], - "description": "Severity. `error` warnings drive `status` to `Declined`; `information` entries are advisory and never decline on their own. (`warning` is not produced by this endpoint — its options only accept `DECLINE` or `NO_ACTION`.)" - }, - "short_description": { - "type": "string", - "description": "Human-readable one-line summary of the risk." - }, - "long_description": { - "type": "string", - "description": "Human-readable explanation of the risk." - } - } - } + "verifyvasp": { + "rail": "VERIFYVASP", + "mode": "unavailable", + "platform_available": false, + "connected": false }, - "barcodes": { - "type": "array", - "description": "Barcodes decoded from the document (PDF417, QR, …). Empty when the document has none or none could be read.", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "Barcode type (e.g. `PDF417`, `QR_CODE`, `UNKNOWN`)." - }, - "position": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Location of the barcode in the image, when available." - }, - "data": { - "type": "string", - "description": "Decoded payload." - }, - "data_raw": { - "type": "string", - "description": "Raw (undecoded) payload, when different." - }, - "side": { - "type": "string", - "nullable": true, - "description": "`front` or `back`." - } - } - } + "sygna": { + "rail": "SYGNA", + "mode": "unavailable", + "platform_available": false, + "connected": false } + }, + "didit_network": { + "listed": false, + "missing": [ + "travel_rule_off", + "legal_name_missing", + "compliance_email_missing" + ], + "compliance_email_confirmed": false + } + } + } + } + } + } + } + } + }, + "put": { + "summary": "Update Travel Rule settings", + "description": "Upsert the application's Travel Rule settings and VASP profile. Partial update — only send the fields you want to change. Setting `is_enabled: true` requires a non-blank `legal_name` (already stored or included in the same request), otherwise returns 400.", + "operationId": "updateTravelRuleSettings", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "update_settings", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleSettingsUpdate" + }, + "example": { + "is_enabled": true, + "legal_name": "Origin CASP SL", + "jurisdiction": "EU", + "name_matching_strictness": "DEFAULT", + "confirmation_timeout_hours": 48, + "timeout_outcome": "HOLD", + "is_discoverable": true + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PUT https://verification.didit.me/v3/travel-rule/settings/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"is_enabled\": true, \"legal_name\": \"Origin CASP SL\", \"jurisdiction\": \"EU\"}'" + } + ], + "responses": { + "200": { + "description": "Updated settings and profile, in the same shape as GET.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleSettingsDetail" + } + } + } + }, + "400": { + "description": "Validation error, e.g. enabling without a legal_name, a sunrise_policy or non_responsive_policy other than HOLD, REJECT, PROCEED or empty, an unknown field in `matching_rules`, or a `data_sharing_fields` choice that cannot meet the originator obligations of your jurisdiction's regime (the errors are keyed by entity type under `data_sharing_fields`).", + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "legal_name": [ + "legal_name is required to enable travel rule." + ] + } + } + } + } + } + } + }, + "/v3/travel-rule/settings/compliance-email/confirmation/": { + "post": { + "summary": "Send the compliance email confirmation", + "description": "Emails a single-use confirmation link to your VASP profile's compliance_email. Opening the link and pressing its confirm button confirms the address, which the Didit network needs before it lists your profile. A new request replaces the previous link; the link stops working when the compliance email changes. One email per minute per address.", + "operationId": "sendTravelRuleComplianceEmailConfirmation", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "send_compliance_email_confirmation", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/settings/compliance-email/confirmation/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "202": { + "description": "The confirmation email was sent.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sent_to": { + "type": "string", + "format": "email" + } + } + }, + "example": { + "sent_to": "compliance@your-vasp.example" + } + } + } + }, + "400": { + "description": "No compliance email is set, or it is already confirmed.", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "examples": { + "No compliance email": { + "value": { + "compliance_email": [ + "Set a compliance email before confirming it." + ] + } + }, + "Already confirmed": { + "value": { + "compliance_email": [ + "This compliance email is already confirmed." + ] + } + } + } + } + } + }, + "429": { + "description": "A confirmation went to this address less than a minute ago." + }, + "503": { + "description": "The email could not be sent. Nothing changed; try again in a moment." + } + } + } + }, + "/v3/travel-rule/wallet-addresses/": { + "get": { + "summary": "List wallet address book entries", + "description": "Paginated list of your application's Travel Rule wallet address book, newest first.", + "operationId": "listTravelRuleWalletAddresses", + "x-didit-sdk": { + "resource": "travel_rule.wallet_addresses", + "method": "list", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50, + "minimum": 1 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + } + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET https://verification.didit.me/v3/travel-rule/wallet-addresses/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Paginated wallet address book entries.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "next": { + "type": "string", + "nullable": true + }, + "previous": { + "type": "string", + "nullable": true + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WalletAddressEntry" } + } + } + } + } + } + } + } + }, + "post": { + "summary": "Register a wallet address", + "description": "Add an entry to your Travel Rule wallet address book so inbound INTERNAL-rail transfers can resolve against it.", + "operationId": "createTravelRuleWalletAddress", + "x-didit-sdk": { + "resource": "travel_rule.wallet_addresses", + "method": "create", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WalletAddressEntryCreate" + }, + "example": { + "address": "0xBeneficiaryWallet01", + "chain": "ETH", + "holder_name": "Ana Diaz", + "holder_vendor_data": "user-042", + "self_declared": true + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/wallet-addresses/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"address\": \"0xBeneficiaryWallet01\", \"chain\": \"ETH\", \"holder_name\": \"Ana Diaz\", \"self_declared\": true}'" + } + ], + "responses": { + "201": { + "description": "The created entry.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WalletAddressEntry" + } + } + } + }, + "400": { + "description": "Duplicate wallet (address, chain and memo) for this application, a memo or X-address the chain cannot carry, a missing `address`, an invalid `travel_address`, `self_declared: true` while `allow_self_declaration` is off, or `self_declared: true` while `ownership_validation_mode` is `FULL`.", + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "examples": { + "Duplicate entry": { + "value": { + "address": [ + "A wallet address entry with this address and chain already exists." + ] + } + }, + "Invalid travel address": { + "value": { + "travel_address": [ + "Invalid travel address." + ] + } + }, + "Self-declaration disabled": { + "value": { + "self_declared": [ + "Self-declared ownership is disabled for this application." + ] + } + } + } + } + } + } + } + } + }, + "/v3/travel-rule/wallet-addresses/{entry_uuid}/": { + "patch": { + "summary": "Update wallet address holder metadata", + "description": "Partial update of holder_name, holder_vendor_data, entity_type, and travel_address. Setting travel_address attaches a TRP routing address to the entry; invalid values are rejected with 400. The response echoes only these four fields.", + "operationId": "updateTravelRuleWalletAddress", + "x-didit-sdk": { + "resource": "travel_rule.wallet_addresses", + "method": "update", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "entry_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WalletAddressEntryUpdate" + }, + "example": { + "holder_name": "Ana Updated", + "travel_address": "ta2ih1MicegQ3JqiKz9xT89KsSuwaDJYw8DgzmeSEp1h9tduJcNDZ5SgKLEHeyGMcS1s2HgbM6undFrMQknhM5aTXw3G9KBzi16hx" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH https://verification.didit.me/v3/travel-rule/wallet-addresses/{entry_uuid}/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"holder_name\": \"Ana Updated\", \"travel_address\": \"ta2ih1MicegQ3JqiKz9xT89KsSuwaDJYw8DgzmeSEp1h9tduJcNDZ5SgKLEHeyGMcS1s2HgbM6undFrMQknhM5aTXw3G9KBzi16hx\"}'" + } + ], + "responses": { + "200": { + "description": "The updated fields.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WalletAddressEntryUpdate" + } + } + } + }, + "400": { + "description": "`travel_address` is not a valid travel address.", + "content": { + "application/json": { + "examples": { + "Invalid travel address": { + "value": { + "travel_address": [ + "Invalid travel address." + ] + } + } + } + } + } + }, + "404": { + "description": "Unknown or deleted entry." + } + } + }, + "delete": { + "summary": "Delete a wallet address", + "description": "Deletes the entry together with its ownership proofs and the screenshots they stored. It stops matching new transfers immediately and the address is freed up for re-registration. Transfers that already used the wallet keep their own record of it and read the same. Deletion is irreversible.", + "operationId": "deleteTravelRuleWalletAddress", + "x-didit-sdk": { + "resource": "travel_rule.wallet_addresses", + "method": "delete", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "entry_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X DELETE https://verification.didit.me/v3/travel-rule/wallet-addresses/{entry_uuid}/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "Unknown or already deleted entry." + }, + "503": { + "description": "`wallet_address_deletion_incomplete`: a stored file could not be deleted, so the deletion stopped before it finished. The address-book entry is still there; retry the same request to finish it.", + "content": { + "application/json": { + "example": { + "detail": "A stored file could not be deleted, so the deletion stopped before it finished. Try again.", + "code": "wallet_address_deletion_incomplete" + } + } + } + } + } + } + }, + "/v3/travel-rule/vasps/": { + "get": { + "summary": "Search the VASP directory", + "description": "Paginated, name-sorted search of the VASP directory: the shared directory entries, which Didit builds from the ESMA interim MiCA register, the public TRISA directory, the global LEI register and its own curation, plus the members of the Didit network. Each row carries the routes your application has to it today. Your own application's profile is never listed. Rows also carry the shared due-diligence assessment and your application's own decision, entity screening and questionnaires.", + "operationId": "listTravelRuleVasps", + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "search", + "in": "query", + "schema": { + "type": "string", + "maxLength": 255 + }, + "description": "A name or alias fragment (case-insensitive), an exact LEI, or a domain (example.com or a URL on it). Didit network members match on their legal name, LEI or the domain of their compliance email." + }, + { + "name": "network", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "GTR", + "TRUST", + "VERIFYVASP", + "SYGNA", + "TRISA" + ] + }, + "description": "Only directory entries a network's member directory currently lists." + }, + { + "name": "jurisdiction", + "in": "query", + "schema": { + "type": "string" + }, + "description": "ISO 3166 alpha-2 or alpha-3 country code. 400 for anything else." + }, + { + "name": "reachable", + "in": "query", + "schema": { + "type": "boolean" + }, + "description": "true: only VASPs your application has an available route to (Didit network members included). false: only directory entries it cannot reach today." + }, + { + "name": "kind", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "directory", + "didit" + ] + }, + "description": "Only directory entries, or only Didit network members." + }, + { + "name": "risk", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "LOW", + "MEDIUM", + "HIGH" + ] + }, + "description": "Only directory entries whose risk_level is this band. Didit network members, which are not scored, never match." + }, + { + "name": "responsiveness", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "RESPONSIVE", + "UNRESPONSIVE", + "UNKNOWN" + ] + }, + "description": "Only directory entries whose responsiveness.status is this value. Didit network members never match." + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50, + "minimum": 1 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + } + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/travel-rule/vasps/?search=counterparty&reachable=true' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Paginated, name-sorted VASP directory results.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer" }, - "vendor_data": { + "next": { "type": "string", - "nullable": true, - "description": "Echo of the `vendor_data` you sent, or `null`." + "nullable": true + }, + "previous": { + "type": "string", + "nullable": true + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VaspDirectoryEntry" + } + } + } + } + } + } + }, + "400": { + "description": "A filter is not valid: the body names each field, for example an unknown network, country code, risk band or responsiveness status.", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "jurisdiction": [ + "Use an ISO 3166 alpha-2 or alpha-3 country code." + ], + "network": [ + "\"SWIFT\" is not a valid choice." + ] + } + } + } + } + }, + "x-didit-sdk": { + "resource": "travel_rule", + "method": "list_vasps", + "auth": "api_key", + "server": true + } + } + }, + "/v3/travel-rule/vasps/{vasp_id}/": { + "get": { + "summary": "Get a VASP directory entry", + "description": "One directory entry or Didit network member by the id the search returned, with the same fields and the routes your application has to it.", + "operationId": "getTravelRuleVasp", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "retrieve_vasp", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "vasp_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The id of a row of GET /v3/travel-rule/vasps/." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET https://verification.didit.me/v3/travel-rule/vasps/{vasp_id}/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "The entry.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VaspDirectoryEntry" + } + } + } + }, + "404": { + "description": "No entry or listed Didit network member has this id.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "example": { + "detail": "No directory entry has this id." + } + } + } + } + } + } + }, + "/v3/travel-rule/vasps/{vasp_id}/decision/": { + "get": { + "summary": "Get your decision on a VASP", + "description": "Your application's decision on a counterparty (a directory entry or a Didit network member): approved, restricted or blocked. An entity of a brand with no decision of its own follows the brand's decision, named in inherited_from. Private to your application.", + "operationId": "getTravelRuleVaspDecision", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "retrieve_vasp_decision", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "vasp_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET https://verification.didit.me/v3/travel-rule/vasps/{vasp_id}/decision/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "The decision that applies, or null.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleCounterpartyDecisionResponse" + }, + "examples": { + "Own decision": { + "value": { + "decision": { + "decision": "APPROVED", + "reason": "Licensed in DE, questionnaire reviewed", + "decided_by": "", + "updated_at": "2026-10-14T10:05:00Z", + "inherited_from": null + } + } + }, + "Inherited from the brand": { + "value": { + "decision": { + "decision": "BLOCKED", + "reason": "Group refused after review", + "decided_by": "", + "updated_at": "2026-10-14T10:05:00Z", + "inherited_from": { + "id": "5e4d3c2b-1a0f-4e9d-8c7b-6a5f4e3d2c1b", + "name": "Example Group" + } + } + } + }, + "No decision": { + "value": { + "decision": null + } + } + } + } + } + }, + "404": { + "description": "No directory entry or listed Didit network member has this id. Your own VASP profile is never a counterparty of yours.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "example": { + "detail": "No directory entry or Didit network member has this id." + } + } + } + } + } + }, + "put": { + "summary": "Set your decision on a VASP", + "description": "Make or change your application's decision on a counterparty: APPROVED, RESTRICTED or BLOCKED, with an optional reason. It never changes the shared directory entry and acts only through your transaction rules (counterparty_dd_status). A decision on a brand also covers each of its entities that has no decision of its own.", + "operationId": "updateTravelRuleVaspDecision", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "update_vasp_decision", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "vasp_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleCounterpartyDecisionRequest" + }, + "example": { + "decision": "BLOCKED", + "reason": "Unlicensed in its home jurisdiction" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PUT https://verification.didit.me/v3/travel-rule/vasps/{vasp_id}/decision/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"decision\": \"BLOCKED\", \"reason\": \"Unlicensed in its home jurisdiction\"}'" + } + ], + "responses": { + "200": { + "description": "The decision as it now applies.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleCounterpartyDecisionResponse" + }, + "example": { + "decision": { + "decision": "BLOCKED", + "reason": "Unlicensed in its home jurisdiction", + "decided_by": "", + "updated_at": "2026-10-14T10:05:00Z", + "inherited_from": null + } + } + } + } + }, + "400": { + "description": "The decision is missing or not one of APPROVED, RESTRICTED or BLOCKED, or the reason is longer than 255 characters.", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "decision": [ + "\"MAYBE\" is not a valid choice." + ] + } + } + } + }, + "404": { + "description": "No directory entry or listed Didit network member has this id. Your own VASP profile is never a counterparty of yours.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "example": { + "detail": "No directory entry or Didit network member has this id." + } + } + } + } + } + }, + "delete": { + "summary": "Clear your decision on a VASP", + "description": "Clear your application's own decision on a counterparty. The response shows the decision that applies afterwards: null, or for an entity of a brand the brand's decision, which the entity follows again.", + "operationId": "clearTravelRuleVaspDecision", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "clear_vasp_decision", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "vasp_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X DELETE https://verification.didit.me/v3/travel-rule/vasps/{vasp_id}/decision/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "The decision cleared; the body shows the decision that applies now.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleCounterpartyDecisionResponse" + }, + "example": { + "decision": null + } + } + } + }, + "404": { + "description": "No counterparty has this id, or your application has no decision of its own on it (a decision it only inherits from its brand cannot be cleared here: clear the brand's).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Unknown id": { + "value": { + "detail": "No directory entry or Didit network member has this id." + } + }, + "No decision of its own": { + "value": { + "detail": "This application has no decision of its own on this counterparty." + } + } + } + } + } + } + } + } + }, + "/v3/travel-rule/vasps/{vasp_id}/screening/": { + "get": { + "summary": "Get a VASP's entity screening", + "description": "Whether counterparty screening is on for your application, what one screening costs your organization, how long a result stays fresh, and your latest sanctions and adverse-media screening of the counterparty. Reading it is free.", + "operationId": "getTravelRuleVaspScreening", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "retrieve_vasp_screening", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "vasp_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET https://verification.didit.me/v3/travel-rule/vasps/{vasp_id}/screening/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Your screening setting, its price and your latest screening.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleCounterpartyScreeningStatus" + }, + "example": { + "enabled": true, + "price": "0.20", + "fresh_days": 30, + "screening": { + "id": "7a1d2c3b-4e5f-4a6b-8c7d-9e0f1a2b3c4d", + "status": "Approved", + "total_hits": 0, + "score": null, + "screened_at": "2026-10-14T09:58:00Z", + "fresh": true, + "flagged": false + } + } + } + } + }, + "403": { + "description": "Your organization's contract has no rate configured for counterparty screening.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "example": { + "detail": "No contract rate is configured for this usage type." + } + } + } + }, + "404": { + "description": "No directory entry or listed Didit network member has this id. Your own VASP profile is never a counterparty of yours.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "example": { + "detail": "No directory entry or Didit network member has this id." + } + } + } + } + } + }, + "post": { + "summary": "Screen a VASP", + "description": "Screen the counterparty entity against sanctions and adverse media for your application. Opt-in: counterparty_screening_enabled must be on in your Travel Rule settings. Each new screening is billed at your organization's Company AML Screening price; a screening younger than 30 days is returned instead (200, not billed) unless you send refresh: true. One screening of a counterparty runs at a time per application. The result is private to your application. Sandbox applications are never screened.", + "operationId": "screenTravelRuleVasp", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "screen_vasp", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "vasp_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member." + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleCounterpartyScreeningRequest" + }, + "example": { + "refresh": false + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/vasps/{vasp_id}/screening/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"refresh\": false}'" + } + ], + "responses": { + "200": { + "description": "A fresh screening already existed and was returned. Nothing was billed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleCounterpartyScreeningResult" + }, + "example": { + "outcome": "reused", + "screening": { + "id": "7a1d2c3b-4e5f-4a6b-8c7d-9e0f1a2b3c4d", + "status": "Approved", + "total_hits": 0, + "score": null, + "screened_at": "2026-10-14T09:58:00Z", + "fresh": true, + "flagged": false + } + } + } + } + }, + "201": { + "description": "A new screening ran and was billed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleCounterpartyScreeningResult" + }, + "example": { + "outcome": "screened", + "screening": { + "id": "7a1d2c3b-4e5f-4a6b-8c7d-9e0f1a2b3c4d", + "status": "In Review", + "total_hits": 2, + "score": 85.0, + "screened_at": "2026-10-14T09:58:00Z", + "fresh": true, + "flagged": true + } + } + } + } + }, + "400": { + "description": "The body is not valid, or the counterparty has no name to screen.", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "detail": [ + "This counterparty has no name to screen." + ] + } + } + } + }, + "403": { + "description": "Your organization's balance cannot cover the screening (code insufficient_credits; nothing ran and nothing was billed), or your contract has no rate configured for it.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" }, - "metadata": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Echo of the `metadata` object you sent, or `null`." + "code": { + "type": "string" + } + } + }, + "example": { + "detail": "You don't have enough credits to perform this request. Please top up at https://business.didit.me", + "code": "insufficient_credits" + } + } + } + }, + "404": { + "description": "No directory entry or listed Didit network member has this id. Your own VASP profile is never a counterparty of yours.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "example": { + "detail": "No directory entry or Didit network member has this id." + } + } + } + }, + "409": { + "description": "Nothing ran and nothing was billed: counterparty screening is off for your application, the application is a sandbox one, or a screening of this counterparty is already running.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Screening off": { + "value": { + "detail": "Counterparty screening is turned off. Enable it in the Travel Rule settings first." + } + }, + "Sandbox application": { + "value": { + "detail": "Counterparty screening does not run for sandbox applications." + } + }, + "Already running": { + "value": { + "detail": "A screening of this counterparty is already running. Try again in a moment." + } + } + } + } + } + }, + "502": { + "description": "The screening could not be completed. Nothing was billed; try again in a moment.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "example": { + "detail": "The screening provider did not answer. Try again in a moment." + } + } + } + } + } + } + }, + "/v3/travel-rule/vasps/{vasp_id}/questionnaires/": { + "get": { + "summary": "List due-diligence questionnaires with a VASP", + "description": "Every due-diligence questionnaire your application sent the counterparty, newest first, with its status and, once answered, the answers. Private to your application.", + "operationId": "listTravelRuleVaspQuestionnaires", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "list_vasp_questionnaires", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "vasp_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET https://verification.didit.me/v3/travel-rule/vasps/{vasp_id}/questionnaires/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Your questionnaires with this counterparty.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleCounterpartyQuestionnaireList" + }, + "example": { + "questionnaires": [ + { + "id": "3f9e8d7c-6b5a-4c3d-9e2f-1a0b9c8d7e6f", + "status": "SENT", + "form_version": "1", + "sent_to": "travelrule@counterparty.example", + "sent_at": "2026-10-14T10:00:00Z", + "expires_at": "2026-11-13T10:00:00Z", + "answered_at": null, + "valid_until": null, + "current": false, + "answers": {} }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 timestamp (UTC) of when the response was generated." + { + "id": "8b7a6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d", + "status": "ANSWERED", + "form_version": "1", + "sent_to": "travelrule@counterparty.example", + "sent_at": "2026-09-02T08:00:00Z", + "expires_at": "2026-10-02T08:00:00Z", + "answered_at": "2026-09-04T15:20:00Z", + "valid_until": "2027-09-04T15:20:00Z", + "current": true, + "answers": { + "legal_name": "Example Counterparty VASP, incorporated in the example jurisdiction.", + "licensing": "Crypto-asset service provider authorised under MiCA by its national regulator, licence active.", + "jurisdictions": "EU and EEA. We do not serve the United States.", + "aml_program": "Written AML/CFT policy, annual independent review, last completed in March 2026.", + "compliance_officer": "Head of Compliance, reachable at compliance@counterparty.example.", + "sanctions_screening": "Customers at onboarding and daily; counterparties on every transfer.", + "transaction_monitoring": "Rule-based monitoring with analyst review; suspicious activity reports filed with the FIU.", + "travel_rule": "TRP and email; we answer requests within one business day.", + "custody": "Segregated customer wallets held with a qualified custodian, insured.", + "ownership": "No shareholder holds 25 percent or more; parent company Example Holding Group.", + "record_keeping": "Five-year record-keeping for Travel Rule and transaction data, as our regulator requires.", + "data_protection": "Encrypted at rest and in transit, stored in the EU." + } + } + ] + } + } + } + }, + "404": { + "description": "No directory entry or listed Didit network member has this id. Your own VASP profile is never a counterparty of yours.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "example": { + "detail": "No directory entry or Didit network member has this id." + } + } + } + } + } + }, + "post": { + "summary": "Send a due-diligence questionnaire to a VASP", + "description": "Email Didit's standard due-diligence questionnaire to a compliance address on record for the counterparty, with a single-use answer link. The counterparty has 30 days to answer every question; the answers stay current for 365 days. A new send replaces your earlier unanswered questionnaire to the same counterparty, whose link stops working. One email per minute per address.", + "operationId": "sendTravelRuleVaspQuestionnaire", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "send_vasp_questionnaire", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "vasp_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member." + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleCounterpartyQuestionnaireRequest" + }, + "example": { + "sent_to": "travelrule@counterparty.example" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/vasps/{vasp_id}/questionnaires/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"sent_to\": \"travelrule@counterparty.example\"}'" + } + ], + "responses": { + "202": { + "description": "The questionnaire was emailed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleCounterpartyQuestionnaire" + }, + "example": { + "id": "3f9e8d7c-6b5a-4c3d-9e2f-1a0b9c8d7e6f", + "status": "SENT", + "form_version": "1", + "sent_to": "travelrule@counterparty.example", + "sent_at": "2026-10-14T10:00:00Z", + "expires_at": "2026-11-13T10:00:00Z", + "answered_at": null, + "valid_until": null, + "current": false, + "answers": {} + } + } + } + }, + "400": { + "description": "sent_to is not a valid email address or not a compliance address on record for the counterparty, or the counterparty has no compliance address on record.", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "examples": { + "Not on record": { + "value": { + "sent_to": [ + "Send the questionnaire to a compliance address on record for this counterparty." + ] + } + }, + "No address": { + "value": { + "sent_to": [ + "No compliance address is on record for this counterparty. Add a contact for it first." + ] + } + } + } + } + } + }, + "404": { + "description": "No directory entry or listed Didit network member has this id. Your own VASP profile is never a counterparty of yours.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "example": { + "detail": "No directory entry or Didit network member has this id." + } + } + } + }, + "429": { + "description": "A questionnaire went to this address less than a minute ago." + }, + "503": { + "description": "The email could not be sent and no questionnaire was created. Try again in a moment." + } + } + } + }, + "/v3/transactions/{transaction_id}/travel-rule/": { + "patch": { + "summary": "Finish, cancel, resend, or update the originator wallet of a Travel Rule transfer", + "description": "Provide `payment_txn_id` to report the on-chain hash and move an outbound COMPLETED transfer to FINISHED (its INTERNAL-rail counterpart also moves to FINISHED), `{\"action\": \"cancel\"}` to cancel a non-terminal transfer, or `{\"action\": \"resend\"}` to route an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA again (improve the counterparty data first, e.g. add a travel_address to the destination wallet entry). resend applies to outbound transfers only. `{\"action\": \"update_compliance_email\", \"compliance_email\": \"desk@example.com\"}` replaces the compliance email of an outbound transfer in one of those three statuses, the fix for `reason: email_undeliverable`: the status does not change, and the next `resend` sends the request to that address. A resend is queued, not run inside the request: the response shows the transfer back in AWAITING_COUNTERPARTY with a `resend queued` timeline entry and a queued `delivery`, and where it went arrives as a `travel_rule.status.updated` webhook. When the transfer went over GTR, VerifyVASP or Sygna Bridge, the network learns the on-chain hash or the cancellation after the response, with retries; `delivery` shows how that message stands. `{\"action\": \"update_originator_wallet\", \"wallet_address\": \"...\"}` records the wallet the funds actually left from on an outbound transfer, for example the address a UTXO withdrawal was broadcast from rather than the main wallet you declared. It changes the record only: nothing is sent to the counterparty again and the status stays. The originator becomes `chain_details.originator` with `source: updated`, the change is added to the `timeline` with the previous and the new wallet, and the INTERNAL-rail counterpart of the transfer is updated the same way.", + "operationId": "patchTravelRuleTransfer", + "x-didit-sdk": { + "resource": "transactions.travel_rule", + "method": "update_transfer", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "transaction_id", + "in": "path", + "required": true, + "description": "Didit-stable transaction UUID that owns the transfer.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleTransferPatchRequest" + }, + "examples": { + "Finish": { + "value": { + "payment_txn_id": "0xchainhash" + } + }, + "Cancel": { + "value": { + "action": "cancel" + } + }, + "Resend": { + "value": { + "action": "resend" + } + }, + "Update compliance email": { + "value": { + "action": "update_compliance_email", + "compliance_email": "desk@example.com" + } + }, + "Update originator wallet": { + "value": { + "action": "update_originator_wallet", + "wallet_address": "bc1qOriginatorInputAddress01" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH https://verification.didit.me/v3/transactions/{transaction_id}/travel-rule/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"payment_txn_id\": \"0xchainhash\"}'" + } + ], + "responses": { + "200": { + "description": "The updated transfer. After `update_compliance_email` its status is unchanged and it keeps `reason: email_undeliverable` until the next `resend`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleTransferDetail" + } + } + } + }, + "400": { + "description": "Neither payment_txn_id nor a supported action was provided, `compliance_email` is missing or not an email address on `update_compliance_email`, or, for `update_originator_wallet`, no wallet_address, a wallet or memo the transfer's chain cannot carry, or a wallet that is not one of the transfer's UTXO inputs.", + "content": { + "application/json": { + "examples": { + "No action": { + "value": { + "detail": [ + "Provide payment_txn_id, action=cancel, action=resend, action=beneficiary_vasp, action=update_originator_wallet or action=update_compliance_email." + ] + } + }, + "Invalid compliance email": { + "value": { + "compliance_email": [ + "Enter a valid email address." + ] + } + } + } + } + } + }, + "404": { + "description": "Unknown transaction, or the transaction has no Travel Rule transfer." + }, + "409": { + "description": "The transfer is not in a state that allows this operation (e.g. not COMPLETED/outbound for finish, already terminal for cancel, or not an outbound transfer in a resendable status for resend). `update_compliance_email` answers `409` for an inbound transfer, and for an outbound one outside COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE and NOT_ENOUGH_COUNTERPARTY_DATA. `update_originator_wallet` answers `409` for an inbound, cancelled or expired transfer.", + "content": { + "application/json": { + "examples": { + "Cancel from a terminal status": { + "value": { + "detail": "Transfer is already in a terminal state." + } + }, + "Resend not allowed": { + "value": { + "detail": "Transfer cannot be resent from its current state." + } + }, + "Finish not allowed": { + "value": { + "detail": "Transfer must be an outbound transfer in COMPLETED status to finish." + } + }, + "Compliance email cannot change": { + "value": { + "detail": "The compliance email of this transfer cannot change from its current state." } } } } } + } + } + } + }, + "/v3/transactions/{transaction_id}/travel-rule/ownership/": { + "post": { + "summary": "Confirm or deny wallet ownership", + "description": "Resolve a transfer that waits on wallet ownership: call it on your own side of the transfer when it is UNCONFIRMED_OWNERSHIP, or when it is an inbound transfer in ON_HOLD. Confirming marks the matching wallet address book entry as ownership-verified (for an inbound ON_HOLD transfer with no entry, Didit creates one from beneficiary_data.wallet_address) and runs the beneficiary name match on both sides of the transfer; denying declines both sides. An inbound transfer whose deposit already settled moves on to FINISHED when the match succeeds. When the counterparty wallet was declared self-hosted (counterparty.payment_method.method_type: unhosted_wallet), confirming records that the customer verified who controls that wallet and completes the transfer (COMPLETED) in both directions, or FINISHED for an inbound deposit whose on-chain hash is already known. The wallet verified is the counterparty's: the destination of an outbound withdrawal, or the source (originator's wallet) of an inbound deposit. If it is not in your wallet address book yet, Didit adds it as ownership-verified under the counterparty's name; if the book already holds it under a different holder name, the transfer moves to COUNTERPARTY_MISMATCHED_DATA. For an inbound TRP transfer in UNCONFIRMED_OWNERSHIP, ON_HOLD or COUNTERPARTY_MISMATCHED_DATA, Didit sends your decision to the originating VASP instead: approving returns the payment address (wallet_address, or the matched entry's address) and moves the transfer to COMPLETED, and denying declines it. The same call also decides an inbound transfer that a GTR or Sygna Bridge counterparty sent and that waits for your review (`UNCONFIRMED_OWNERSHIP`, `ON_HOLD` or `COUNTERPARTY_MISMATCHED_DATA`): `confirmed: true` approves it (pass `wallet_address` when the transfer names none) and `confirmed: false` declines it, and the network is answered after the response, with retries. VerifyVASP takes its answer when it asks, so a VerifyVASP transfer that would need your review is declined at once and cannot be decided later.", + "operationId": "confirmTravelRuleOwnership", + "x-didit-sdk": { + "resource": "transactions.travel_rule", + "method": "confirm_ownership", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "transaction_id", + "in": "path", + "required": true, + "description": "Didit-stable UUID of the transaction your application holds for this transfer: the beneficiary side's transaction on an INTERNAL-rail transfer, or the transaction you created (outbound or inbound) when the counterparty wallet was declared self-hosted.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleOwnershipConfirmRequest" + }, + "example": { + "confirmed": true + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/transactions/{transaction_id}/travel-rule/ownership/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"confirmed\": true}'" + } + ], + "responses": { + "200": { + "description": "The updated transfer. For an inbound transfer the counterparty's originator_data/beneficiary_data stay masked (`data_masked: true`) until ownership is confirmed, then `data_unmasked` records when, why and by whom.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleTransferDetail" + } + } + } }, - "400": { - "description": "Validation or processing error. Field-level problems return DRF's standard envelope (one array of messages per offending field). Encrypted-PDF problems return `{\"detail\": ...}`. A document image in which no document can be recognized at all returns `{\"error\": \"COULD_NOT_RECOGNIZE_DOCUMENT\"}`. PDF uploads can additionally fail with \"The submitted PDF file is corrupted or invalid.\" or a no-pages error.", + "409": { + "description": "The transfer is not waiting on ownership confirmation, no wallet address book entry matches its counterparty wallet, an INTERNAL transfer from another Didit customer needs a control proof (a signed message or a test deposit) before it can be confirmed, or a network transfer was already answered over its network (a VerifyVASP transfer, or a GTR or Sygna Bridge transfer you or Didit already decided).", "content": { "application/json": { "examples": { - "Missing front image": { - "summary": "`front_image` not included in the form data", + "Not awaiting confirmation": { "value": { - "front_image": [ - "No file was submitted." - ] + "detail": "Transfer is not awaiting ownership confirmation." } }, - "Unsupported file extension": { - "summary": "File extension outside tiff/jpg/jpeg/png/webp/pdf", + "No matching address book entry": { "value": { - "front_image": [ - "File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, webp, pdf." - ] + "detail": "wallet address entry no longer available" } - }, - "File too large": { - "summary": "Upload exceeds the 10 MB limit", + } + } + } + } + }, + "400": { + "description": "Validation error: confirmed is missing, an inbound TRP approval has no wallet_address and no address to fall back on, or `confirmed: true` on a declared self-hosted wallet transfer that names no counterparty wallet and no `wallet_address` was sent.", + "content": { + "application/json": { + "examples": { + "No payment address": { "value": { - "front_image": [ - "File size should not exceed 10 MB" + "wallet_address": [ + "wallet_address is required to approve." ] } - }, - "Invalid option value": { - "summary": "Options only accept DECLINE or NO_ACTION", + } + } + } + } + }, + "404": { + "description": "Unknown transaction, or the transaction has no Travel Rule transfer." + }, + "502": { + "description": "Inbound TRP transfer only: Didit could not deliver your decision to the originating VASP, so the transfer status does not change.", + "content": { + "application/json": { + "examples": { + "Callback not delivered": { "value": { - "invalid_mrz_action": [ - "\"REVIEW\" is not a valid choice." - ] + "detail": "Counterparty inquiry-resolution callback could not be delivered." } - }, - "Encrypted PDF": { - "summary": "PDF uploaded without the required password", - "value": { - "detail": "The PDF is encrypted. Please upload a decrypted PDF or a photo instead." + } + } + } + } + } + } + } + }, + "/v3/transactions/{transaction_id}/travel-rule/messages/": { + "get": { + "summary": "List Travel Rule messages", + "description": "Messages and document requests exchanged with the counterparty VASP about this transfer.", + "operationId": "listTravelRuleMessages", + "x-didit-sdk": { + "resource": "transactions.travel_rule", + "method": "list_messages", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "transaction_id", + "in": "path", + "required": true, + "description": "Didit-stable transaction UUID that owns the transfer.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET https://verification.didit.me/v3/transactions/{transaction_id}/travel-rule/messages/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "The messages, oldest first.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TravelRuleMessage" + } + } + } + } + }, + "404": { + "description": "Unknown transaction or token, or no Travel Rule transfer on the transaction." + } + } + }, + "post": { + "summary": "Send a Travel Rule message or document request", + "description": "Sends a message or a document request to the counterparty VASP. Works on INTERNAL transfers (the other Didit customer sees it on its side of the pair) and EMAIL transfers (the counterparty is emailed and answers in the pickup portal). A transfer over TRP, GTR, TRUST, VerifyVASP or Sygna carries no messages, because those protocols have none. Limits: 100 messages per transfer, 5000 characters per body, 10 requested documents, 5 files of 10 MB each.", + "operationId": "createTravelRuleMessage", + "x-didit-sdk": { + "resource": "transactions.travel_rule", + "method": "create_message", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "transaction_id", + "in": "path", + "required": true, + "description": "Didit-stable transaction UUID that owns the transfer.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleMessageCreateRequest" + }, + "example": { + "kind": "DOCUMENT_REQUEST", + "body": "Please confirm the date of birth of the beneficiary.", + "requested_documents": [ + "Proof of address" + ] + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/TravelRuleMessageCreateRequest" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/transactions/{transaction_id}/travel-rule/messages/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"kind\": \"DOCUMENT_REQUEST\", \"body\": \"Please confirm the date of birth of the beneficiary.\", \"requested_documents\": [\"Proof of address\"]}'" + } + ], + "responses": { + "201": { + "description": "The stored message.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleMessage" + } + } + } + }, + "400": { + "description": "Invalid body, a file type or size that is not accepted, more than 100 messages on the transfer, or a `reply_to` that is not a message of this transfer.", + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "requested_documents": [ + "Name at least one document the counterparty should send." + ] + } + } + } + }, + "404": { + "description": "Unknown transaction or token, or no Travel Rule transfer on the transaction." + }, + "409": { + "description": "No message can reach the counterparty. `detail` gives the reason: the transfer runs over a network rail (TRP, GTR, TRUST, VerifyVASP or Sygna), it has no counterparty yet, an INTERNAL transfer's data did not match the wallet holder so nothing reached the recipient, or an EMAIL transfer's pickup link has expired or the transfer is closed.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" } + } + } + } + } + } + } + } + }, + "/v3/travel-rule/pickup/{token}/": { + "get": { + "summary": "Get email-rail pickup info", + "description": "Public, unauthenticated. Lets a non-Didit counterparty VASP preview the pending exchange from the emailed pickup link. The token itself is the authentication.", + "operationId": "getTravelRulePickup", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "retrieve_pickup", + "auth": "public", + "server": false + }, + "tags": [ + "Travel Rule" + ], + "security": [ + {} + ], + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "contact", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "The link's `?contact=` value, the contact the link was sent to. Echoed back as `contact` unchanged; it is not validated." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl https://verification.didit.me/v3/travel-rule/pickup/{token}/" + } + ], + "responses": { + "200": { + "description": "Pickup preview.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRulePickupInfo" + }, + "example": { + "reference": "33333333-4444-5555-6666-777777777777", + "requested_at": "2026-07-05T10:00:00Z", + "originator_vasp_name": "Origin CASP SL", + "originator_vasp_lei": "529900EXAMPLEORIG001", + "recipient_vasp_name": "Example Counterparty VASP", + "language": "es", + "amount": "0.250000000000000000", + "currency": "ETH", + "fiat_amount": "", + "fiat_currency": "", + "chain": "ETH", + "beneficiary_wallet": "0xede2d7e4f5b097e6391ab6bbcf2f47a53f4778cb", + "status": "AWAITING_COUNTERPARTY", + "ivms_version": "ivms101.2023", + "deadline_at": "2026-07-07T10:00:00Z", + "reminders_sent": 0, + "contact": "", + "decline_reasons": [ + "data_mismatch", + "not_our_customer", + "wallet_not_hosted", + "sanctions", + "risk_appetite", + "jurisdiction", + "insufficient_data", + "other" + ], + "join_invitation": { + "eligible": false, + "url": "" }, - "Wrong PDF password": { - "summary": "`front_image_password` / `back_image_password` does not decrypt the PDF", - "value": { - "detail": "The PDF password is incorrect. Please provide the correct password." + "originator_data": { + "name": "Jane Doe", + "wallet_address": "0x77ccff0cc624a53cd8b8d8703ced826710211ec7" + }, + "beneficiary_data": { + "name": "Ana Diaz", + "wallet_address": "0xede2d7e4f5b097e6391ab6bbcf2f47a53f4778cb" + }, + "ivms_request": { + "originator": { + "originatorPersons": [ + { + "naturalPerson": { + "name": { + "nameIdentifier": [ + { + "primaryIdentifier": "Doe", + "secondaryIdentifier": "Jane", + "nameIdentifierType": "LEGL" + } + ] + } + } + } + ], + "accountNumber": [ + "0x77ccff0cc624a53cd8b8d8703ced826710211ec7" + ] + }, + "beneficiary": { + "beneficiaryPersons": [ + { + "naturalPerson": { + "name": { + "nameIdentifier": [ + { + "primaryIdentifier": "Diaz", + "secondaryIdentifier": "Ana", + "nameIdentifierType": "LEGL" + } + ] + } + } + } + ], + "accountNumber": [ + "0xede2d7e4f5b097e6391ab6bbcf2f47a53f4778cb" + ] } }, - "Document not recognized": { - "summary": "No identity document could be detected in the image", + "data_masked": false + } + } + } + }, + "404": { + "description": "Unknown token." + }, + "410": { + "description": "The window has closed: the counterparty already answered, the transfer left AWAITING_COUNTERPARTY, or the deadline passed." + } + } + } + }, + "/v3/travel-rule/pickup/{token}/respond/": { + "post": { + "summary": "Respond to an email-rail pickup", + "description": "Public, unauthenticated. Accepts or declines the exchange from the emailed pickup link. On accept, beneficiary_name is checked against the originator's name_matching_strictness policy.", + "operationId": "respondTravelRulePickup", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "respond_pickup", + "auth": "public", + "server": false + }, + "tags": [ + "Travel Rule" + ], + "security": [ + {} + ], + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "contact", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "The link's `?contact=` value. Used when the body sends no `contact`." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRulePickupRespondRequest" + }, + "example": { + "decision": "accept", + "beneficiary_name": "Carlos Ruiz" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/pickup/{token}/respond/ \\\n -H 'Content-Type: application/json' \\\n -d '{\"decision\": \"accept\", \"beneficiary_name\": \"Carlos Ruiz\"}'" + } + ], + "responses": { + "200": { + "description": "The resulting exchange status: COMPLETED or COUNTERPARTY_MISMATCHED_DATA after an accept, COUNTERPARTY_VASP_GENERAL_DECLINE after a decline, which also echoes `decline_reason`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "decline_reason": { + "type": "string", + "description": "Present after a decline." + } + } + }, + "examples": { + "accepted": { "value": { - "error": "COULD_NOT_RECOGNIZE_DOCUMENT" + "status": "COMPLETED" } }, - "PDF conversion failure": { - "summary": "Uploaded PDF could not be converted to an image", + "declined": { "value": { - "error": "Failed to convert front image PDF." + "status": "COUNTERPARTY_VASP_GENERAL_DECLINE", + "decline_reason": "not_our_customer" } } } } } }, - "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any image processing happens.", + "400": { + "description": "A field is invalid: for example `decision` missing or not `accept` or `decline`, `decline_detail` missing on a decline with `decline_reason: other`, an `ivms_response` that is not an IVMS 101 object or is longer than 16384 characters, or a `contact` in the body that is not a UUID (`{\"contact\": [\"Must be a valid UUID.\"]}`).", + "content": { + "application/json": { + "example": { + "decline_detail": [ + "Say what the reason is when you choose another reason." + ] + } + } + } + }, + "404": { + "description": "Unknown token." + }, + "410": { + "description": "The window has closed: the counterparty already answered, the transfer left AWAITING_COUNTERPARTY, or the deadline passed." + } + } + } + }, + "/v3/travel-rule/pickup/{token}/messages/": { + "get": { + "summary": "List pickup messages", + "description": "Public, unauthenticated. The messages of an EMAIL-rail transfer as the counterparty VASP reads them: what the customer sent is `INBOUND` and is signed with the customer's VASP name. The token is the authentication.", + "operationId": "listTravelRulePickupMessages", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "list_pickup_messages", + "auth": "public", + "server": false + }, + "tags": [ + "Travel Rule" + ], + "security": [ + {} + ], + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl https://verification.didit.me/v3/travel-rule/pickup/{token}/messages/" + } + ], + "responses": { + "200": { + "description": "The messages, oldest first, from the counterparty's side.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TravelRuleMessage" + } + } + } + } + }, + "404": { + "description": "Unknown token." + }, + "410": { + "description": "The pickup link has expired or the transfer is closed." + } + } + }, + "post": { + "summary": "Reply in the pickup portal", + "description": "Public, unauthenticated. The counterparty VASP writes back on an EMAIL-rail transfer. Same body as the transaction's messages; attachments must be PDF, PNG, JPEG or WebP. The token is the authentication.", + "operationId": "createTravelRulePickupMessage", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "create_pickup_message", + "auth": "public", + "server": false + }, + "tags": [ + "Travel Rule" + ], + "security": [ + {} + ], + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleMessageCreateRequest" + }, + "example": { + "kind": "MESSAGE", + "body": "Attached is the proof of address of the wallet holder." + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/TravelRuleMessageCreateRequest" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/pickup/{token}/messages/ \\\n -H 'Content-Type: application/json' \\\n -d '{\"kind\": \"MESSAGE\", \"body\": \"Attached is the proof of address of the wallet holder.\"}'" + } + ], + "responses": { + "201": { + "description": "The stored message.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleMessage" + } + } + } + }, + "400": { + "description": "Invalid body, a file type or size that is not accepted, more than 100 messages on the transfer, or a `reply_to` that is not a message of this transfer.", + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "requested_documents": [ + "Name at least one document the counterparty should send." + ] + } + } + } + }, + "404": { + "description": "Unknown token." + }, + "410": { + "description": "The pickup link has expired or the transfer is closed." + } + } + } + }, + "/v3/travel-rule/inbound/": { + "post": { + "summary": "Register an after-deposit (sunrise) inbound transfer", + "description": "Register Travel Rule data for a crypto deposit that already settled on-chain before any exchange took place. Creates an inbound travelRule transaction (or reuses an unclaimed deposit) and resolves the transfer against your wallet address book. For a newly created transaction, Didit sends transaction.created first and the travel_rule.status.updated webhooks after it.", + "operationId": "registerTravelRuleInbound", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "register_inbound", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleInboundRequest" + }, + "example": { + "chain": "ETH", + "txid": "0xdeposit-hash", + "wallet_address": "0xYourDepositWallet", + "amount": "500.00", + "currency": "USDC", + "originator_data": { + "name": "Origin CASP SL" + }, + "beneficiary_data": { + "name": "Ana Diaz" + }, + "originating_vasp": { + "name": "Origin CASP SL", + "lei": "", + "travel_address": "ta9JbXbsBSuS5ozArJRFVgvBHMVnDGUnrPPChSZYQsuZTn3kPrJXUQ7RSLUEJ5KjyPvr8C9sAxmPtZfGsQVKCi4n8ShxP9BkbDdbtjtA9aumNybAZ1BxC" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/inbound/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"chain\":\"ETH\",\"txid\":\"0xdeposit-hash\",\"wallet_address\":\"0xYourDepositWallet\",\"amount\":\"500.00\",\"currency\":\"USDC\"}'" + } + ], + "responses": { + "200": { + "description": "A transfer for this txid and wallet_address was already registered; the existing one is returned unchanged (created is false).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleInboundResponse" + } + } + } + }, + "201": { + "description": "A new Travel Rule transfer was registered, on a new inbound transaction or on an existing unclaimed deposit (created is true).", "content": { "application/json": { - "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", - "value": { - "detail": "You do not have permission to perform this action." - } - }, - "Not enough credits": { - "summary": "Organization balance cannot cover the call", - "value": { - "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" - } - } + "schema": { + "$ref": "#/components/schemas/TravelRuleInboundResponse" } } } }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "400": { + "description": "A required field (`chain`, `txid`, `wallet_address`, `amount`, `currency`) is missing, `originating_vasp.lei` is not a valid LEI, `originating_vasp.travel_address` is not a valid travel address, a memo, X-address or `utxo` the chain cannot carry, or `originator_data` / `beneficiary_data` that is not an object.", "content": { "application/json": { "examples": { - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", + "Missing field": { "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + "txid": [ + "This field is required." + ] } - } - }, - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" + }, + "Invalid LEI": { + "value": { + "originating_vasp": { + "lei": [ + "Invalid LEI: expected 20 alphanumeric characters (ISO 17442)." + ] + } } } } } } } + } + } + }, + "/v3/travel-rule/test-vasp/inbound/": { + "post": { + "summary": "Trigger an inbound transfer from the Didit Test VASP (sandbox)", + "description": "Sandbox applications only. The Didit Test VASP sends your application an inbound Travel Rule transfer, so you can exercise the receiving side of an exchange end to end before going live. The transfer is booked and decided by the same logic a real inbound transfer from a network runs against your wallet address book, so the inbox, the decision and the wallet ownership flow behave as they do live. Nothing leaves Didit. A live application receives 400. Send the same `reference` again to get the transfer the first call booked instead of a second one.", + "operationId": "triggerTravelRuleTestVaspInbound", + "x-didit-sdk": { + "resource": "transactions.travel_rule", + "method": "trigger_test_vasp_inbound", + "auth": "api_key", + "server": true }, + "tags": [ + "Travel Rule" + ], "security": [ { "ApiKeyAuth": [] } ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl -X POST 'https://verification.didit.me/v3/id-verification/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'front_image=@./id_front.jpg' \\\n -F 'back_image=@./id_back.jpg' \\\n -F 'perform_document_liveness=true' \\\n -F 'save_api_request=true' \\\n -F 'vendor_data=user-123'" - }, - { - "lang": "python", - "label": "Python", - "source": "import requests\n\nurl = 'https://verification.didit.me/v3/id-verification/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('id_front.jpg', 'rb') as front_f, open('id_back.jpg', 'rb') as back_f:\n files = {\n 'front_image': ('id_front.jpg', front_f, 'image/jpeg'),\n 'back_image': ('id_back.jpg', back_f, 'image/jpeg'),\n }\n data = {\n 'perform_document_liveness': 'true',\n 'save_api_request': 'true',\n 'vendor_data': 'user-123',\n }\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=120)\n\nresp.raise_for_status()\nid_v = resp.json()['id_verification']\nprint('status:', id_v['status'])\nprint('name:', id_v.get('full_name'))\nprint('document:', id_v.get('document_type'), id_v.get('document_number'))\nfor w in id_v.get('warnings', []):\n print('warning:', w['risk'], '-', w['log_type'])" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('front_image', new Blob([fs.readFileSync('./id_front.jpg')]), 'id_front.jpg');\nform.append('back_image', new Blob([fs.readFileSync('./id_back.jpg')]), 'id_back.jpg');\nform.append('perform_document_liveness', 'true');\nform.append('save_api_request', 'true');\nform.append('vendor_data', 'user-123');\n\nconst response = await fetch('https://verification.didit.me/v3/id-verification/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n});\n\nif (!response.ok) throw new Error(`ID verification failed: ${response.status}`);\nconst body = await response.json();\nconsole.log('status:', body.id_verification.status);\nconsole.log('warnings:', body.id_verification.warnings.map((w) => w.risk));" - } - ] - } - }, - "/v3/face-match/": { - "post": { - "summary": "Face Match", - "description": "Compare two face images (1:1) and get back a similarity `score` (0–100) plus an `Approved`/`Declined` `status`.\n\n**How it works.** Both images are decoded and analyzed by a biometric model (set `rotate_image=true` if captures may be sideways or upside down), then a similarity score between the two faces is computed. A score **strictly above** `face_match_score_decline_threshold` (default `30`) returns `status: \"Approved\"`; a score at or below it returns `Declined` with a `LOW_FACE_MATCH_SIMILARITY` warning. If no face pair can be scored at all, the call still returns `200` with `status: \"Declined\"` and a `NO_REFERENCE_IMAGE` warning — `score` is `null` on the default save path, or `0` with `save_api_request=false`. Submit one clear, front-facing face per image for best results.\n\n**Billing.** Each `200` response consumes one Face Match API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing.\n\n**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, both images are stored as the session's face-match source/target images, and a `status.updated` webhook is emitted to your configured webhook endpoints. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later.\n\n**Face search index.** This endpoint never enrolls faces into your face search index and never runs blocklist or duplicate screening — use `POST /v3/passive-liveness/` or `POST /v3/face-search/` for those.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", - "operationId": "post_v3face-match", - "tags": [ - "Standalone APIs" - ], - "parameters": [], "requestBody": { "required": true, "content": { - "multipart/form-data": { + "application/json": { "schema": { - "type": "object", - "required": [ - "user_image", - "ref_image" - ], - "properties": { - "user_image": { - "type": "string", - "format": "binary", - "description": "Live or candidate face image to verify (e.g. a selfie). Submit a single front-facing photo with the subject's face clearly visible and well lit. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy." - }, - "ref_image": { - "type": "string", - "format": "binary", - "description": "Reference face image to compare against (e.g. an ID document portrait or a previously enrolled photo). Same format and size limits as `user_image`." - }, - "face_match_score_decline_threshold": { - "type": "number", - "format": "float", - "default": 30, - "minimum": 0, - "maximum": 100, - "description": "Similarity threshold (0–100) that drives the response `status`. A computed score **at or below** this value sets `status` to `Declined` and adds a `LOW_FACE_MATCH_SIMILARITY` warning; a score strictly above it returns `Approved`. Default `30` is permissive — raise it (e.g. `50`–`60`) for stricter verification. Values outside 0–100 return `400`.", - "example": 50 - }, - "rotate_image": { - "type": "boolean", - "default": false, - "description": "When `true`, the service tries the input images in 90-degree increments (0, 90, 180, 270) and uses the orientation that yields the best face detection — useful for mobile captures with missing EXIF orientation. Adds latency, so only enable it if you cannot guarantee upright images.", - "example": false - }, - "save_api_request": { - "type": "boolean", - "default": true, - "description": "When `true` (default), persists the call as an API-type session — visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, and announced through a `status.updated` webhook. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only.", - "example": true - }, - "vendor_data": { - "type": "string", - "description": "Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later.", - "example": "user-123" - }, - "metadata": { - "type": "object", - "additionalProperties": true, - "description": "Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={\"flow\":\"login\"}`) — it is parsed into an object.", - "example": { - "flow": "login", - "device_id": "abc123" - } - } - } + "$ref": "#/components/schemas/TravelRuleTestVaspInboundRequest" }, "example": { - "user_image": "(binary JPEG/PNG of the live selfie)", - "ref_image": "(binary JPEG/PNG of the reference photo)", - "face_match_score_decline_threshold": 50, - "rotate_image": false, - "save_api_request": true, - "vendor_data": "user-123", - "metadata": { - "flow": "login" - } + "beneficiary_wallet_address": "0xYourSandboxWallet", + "scenario": "accept", + "amount": "1", + "reference": "demo-001" } } } }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/test-vasp/inbound/ \\\n -H 'x-api-key: YOUR_SANDBOX_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"beneficiary_wallet_address\":\"0xYourSandboxWallet\",\"scenario\":\"accept\",\"reference\":\"demo-001\"}'" + } + ], "responses": { - "200": { - "description": "Face match computed. `face_match.score` is a 0–100 float (two decimals); `status` is `Approved` when the score is strictly above `face_match_score_decline_threshold`, otherwise `Declined` with an explanatory warning. A low similarity or unscorable pair still returns `200` — inspect `face_match.status` and `face_match.warnings`, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id.", + "201": { + "description": "The inbound transfer was booked and decided. A repeat with the same `reference` returns the same body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleTestVaspInboundResponse" + } + } + } + }, + "400": { + "description": "Invalid body, an unknown or unsupported `scenario`, or the application is a live application.", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "409": { + "description": "The transfer could not be booked, for example because Travel Rule is not enabled for the application.", "content": { "application/json": { - "examples": { - "Approved": { - "summary": "Same person — score above the threshold", - "value": { - "request_id": "f9f5a777-8002-44c6-baaf-128bcfd6e226", - "face_match": { - "status": "Approved", - "score": 99.41, - "user_image": { - "entities": [ - { - "bbox": [ - 661, - 728, - 1688, - 2188 - ], - "confidence": 0.732973, - "age": 26.91, - "gender": "male", - "race": null - } - ], - "best_angle": 0 - }, - "ref_image": { - "entities": [ - { - "bbox": [ - 653, - 745, - 1691, - 2185 - ], - "confidence": 0.722082, - "age": 27.0, - "gender": "male", - "race": null - } - ], - "best_angle": 0 - }, - "warnings": [] - }, - "vendor_data": "user-123", - "metadata": { - "flow": "login" - }, - "created_at": "2026-06-12T01:04:42.763237+00:00" - } - }, - "Declined - low similarity": { - "summary": "Score at or below the decline threshold", - "value": { - "request_id": "7cdda24e-311a-46d3-a3bb-e5d9b564ce73", - "face_match": { - "status": "Declined", - "score": 24.87, - "user_image": { - "entities": [ - { - "bbox": [ - 661, - 728, - 1688, - 2188 - ], - "confidence": 0.732973, - "age": 26.91, - "gender": "male", - "race": null - } - ], - "best_angle": 0 - }, - "ref_image": { - "entities": [ - { - "bbox": [ - 156, - 234, - 679, - 898 - ], - "confidence": 0.717775, - "age": 41.2, - "gender": "male", - "race": null - } - ], - "best_angle": 0 - }, - "warnings": [ - { - "risk": "LOW_FACE_MATCH_SIMILARITY", - "feature": "FACEMATCH", - "additional_data": null, - "log_type": "error", - "short_description": "Low face match similarity", - "long_description": "The facial features of the provided image don't closely match the reference image, suggesting a potential identity mismatch." - } - ] - }, - "vendor_data": "user-123", - "metadata": null, - "created_at": "2026-06-12T01:08:19.859116+00:00" - } - } - }, "schema": { "type": "object", "properties": { - "request_id": { - "type": "string", - "format": "uuid", - "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." - }, - "face_match": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "Approved", - "Declined" - ], - "description": "`Approved` when the similarity score is strictly above `face_match_score_decline_threshold` and a score could be computed; `Declined` otherwise (a warning explains why)." - }, - "score": { - "type": "number", - "format": "float", - "nullable": true, - "minimum": 0, - "maximum": 100, - "description": "Similarity score from 0 to 100 — the model's confidence that both images depict the same person. Rounded to two decimals only when `save_api_request` is true; with `save_api_request=false` the raw float is returned. `null` when no face pair could be scored (with the default `save_api_request=true`); with `save_api_request=false` the same condition can surface as `score: 0`. Either way a `NO_REFERENCE_IMAGE` warning is present and `status` is `Declined`.", - "example": 99.41 - }, - "user_image": { - "type": "object", - "description": "Face-detection results for the analyzed images.", - "properties": { - "entities": { - "type": "array", - "items": { - "type": "object", - "properties": { - "bbox": { - "type": "array", - "items": { - "type": "integer" - }, - "minItems": 4, - "maxItems": 4, - "description": "Bounding box of the detected face as `[x_min, y_min, x_max, y_max]` pixel coordinates in the processed image.", - "example": [ - 661, - 728, - 1688, - 2188 - ] - }, - "confidence": { - "type": "number", - "format": "float", - "minimum": 0, - "maximum": 1, - "description": "Face-detection confidence (0–1).", - "example": 0.732973 - }, - "age": { - "type": "number", - "format": "float", - "description": "Model-estimated age of the detected face, in years. Informational only — it does not affect the face-match decision.", - "example": 26.91 - }, - "gender": { - "type": "string", - "description": "Model-predicted gender of the detected face (`male` or `female`). Informational only.", - "example": "male" - }, - "race": { - "type": "string", - "nullable": true, - "description": "Reserved field — always `null` in responses from this endpoint.", - "example": null - } - } - }, - "description": "One entry per detected face." - }, - "best_angle": { - "type": "integer", - "description": "Rotation (degrees: 0, 90, 180, or 270) that produced the best face detection. Only non-zero when `rotate_image=true` corrected the orientation.", - "example": 0 - } - } - }, - "ref_image": { - "type": "object", - "description": "Face-detection results for the reference image, same shape as `user_image`.", - "properties": { - "entities": { - "type": "array", - "items": { - "type": "object", - "properties": { - "bbox": { - "type": "array", - "items": { - "type": "integer" - }, - "minItems": 4, - "maxItems": 4, - "description": "Bounding box of the detected face as `[x_min, y_min, x_max, y_max]` pixel coordinates in the processed image.", - "example": [ - 661, - 728, - 1688, - 2188 - ] - }, - "confidence": { - "type": "number", - "format": "float", - "minimum": 0, - "maximum": 1, - "description": "Face-detection confidence (0–1).", - "example": 0.732973 - }, - "age": { - "type": "number", - "format": "float", - "description": "Model-estimated age of the detected face, in years. Informational only — it does not affect the face-match decision.", - "example": 26.91 - }, - "gender": { - "type": "string", - "description": "Model-predicted gender of the detected face (`male` or `female`). Informational only.", - "example": "male" - }, - "race": { - "type": "string", - "nullable": true, - "description": "Reserved field — always `null` in responses from this endpoint.", - "example": null - } - } - }, - "description": "One entry per detected face." - }, - "best_angle": { - "type": "integer", - "example": 0 - } - } - }, - "warnings": { - "type": "array", - "description": "Empty on a clean approval. `NO_REFERENCE_IMAGE` (`error`) — no face pair could be scored; `LOW_FACE_MATCH_SIMILARITY` (`error`) — score at or below the decline threshold. Any of these sets `status` to `Declined`.", - "items": { - "type": "object", - "properties": { - "risk": { - "type": "string", - "enum": [ - "NO_REFERENCE_IMAGE", - "LOW_FACE_MATCH_SIMILARITY" - ], - "description": "Machine-readable risk code." - }, - "feature": { - "type": "string", - "enum": [ - "FACEMATCH" - ], - "description": "Feature that raised the warning. Always `FACEMATCH` on this endpoint." - }, - "additional_data": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Always `null` for face-match warnings." - }, - "log_type": { - "type": "string", - "enum": [ - "error", - "warning", - "information" - ], - "description": "Severity. `error` warnings drive `status` to `Declined`; `warning` and `information` entries are advisory and never decline on their own." - }, - "short_description": { - "type": "string", - "description": "Human-readable one-line summary of the risk." - }, - "long_description": { - "type": "string", - "description": "Human-readable explanation of the risk." - } - } - } - } - } - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Echo of the `vendor_data` you sent, or `null`." - }, - "metadata": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Echo of the `metadata` object you sent, or `null`." - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 timestamp (UTC) of when the response was generated, e.g. `2026-06-12T01:04:42.763237+00:00`." + "detail": { + "type": "string" } } } } } + } + } + } + }, + "/v3/travel-rule/widget-session/": { + "post": { + "summary": "Mint a wallet-ownership widget session", + "description": "Mint a hosted wallet-ownership widget URL/token. Send the customer to the returned url to prove control of a wallet (message signing, Satoshi test, screenshot, or self-declaration). The widget is web-only in this release - open the url in the system browser or a custom tab, never inside a mobile SDK webview.", + "operationId": "createTravelRuleWidgetSession", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "create_widget_session", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWidgetSessionCreate" + }, + "example": { + "wallet_address": "0xd3b62acb76fb27a7f8961ec40fa11746b113aca9", + "chain": "ETH", + "holder_name": "Jane Doe", + "vendor_data": "user-042", + "transaction_id": "99999999-aaaa-bbbb-cccc-dddddddddddd", + "expires_in_minutes": 60 + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/widget-session/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"wallet_address\":\"0xd3b62acb76fb27a7f8961ec40fa11746b113aca9\",\"chain\":\"ETH\",\"holder_name\":\"Jane Doe\",\"transaction_id\":\"99999999-aaaa-bbbb-cccc-dddddddddddd\"}'" + } + ], + "responses": { + "201": { + "description": "The minted widget session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWidgetSessionResponse" + } + } + } }, "400": { - "description": "Validation error. Returned when a required image is missing, exceeds 5 MB, has an unsupported extension, or when an option is out of range. The body is DRF's standard field-error envelope: one array of messages per offending field.", + "description": "Invalid body: `callback_url` is not an absolute http(s) URL, `expires_in_minutes` is outside 5 to 10080, `transaction_id` is not a UUID, or a field is longer than its limit.", "content": { "application/json": { "examples": { - "Missing required image": { - "summary": "`ref_image` (or `user_image`) not included in the form data", - "value": { - "ref_image": [ - "No file was submitted." - ] - } - }, - "Unsupported file extension": { - "summary": "File extension outside tiff/jpg/jpeg/png/webp", - "value": { - "user_image": [ - "File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, webp." - ] - } - }, - "File too large": { - "summary": "Upload exceeds the 5 MB limit", - "value": { - "user_image": [ - "File size should not exceed 5 MB" - ] - } - }, - "Threshold out of range": { - "summary": "`face_match_score_decline_threshold` outside 0–100", + "Invalid callback_url": { "value": { - "face_match_score_decline_threshold": [ - "Ensure this value is less than or equal to 100." + "callback_url": [ + "callback_url must be an absolute http(s) URL." ] } } @@ -12883,1281 +33532,1370 @@ } } }, - "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any image processing happens.", + "404": { + "description": "`transaction_id` does not name a Travel Rule transfer in this application." + } + } + } + }, + "/v3/travel-rule/widget/{token}/": { + "get": { + "summary": "Get wallet-ownership widget context", + "description": "Public read endpoint keyed by the widget session token - no API key required. Returns the widget context plus the status of every proof, so you can poll it to detect completion; completed_at becomes non-null once a proof is verified. A SATOSHI_TEST proof can sit at status PENDING while its deposit is seen on-chain but unconfirmed - a background job automatically rechecks it and completes it once the deposit confirms, so no client action is needed while it waits. Each proof carries the fields its method needs: deposit_address, expected_amount, and its own expires_at for SATOSHI_TEST; challenge for MESSAGE_SIGNING. Prefer the travel_rule.status.updated webhook over polling when the session is linked to a transfer.", + "operationId": "getTravelRuleWidgetContext", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "retrieve_widget_context", + "auth": "public", + "server": false + }, + "tags": [ + "Travel Rule" + ], + "security": [ + {} + ], + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "description": "Opaque session token returned when the widget session was minted.", + "schema": { + "type": "string" + } + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl https://verification.didit.me/v3/travel-rule/widget/{token}/" + } + ], + "responses": { + "200": { + "description": "The widget context and proof statuses.", "content": { "application/json": { - "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", - "value": { - "detail": "You do not have permission to perform this action." + "schema": { + "$ref": "#/components/schemas/TravelRuleWidgetContext" + } + } + } + }, + "404": { + "description": "Unknown token." + }, + "410": { + "description": "The session expired without completing." + } + } + } + }, + "/v3/travel-rule/widget/{token}/vasps/": { + "get": { + "summary": "Search VASPs from the wallet widget", + "description": "Public read endpoint keyed by the widget session token, no API key required: the VASP picker of the end user's widget. Same search and filters as GET /v3/travel-rule/vasps/ for the widget's application, answering only who each VASP is and whether it can be reached.", + "operationId": "listTravelRuleWidgetVasps", + "x-didit-sdk": { + "resource": "travel_rule", + "method": "list_widget_vasps", + "auth": "public", + "server": false + }, + "tags": [ + "Travel Rule" + ], + "security": [ + {} + ], + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "description": "Opaque session token returned when the widget session was minted.", + "schema": { + "type": "string" + } + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string", + "maxLength": 255 + }, + "description": "A name or alias fragment (case-insensitive), an exact LEI, or a domain (example.com or a URL on it). Didit network members match on their legal name, LEI or the domain of their compliance email." + }, + { + "name": "network", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "GTR", + "TRUST", + "VERIFYVASP", + "SYGNA", + "TRISA" + ] + }, + "description": "Only directory entries a network's member directory currently lists." + }, + { + "name": "jurisdiction", + "in": "query", + "schema": { + "type": "string" + }, + "description": "ISO 3166 alpha-2 or alpha-3 country code. 400 for anything else." + }, + { + "name": "reachable", + "in": "query", + "schema": { + "type": "boolean" + }, + "description": "true: only VASPs your application has an available route to (Didit network members included). false: only directory entries it cannot reach today." + }, + { + "name": "kind", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "directory", + "didit" + ] + }, + "description": "Only directory entries, or only Didit network members." + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50, + "minimum": 1 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + } + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl 'https://verification.didit.me/v3/travel-rule/widget/{token}/vasps/?search=counterparty'" + } + ], + "responses": { + "200": { + "description": "Paginated, name-sorted results.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "next": { + "type": "string", + "nullable": true + }, + "previous": { + "type": "string", + "nullable": true + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VaspDirectoryWidgetEntry" + } } - }, - "Not enough credits": { - "summary": "Organization balance cannot cover the call", - "value": { - "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + } + } + } + } + }, + "400": { + "description": "A filter is not valid: the body names each field, for example an unknown network or country code.", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" } } + }, + "example": { + "jurisdiction": [ + "Use an ISO 3166 alpha-2 or alpha-3 country code." + ], + "network": [ + "\"SWIFT\" is not a valid choice." + ] } } } }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "404": { + "description": "Unknown token." + }, + "410": { + "description": "The session expired." + } + } + } + }, + "/v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/": { + "patch": { + "summary": "Toggle AML ongoing monitoring for users", + "description": "Enables or disables AML ongoing monitoring at the user level. Exactly one monitored AML screening is kept per user; users without any AML screening (for example imported users) get one created and screened first, billed as a regular AML screening. Set estimate_only to preview counts and costs without writing. Batches of 20 or fewer resolve inline (200, COMPLETED); larger batches return 202 and run asynchronously.", + "operationId": "toggle_vendor_users_ongoing_monitoring", + "x-didit-sdk": { + "resource": "users", + "method": "set_ongoing_monitoring", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OngoingMonitoringToggleRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Estimate (when estimate_only) or the completed job for small batches processed inline.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OngoingMonitoringToggleResult" + } + } + } + }, + "202": { + "description": "Larger batches are processed asynchronously; poll the job endpoint with the returned job_id.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OngoingMonitoringJob" + } + } + } + }, + "403": { + "description": "Missing write:sessions privilege, or insufficient balance to enable (the estimate is included in the error body)." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/{job_id}/": { + "get": { + "summary": "Get AML ongoing monitoring job", + "description": "Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING.", + "operationId": "get_vendor_users_ongoing_monitoring_job", + "x-didit-sdk": { + "resource": "users", + "method": "retrieve_ongoing_monitoring_job", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Monitoring job UUID returned by the toggle endpoint." + } + ], + "responses": { + "200": { + "description": "The job.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OngoingMonitoringJob" + } + } + } + }, + "404": { + "description": "Unknown job for this application." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/": { + "patch": { + "summary": "Toggle AML ongoing monitoring for businesses", + "description": "Business-level equivalent of the user toggle: one monitored AML screening per business, with businesses lacking any AML screening screened first (company entity). Same estimate_only, inline fast-path and async job contract.", + "operationId": "toggle_vendor_businesses_ongoing_monitoring", + "x-didit-sdk": { + "resource": "businesses", + "method": "set_ongoing_monitoring", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Businesses" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OngoingMonitoringToggleRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Estimate (when estimate_only) or the completed job for small batches processed inline.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OngoingMonitoringToggleResult" + } + } + } + }, + "202": { + "description": "Larger batches are processed asynchronously; poll the job endpoint with the returned job_id.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OngoingMonitoringJob" + } + } + } + }, + "403": { + "description": "Missing write:sessions privilege, or insufficient balance to enable (the estimate is included in the error body)." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/{job_id}/": { + "get": { + "summary": "Get AML ongoing monitoring job", + "description": "Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING.", + "operationId": "get_vendor_businesses_ongoing_monitoring_job", + "x-didit-sdk": { + "resource": "businesses", + "method": "retrieve_ongoing_monitoring_job", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Businesses" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Monitoring job UUID returned by the toggle endpoint." + } + ], + "responses": { + "200": { + "description": "The job.", "content": { "application/json": { - "examples": { - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." - } - } - }, "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } + "$ref": "#/components/schemas/OngoingMonitoringJob" } } } + }, + "404": { + "description": "Unknown job for this application." } }, "security": [ { "ApiKeyAuth": [] } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/": { + "patch": { + "summary": "Toggle AML ongoing monitoring for business sessions", + "description": "Session-level toggle for KYB verifications, addressed by session number. Sessions without an AML screening get one created and attached to the session itself. Same estimate_only, inline fast-path and async job contract as the user-level endpoints. Replaces the legacy synchronous behavior that skipped sessions without an AML screening.", + "operationId": "toggle_business_sessions_ongoing_monitoring", + "x-didit-sdk": { + "resource": "businesses", + "method": "set_session_ongoing_monitoring", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Businesses" ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl -X POST 'https://verification.didit.me/v3/face-match/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'user_image=@./selfie.jpg' \\\n -F 'ref_image=@./id_portrait.jpg' \\\n -F 'face_match_score_decline_threshold=50' \\\n -F 'save_api_request=true' \\\n -F 'vendor_data=user-123'" - }, + "parameters": [ { - "lang": "python", - "label": "Python", - "source": "import requests\n\nurl = 'https://verification.didit.me/v3/face-match/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('selfie.jpg', 'rb') as user_f, open('id_portrait.jpg', 'rb') as ref_f:\n files = {\n 'user_image': ('selfie.jpg', user_f, 'image/jpeg'),\n 'ref_image': ('id_portrait.jpg', ref_f, 'image/jpeg'),\n }\n data = {\n 'face_match_score_decline_threshold': 50,\n 'save_api_request': 'true',\n 'vendor_data': 'user-123',\n }\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=60)\n\nresp.raise_for_status()\nbody = resp.json()\nprint('status:', body['face_match']['status'])\nprint('score:', body['face_match']['score'])" + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, { - "lang": "javascript", - "label": "JavaScript", - "source": "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('user_image', new Blob([fs.readFileSync('./selfie.jpg')]), 'selfie.jpg');\nform.append('ref_image', new Blob([fs.readFileSync('./id_portrait.jpg')]), 'id_portrait.jpg');\nform.append('face_match_score_decline_threshold', '50');\nform.append('save_api_request', 'true');\nform.append('vendor_data', 'user-123');\n\nconst response = await fetch('https://verification.didit.me/v3/face-match/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n});\n\nif (!response.ok) throw new Error(`Face match failed: ${response.status}`);\nconst body = await response.json();\nconsole.log('status:', body.face_match.status, 'score:', body.face_match.score);" + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." } - ] - } - }, - "/v3/age-estimation/": { - "post": { - "summary": "Age Estimation (face age + passive liveness)", - "description": "Estimate a person's age from a single face photo and run a passive liveness check in the same call — returns the model-predicted `age_estimation` (in years), a liveness `score` (0–100), and an `Approved`/`Declined` `status`. The age is **model-predicted from the face, not verified against a document** — use it for age gates and low-friction checks, not as legal proof of age.\n\n**How it works.** The image is analyzed for faces (set `rotate_image=true` if captures may be sideways or upside down); each detected face gets an estimated age, and the largest face in the frame drives `age_estimation`. In parallel, a passive liveness model scores whether the photo shows a live person (no user challenge required) and detects presentation attacks (photos of photos, screens, masks). Submit one clear, front-facing, well-lit face per image for best results.\n\n**Decision logic.** Any warning declines: `status` is `Approved` only when the `warnings` array is empty. A liveness `score` at or below `face_liveness_score_decline_threshold` (default `30`) adds `LOW_LIVENESS_SCORE`; a detected attack adds `LIVENESS_FACE_ATTACK`; no face found by the liveness model adds `NO_FACE_DETECTED`; no estimable face age adds `AGE_NOT_DETECTED` (`age_estimation` is `null`); an estimated age below `age_estimation_decline_threshold` (default `18`) adds `AGE_BELOW_MINIMUM`. Borderline ages near your threshold deserve a fallback to document-based verification — the model's error grows for faces close to the limit.\n\n**Billing.** Each `200` response consumes one Age Estimation API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing.\n\n**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the result is exposed there in `liveness_checks[]`, with `features: [\"AGE_ESTIMATION\"]`), the face crop and reference image are stored, the face embedding is added to your application's face index (used by face search and duplicate detection), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (`age_estimation: 30`, no warnings), no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", - "operationId": "post_v3age-estimation", - "tags": [ - "Standalone APIs" ], - "parameters": [], "requestBody": { "required": true, "content": { - "multipart/form-data": { + "application/json": { "schema": { "type": "object", "required": [ - "user_image" + "is_enabled" ], "properties": { - "user_image": { - "type": "string", - "format": "binary", - "description": "Face photo to analyze (e.g. a selfie). Submit a single front-facing photo with the subject's face clearly visible and well lit. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp` (no PDF). Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.3 MB before processing, so very high resolutions do not improve accuracy." - }, - "face_liveness_score_decline_threshold": { - "type": "number", - "format": "float", - "default": 30, - "minimum": 0, - "maximum": 100, - "description": "Liveness threshold (0–100). A liveness `score` **at or below** this value adds a `LOW_LIVENESS_SCORE` warning and declines. Raise it for stricter anti-spoofing; values outside 0–100 return `400`.", - "example": 30 - }, - "age_estimation_decline_threshold": { - "type": "integer", - "default": 18, - "minimum": 0, - "maximum": 100, - "description": "Minimum acceptable estimated age in years. An estimated age **below** this value adds an `AGE_BELOW_MINIMUM` warning and declines. Set `0` to disable the age check (the call then only runs liveness + age estimation without an age-based decision).", - "example": 18 + "is_enabled": { + "type": "boolean", + "description": "true to enable monitoring, false to disable it." }, - "rotate_image": { + "estimate_only": { "type": "boolean", "default": false, - "description": "When `true`, the service tries the input image in 90-degree increments (0, 90, 180, 270) and uses the orientation that yields the best face detection — useful for mobile captures with missing EXIF orientation. Adds latency, so only enable it if you cannot guarantee upright images.", - "example": false + "description": "When true, returns the cost estimate and performs no writes." }, - "save_api_request": { + "update_all": { "type": "boolean", - "default": true, - "description": "When `true` (default), persists the call as an API-type session — visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with the detected face added to your application's face index. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only.", - "example": true + "default": false, + "description": "Apply to every entity of the application (scoped by the filter_* params when present)." }, - "vendor_data": { - "type": "string", - "description": "Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later.", - "example": "user-123" + "filter_search": { + "type": "string" }, - "metadata": { - "type": "object", - "additionalProperties": true, - "description": "Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={\"flow\":\"age-gate\"}`) — it is parsed into an object.", - "example": { - "flow": "age-gate" - } - } - } - }, - "example": { - "user_image": "(binary JPEG/PNG of the selfie)", - "face_liveness_score_decline_threshold": 30, - "age_estimation_decline_threshold": 18, - "rotate_image": false, - "save_api_request": true, - "vendor_data": "user-123", - "metadata": { - "flow": "age-gate" - } - } - } - } - }, - "responses": { - "200": { - "description": "Age estimation and passive liveness completed. `age_estimation.age_estimation` is the model-predicted age in years of the largest detected face (`null` if none); `age_estimation.score` is the passive-liveness score (0–100); `status` is `Approved` only when `warnings` is empty. An underage or spoofed capture still returns `200` with `status: \"Declined\"` — inspect the body, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id.", - "content": { - "application/json": { - "examples": { - "Approved": { - "summary": "Live adult face — both checks passed", - "value": { - "request_id": "0c40ba43-64ab-4e2e-b4b8-7d1f12f81bc1", - "age_estimation": { - "status": "Approved", - "method": "PASSIVE", - "score": 97.5, - "user_image": { - "entities": [ - { - "age": 27.33, - "bbox": [ - 40, - 40, - 100, - 100 - ], - "confidence": 0.7177750468254089, - "gender": "male" - } - ], - "best_angle": 0 - }, - "age_estimation": 27.33, - "warnings": [] - }, - "vendor_data": "user-123", - "metadata": { - "flow": "age-gate" - }, - "created_at": "2026-06-12T02:24:11.512941+00:00" + "filter_status": { + "type": "array", + "items": { + "type": "string" } }, - "Declined - age below threshold": { - "summary": "Estimated age below `age_estimation_decline_threshold`", - "value": { - "request_id": "9be41a6f-2f4e-4f57-92f4-b3a4f6f0a1c2", - "age_estimation": { - "status": "Declined", - "method": "PASSIVE", - "score": 95.0, - "user_image": { - "entities": [ - { - "age": 16.5, - "bbox": [ - 40, - 40, - 100, - 100 - ], - "confidence": 0.7177750468254089, - "gender": "female" - } - ], - "best_angle": 0 - }, - "age_estimation": 16.5, - "warnings": [ - { - "risk": "AGE_BELOW_MINIMUM", - "feature": "LIVENESS", - "additional_data": null, - "log_type": "error", - "short_description": "Age below minimum", - "long_description": "The age of the face is below the minimum age threshold for the application." - } - ] - }, - "vendor_data": "user-123", - "metadata": null, - "created_at": "2026-06-12T02:26:40.118332+00:00" - } + "filter_country": { + "type": "string" }, - "Declined - liveness too low": { - "summary": "Liveness score at or below the decline threshold (possible spoof)", - "value": { - "request_id": "5f6f2f1f-7c4f-43b9-8d62-0a8f4c2d9e77", - "age_estimation": { - "status": "Declined", - "method": "PASSIVE", - "score": 5.0, - "user_image": { - "entities": [ - { - "age": 25.0, - "bbox": [ - 40, - 40, - 100, - 100 - ], - "confidence": 0.7177750468254089, - "gender": "male" - } - ], - "best_angle": 0 - }, - "age_estimation": 25.0, - "warnings": [ - { - "risk": "LOW_LIVENESS_SCORE", - "feature": "LIVENESS", - "additional_data": null, - "log_type": "error", - "short_description": "Low liveness score", - "long_description": "The liveness check resulted in a low score, indicating potential use of non-live facial representations or poor-quality biometric data." - } - ] - }, - "vendor_data": "user-123", - "metadata": null, - "created_at": "2026-06-12T02:27:02.901547+00:00" - } - } - }, - "schema": { - "type": "object", - "properties": { - "request_id": { - "type": "string", - "format": "uuid", - "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." - }, - "age_estimation": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "Approved", - "Declined" - ], - "description": "`Approved` when no warnings were raised; `Declined` otherwise (every warning this endpoint produces declines)." - }, - "method": { - "type": "string", - "enum": [ - "PASSIVE" - ], - "description": "Liveness method. Always `PASSIVE` on this endpoint (single-image, no user challenge)." - }, - "score": { - "type": "number", - "format": "float", - "nullable": true, - "minimum": 0, - "maximum": 100, - "description": "Passive-liveness score from 0 to 100 — the model's confidence that the photo shows a live person. Compared against `face_liveness_score_decline_threshold`; at or below it the call declines with `LOW_LIVENESS_SCORE`. `null` when the liveness model returns no confidence — the call still declines with `LOW_LIVENESS_SCORE` because null is treated as 0 for the threshold comparison.", - "example": 97.5 - }, - "user_image": { - "type": "object", - "description": "Face-detection results for the analyzed image.", - "properties": { - "entities": { - "type": "array", - "description": "One entry per detected face. Empty when no face was found (then `age_estimation` is `null` and an `AGE_NOT_DETECTED` warning is added).", - "items": { - "type": "object", - "properties": { - "age": { - "type": "number", - "format": "float", - "description": "Model-estimated age of this face, in years.", - "example": 27.33 - }, - "bbox": { - "type": "array", - "items": { - "type": "integer" - }, - "minItems": 4, - "maxItems": 4, - "description": "Bounding box of the detected face as `[x_min, y_min, x_max, y_max]` pixel coordinates in the processed image.", - "example": [ - 40, - 40, - 100, - 100 - ] - }, - "confidence": { - "type": "number", - "format": "float", - "minimum": 0, - "maximum": 1, - "description": "Face-detection confidence (0–1).", - "example": 0.7177750468254089 - }, - "gender": { - "type": "string", - "description": "Model-predicted gender of the detected face (`male` or `female`). Informational only.", - "example": "male" - } - } - } - }, - "best_angle": { - "type": "integer", - "nullable": true, - "description": "Rotation (degrees: 0, 90, 180, or 270) that produced the best face detection. Only non-zero when `rotate_image=true` corrected the orientation.", - "example": 0 - } - } - }, - "age_estimation": { - "type": "number", - "format": "float", - "nullable": true, - "description": "Model-predicted age in years of the **largest** detected face. `null` when no face age could be estimated (an `AGE_NOT_DETECTED` warning is added and the call declines).", - "example": 27.33 - }, - "warnings": { - "type": "array", - "description": "Empty on a clean approval. Any entry sets `status` to `Declined`.", - "items": { - "type": "object", - "properties": { - "risk": { - "type": "string", - "enum": [ - "NO_FACE_DETECTED", - "LOW_LIVENESS_SCORE", - "LIVENESS_FACE_ATTACK", - "AGE_NOT_DETECTED", - "AGE_BELOW_MINIMUM" - ], - "description": "Machine-readable risk code. `NO_FACE_DETECTED` — the liveness model found no face; `LOW_LIVENESS_SCORE` — liveness `score` at or below `face_liveness_score_decline_threshold`; `LIVENESS_FACE_ATTACK` — a presentation attack (photo/screen/mask) was detected; `AGE_NOT_DETECTED` — no face age could be estimated; `AGE_BELOW_MINIMUM` — estimated age below `age_estimation_decline_threshold`. `NO_FACE_DETECTED` and `LOW_LIVENESS_SCORE` are mutually exclusive — when no face is found only `NO_FACE_DETECTED` is emitted." - }, - "feature": { - "type": "string", - "enum": [ - "LIVENESS" - ], - "description": "Feature that raised the warning. Always `LIVENESS` on this endpoint." - }, - "additional_data": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Always `null` for age-estimation warnings." - }, - "log_type": { - "type": "string", - "enum": [ - "error" - ], - "description": "Severity. Every warning this endpoint produces is an `error` and sets `status` to `Declined`." - }, - "short_description": { - "type": "string", - "description": "Human-readable one-line summary of the risk." - }, - "long_description": { - "type": "string", - "description": "Human-readable explanation of the risk." - } - } - } - } - } - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Echo of the `vendor_data` you sent, or `null`." - }, - "metadata": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Echo of the `metadata` object you sent, or `null`." - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 timestamp (UTC) of when the response was generated." - } - } - } - } - } - }, - "400": { - "description": "Validation error. Returned when `user_image` is missing, exceeds 5 MB, has an unsupported extension, or when a threshold is out of range. The body is DRF's standard field-error envelope: one array of messages per offending field.", - "content": { - "application/json": { - "examples": { - "Missing user image": { - "summary": "`user_image` not included in the form data", - "value": { - "user_image": [ - "No file was submitted." - ] - } + "filter_date_from": { + "type": "string", + "format": "date" }, - "Unsupported file extension": { - "summary": "File extension outside tiff/jpg/jpeg/png/webp", - "value": { - "user_image": [ - "File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, webp." - ] - } + "filter_date_to": { + "type": "string", + "format": "date" }, - "File too large": { - "summary": "Upload exceeds the 5 MB limit", - "value": { - "user_image": [ - "File size should not exceed 5 MB" - ] - } + "session_numbers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Business session numbers to toggle. Required unless update_all (or the legacy toggle_all alias) is true." }, - "Threshold out of range": { - "summary": "`face_liveness_score_decline_threshold` outside 0–100", - "value": { - "face_liveness_score_decline_threshold": [ - "Ensure this value is less than or equal to 100." - ] - } + "toggle_all": { + "type": "boolean", + "default": false, + "description": "Legacy alias of update_all." + }, + "filter_workflow_id": { + "type": "string" } } } } - }, - "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any image processing happens.", - "content": { - "application/json": { - "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", - "value": { - "detail": "You do not have permission to perform this action." - } - }, - "Not enough credits": { - "summary": "Organization balance cannot cover the call", - "value": { - "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" - } - } + } + }, + "responses": { + "200": { + "description": "Estimate (when estimate_only) or the completed job for small batches processed inline.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OngoingMonitoringToggleResult" } } } }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "202": { + "description": "Larger batches are processed asynchronously; poll the job endpoint with the returned job_id.", "content": { "application/json": { - "examples": { - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." - } - } - }, "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } + "$ref": "#/components/schemas/OngoingMonitoringJob" } } } + }, + "403": { + "description": "Missing write:sessions privilege, or insufficient balance to enable (the estimate is included in the error body)." } }, "security": [ { "ApiKeyAuth": [] } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/{job_id}/": { + "get": { + "summary": "Get AML ongoing monitoring job", + "description": "Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING.", + "operationId": "get_business_sessions_ongoing_monitoring_job", + "x-didit-sdk": { + "resource": "businesses", + "method": "retrieve_session_ongoing_monitoring_job", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Businesses" ], - "x-codeSamples": [ + "parameters": [ { - "lang": "curl", - "label": "cURL", - "source": "curl -X POST 'https://verification.didit.me/v3/age-estimation/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'user_image=@./selfie.jpg' \\\n -F 'age_estimation_decline_threshold=18' \\\n -F 'face_liveness_score_decline_threshold=30' \\\n -F 'save_api_request=true' \\\n -F 'vendor_data=user-123'" + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, { - "lang": "python", - "label": "Python", - "source": "import requests\n\nurl = 'https://verification.didit.me/v3/age-estimation/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('selfie.jpg', 'rb') as f:\n files = {'user_image': ('selfie.jpg', f, 'image/jpeg')}\n data = {\n 'age_estimation_decline_threshold': 18,\n 'face_liveness_score_decline_threshold': 30,\n 'save_api_request': 'true',\n 'vendor_data': 'user-123',\n }\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=60)\n\nresp.raise_for_status()\nresult = resp.json()['age_estimation']\nprint('status:', result['status'])\nprint('estimated age:', result['age_estimation'])\nprint('liveness score:', result['score'])\nfor w in result['warnings']:\n print('warning:', w['risk'])" + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." }, { - "lang": "javascript", - "label": "JavaScript", - "source": "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('user_image', new Blob([fs.readFileSync('./selfie.jpg')]), 'selfie.jpg');\nform.append('age_estimation_decline_threshold', '18');\nform.append('face_liveness_score_decline_threshold', '30');\nform.append('save_api_request', 'true');\nform.append('vendor_data', 'user-123');\n\nconst response = await fetch('https://verification.didit.me/v3/age-estimation/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n});\n\nif (!response.ok) throw new Error(`Age estimation failed: ${response.status}`);\nconst { age_estimation } = await response.json();\nconsole.log('status:', age_estimation.status, 'age:', age_estimation.age_estimation, 'liveness:', age_estimation.score);" + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Monitoring job UUID returned by the toggle endpoint." } - ] - } - }, - "/v3/poa/": { - "post": { - "summary": "Proof of Address (document OCR + address checks)", - "description": "Extract and validate a Proof of Address document — utility bills, bank statements, and government-issued documents — in one call. Returns the issuer, issue/expiry dates, the holder name, the raw and parsed/geocoded address, an `Approved`/`Declined` `status`, and a `warnings` list explaining every issue found.\n\n**Latency.** Extraction is LLM-based: typical calls take **5–15 seconds**, multi-page PDFs up to **~30 seconds**. Configure a client timeout of **at least 45 seconds** and do not retry before the call completes.\n\n**How it works.** The document (image or PDF, up to 15 MB; encrypted PDFs require `document_password`) is classified into a document type and subtype (e.g. `UTILITY_BILL` / `WATER_BILL`), and the issuer, dates, holder name, and address are extracted with LLM-based extraction. The address is parsed and geocoded into `poa_parsed_address`. PDF/EXIF forensics screen for tampering — modification after digital signing, known PDF editors, overlay-text manipulation, suspicious re-exports — surfacing `SUSPECTED_DOCUMENT_MANIPULATION` or `DOCUMENT_METADATA_MISMATCH`. Document age is validated against `poa_document_age_months`, and the language against `poa_languages_allowed`. If you pass `expected_address`, `expected_country`, or `expected_first_name`/`expected_last_name`, the extracted values are cross-checked and mismatches are flagged.\n\n**Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Hard failures always decline: `MISSING_ADDRESS_INFORMATION`, `POA_DOCUMENT_EXPIRED`, `INVALID_DOCUMENT_TYPE`, `UNABLE_TO_VALIDATE_DOCUMENT_AGE`, plus `UNABLE_TO_EXTRACT_ISSUE_DATE` and `POA_NAME_NOT_DETECTED` (not configurable on this endpoint). Five risk groups are configurable per request via the `poa_*_action` options (`DECLINE` or `NO_ACTION`, all defaulting to `DECLINE`). `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION` and `UNPARSABLE_OR_INVALID_ADDRESS` are informational on this endpoint and never decline. A document that cannot be processed at all returns `400`; a readable but problematic document returns `200` with `status: \"Declined\"` — always inspect `poa.status` and `poa.warnings`, not just the HTTP code.\n\n**Billing.** Each `200` response consumes one Proof of Address API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any document processing.\n\n**Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the uploaded document is stored, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the decision additionally exposes `document_file` and the `document_metadata` forensics, which this response omits), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (a fictional \"Sandbox Power Co.\" electricity bill), no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.\n\nNote: `FUTURE_ISSUE_DATE` and `POOR_DOCUMENT_QUALITY` exist in workflow sessions but are never produced by this standalone endpoint — a future-dated document is not auto-declined here.", - "operationId": "post_v3poa", - "tags": [ - "Standalone APIs" ], - "parameters": [], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": [ - "document" - ], - "properties": { - "document": { - "type": "string", - "format": "binary", - "description": "The Proof of Address document. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `pdf`, `webp`. Maximum upload size: **15 MB** (larger files are rejected with `400`). Multi-page PDFs are supported (expect higher latency); encrypted PDFs require `document_password`. Images are automatically compressed before processing." - }, - "document_password": { - "type": "string", - "description": "Password to decrypt `document` when it is an encrypted PDF. Sending an encrypted PDF without (or with a wrong) password returns `400`.", - "writeOnly": true - }, - "expected_address": { - "type": "string", - "description": "Address to verify against the document. It is geocoded and compared with the extracted address; a mismatch (or an expected address that cannot be geocoded/verified) adds `ADDRESS_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`. The geocoded form is echoed back in `expected_details_formatted_address` / `expected_details_parsed_address`.", - "example": "Av. Monseñor Tavella 1291, Salta, Argentina" - }, - "expected_country": { - "type": "string", - "description": "Country to verify against the document (ISO 3166-1 code, e.g. `ARG` or `AR`). A mismatch with the document's country adds `POA_COUNTRY_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`.", - "example": "ARG" - }, - "expected_first_name": { - "type": "string", - "description": "First name to verify against `name_on_document` (fuzzy match, transliteration-aware). A score below the match threshold adds `NAME_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`.", - "example": "Sophia" - }, - "expected_last_name": { - "type": "string", - "description": "Last name to verify against `name_on_document`, combined with `expected_first_name` for the match score.", - "example": "Martinez" - }, - "poa_languages_allowed": { - "type": "string", - "description": "Comma-separated list of accepted document languages as ISO 639-1 codes (e.g. `en,es,fr`). When omitted or blank, **all 51 supported languages** are accepted. A document in a language outside the list adds `UNSUPPORTED_DOCUMENT_LANGUAGE` (governed by `poa_unsupported_language_action`); an unknown code in the list returns `400` with the full set of supported codes.", - "example": "en,es" - }, - "poa_document_age_months": { - "type": "string", - "description": "Comma-separated `key:value` pairs setting the maximum document age in months per document type — keys: `utility_bill`, `bank_statement`, `government_issued_document`, `other_poa_document`; values: `1`–`120`, or `-1` for unlimited (no age check). Defaults when omitted: `utility_bill:3,bank_statement:3,government_issued_document:12,other_poa_document:12`. A document older than its limit declines with `POA_DOCUMENT_EXPIRED`. **Caution:** when you provide this field, list *every* type you accept — omitted types are treated as not accepted (informational `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION`) and their age check is skipped.", - "example": "utility_bill:3,bank_statement:6" - }, - "poa_name_mismatch_action": { - "type": "string", - "enum": [ - "DECLINE", - "NO_ACTION" - ], - "default": "DECLINE", - "description": "Accepted but **currently not applied** — name-mismatch warnings (`NAME_MISMATCH_WITH_PROVIDED`) follow `poa_address_mismatch_action` instead. Set `poa_address_mismatch_action` to control both name and address mismatch behavior." - }, - "poa_document_issues_action": { - "type": "string", - "enum": [ - "DECLINE", - "NO_ACTION" - ], - "default": "DECLINE", - "description": "What to do on file-integrity problems — in practice only `DOCUMENT_METADATA_MISMATCH` is produced by this endpoint." - }, - "poa_document_authenticity_action": { - "type": "string", - "enum": [ - "DECLINE", - "NO_ACTION" - ], - "default": "DECLINE", - "description": "What to do when document manipulation is suspected (`SUSPECTED_DOCUMENT_MANIPULATION`) — e.g. the PDF was modified after digital signing, processed by a known PDF editor, shows overlay-text manipulation, or has inconsistent EXIF dates." - }, - "poa_unsupported_language_action": { - "type": "string", - "enum": [ - "DECLINE", - "NO_ACTION" - ], - "default": "DECLINE", - "description": "What to do when the document language is outside `poa_languages_allowed` (`UNSUPPORTED_DOCUMENT_LANGUAGE`)." - }, - "poa_address_mismatch_action": { - "type": "string", - "enum": [ - "DECLINE", - "NO_ACTION" - ], - "default": "DECLINE", - "description": "What to do when the extracted details disagree with the `expected_*` fields: `ADDRESS_MISMATCH_WITH_PROVIDED`, `NAME_MISMATCH_WITH_PROVIDED`, and `POA_COUNTRY_MISMATCH_WITH_PROVIDED` all follow this action." - }, - "poa_issuer_not_identified_action": { - "type": "string", - "enum": [ - "DECLINE", - "NO_ACTION" - ], - "default": "DECLINE", - "description": "What to do when no issuing company/organization can be identified on the document (`ISSUER_NOT_IDENTIFIED`)." - }, - "save_api_request": { - "type": "boolean", - "default": true, - "description": "When `true` (default), persists the call as an API-type session — visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with the uploaded document stored. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only.", - "example": true - }, - "vendor_data": { - "type": "string", - "description": "Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later.", - "example": "user-123" - }, - "metadata": { - "type": "object", - "additionalProperties": true, - "description": "Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={\"flow\":\"onboarding\"}`) — it is parsed into an object.", - "example": { - "flow": "onboarding" - } - } + "responses": { + "200": { + "description": "The job.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OngoingMonitoringJob" } - }, - "example": { - "document": "(binary JPEG/PNG/PDF of the utility bill or bank statement)", - "expected_address": "Av. Monseñor Tavella 1291, Salta, Argentina", - "expected_country": "ARG", - "expected_first_name": "Sophia", - "expected_last_name": "Martinez", - "poa_languages_allowed": "en,es", - "save_api_request": true, - "vendor_data": "user-123" } } + }, + "404": { + "description": "Unknown job for this application." } }, - "responses": { - "200": { - "description": "Document processed. `poa.status` is `Approved` or `Declined`; every detected issue is itemized in `poa.warnings`. An expired, mismatched, or suspicious document still returns `200` with `status: \"Declined\"` — inspect the body, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id.", - "content": { - "application/json": { - "examples": { - "Approved": { - "summary": "Utility bill read cleanly, no expected-details checks", - "value": { - "request_id": "4b523a8d-ed32-45e4-8aa8-a89eda459d88", - "poa": { - "issuing_state": "ARG", - "document_type": "UTILITY_BILL", - "document_subtype": "WATER_BILL", - "document_language": "es", - "issuer": "Aguas del Norte", - "issue_date": "2026-04-10", - "expiration_date": null, - "poa_address": "AVDA. MONSEÑOR TAVELLA N° 1291, B° VILLA LAVALLE CP: 4400, CAPITAL - SALTA", - "poa_formatted_address": "Av. Monseñor Tavella 1291 b, A4400 Salta, Argentina", - "poa_parsed_address": { - "street_1": "Avenida Monseñor Tavella 1291", - "street_2": "b", - "city": "Salta", - "region": "Salta", - "country": "AR", - "postal_code": "A4400", - "document_location": { - "latitude": -24.8208682, - "longitude": -65.4130954 - } - }, - "expected_details_address": null, - "expected_details_formatted_address": null, - "expected_details_parsed_address": null, - "name_on_document": "OESTE EMBOTELLADORA S.A.", - "extra_fields": { - "bank_account_number": null, - "bank_iban": null, - "bank_sort_code": null, - "bank_routing_number": null, - "bank_swift_bic": null, - "bank_branch_name": null, - "bank_branch_address": null, - "document_phone_number": null, - "additional_names": [ - "LA EMBOTELLADORA DEL NORTE SA" - ] - }, - "detected_codes": [ - { - "type": "QR_CODE", - "page_number": 1, - "position": [ - [ - 820, - 1120 - ], - [ - 1002, - 1120 - ], - [ - 1002, - 1302 - ], - [ - 820, - 1302 - ] - ], - "x": 820, - "y": 1120, - "width": 182, - "height": 182, - "page_width": 1190, - "page_height": 1684, - "raw_payload": "aHR0cHM6Ly9hZ3Vhc2RlbG5vcnRlLmNvbS9mYWN0dXJhLzk5ODgxMQ==", - "parsed_payload": "https://aguasdelnorte.com/factura/998811" - }, - { - "type": "CODE_128", - "page_number": 1, - "position": [ - [ - 112, - 1480 - ], - [ - 560, - 1480 - ], - [ - 560, - 1540 - ], - [ - 112, - 1540 - ] - ], - "x": 112, - "y": 1480, - "width": 448, - "height": 60, - "page_width": 1190, - "page_height": 1684, - "raw_payload": "MDA5OTg4MTEwMDA0Mg==", - "parsed_payload": "0099881100042" - } - ], - "status": "Approved", - "warnings": [] - }, - "vendor_data": "user-123", - "metadata": null, - "created_at": "2026-06-12T02:29:02.439655+00:00" - } - }, - "Declined - document too old": { - "summary": "Issue date older than the allowed `poa_document_age_months`", - "value": { - "request_id": "f61681b8-9422-4378-ac3e-04c6a247fb45", - "poa": { - "issuing_state": "ARG", - "document_type": "UTILITY_BILL", - "document_subtype": "WATER_BILL", - "document_language": "es", - "issuer": "Aguas del Norte", - "issue_date": "2021-02-10", - "expiration_date": null, - "poa_address": "AVDA. MONSEÑOR TAVELLA N° 1291, B° VILLA LAVALLE CP: 4400, CAPITAL - SALTA", - "poa_formatted_address": "Av. Monseñor Tavella 1291 b, A4400 Salta, Argentina", - "poa_parsed_address": { - "street_1": "Avenida Monseñor Tavella 1291", - "street_2": "b", - "city": "Salta", - "region": "Salta", - "country": "AR", - "postal_code": "A4400", - "document_location": { - "latitude": -24.8208682, - "longitude": -65.4130954 - } - }, - "expected_details_address": null, - "expected_details_formatted_address": null, - "expected_details_parsed_address": null, - "name_on_document": "OESTE EMBOTELLADORA S.A.", - "extra_fields": { - "bank_account_number": null, - "bank_iban": null, - "bank_sort_code": null, - "bank_routing_number": null, - "bank_swift_bic": null, - "bank_branch_name": null, - "bank_branch_address": null, - "document_phone_number": null, - "additional_names": [ - "LA EMBOTELLADORA DEL NORTE SA" - ] - }, - "status": "Declined", - "warnings": [ - { - "risk": "POA_DOCUMENT_EXPIRED", - "feature": "PROOF_OF_ADDRESS", - "additional_data": { - "expiration_reason": "age_window", - "max_age_months": 3, - "document_subtype": "WATER_BILL", - "document_type": "UTILITY_BILL", - "issue_date": "2021-02-10" - }, - "log_type": "error", - "short_description": "Document expired", - "long_description": "The submitted document is older than the maximum accepted age of 3 months from its issue date, which exceeds the acceptable time period for validity." - } - ] - }, - "vendor_data": "user-123", - "metadata": null, - "created_at": "2026-06-12T02:23:02.165493+00:00" - } - }, - "Declined - name mismatch": { - "summary": "`expected_first_name`/`expected_last_name` do not match `name_on_document`", - "value": { - "request_id": "0a7f2f9c-58f1-4b8e-b1e3-2f6f9d4f7a21", - "poa": { - "issuing_state": "ARG", - "document_type": "UTILITY_BILL", - "document_subtype": "WATER_BILL", - "document_language": "es", - "issuer": "Aguas del Norte", - "issue_date": "2021-02-10", - "expiration_date": null, - "poa_address": "AVDA. MONSEÑOR TAVELLA N° 1291, B° VILLA LAVALLE CP: 4400, CAPITAL - SALTA", - "poa_formatted_address": "Av. Monseñor Tavella 1291 b, A4400 Salta, Argentina", - "poa_parsed_address": { - "street_1": "Avenida Monseñor Tavella 1291", - "street_2": "b", - "city": "Salta", - "region": "Salta", - "country": "AR", - "postal_code": "A4400", - "document_location": { - "latitude": -24.8208682, - "longitude": -65.4130954 - } - }, - "expected_details_address": null, - "expected_details_formatted_address": null, - "expected_details_parsed_address": null, - "name_on_document": "OESTE EMBOTELLADORA S.A.", - "extra_fields": { - "bank_account_number": null, - "bank_iban": null, - "bank_sort_code": null, - "bank_routing_number": null, - "bank_swift_bic": null, - "bank_branch_name": null, - "bank_branch_address": null, - "document_phone_number": null, - "additional_names": [ - "LA EMBOTELLADORA DEL NORTE SA" - ] - }, - "status": "Declined", - "warnings": [ - { - "risk": "NAME_MISMATCH_WITH_PROVIDED", - "feature": "PROOF_OF_ADDRESS", - "additional_data": null, - "log_type": "error", - "short_description": "Name mismatch with provided information", - "long_description": "The full name on the document does not match the name from the user's verified identity documents, or the full name sent by API." - } - ] - }, - "vendor_data": "user-123", - "metadata": null, - "created_at": "2026-06-12T02:27:21.330172+00:00" - } - } - }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/": { + "get": { + "summary": "List cases", + "description": "List cases for the application, newest first. Supports filtering, search, and sorting; see query parameters. Paginated with `limit`/`offset`.", + "operationId": "list_cases", + "x-didit-sdk": { + "resource": "cases", + "method": "list", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50, + "minimum": 1 + }, + "description": "Page size. Defaults to 50." + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + }, + "description": "Zero-based offset of the first record." + }, + { + "name": "status__in", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Comma-separated statuses to include, e.g. OPEN,AWAITING_USER." + }, + { + "name": "priority", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Comma-separated priorities to include." + }, + { + "name": "source__in", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Comma-separated sources to include." + }, + { + "name": "blueprint", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Filter to cases on this blueprint." + }, + { + "name": "assigned_to", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Filter to cases assigned to this user." + }, + { + "name": "unassigned", + "in": "query", + "schema": { + "type": "boolean" + }, + "description": "true for unassigned cases, false for assigned cases." + }, + { + "name": "tag", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Filter to cases carrying this exact tag." + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Matches title, case_number, tag, subject name/UUID/external ID, linked session UUID, or linked transaction UUID." + }, + { + "name": "date_from", + "in": "query", + "schema": { + "type": "string", + "format": "date" + }, + "description": "Created-at lower bound (inclusive)." + }, + { + "name": "date_to", + "in": "query", + "schema": { + "type": "string", + "format": "date" + }, + "description": "Created-at upper bound (inclusive)." + }, + { + "name": "due_before", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + }, + "description": "due_at upper bound." + }, + { + "name": "overdue", + "in": "query", + "schema": { + "type": "boolean" + }, + "description": "true to return only overdue open/awaiting-user cases." + }, + { + "name": "ordering", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "created_at", + "-created_at", + "due_at", + "-due_at" + ], + "default": "-created_at" + }, + "description": "Sort order. due_at sorts nulls last." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/?status__in=OPEN,AWAITING_USER&ordering=-due_at' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n params={'status__in': 'OPEN,AWAITING_USER', 'ordering': '-due_at'},\n timeout=10,\n)\nresp.raise_for_status()\nfor case in resp.json()['results']:\n print(case['case_number'], case['title'], case['priority'], case['due_at'])" + } + ], + "responses": { + "200": { + "description": "Paginated case list.", + "content": { + "application/json": { "schema": { "type": "object", "properties": { - "request_id": { - "type": "string", - "format": "uuid", - "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." - }, - "poa": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "Approved", - "Declined" - ], - "description": "`Approved` when no warning resolves to a decline; `Declined` otherwise — the `warnings` list explains why." - }, - "issuing_state": { - "type": "string", - "nullable": true, - "description": "Country the document was issued in, as extracted from the document — usually ISO 3166-1 alpha-3 (e.g. `ARG`).", - "example": "ARG" - }, - "document_type": { - "type": "string", - "enum": [ - "UTILITY_BILL", - "BANK_STATEMENT", - "GOVERNMENT_ISSUED_DOCUMENT", - "OTHER_POA_DOCUMENT", - "UNKNOWN" - ], - "description": "Detected document category. `UNKNOWN` means the document could not be classified as an acceptable Proof of Address and auto-declines with `INVALID_DOCUMENT_TYPE`. Never null — classification failures fall back to `UNKNOWN`." - }, - "document_subtype": { - "type": "string", - "description": "Finer-grained classification within `document_type` — e.g. `ELECTRICITY_BILL`, `WATER_BILL`, `GAS_BILL`, `INTERNET_BILL`, `PHONE_BILL` (utility bills); `ACCOUNT_STATEMENT`, `CREDIT_CARD_STATEMENT` (bank statements); `TAX_ASSESSMENT`, `RESIDENCY_CERTIFICATE` (government documents). `UNKNOWN` when no subtype could be determined. Never null — classification failures fall back to `UNKNOWN`.", - "example": "WATER_BILL" - }, - "document_language": { - "type": "string", - "nullable": true, - "description": "Detected document language (ISO 639-1).", - "example": "es" - }, - "issuer": { - "type": "string", - "nullable": true, - "description": "Company or organization that issued the document. `null` adds `ISSUER_NOT_IDENTIFIED`.", - "example": "Aguas del Norte" - }, - "issue_date": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Document issue date (`YYYY-MM-DD`). `null` when it could not be extracted — that declines with `UNABLE_TO_EXTRACT_ISSUE_DATE` on this endpoint.", - "example": "2021-02-10" - }, - "expiration_date": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Explicit expiry/validity date printed on the document, when present. Independent of the `poa_document_age_months` age check." - }, - "poa_address": { - "type": "string", - "nullable": true, - "description": "Address exactly as printed on the document. `null` auto-declines with `MISSING_ADDRESS_INFORMATION`." - }, - "poa_formatted_address": { - "type": "string", - "nullable": true, - "description": "Geocoded, normalized version of `poa_address`.", - "example": "Av. Monseñor Tavella 1291 b, A4400 Salta, Argentina" - }, - "poa_parsed_address": { - "type": "object", - "nullable": true, - "description": "Structured, geocoded breakdown of the document address. `null` when the address could not be parsed.", - "properties": { - "street_1": { - "type": "string", - "nullable": true, - "example": "Avenida Monseñor Tavella 1291" - }, - "street_2": { - "type": "string", - "nullable": true - }, - "city": { - "type": "string", - "nullable": true, - "example": "Salta" - }, - "region": { - "type": "string", - "nullable": true, - "example": "Salta" - }, - "country": { - "type": "string", - "nullable": true, - "description": "ISO 3166-1 alpha-2 country code.", - "example": "AR" - }, - "postal_code": { - "type": "string", - "nullable": true, - "example": "A4400" - }, - "document_location": { - "type": "object", - "nullable": true, - "properties": { - "latitude": { - "type": "number" - }, - "longitude": { - "type": "number" - } - }, - "description": "Geocoded coordinates of the document address." - } - } - }, - "expected_details_address": { - "type": "string", - "nullable": true, - "description": "Echo of the `expected_address` you sent, or `null`." - }, - "expected_details_formatted_address": { - "type": "string", - "nullable": true, - "description": "Geocoded, normalized version of `expected_address`; `null` when not provided or not geocodable." - }, - "expected_details_parsed_address": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Full geocoding result for `expected_address` (street/city/region/country/postal code plus raw geocoder output and an `is_verified` flag). `null` when `expected_address` was not provided." - }, - "name_on_document": { - "type": "string", - "nullable": true, - "description": "Primary holder name extracted from the document; compared against `expected_first_name`/`expected_last_name` when provided. `null` declines with `POA_NAME_NOT_DETECTED` on this endpoint.", - "example": "OESTE EMBOTELLADORA S.A." - }, - "extra_fields": { - "type": "object", - "description": "Document-specific extras. Bank fields are populated for bank statements when printed on the document; `null` otherwise.", - "properties": { - "bank_account_number": { - "type": "string", - "nullable": true - }, - "bank_iban": { - "type": "string", - "nullable": true - }, - "bank_sort_code": { - "type": "string", - "nullable": true - }, - "bank_routing_number": { - "type": "string", - "nullable": true - }, - "bank_swift_bic": { - "type": "string", - "nullable": true - }, - "bank_branch_name": { - "type": "string", - "nullable": true - }, - "bank_branch_address": { - "type": "string", - "nullable": true - }, - "document_phone_number": { - "type": "string", - "nullable": true, - "description": "Contact phone number printed on the document." - }, - "additional_names": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Other holder names found on the document (e.g. co-account holders, previous account names)." - } - } - }, - "warnings": { - "type": "array", - "description": "Empty on a clean approval. Every entry explains one detected risk; entries with `log_type: \"error\"` set `status` to `Declined`.", - "items": { - "type": "object", - "properties": { - "risk": { - "type": "string", - "description": "Machine-readable risk code. Always-decline: `MISSING_ADDRESS_INFORMATION`, `POA_DOCUMENT_EXPIRED`, `INVALID_DOCUMENT_TYPE`, `UNABLE_TO_VALIDATE_DOCUMENT_AGE`, plus `UNABLE_TO_EXTRACT_ISSUE_DATE` and `POA_NAME_NOT_DETECTED` (fixed to decline on this endpoint). Governed by request options: `NAME_MISMATCH_WITH_PROVIDED` / `ADDRESS_MISMATCH_WITH_PROVIDED` / `POA_COUNTRY_MISMATCH_WITH_PROVIDED` (`poa_address_mismatch_action`), `POOR_DOCUMENT_QUALITY` (never produced by this endpoint) / `DOCUMENT_METADATA_MISMATCH` (`poa_document_issues_action`), `SUSPECTED_DOCUMENT_MANIPULATION` (`poa_document_authenticity_action`), `UNSUPPORTED_DOCUMENT_LANGUAGE` (`poa_unsupported_language_action`), `ISSUER_NOT_IDENTIFIED` (`poa_issuer_not_identified_action`). Informational on this endpoint: `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION`, `UNPARSABLE_OR_INVALID_ADDRESS`.", - "example": "POA_DOCUMENT_EXPIRED" - }, - "feature": { - "type": "string", - "enum": [ - "PROOF_OF_ADDRESS" - ], - "description": "Feature that raised the warning. Always `PROOF_OF_ADDRESS` on this endpoint." - }, - "additional_data": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Extra context for some risks — e.g. `POA_DOCUMENT_EXPIRED` includes `expiration_reason` (`printed_expiration` or `age_window`), `document_type`, `document_subtype`, and `issue_date`, plus `expiration_date` on the `printed_expiration` path or `max_age_months` on the `age_window` path; `SUSPECTED_DOCUMENT_MANIPULATION` includes `reason` and `detection_method`. `null` for most risks." - }, - "log_type": { - "type": "string", - "enum": [ - "error", - "information" - ], - "description": "Severity. `error` warnings drive `status` to `Declined`; `information` entries are advisory and never decline on their own. (`warning` is not produced by this endpoint — its options only accept `DECLINE` or `NO_ACTION`.)" - }, - "short_description": { - "type": "string", - "description": "Human-readable one-line summary of the risk." - }, - "long_description": { - "type": "string", - "description": "Human-readable explanation of the risk." - } - } - } - }, - "detected_codes": { - "type": "array", - "description": "QR codes and barcodes detected across every processed page of the document. Empty when none are found. A located but undecodable QR code has `parsed_payload: null`.", - "items": { - "$ref": "#/components/schemas/DocumentAIDetectedCode" - } - } - } + "count": { + "type": "integer", + "description": "Number of matching rows. Exact up to 100, capped at 100 beyond that for performance. Rely on `next` being null to detect the last page." }, - "vendor_data": { + "next": { "type": "string", "nullable": true, - "description": "Echo of the `vendor_data` you sent, or `null`." + "description": "Absolute URL of the next page, or null." }, - "metadata": { - "type": "object", + "previous": { + "type": "string", "nullable": true, - "additionalProperties": true, - "description": "Echo of the `metadata` object you sent, or `null`." + "description": "Absolute URL of the previous page, or null." }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 timestamp (UTC) of when the response was generated." + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseListItem" + } + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "post": { + "summary": "Create case", + "description": "Create a case anchored to exactly one subject (a user or a business), directly or by deriving the subject from an attached session/transaction link. Source is always `MANUAL` for API-created cases.", + "operationId": "create_case", + "x-didit-sdk": { + "resource": "cases", + "method": "create", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "title" + ], + "properties": { + "title": { + "type": "string", + "maxLength": 255 + }, + "blueprint": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "CaseBlueprint UUID. Omit for no blueprint." + }, + "priority": { + "type": "string", + "enum": [ + "LOW", + "MEDIUM", + "HIGH" + ], + "default": "MEDIUM" + }, + "due_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "subject": { + "type": "object", + "description": "Required unless links is provided; the case's exactly-one subject.", + "properties": { + "type": { + "type": "string", + "enum": [ + "user", + "business" + ] + }, + "uuid": { + "type": "string", + "format": "uuid" + } + } + }, + "links": { + "type": "array", + "description": "Sessions/business sessions/transactions to attach on creation, e.g. [{\"entity_type\": \"transaction\", \"entity_uuid\": \"...\"}]. When subject is omitted, the subject is derived from the first link's owner.", + "items": { + "type": "object" } + }, + "assigned_to": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "User UUID to assign immediately." + }, + "review_duplicates": { + "type": "boolean", + "default": false, + "description": "Recheck existing investigations before creating the case. When true, a subject with existing cases requires a nonblank separate_incident_reason. The console always enables this protection. Omitting it preserves existing API behavior." + }, + "separate_incident_reason": { + "type": "string", + "maxLength": 1000 + }, + "source_context": { + "$ref": "#/components/schemas/CaseDuplicateCaseFraudSourceContextRequest" } } } } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"title\": \"Manual - Jane Doe\", \"blueprint\": \"b1de0000-0000-4000-8000-000000000002\", \"priority\": \"HIGH\", \"subject\": {\"type\": \"user\", \"uuid\": \"5b9a0000-0000-4000-8000-00000000f00d\"}, \"tags\": [\"review\"]}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={\n 'title': 'Manual - Jane Doe',\n 'blueprint': 'b1de0000-0000-4000-8000-000000000002',\n 'priority': 'HIGH',\n 'subject': {'type': 'user', 'uuid': '5b9a0000-0000-4000-8000-00000000f00d'},\n },\n timeout=10,\n)\nresp.raise_for_status()\ncase = resp.json()\nprint(case['case_number'], case['uuid'])" + } + ], + "responses": { + "201": { + "description": "Case created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseDetail" + } + } + } }, "400": { - "description": "Validation or processing error. Field-level problems return DRF's standard envelope (one array of messages per offending field). Encrypted-PDF problems return `{\"detail\": ...}`. A document the extraction pipeline cannot process returns `{\"error\": [\"Error extracting POA information\"]}`.", + "description": "Neither subject nor links provided, or the subject/blueprint/assignee was not found.", "content": { "application/json": { - "examples": { - "Missing document": { - "summary": "`document` not included in the form data", - "value": { - "document": [ - "No file was submitted." - ] - } - }, - "Unsupported file extension": { - "summary": "File extension outside tiff/jpg/jpeg/png/pdf/webp", - "value": { - "document": [ - "File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, pdf, webp." - ] + "schema": { + "type": "object" + } + } + } + }, + "409": { + "description": "Duplicate review is enabled and an existing case was found without a separate-incident reason. No case was created." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/": { + "get": { + "summary": "Get case", + "description": "Retrieve a case with its subject, blueprint, notes, events, links, and checklist.", + "operationId": "get_case", + "x-didit-sdk": { + "resource": "cases", + "method": "retrieve", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Case detail.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseDetail" + } + } + } + }, + "404": { + "description": "Case (or nested resource) not found in this application.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "patch": { + "summary": "Update case", + "description": "Update the case's title, priority, due date, tags, or owning review team. Send only the fields you want to change; fields you omit keep their value. `tags` replaces the whole tag list. `team` takes the uuid of an active review team of this application (list them with `GET .../cases/teams/`), or null to clear the team. Requires a full-access API key, or `write:cases` for a console member: a key with restricted access gets 403. Logs PRIORITY_CHANGED, DUE_DATE_CHANGED, TAG_ADDED, TAG_REMOVED and TEAM_CHANGED events as applicable; a title change is saved without a timeline event. Returns the updated case.", + "operationId": "update_case", + "x-didit-sdk": { + "resource": "cases", + "method": "update", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "maxLength": 255, + "description": "New case title." }, - "File too large": { - "summary": "Upload exceeds the 15 MB limit", - "value": { - "document": [ - "File size should not exceed 15 MB" - ] - } + "priority": { + "type": "string", + "enum": [ + "LOW", + "MEDIUM", + "HIGH" + ] }, - "Encrypted PDF": { - "summary": "PDF uploaded without the required password", - "value": { - "detail": "The PDF is encrypted. Please upload a decrypted PDF or a photo instead." - } + "due_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "New deadline. Send null to clear it." }, - "Wrong PDF password": { - "summary": "`document_password` does not decrypt the PDF", - "value": { - "detail": "The PDF password is incorrect. Please provide the correct password." - } + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The complete tag list. It replaces the current tags." }, - "Invalid language code": { - "summary": "`poa_languages_allowed` contains an unsupported code", - "value": { - "poa_languages_allowed": [ - "Invalid language code: 'xx'. Must be one of the supported languages: ['ar', 'bn', 'hy', 'bg', 'bs', 'ca', 'cnr', 'sq', 'zh', 'hr', 'cs', 'da', 'nl', 'en', 'et', 'fi', 'fr', 'ka', 'kk', 'de', 'el', 'he', 'hi', 'hu', 'id', 'it', 'ja', 'ko', 'ky', 'lv', 'lt', 'mk', 'mn', 'ms', 'no', 'fa', 'pl', 'pt', 'ro', 'ru', 'sr', 'sk', 'sl', 'so', 'es', 'sv', 'th', 'tr', 'uk', 'uz', 'vi']." - ] + "team": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "UUID of the review team that owns the case. It must be an active team of this application. Send null to clear the team." + } + } + }, + "examples": { + "Change priority and deadline": { + "value": { + "priority": "HIGH", + "due_at": "2026-11-01T00:00:00Z" + } + }, + "Move the case to a team": { + "value": { + "team": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + }, + "Clear the team": { + "value": { + "team": null + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"priority\": \"HIGH\", \"due_at\": \"2026-08-01T00:00:00Z\"}'" + }, + { + "lang": "curl", + "label": "curl (change team)", + "source": "curl -X PATCH 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"team\": \"3fa85f64-5717-4562-b3fc-2c963f66afa6\"}'" + } + ], + "responses": { + "200": { + "description": "Updated case.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseDetail" + } + } + } + }, + "400": { + "description": "A field failed validation. The body is keyed by field name. `team` is rejected when it is not an active review team of this application.", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" } - }, - "Invalid document age value": { - "summary": "`poa_document_age_months` value outside 1–120 / -1", + } + }, + "examples": { + "Team not active in this application": { "value": { - "poa_document_age_months": [ - "Invalid integer value for 'utility_bill': '999'. Must be -1 (unlimited) or a positive integer between 1 and 120." + "team": [ + "Choose an active team of this application." ] } }, - "Extraction failure": { - "summary": "The document could not be processed by the extraction pipeline", + "Invalid priority": { "value": { - "error": [ - "Error extracting POA information" + "priority": [ + "\"URGENT\" is not a valid choice." ] } } @@ -14166,38 +34904,30 @@ } }, "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any document processing happens.", + "description": "The API key belongs to another application, or it is a key with restricted access: Cases endpoints need a full-access key. A console member token needs `write:cases`.", "content": { "application/json": { "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", + "Restricted API Key": { + "summary": "API key with restricted access", "value": { - "detail": "You do not have permission to perform this action." + "detail": "This API key has no access to this endpoint." } }, - "Not enough credits": { - "summary": "Organization balance cannot cover the call", + "Missing Privilege": { + "summary": "Console member without write:cases, or a key of another application", "value": { - "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + "detail": "You do not have permission to perform this action." } } } } } }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "404": { + "description": "Case not found in this application.", "content": { "application/json": { - "examples": { - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." - } - } - }, "schema": { "type": "object", "properties": { @@ -14205,6 +34935,9 @@ "type": "string" } } + }, + "example": { + "detail": "No Case matches the given query." } } } @@ -14214,831 +34947,653 @@ { "ApiKeyAuth": [] } + ] + }, + "delete": { + "summary": "Delete case", + "description": "Delete a case. There is no undelete endpoint.", + "operationId": "delete_case", + "x-didit-sdk": { + "resource": "cases", + "method": "delete", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" ], - "x-codeSamples": [ + "parameters": [ { - "lang": "curl", - "label": "cURL", - "source": "curl -X POST 'https://verification.didit.me/v3/poa/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'document=@./utility-bill.pdf' \\\n -F 'expected_address=Av. Monseñor Tavella 1291, Salta, Argentina' \\\n -F 'expected_country=ARG' \\\n -F 'expected_first_name=Sophia' \\\n -F 'expected_last_name=Martinez' \\\n -F 'poa_languages_allowed=en,es' \\\n -F 'vendor_data=user-123' \\\n --max-time 45" + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, { - "lang": "python", - "label": "Python", - "source": "import requests\n\nurl = 'https://verification.didit.me/v3/poa/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('utility-bill.pdf', 'rb') as f:\n files = {'document': ('utility-bill.pdf', f, 'application/pdf')}\n data = {\n 'expected_address': 'Av. Monseñor Tavella 1291, Salta, Argentina',\n 'expected_country': 'ARG',\n 'expected_first_name': 'Sophia',\n 'expected_last_name': 'Martinez',\n 'poa_languages_allowed': 'en,es',\n 'vendor_data': 'user-123',\n }\n # LLM-based extraction takes 5-15s (multi-page PDFs up to ~30s) - allow headroom\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=45)\n\nresp.raise_for_status()\npoa = resp.json()['poa']\nprint('status:', poa['status'])\nprint('issuer:', poa['issuer'], '| issued:', poa['issue_date'])\nprint('address:', poa['poa_formatted_address'])\nfor w in poa['warnings']:\n print('warning:', w['risk'])" + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." }, { - "lang": "javascript", - "label": "JavaScript", - "source": "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('document', new Blob([fs.readFileSync('./utility-bill.pdf')]), 'utility-bill.pdf');\nform.append('expected_address', 'Av. Monseñor Tavella 1291, Salta, Argentina');\nform.append('expected_country', 'ARG');\nform.append('expected_first_name', 'Sophia');\nform.append('expected_last_name', 'Martinez');\nform.append('poa_languages_allowed', 'en,es');\nform.append('vendor_data', 'user-123');\n\n// LLM-based extraction takes 5-15s (multi-page PDFs up to ~30s) - allow >=45s\nconst response = await fetch('https://verification.didit.me/v3/poa/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n signal: AbortSignal.timeout(45_000),\n});\n\nif (!response.ok) throw new Error(`POA verification failed: ${response.status}`);\nconst { poa } = await response.json();\nconsole.log('status:', poa.status);\nconsole.log('warnings:', poa.warnings.map((w) => w.risk));" + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X DELETE 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "204": { + "description": "Case deleted." + } + }, + "security": [ + { + "ApiKeyAuth": [] } ] } }, - "/v3/document-ai/": { + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/assign/": { "post": { - "summary": "Document AI (custom field extraction + tamper checks)", - "description": "Extract the fields **you** define from any supporting document — payslips, proof of funds, source-of-wealth letters, tax statements, contracts, invoices — in one call. Send the file plus a `fields` definition (key, name, extraction instruction, type) and get back typed values (`text`, `number`, `date` as `YYYY-MM-DD`), an optional name-match score against the expected person or company, PDF/EXIF tamper signals, an `Approved`/`Declined`/`In Review` `status`, and a `warnings` list explaining every issue found. This is the server-to-server counterpart of the hosted Document AI workflow step (see `/core-technology/document-ai/overview`); it uses Didit's default extraction model and does not expose provider selection.\n\n**Latency.** Extraction is LLM-based: typical calls take **5–15 seconds**, multi-page PDFs up to **~30 seconds**. Configure a client timeout of **at least 45 seconds** and do not retry before the call completes.\n\n**Request format.** `multipart/form-data` only. `fields` is **one JSON-encoded array string** (1–30 items, at most 16 KB); bracket-style form arrays are rejected with `400`. Each item is `{key, name, instruction, type, required, is_full_name}` — `key` is the JSON key you get back (`^[a-z0-9_]{1,64}$`, unique), `instruction` tells the model what to read, `type` is `text` (default) | `number` | `date`, `required` raises `DOCUMENT_AI_MISSING_REQUIRED_FIELDS` when the value cannot be read, and at most one field may set `is_full_name: true` to mark the value compared against the expected name.\n\n**Name matching.** Pass `expected_first_name` + `expected_last_name` (person) **or** `expected_company_name` (+ optional `expected_company_country`) — not both. The value of the `is_full_name` field (or, when none is marked, the first text field whose key looks like a name) is compared fuzzily; `name_match_score` (0–100) and `name_on_document` are always returned when both sides exist, and a score below `document_ai_name_match_score_threshold` (default 80) adds `DOCUMENT_AI_NAME_MISMATCH`, governed by `document_ai_name_mismatch_action`.\n\n**Decision logic.** Each risk maps to a configurable action: `DECLINE` → `status: \"Declined\"`, `REVIEW` → `status: \"In Review\"`, `NO_ACTION` → informational only. Defaults mirror the workflow step: unreadable document `REVIEW`, missing required fields `REVIEW`, suspected manipulation `DECLINE`, name mismatch `NO_ACTION`, unsupported file `REVIEW`. A document that cannot be processed at all returns `400`; a readable but problematic document returns `200` with `Declined`/`In Review` — always inspect `document_ai.status` and `document_ai.warnings`, not just the HTTP code.\n\n**Billing.** Each `200` response consumes one Document AI API credit — **$0.20 per document, no free tier** (standalone APIs never have one). When the organization's balance cannot cover the call, the endpoint returns `403` before any processing. Sandbox API keys return a deterministic mock response and are not charged.\n\n**Persistence.** With `save_api_request=true` (default) the call is stored as an API-type session: the document, the extracted values, the field definitions and the name-match snapshot are kept, the session shows up in the Business Console, and `request_id` works with `GET /v3/session/{sessionId}/decision/`.", - "operationId": "post_v3document-ai", + "summary": "Assign or unassign case", + "description": "Assign the case to a user, or unassign it by sending assignee_user_id: null. assignee_email/assignee_name auto-provision a User row when the assignee does not exist yet.", + "operationId": "assign_case", + "x-didit-sdk": { + "resource": "cases", + "method": "assign", + "auth": "api_key", + "server": true, + "scope": "application" + }, "tags": [ - "Standalone APIs" + "Cases" ], - "security": [ + "parameters": [ { - "ApiKeyAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": [ - "document", - "fields" - ], - "properties": { - "document": { - "type": "string", - "format": "binary", - "description": "The document to read. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `pdf`, `webp`, `zip`. Maximum upload size: **30 MB** (larger files are rejected with `400`). Multi-page PDFs are supported (expect higher latency); encrypted PDFs are rejected with `400`. Images are automatically compressed before processing." - }, - "fields": { - "type": "string", - "description": "One JSON-encoded array string with **1–30** field definitions (maximum 16 KB). Each item: `key` (required, `^[a-z0-9_]{1,64}$`, unique — the key returned in `extracted_data`), `name` (≤100 chars, shown in messages), `instruction` (≤500 chars, what the model should read), `type` (`text` default | `number` | `date`), `required` (boolean, default `false`), `is_full_name` (boolean, default `false`, at most one per request). Bracket-style form arrays, invalid JSON, duplicate keys, or more than one `is_full_name` return `400` before any processing or billing.", - "example": "[{\"key\": \"employer_name\", \"name\": \"Employer name\", \"instruction\": \"The employer or company issuing the payslip.\", \"type\": \"text\", \"required\": true, \"is_full_name\": false}, {\"key\": \"employee_name\", \"name\": \"Employee name\", \"instruction\": \"The full name of the employee as printed on the payslip.\", \"type\": \"text\", \"required\": true, \"is_full_name\": true}, {\"key\": \"net_pay\", \"name\": \"Net pay\", \"instruction\": \"The net amount paid to the employee for the period.\", \"type\": \"number\", \"required\": true, \"is_full_name\": false}, {\"key\": \"pay_date\", \"name\": \"Pay date\", \"instruction\": \"The payment date shown on the payslip.\", \"type\": \"date\", \"required\": false, \"is_full_name\": false}]" - }, - "expected_first_name": { - "type": "string", - "description": "First name to verify against the document's name field (fuzzy match, transliteration-aware). Use together with `expected_last_name`; cannot be combined with `expected_company_name`.", - "example": "Sophia" - }, - "expected_last_name": { - "type": "string", - "description": "Last name to verify against the document's name field, combined with `expected_first_name` for `name_match_score`.", - "example": "Martinez" - }, - "expected_company_name": { - "type": "string", - "description": "Company name to verify against the document's name field (for business documents). Cannot be combined with `expected_first_name` / `expected_last_name` (no example value on purpose — the generated request sample uses the person fields; swap them for this one when verifying a business document)." - }, - "expected_company_country": { - "type": "string", - "description": "Optional ISO 3166-1 alpha-2 country code (two ASCII letters, e.g. `FR`) that tunes company-name normalization for `expected_company_name`. Only used with `expected_company_name`." - }, - "document_ai_name_match_score_threshold": { - "type": "integer", - "minimum": 0, - "maximum": 100, - "default": 80, - "description": "Minimum `name_match_score` (0–100) to consider the name matched. A lower score adds `DOCUMENT_AI_NAME_MISMATCH`, governed by `document_ai_name_mismatch_action`.", - "example": 80 - }, - "document_ai_unreadable_document_action": { - "type": "string", - "enum": [ - "DECLINE", - "REVIEW", - "NO_ACTION" - ], - "default": "REVIEW", - "description": "What to do when no field could be read from the document (`DOCUMENT_AI_UNREADABLE_DOCUMENT`)." - }, - "document_ai_missing_required_fields_action": { - "type": "string", - "enum": [ - "DECLINE", - "REVIEW", - "NO_ACTION" - ], - "default": "REVIEW", - "description": "What to do when at least one `required` field could not be read (`DOCUMENT_AI_MISSING_REQUIRED_FIELDS`; `additional_data.missing_fields` lists them)." - }, - "document_ai_document_tampering_action": { - "type": "string", - "enum": [ - "DECLINE", - "REVIEW", - "NO_ACTION" - ], - "default": "DECLINE", - "description": "What to do when PDF/EXIF forensics suspect manipulation — modified after digital signing, known PDF editor, overlay-text manipulation, suspicious re-export, image anomalies (`DOCUMENT_AI_SUSPECTED_MANIPULATION`)." - }, - "document_ai_name_mismatch_action": { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "assignee_user_id" + ], + "properties": { + "assignee_user_id": { "type": "string", - "enum": [ - "DECLINE", - "REVIEW", - "NO_ACTION" - ], - "default": "NO_ACTION", - "description": "What to do when `name_match_score` is below the threshold (`DOCUMENT_AI_NAME_MISMATCH`)." + "format": "uuid", + "nullable": true, + "description": "null to unassign." }, - "document_ai_unsupported_file_action": { + "assignee_email": { "type": "string", - "enum": [ - "DECLINE", - "REVIEW", - "NO_ACTION" - ], - "default": "REVIEW", - "description": "What to do when the upload is empty or not a processable file (`DOCUMENT_AI_UNSUPPORTED_FILE`)." - }, - "save_api_request": { - "type": "boolean", - "default": true, - "description": "When `true` (default), persists the call as an API-type session — visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, with the uploaded document, the extracted values, the field definitions and the name-match snapshot stored. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only; the call is still billed.", - "example": true + "format": "email", + "nullable": true }, - "vendor_data": { + "assignee_name": { "type": "string", - "description": "Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later.", - "example": "user-123" - }, - "metadata": { - "type": "object", - "additionalProperties": true, - "description": "Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={\"flow\":\"onboarding\"}`) — it is parsed into an object.", - "example": { - "flow": "onboarding" - } + "nullable": true } } } } } }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/assign/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"assignee_user_id\": \"9a000000-0000-4000-8000-0000000000aa\"}'" + } + ], "responses": { "200": { - "description": "Document processed. `document_ai.status` is `Approved`, `Declined`, or `In Review`; every detected issue is itemized in `document_ai.warnings`. A problematic document still returns `200` — inspect the body, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id.", + "description": "Case assigned/unassigned.", "content": { "application/json": { - "examples": { - "Approved": { - "summary": "Payslip read cleanly, name matched", - "value": { - "request_id": "9d6a3b1e-2f0c-4a8e-9b7d-1c2e3f4a5b6c", - "document_ai": { - "status": "Approved", - "extracted_data": { - "employer_name": "Acme Payments Ltd", - "employee_name": "Sophia Martinez", - "net_pay": 3120.5, - "pay_date": "2026-07-31" - }, - "detected_codes": [ - { - "type": "CODE_128", - "page_number": 1, - "position": [ - [ - 96, - 456 - ], - [ - 96, - 616 - ], - [ - 1012, - 611 - ], - [ - 1015, - 456 - ] - ], - "x": 96, - "y": 456, - "width": 919, - "height": 160, - "page_width": 1200, - "page_height": 1700, - "raw_payload": "SU5WLTIwMjYtMDA0Mg==", - "parsed_payload": "INV-2026-0042" - } - ], - "fields": [ - { - "key": "employer_name", - "name": "Employer name", - "instruction": "The employer or company issuing the payslip.", - "type": "text", - "required": true, - "is_full_name": false - }, - { - "key": "employee_name", - "name": "Employee name", - "instruction": "The full name of the employee as printed on the payslip.", - "type": "text", - "required": true, - "is_full_name": true - }, - { - "key": "net_pay", - "name": "Net pay", - "instruction": "The net amount paid to the employee for the period.", - "type": "number", - "required": true, - "is_full_name": false - }, - { - "key": "pay_date", - "name": "Pay date", - "instruction": "The payment date shown on the payslip.", - "type": "date", - "required": false, - "is_full_name": false - } - ], - "name_match_score": 100.0, - "name_on_document": "Sophia Martinez", - "warnings": [] - }, - "vendor_data": "user-123", - "metadata": { - "flow": "onboarding" - }, - "created_at": "2026-08-19T16:40:12.318Z" - } - }, - "In Review — missing required field": { - "summary": "`net_pay` could not be read; default action REVIEW", - "value": { - "request_id": "3f2a9c7b-5d4e-4b1a-8c6f-0e9d8c7b6a5f", - "document_ai": { - "status": "In Review", - "extracted_data": { - "employer_name": "Acme Payments Ltd", - "employee_name": "Sophia Martinez", - "net_pay": null, - "pay_date": "2026-07-31" - }, - "detected_codes": [], - "fields": [ - { - "key": "employer_name", - "name": "Employer name", - "instruction": "The employer or company issuing the payslip.", - "type": "text", - "required": true, - "is_full_name": false - }, - { - "key": "employee_name", - "name": "Employee name", - "instruction": "The full name of the employee as printed on the payslip.", - "type": "text", - "required": true, - "is_full_name": true - }, - { - "key": "net_pay", - "name": "Net pay", - "instruction": "The net amount paid to the employee for the period.", - "type": "number", - "required": true, - "is_full_name": false - }, - { - "key": "pay_date", - "name": "Pay date", - "instruction": "The payment date shown on the payslip.", - "type": "date", - "required": false, - "is_full_name": false - } - ], - "name_match_score": 100.0, - "name_on_document": "Sophia Martinez", - "warnings": [ - { - "risk": "DOCUMENT_AI_MISSING_REQUIRED_FIELDS", - "feature": "DOCUMENT_AI", - "additional_data": { - "missing_fields": [ - "Net pay" - ] - }, - "log_type": "warning", - "short_description": "Required fields could not be extracted", - "long_description": "One or more fields configured as required could not be extracted from the uploaded document." - } - ] - }, - "vendor_data": null, - "metadata": null, - "created_at": "2026-08-19T16:41:05.002Z" - } - }, - "Declined — suspected manipulation": { - "summary": "PDF forensics flagged a known editor; default action DECLINE", - "value": { - "request_id": "7c1d2e3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f", - "document_ai": { - "status": "Declined", - "extracted_data": { - "employer_name": "Acme Payments Ltd", - "employee_name": "Sophia Martinez", - "net_pay": 3120.5, - "pay_date": "2026-07-31" - }, - "detected_codes": [], - "fields": [ - { - "key": "employer_name", - "name": "Employer name", - "instruction": "The employer or company issuing the payslip.", - "type": "text", - "required": true, - "is_full_name": false - }, - { - "key": "employee_name", - "name": "Employee name", - "instruction": "The full name of the employee as printed on the payslip.", - "type": "text", - "required": true, - "is_full_name": true - }, - { - "key": "net_pay", - "name": "Net pay", - "instruction": "The net amount paid to the employee for the period.", - "type": "number", - "required": true, - "is_full_name": false - }, - { - "key": "pay_date", - "name": "Pay date", - "instruction": "The payment date shown on the payslip.", - "type": "date", - "required": false, - "is_full_name": false - } - ], - "name_match_score": null, - "name_on_document": "Sophia Martinez", - "warnings": [ - { - "risk": "DOCUMENT_AI_SUSPECTED_MANIPULATION", - "feature": "DOCUMENT_AI", - "additional_data": { - "detection_method": "known_editor", - "processed_by_known_editor": "ilovepdf" - }, - "log_type": "error", - "short_description": "Suspected document manipulation", - "long_description": "The system detected signs of potential manipulation or editing in the uploaded document." - } - ] - }, - "vendor_data": null, - "metadata": null, - "created_at": "2026-08-19T16:42:40.771Z" - } - } - }, "schema": { - "type": "object", - "properties": { - "request_id": { - "type": "string", - "format": "uuid", - "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." - }, - "document_ai": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "Approved", - "Declined", - "In Review" - ], - "description": "`Approved` when no warning resolves to a decline or review; `Declined` when at least one risk maps to `DECLINE`; otherwise `In Review` when at least one maps to `REVIEW`. The `warnings` list explains why." - }, - "extracted_data": { - "type": "object", - "additionalProperties": true, - "description": "One entry per requested field key. Values are typed per the field `type` — strings for `text`, numbers for `number`, `YYYY-MM-DD` strings for `date` — or `null` when the value could not be read.", - "example": { - "employer_name": "Acme Payments Ltd", - "employee_name": "Sophia Martinez", - "net_pay": 3120.5, - "pay_date": "2026-07-31" - } - }, - "detected_codes": { - "type": "array", - "description": "QR codes and barcodes detected across every processed page. Empty when none are found. A located but undecodable QR code has `parsed_payload: null`.", - "items": { - "$ref": "#/components/schemas/DocumentAIDetectedCode" - } - }, - "fields": { - "type": "array", - "description": "The normalized field definitions that were used for this extraction, echoed back (also stored with the session when `save_api_request=true`).", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "name": { - "type": "string" - }, - "instruction": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "text", - "number", - "date" - ] - }, - "required": { - "type": "boolean" - }, - "is_full_name": { - "type": "boolean" - } - } - } - }, - "name_match_score": { - "type": "number", - "nullable": true, - "minimum": 0, - "maximum": 100, - "description": "Fuzzy similarity (0–100) between `name_on_document` and the expected person/company name. Returned whenever both a name value and an expected name exist — on matches **and** mismatches; `null` otherwise.", - "example": 96.0 - }, - "name_on_document": { - "type": "string", - "nullable": true, - "description": "The name value taken from the document (the `is_full_name` field, or the first name-like text field) that was compared against the expected name. `null` when no name value was available.", - "example": "Sophia Martinez" - }, - "warnings": { - "type": "array", - "description": "Empty on a clean approval. Every entry explains one detected risk; entries with `log_type: \"error\"` set `status` to `Declined`, entries with `log_type: \"warning\"` set it to `In Review`.", - "items": { - "type": "object", - "properties": { - "risk": { - "type": "string", - "enum": [ - "DOCUMENT_AI_UNREADABLE_DOCUMENT", - "DOCUMENT_AI_MISSING_REQUIRED_FIELDS", - "DOCUMENT_AI_SUSPECTED_MANIPULATION", - "DOCUMENT_AI_NAME_MISMATCH", - "DOCUMENT_AI_UNSUPPORTED_FILE" - ], - "description": "Machine-readable risk code. Each one is governed by the matching `document_ai_*_action` request option.", - "example": "DOCUMENT_AI_MISSING_REQUIRED_FIELDS" - }, - "feature": { - "type": "string", - "enum": [ - "DOCUMENT_AI" - ], - "description": "Feature that raised the warning. Always `DOCUMENT_AI` on this endpoint." - }, - "additional_data": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Extra context for the risk: `{\"missing_fields\": [\"Net pay\"]}` for missing required fields, `{\"name_on_document\", \"score\", \"threshold\"}` for a name mismatch, and the forensic finding (`detection_method`, producer/editor details) for suspected manipulation. `null` when there is nothing to add.", - "example": { - "missing_fields": [ - "Net pay" - ] - } - }, - "log_type": { - "type": "string", - "enum": [ - "error", - "warning", - "information" - ], - "description": "Severity derived from the configured action: `error` (DECLINE), `warning` (REVIEW), `information` (NO_ACTION)." - }, - "short_description": { - "type": "string", - "description": "Human-readable one-liner for the risk.", - "example": "Required fields could not be extracted" - }, - "long_description": { - "type": "string", - "description": "Human-readable explanation, interpolated with `additional_data` where relevant." - } - } - } - } - } - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Echo of the `vendor_data` you sent, or `null`." - }, - "metadata": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Echo of the `metadata` object you sent, or `null`." - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "When the request was processed (UTC, ISO 8601)." - } + "$ref": "#/components/schemas/CaseDetail" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/resolve/": { + "post": { + "summary": "Resolve case", + "description": "Resolve the case with one of five resolutions: FALSE_POSITIVE (no concern found), VALID_THREAT (concern confirmed), INCONCLUSIVE, REQUIREMENTS_NOT_MET or REQUIREMENTS_SATISFIED. Blocked with 400 if the blueprint's checklist is not optional and items remain incomplete, or if the blueprint requires a resolution note and none is given. When the case's blueprint has a four-eyes checker step (four_eyes_checker_blueprint), the resolution is staged (case.pending_resolution) and the case moves to the checker blueprint instead of resolving immediately; a different officer must call approve or reject-approval. Submit a four-eyes resolution as a console member: a resolution an API key submits has no recorded maker, so approve and reject-approval return 403 for every caller. On a four-eyes blueprint you can name the member who must approve with `checker`: then only that member can approve or reject the resolution, and the response's `checker` names them. Without `checker`, the resolution waits on the checker blueprint's queue. Sending `checker` for a blueprint without a checker step returns 400.", + "operationId": "resolve_case", + "x-didit-sdk": { + "resource": "cases", + "method": "resolve", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "resolution" + ], + "properties": { + "resolution": { + "type": "string", + "enum": [ + "FALSE_POSITIVE", + "VALID_THREAT", + "INCONCLUSIVE", + "REQUIREMENTS_NOT_MET", + "REQUIREMENTS_SATISFIED" + ] + }, + "note": { + "type": "string", + "nullable": true, + "description": "Required when the blueprint sets require_resolution_note." + }, + "checker": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Four-eyes blueprints only. The user uuid of the member who must approve this resolution (the same id as `assigned_to.uuid` on a case, or `members[].user_id` on a review team). The member must have case access to this application and cannot be the officer who submits the resolution. Omit it or send null to leave the resolution to the checker queue." + } + } + }, + "examples": { + "Resolve": { + "value": { + "resolution": "FALSE_POSITIVE", + "note": "Confirmed legitimate activity after reviewing documents." + } + }, + "Four-eyes blueprint with a named checker": { + "value": { + "resolution": "VALID_THREAT", + "note": "Document tampering confirmed.", + "checker": "3fa85f64-5717-4562-b3fc-2c963f66afa6" } } } } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/resolve/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"resolution\": \"FALSE_POSITIVE\", \"note\": \"Confirmed legitimate activity after reviewing documents.\"}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/resolve/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={'resolution': 'FALSE_POSITIVE', 'note': 'Confirmed legitimate activity.'},\n timeout=10,\n)\nresp.raise_for_status()\ncase = resp.json()\nprint(case['status'], case['resolution'], case.get('pending_resolution'))" + } + ], + "responses": { + "200": { + "description": "Resolved, or staged for 4-eyes approval.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseDetail" + } + } + } }, "400": { - "description": "Validation or processing error. Field-level problems return DRF's standard envelope (one array of messages per offending field). Encrypted or unreadable documents and extraction failures return `{\"error\": ...}`.", + "description": "The resolution was refused. Non-field errors come as a list of strings: an incomplete checklist, a missing required note, or a `checker` that could not be checked or is not an active reviewer with case access to this application. `checker` errors come keyed by the field: the named member is the officer submitting the resolution, or the case's blueprint has no checker step.", "content": { "application/json": { - "examples": { - "Missing document": { - "summary": "`document` not included in the form data", - "value": { - "document": [ - "No file was submitted." - ] - } - }, - "Invalid fields payload": { - "summary": "`fields` is not one JSON-encoded array string", - "value": { - "fields": [ - "fields must be a valid JSON array string." - ] + "schema": { + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } } + ] + }, + "examples": { + "Incomplete checklist": { + "value": [ + "Case has incomplete checklist items: Verify source of funds" + ] }, - "Duplicate field keys": { - "summary": "Two items share the same `key`", - "value": { - "fields": [ - "Every fields item must have a unique key." - ] - } + "Resolution note required": { + "value": [ + "A resolution note is required to resolve this case." + ] }, - "Conflicting expectations": { - "summary": "Person and company expectations sent together", - "value": { - "non_field_errors": [ - "Provide either person expected-name fields or expected_company_name, not both." - ] - } + "Reviewer access could not be verified": { + "value": [ + "Reviewer access could not be verified. Try again." + ] }, - "Invalid metadata": { - "summary": "`metadata` is not a JSON object (or contains a NUL character)", - "value": { - "metadata": [ - "Metadata must be a JSON object." - ] - } + "Checker is not an active reviewer": { + "value": [ + "Choose an active reviewer with case access to this application." + ] }, - "Oversized vendor_data": { - "summary": "`vendor_data` over the 2,000-byte cap", + "Checker is the submitting officer": { "value": { - "vendor_data": [ - "vendor_data is too long (maximum 2000 bytes)." + "checker": [ + "The officer who submitted the resolution cannot be its checker." ] } }, - "Encrypted PDF": { - "summary": "Password-protected PDF", - "value": { - "error": "The PDF is encrypted. Please upload a decrypted PDF or a photo instead." - } - }, - "Extraction failed": { - "summary": "The extraction pipeline could not process the document", + "Policy has no checker step": { "value": { - "error": [ - "Error extracting Document AI information" + "checker": [ + "This review policy has no checker step, so the case resolves directly." ] } } - }, - "schema": { - "type": "object", - "additionalProperties": true } } } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reopen/": { + "post": { + "summary": "Reopen case", + "description": "Move an awaiting-user or resolved case back to open, clearing resolution, resolution_notes, resolved_at, and resolved_by_email.", + "operationId": "reopen_case", + "x-didit-sdk": { + "resource": "cases", + "method": "reopen", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, - "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls — and contract-billed organizations whose rate card has no Document AI API entry — return `403` with `{\"error\": ...}` before any document processing happens.", + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/reopen/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Reopen case.", "content": { "application/json": { - "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", - "value": { - "detail": "You do not have permission to perform this action." - } - }, - "Not enough credits": { - "summary": "Organization balance cannot cover the call", - "value": { - "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" - } - }, - "Contract rate missing": { - "summary": "Contract-billed organization whose rate card has no Document AI API entry", - "value": { - "error": "Document AI API is not included in your contract rate card. Contact support@didit.me to enable it." - } - } - }, "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - }, - "error": { - "type": "string" - } - } + "$ref": "#/components/schemas/CaseDetail" } } } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/request-info/": { + "post": { + "summary": "Request info (awaiting user)", + "description": "Move an open case to awaiting_user while more information is collected from the verified user. Fails with 400 if a resolution is currently pending 4-eyes approval.", + "operationId": "request_case_info", + "x-didit-sdk": { + "resource": "cases", + "method": "request_info", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/request-info/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Request info (awaiting user).", "content": { "application/json": { - "examples": { - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." - } - } - }, "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } + "$ref": "#/components/schemas/CaseDetail" } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/aml/": { + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/escalate/": { "post": { - "summary": "AML screening (standalone)", - "operationId": "post_v3aml", + "summary": "Escalate case", + "description": "Move the case to its blueprint's configured escalation_blueprint. Requires the current blueprint to have escalation_enabled and an escalation_blueprint set, otherwise 400.", + "operationId": "escalate_case", + "x-didit-sdk": { + "resource": "cases", + "method": "escalate", + "auth": "api_key", + "server": true, + "scope": "application" + }, "tags": [ - "Standalone APIs" + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/escalate/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } ], + "responses": { + "200": { + "description": "Escalate case.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseDetail" + } + } + } + } + }, "security": [ { "ApiKeyAuth": [] } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/transfer/": { + "post": { + "summary": "Transfer case", + "description": "Move the case to a different, active blueprint of the same application. Requires the current blueprint to have transfer_enabled, otherwise 400.", + "operationId": "transfer_case", + "x-didit-sdk": { + "resource": "cases", + "method": "transfer", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" ], "parameters": [ { - "name": "Idempotency-Key", - "in": "header", - "required": false, - "description": "Optional retry-safety key, up to 255 characters. Reusing the same key with the same request body returns the original response without running or billing a second screening. Reusing the key with a different body returns `409`.", + "name": "organization_id", + "in": "path", + "required": true, "schema": { "type": "string", - "maxLength": 255 - } - } - ], - "description": "Screen a person or company against PEP, sanctions, warning/watchlist, and adverse-media datasets in one call.\n\n**Two-score model.** Each hit carries a `match_score` (0–100, identity confidence) and a `risk_score` (0–100, entity risk). Hits whose `match_score` is below `aml_match_score_threshold` (default **93**) are tagged `review_status: \"False Positive\"` and ignored for risk purposes; the highest `risk_score` among the remaining hits becomes `aml.score`. The final `aml.status` follows your thresholds: `score` > `aml_score_review_threshold` (default **100**) → `Declined`; `score` > `aml_score_approve_threshold` (default **80**) → `In Review`; otherwise `Approved`. Whenever `score` is non-zero and ≥ the approve threshold a `POSSIBLE_MATCH_FOUND` warning is added (a score of exactly 0 never warns).\n\n**Match-score weights.** `aml_name_weight` + `aml_dob_weight` + `aml_country_weight` must sum to exactly 100 (defaults 60/25/15). Components missing from the request are re-normalized away; a matching `document_number` acts as a golden key that overrides `match_score` to 100.\n\n**Latency.** Plain screenings typically complete in a few seconds. With `include_adverse_media=true` the API waits for adverse-media enrichment (an initial ~5 s pause plus up to ~25 s of polling) — configure a client timeout of **at least 60 seconds**.\n\n**Persistence.** `save_api_request` defaults to **true**: the screening is stored as a session (`request_id` works with `GET /v3/session/{sessionId}/decision/`), it appears in the console, a `status.updated` webhook fires, and hit review statuses can be updated later. With `save_api_request=false` nothing is stored and `request_id` is a transient correlation id. `include_ongoing_monitoring=true` keeps re-screening the profile and requires `save_api_request=true` (enforced with a `400`).\n\n**Idempotency.** Send an `Idempotency-Key` header when retrying after a timeout or transient error. The key is scoped to your application and the exact request body. The first completed request stores its response; later identical retries return that same response and do not run or bill another AML screening. If the original request is still running, retry later with the same key. If the same key is reused with a different body, the API returns `409`.\n\n**Billing.** One AML API credit is charged after the first successful screening. Replayed idempotent responses are not charged again; an insufficient balance returns `403` before any new screening happens.\n\n**Sandbox.** Keys from sandbox applications skip screening and billing entirely and return a mocked result — `Approved` with zero hits unless you pass `sandbox_scenario`. `save_api_request` works the same as on live keys, so a saved sandbox screening is retrievable through `GET /v3/session/{sessionId}/decision/` and `GET /v3/session/{sessionId}/generate-pdf/`.", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "full_name" - ], - "properties": { - "full_name": { - "type": "string", - "minLength": 2, - "description": "Full name of the person or company to screen. Minimum **2 characters** - the screening provider's own floor. A shorter or blank name returns `400` on this field before any screening runs, so nothing is billed.", - "example": "Antonio Ejemplo Modelo" - }, - "entity_type": { - "type": "string", - "enum": [ - "person", - "company" - ], - "default": "person", - "description": "Type of entity to screen. Defaults to `person`." - }, - "date_of_birth": { - "type": "string", - "format": "date", - "description": "Date of birth (persons) or incorporation date (companies), `YYYY-MM-DD`. Improves match precision via the DOB weight.", - "example": "1972-02-29" - }, - "nationality": { - "type": "string", - "description": "ISO 3166-1 **alpha-2** country code (e.g. `ES`). Invalid codes return `400`. Improves match precision via the country weight.", - "example": "ES" - }, - "document_number": { - "type": "string", - "description": "Identity document number. Not weighted — acts as a golden key: an exact match overrides the hit's `match_score` to 100, a hard mismatch penalizes it. Sent to the screening provider as a search filter only when it is **2-50 characters**; a value outside that range is accepted and stored, and the screening still runs and is billed, just not narrowed by document number." - }, - "aml_score_approve_threshold": { - "type": "number", - "minimum": 0, - "maximum": 100, - "default": 80, - "description": "Risk-score threshold at or below which the result is `Approved`. Must be ≤ `aml_score_review_threshold` (else `400`)." - }, - "aml_score_review_threshold": { - "type": "number", - "minimum": 0, - "maximum": 100, - "default": 100, - "description": "Risk-score threshold above which the result is `Declined`. Scores between the two thresholds produce `In Review`." - }, - "aml_name_weight": { - "type": "integer", - "minimum": 0, - "maximum": 100, - "default": 60, - "description": "Weight of name similarity in the per-hit match score. The three weights must sum to exactly 100 (else `400`)." - }, - "aml_dob_weight": { - "type": "integer", - "minimum": 0, - "maximum": 100, - "default": 25, - "description": "Weight of date of birth in the per-hit match score." - }, - "aml_country_weight": { - "type": "integer", - "minimum": 0, - "maximum": 100, - "default": 15, - "description": "Weight of country/nationality in the per-hit match score." - }, - "aml_match_score_threshold": { - "type": "integer", - "minimum": 0, - "maximum": 100, - "default": 93, - "description": "Per-hit cutoff: hits with `match_score` below this are `False Positive` (excluded from risk assessment); at or above are `Unreviewed` possible matches." - }, - "include_adverse_media": { - "type": "boolean", - "default": false, - "description": "Also search news media for negative coverage. Adds up to ~30 s of latency while results are gathered — use a ≥60 s client timeout." - }, - "include_ongoing_monitoring": { - "type": "boolean", - "default": false, - "description": "Keep re-screening this profile and push changes via webhook. Requires `save_api_request=true` (else `400`)." - }, - "save_api_request": { - "type": "boolean", - "default": true, - "description": "Persist the screening as a session (console visibility, decision endpoint, webhooks, hit review). Set to `false` for a stateless check." - }, - "vendor_data": { - "type": "string", - "description": "Your identifier for the screened user; echoed back and stored with the session." - }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Free-form JSON stored with the request and echoed back." - }, - "sandbox_scenario": { + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "target_blueprint" + ], + "properties": { + "target_blueprint": { "type": "string", - "description": "Sandbox-only scenario slug. Only accepted when the API key belongs to a sandbox-mode application; rejected on live applications. A standalone screening has no session to arm, so the slug goes on the screening request itself: `decline_aml_hit` returns one production-shaped hit and settles the screening as `Declined`, `review_aml_possible_match` settles it `In Review`, and a scenario that arms no AML outcome leaves the screening clean. See the scenario catalog at GET /v1/sandbox/scenarios/ or [Sandbox & Test Data](/integration/sandbox-testing).", - "example": "decline_aml_hit" + "format": "uuid" } } } @@ -15047,1328 +35602,1203 @@ }, "x-codeSamples": [ { - "lang": "curl", - "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/aml/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -H 'Idempotency-Key: aml-user-1234-2026-08-20' \\\n -d '{\n \"full_name\": \"Antonio Ejemplo Modelo\",\n \"entity_type\": \"person\",\n \"date_of_birth\": \"1972-02-29\",\n \"nationality\": \"ES\",\n \"include_adverse_media\": false,\n \"vendor_data\": \"user-1234\"\n }'" - }, - { - "lang": "python", - "label": "Python", - "source": "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/aml/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n \"Idempotency-Key\": \"aml-user-1234-2026-08-20\",\n },\n json={\n \"full_name\": \"Antonio Ejemplo Modelo\",\n \"entity_type\": \"person\",\n \"date_of_birth\": \"1972-02-29\",\n \"nationality\": \"ES\",\n \"include_adverse_media\": False,\n \"vendor_data\": \"user-1234\",\n },\n timeout=60, # adverse media enrichment can add ~30s\n)\nresp.raise_for_status()\nprint(resp.json()[\"aml\"][\"status\"]) # Approved / In Review / Declined" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const res = await fetch('https://verification.didit.me/v3/aml/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n 'Idempotency-Key': 'aml-user-1234-2026-08-20',\n },\n body: JSON.stringify({\n full_name: 'Antonio Ejemplo Modelo',\n entity_type: 'person',\n date_of_birth: '1972-02-29',\n nationality: 'ES',\n include_adverse_media: false,\n vendor_data: 'user-1234',\n }),\n});\nconst data = await res.json();\nconsole.log(data.aml.status);" - } - ], - "responses": { - "200": { - "description": "Screening completed. `aml.status` reflects your thresholds; every hit at or above the internal inclusion floor is listed in `aml.hits` with its identity `match_score`, entity `risk_score`, and a `score_breakdown` explaining the match.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "request_id": { - "type": "string", - "format": "uuid", - "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." - }, - "aml": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "Approved", - "In Review", - "Declined" - ], - "description": "`Declined` when `score` > `aml_score_review_threshold`; `In Review` when `score` > `aml_score_approve_threshold`; `Approved` otherwise (including when no score is available)." - }, - "total_hits": { - "type": "integer", - "description": "Number of entries in `hits`." - }, - "entity_type": { - "type": "string", - "enum": [ - "person", - "company" - ], - "description": "Echo of the screened entity type." - }, - "hits": { - "type": "array", - "items": { - "type": "object", - "description": "One screening hit. `match_score` (0–100) measures identity confidence — hits below `aml_match_score_threshold` are tagged `review_status: \"False Positive\"` and excluded from the risk assessment; hits at or above it are `\"Unreviewed\"` possible matches. `risk_score` (0–100) measures how risky the matched entity is and drives the top-level `aml.score`.", - "properties": { - "id": { - "type": "string", - "nullable": true, - "description": "Stable identifier of the matched entity in the screening datasets." - }, - "url": { - "type": "string", - "nullable": true, - "description": "Source URL for the matched entity, when available." - }, - "match": { - "type": "boolean", - "nullable": true, - "description": "Legacy exact-match flag. Prefer `match_score` + `review_status`." - }, - "score": { - "type": "number", - "nullable": true, - "description": "DEPRECATED weighted match score in the 0–1 range. Use `match_score` instead." - }, - "match_score": { - "type": "number", - "nullable": true, - "description": "Identity confidence (0–100). Compared against `aml_match_score_threshold` to split False Positives from Possible Matches." - }, - "risk_score": { - "type": "number", - "nullable": true, - "description": "Entity risk score (0–100). The highest `risk_score` among non-false-positive hits becomes the top-level `aml.score`." - }, - "target": { - "type": "boolean", - "nullable": true - }, - "caption": { - "type": "string", - "nullable": true, - "description": "Display name of the matched entity." - }, - "datasets": { - "type": "array", - "nullable": true, - "items": { - "type": "string" - }, - "description": "Datasets the entity appears in (e.g. `PEP`, `PEP Level 1`, sanctions list names)." - }, - "features": { - "type": "object", - "nullable": true - }, - "properties": { - "type": "object", - "nullable": true, - "description": "Entity profile: `name`, `alias`, `notes`, `gender`, `birthDate`, `birthPlace`, `nationality`, `position`, and other biographical arrays (each nullable)." - }, - "rca_name": { - "type": "string", - "nullable": true, - "description": "Name of the related close associate through which the entity was matched, when the hit is an RCA." - }, - "pep_matches": { - "type": "array", - "description": "PEP list entries backing this hit.", - "items": { - "type": "object", - "properties": { - "list_name": { - "type": "string" - }, - "publisher": { - "type": "string" - }, - "source_url": { - "type": "string" - }, - "description": { - "type": "string" - }, - "matched_name": { - "type": "string" - }, - "pep_position": { - "type": "string" - }, - "date_of_birth": { - "type": "string" - }, - "place_of_birth": { - "type": "string" - }, - "aliases": { - "type": "array", - "items": { - "type": "string" - } - }, - "education": { - "type": "array", - "items": { - "type": "string" - } - }, - "other_sources": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "sanction_matches": { - "type": "array", - "items": { - "type": "object" - }, - "description": "Sanctions list entries backing this hit (same backing-list shape as `pep_matches`, plus sanction-specific fields)." - }, - "warning_matches": { - "type": "array", - "items": { - "type": "object" - }, - "description": "Warning/watchlist entries backing this hit." - }, - "adverse_media_matches": { - "type": "array", - "description": "Adverse-media articles. Populated only when `include_adverse_media=true`.", - "items": { - "type": "object", - "properties": { - "headline": { - "type": "string" - }, - "summary": { - "type": "string" - }, - "sentiment": { - "type": "string" - }, - "sentiment_score": { - "type": "number" - }, - "source_url": { - "type": "string" - }, - "thumbnail": { - "type": "string", - "nullable": true - }, - "country": { - "type": "string", - "nullable": true - }, - "author_name": { - "type": "string", - "nullable": true - }, - "publication_date": { - "type": "string", - "nullable": true - }, - "adverse_keywords": { - "type": "array", - "items": { - "type": "string" - } - }, - "other_sources": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "adverse_media_details": { - "type": "object", - "nullable": true, - "description": "Aggregated adverse-media sentiment for the entity (`sentiment`, `sentiment_score`, `adverse_keywords` frequency map). Populated only when `include_adverse_media=true`." - }, - "first_seen": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the entity first appeared in the datasets." - }, - "last_seen": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "Most recent dataset refresh that included the entity." - }, - "linked_entities": { - "type": "array", - "description": "Known relationships of the matched entity (family, associates, companies).", - "items": { - "type": "object", - "properties": { - "name": { - "type": "array", - "items": { - "type": "string" - } - }, - "relation": { - "type": "array", - "items": { - "type": "string" - } - }, - "status": { - "type": "array", - "items": { - "type": "string" - } - }, - "details": { - "type": "array", - "items": { - "type": "string" - } - }, - "active": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - }, - "risk_view": { - "type": "object", - "nullable": true, - "description": "Per-dimension risk breakdown (`crimes`, `countries`, `categories`, `custom_list`), each with `score`, `weightage`, `risk_level`, and a `risk_scores` map." - }, - "additional_information": { - "type": "object", - "nullable": true, - "description": "Free-form extra context, e.g. a `flag_summary` array explaining why the entity is flagged." - }, - "review_status": { - "type": "string", - "nullable": true, - "enum": [ - "False Positive", - "Unreviewed", - "Confirmed Match", - "Inconclusive" - ], - "description": "`False Positive` — `match_score` below `aml_match_score_threshold`, excluded from risk assessment. `Unreviewed` — possible match awaiting review. `Confirmed Match` / `Inconclusive` — set manually by a reviewer (via the Business Console) on saved sessions; that review capability is not part of this spec." - }, - "score_breakdown": { - "type": "object", - "nullable": true, - "description": "How `match_score` was computed. Name, date of birth, and country each contribute `_score` × `_weight_normalized` points; weights are re-normalized when a component is missing from the screened data. The document number is not weighted — it acts as a 'golden key': `MATCH` overrides the score to 100, `NEUTRAL` leaves it unchanged, `HARD_MISMATCH` applies a penalty.", - "properties": { - "name_score": { - "type": "integer", - "nullable": true, - "description": "Raw name similarity (0–100)." - }, - "name_weight": { - "type": "integer", - "nullable": true, - "description": "Configured name weight (`aml_name_weight`)." - }, - "name_weight_normalized": { - "type": "number", - "nullable": true, - "description": "Name weight after re-normalization (0–100)." - }, - "name_contribution": { - "type": "number", - "nullable": true, - "description": "Points the name contributed to `total_score`." - }, - "dob_score": { - "type": "integer", - "nullable": true, - "description": "Date-of-birth score as a percentage of its weight (−100 to 100)." - }, - "dob_weight": { - "type": "integer", - "nullable": true, - "description": "Configured DOB weight (`aml_dob_weight`)." - }, - "dob_weight_normalized": { - "type": "number", - "nullable": true, - "description": "DOB weight after re-normalization (0–100)." - }, - "dob_contribution": { - "type": "number", - "nullable": true, - "description": "Points the DOB contributed to `total_score`." - }, - "country_score": { - "type": "integer", - "nullable": true, - "description": "Country score as a percentage of its weight (−100 to 100)." - }, - "country_weight": { - "type": "integer", - "nullable": true, - "description": "Configured country weight (`aml_country_weight`)." - }, - "country_weight_normalized": { - "type": "number", - "nullable": true, - "description": "Country weight after re-normalization (0–100)." - }, - "country_contribution": { - "type": "number", - "nullable": true, - "description": "Points the country contributed to `total_score`." - }, - "document_number_match_type": { - "type": "string", - "nullable": true, - "enum": [ - "MATCH", - "NEUTRAL", - "HARD_MISMATCH" - ], - "description": "Golden-key result for the document number comparison." - }, - "document_number_effect": { - "type": "string", - "nullable": true, - "description": "Human-readable description of the document-number effect." - }, - "total_score": { - "type": "integer", - "nullable": true, - "description": "Final weighted match score (0–100) — mirrors `match_score`." - } - } - } - } - } - }, - "score": { - "type": "number", - "nullable": true, - "description": "Overall AML risk score (0–100): the highest `risk_score` among hits that are not False Positives. Drives `status` via the two thresholds." - }, - "screened_data": { - "type": "object", - "description": "Echo of what was screened: `full_name`, `date_of_birth`, `nationality`, `document_number` (absent fields are `null`).", - "properties": { - "full_name": { - "type": "string" - }, - "date_of_birth": { - "type": "string", - "nullable": true - }, - "nationality": { - "type": "string", - "nullable": true - }, - "document_number": { - "type": "string", - "nullable": true - } - } - }, - "warnings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "description": "Feature that raised the warning." - }, - "risk": { - "type": "string", - "description": "Machine-readable risk identifier." - }, - "additional_data": { - "type": "object", - "nullable": true, - "description": "Extra context for the warning, when available." - }, - "log_type": { - "type": "string", - "description": "`warning`, `information`, or `error` — how the risk affected the outcome." - }, - "short_description": { - "type": "string", - "description": "One-line human-readable summary." - }, - "long_description": { - "type": "string", - "description": "Full human-readable explanation." - } - } - }, - "description": "Contains a `POSSIBLE_MATCH_FOUND` warning whenever `score` is non-zero and ≥ `aml_score_approve_threshold` (a score of exactly 0 never warns); empty otherwise." - } - } - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Echo of the `vendor_data` you sent." - }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Echo of the `metadata` you sent." - }, - "created_at": { - "type": "string", - "format": "date-time" + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/transfer/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"target_blueprint\": \"b1de0000-0000-4000-8000-000000000002\"}'" + } + ], + "responses": { + "200": { + "description": "Case transferred.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseDetail" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/approve/": { + "post": { + "summary": "Approve staged resolution (4-eyes)", + "description": "Apply a resolution staged by resolve on a four-eyes blueprint. The caller must be a different officer than the one who submitted it, otherwise 403. When the resolution names a checker (the case's `checker`), only that member can approve it: any other caller, including an API key, gets 403.", + "operationId": "approve_case_resolution", + "x-didit-sdk": { + "resource": "cases", + "method": "approve_resolution", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/approve/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Resolution applied.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseDetail" + } + } + } + }, + "409": { + "description": "Case has no resolution pending approval.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + }, + "403": { + "description": "The caller cannot approve this resolution: they submitted it, the case has no recorded maker, or the resolution names a different checker. An API key always gets this 403 when a checker is named.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" } } }, "examples": { - "No hits (Approved)": { - "summary": "Clean screening — zero hits", + "Caller submitted the resolution": { "value": { - "request_id": "d27c5509-7a66-45a9-b05e-d69d9f4c336a", - "aml": { - "status": "Approved", - "total_hits": 0, - "entity_type": "person", - "hits": [], - "score": 0, - "screened_data": { - "full_name": "Maximilian Mustermann-Doe", - "date_of_birth": "1992-03-14", - "nationality": "DE", - "document_number": null - }, - "warnings": [] - }, - "vendor_data": "user-1234", - "metadata": { - "internal_ref": "abc-123" - }, - "created_at": "2026-06-11T10:21:07.013938+00:00" + "detail": "The officer who submitted the resolution cannot approve it." } }, - "PEP hit (Approved — risk below approve threshold)": { - "summary": "One PEP possible match; risk score 73 stays under the default approve threshold of 80", + "No recorded maker": { "value": { - "request_id": "0b7e7a3e-65a1-4f3d-9c7a-1de2f9a40111", - "aml": { - "status": "Approved", - "total_hits": 1, - "entity_type": "person", - "hits": [ - { - "id": "kDpfszrbkDdaxpMVZw3NLg", - "url": "https://news.example.org/articles/sample", - "match": false, - "score": 0.93, - "match_score": 93.0, - "risk_score": 73.0, - "target": null, - "caption": "Antonio Ejemplo Modelo", - "datasets": [ - "PEP Level 1", - "PEP" - ], - "features": null, - "rca_name": "", - "first_seen": "2026-01-07T00:00:00", - "last_seen": "2026-01-07T00:00:00", - "review_status": "Unreviewed", - "properties": { - "name": [ - "Antonio Ejemplo Modelo", - "Antonio Ejemplo" - ], - "alias": [ - "Antonio Ejemplo Modelo", - "Ejemplo Modelo, Pedro" - ], - "notes": [ - "Prime Minister of Spain" - ], - "gender": [ - "male" - ], - "birthDate": [ - "1972-02-29" - ], - "birthPlace": [ - "Madrid, Spain" - ], - "nationality": null, - "position": null - }, - "risk_view": { - "crimes": { - "score": 0.0, - "weightage": 20, - "risk_level": "Low", - "risk_scores": {} - }, - "countries": { - "score": 9.64, - "weightage": 30, - "risk_level": "Low", - "risk_scores": { - "Spain": 25.71 - } - }, - "categories": { - "score": 62.5, - "weightage": 50, - "risk_level": "High", - "risk_scores": { - "PEP": 100.0, - "PEP Level 1": 100.0 - } - }, - "custom_list": {} - }, - "score_breakdown": { - "name_score": 89, - "name_weight": 60, - "name_weight_normalized": 60.0, - "name_contribution": 53.4, - "dob_score": 100, - "dob_weight": 25, - "dob_weight_normalized": 25.0, - "dob_contribution": 25.0, - "country_score": 100, - "country_weight": 15, - "country_weight_normalized": 15.0, - "country_contribution": 15.0, - "document_number_match_type": "NEUTRAL", - "document_number_effect": "No document number provided for screening", - "total_score": 93 - }, - "pep_matches": [ - { - "aliases": [], - "education": [], - "list_name": "Presidency of the Government of Spain", - "publisher": "Presidency of the Government of Spain", - "source_url": "https://news.example.org/articles/sample", - "description": "The Presidency of the Government is the central institutional body in Spain that supports the Prime Minister and coordinates the general action of the central government.", - "matched_name": "Antonio Ejemplo Modelo", - "pep_position": "", - "date_of_birth": "1972-02-29", - "other_sources": [], - "place_of_birth": "Madrid, Spain" - } - ], - "sanction_matches": [], - "warning_matches": [], - "adverse_media_matches": [], - "adverse_media_details": null, - "linked_entities": [ - { - "name": [ - "Maria Begona Gomez Fernandez" - ], - "active": [], - "status": [], - "details": [], - "relation": [ - "spouse" - ] - } - ], - "additional_information": { - "flag_summary": [ - "Antonio Ejemplo Modelo has been flagged as a Level 1 Politically Exposed Person (PEP) from Spain, serving as the President of the Government of Spain." - ] - } - } - ], - "score": 73.0, - "screened_data": { - "full_name": "Pedro Sanchez Perez-Castejon", - "date_of_birth": "1972-02-29", - "nationality": "ES", - "document_number": null - }, - "warnings": [] - }, - "vendor_data": null, - "metadata": null, - "created_at": "2026-06-11T10:23:44.812000+00:00" + "detail": "Case has no recorded maker for this pending resolution; approve is not permitted." + } + }, + "Caller is not the named checker": { + "value": { + "detail": "Only the named checker can approve this resolution." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reject-approval/": { + "post": { + "summary": "Reject staged resolution (4-eyes)", + "description": "Reject a resolution staged by resolve and return the case to its maker blueprint. The caller must be a different officer than the one who submitted it, otherwise 403. When the resolution names a checker (the case's `checker`), only that member can reject it: any other caller, including an API key, gets 403.", + "operationId": "reject_case_resolution", + "x-didit-sdk": { + "resource": "cases", + "method": "reject_resolution", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "notes": { + "type": "string", + "nullable": true + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/reject-approval/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"notes\": \"Needs a clearer rationale before this can be approved.\"}'" + } + ], + "responses": { + "200": { + "description": "Resolution rejected, case returned to the maker blueprint.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseDetail" + } + } + } + }, + "409": { + "description": "Case has no resolution pending approval.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + }, + "403": { + "description": "The caller cannot reject this resolution: they submitted it, the case has no recorded maker, or the resolution names a different checker. An API key always gets this 403 when a checker is named.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Caller submitted the resolution": { + "value": { + "detail": "The officer who submitted the resolution cannot reject it." + } + }, + "No recorded maker": { + "value": { + "detail": "Case has no recorded maker for this pending resolution; reject is not permitted." + } + }, + "Caller is not the named checker": { + "value": { + "detail": "Only the named checker can reject this resolution." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/entity-actions/": { + "post": { + "operationId": "apply_case_entity_action", + "x-didit-sdk": { + "resource": "cases", + "method": "apply_entity_action", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "case_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + } + ], + "tags": [ + "Case Management" + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CaseActionCaseEntityActionRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseActionCaseEntityActionRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CaseActionCaseEntityActionRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseActionCaseEntityActionResult" + } + } + }, + "description": "" + }, + "409": { + "description": "The reference belongs to a different action payload." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Apply one explicit action to the case subject. Supply a unique client_reference to obtain an action receipt and make retries idempotent. Reusing that reference with a different payload returns 409. With a reference, inspect receipt.status even when HTTP status is 200: FAILED means no action was applied. Repeating a failed request does not execute it again unless retry is true. Omitting client_reference preserves the legacy response." + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/print/": { + "get": { + "summary": "Print case (PDF)", + "description": "Render the full case (summary, subject, checklist state, notes, attached sessions/transactions, events, FIU report list) as a PDF and return a presigned download URL.", + "operationId": "print_case", + "x-didit-sdk": { + "resource": "cases", + "method": "print", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/print/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Presigned PDF URL.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" } } } } } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/": { + "get": { + "operationId": "list_case_notes", + "x-didit-sdk": { + "resource": "cases.notes", + "method": "list", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true }, - "400": { - "description": "Validation error. Field-level problems return DRF's standard envelope (one array of messages per offending field); cross-field rules (weight sum, threshold ordering, monitoring-without-save) return `{\"error\": [...]}`.", + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "case_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "name": "ordering", + "required": false, + "in": "query", + "description": "Which field to use when ordering the results.", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Number of results to return per page.", + "schema": { + "type": "integer" + } + }, + { + "name": "offset", + "required": false, + "in": "query", + "description": "The initial index from which to return the results.", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { "content": { "application/json": { - "examples": { - "Missing full_name": { - "summary": "`full_name` is the only required field", - "value": { - "full_name": [ - "This field is required." - ] - } - }, - "Short full_name": { - "summary": "`full_name` must be at least 2 characters", - "value": { - "full_name": [ - "Ensure this field has at least 2 characters." - ] - } - }, - "Invalid nationality": { - "summary": "`nationality` must be ISO 3166-1 alpha-2", - "value": { - "nationality": [ - "Invalid ISO 3166-1 alpha-2 country code." - ] - } - }, - "Weights do not sum to 100": { - "summary": "`aml_name_weight` + `aml_dob_weight` + `aml_country_weight` ≠ 100", - "value": { - "error": [ - "AML weights must sum to 100. Current sum: 90 (name: 50, dob: 25, country: 15)" - ] - } - }, - "Thresholds out of order": { - "summary": "Approve threshold above review threshold", - "value": { - "error": [ - "Approve threshold (90.0) cannot be greater than review threshold (80.0)" - ] - } - }, - "Monitoring without saving": { - "summary": "`include_ongoing_monitoring=true` with `save_api_request=false`", - "value": { - "error": [ - "Ongoing monitoring can only be enabled when the API request is saved." - ] - } - } + "schema": { + "$ref": "#/components/schemas/CaseCollaborationPaginatedCaseNoteList" } } - } + }, + "description": "" }, "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any screening or provider call happens.", + "description": "The caller lacks the required cases permission." + }, + "404": { + "description": "The case or note is unavailable for this caller." + } + }, + "tags": [ + "Case Management" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "post": { + "operationId": "create_case_note", + "x-didit-sdk": { + "resource": "cases.notes", + "method": "create", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "case_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CaseCollaborationCaseNoteCreateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseCollaborationCaseNoteCreateRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CaseCollaborationCaseNoteCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { "content": { "application/json": { - "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", - "value": { - "detail": "You do not have permission to perform this action." - } - }, - "Not enough credits": { - "summary": "Organization balance cannot cover the call", - "value": { - "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" - } - } + "schema": { + "$ref": "#/components/schemas/CaseCollaborationCaseNote" } } - } + }, + "description": "" }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "200": { "content": { "application/json": { - "examples": { - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." - } - } - }, "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } + "$ref": "#/components/schemas/CaseCollaborationCaseNote" } } - } + }, + "description": "" + }, + "403": { + "description": "The caller lacks the required cases permission." + }, + "404": { + "description": "The case or note is unavailable for this caller." + }, + "409": { + "description": "This client reference was already used for another note or for a note that has been deleted." } - } + }, + "tags": [ + "Case Management" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Add a case-scoped note. An optional client_reference makes an unchanged retry return the same note without duplicate activity or notifications. reply_to must identify a live note in this case. The response includes a bounded parent excerpt without copying that excerpt into the saved reply." } }, - "/v3/face-search/": { - "post": { - "summary": "Face Search", - "description": "Search a face image (1:N) against your application's face search index — faces enrolled from verification sessions, saved standalone API calls, direct user-profile face uploads, and your block/allow lists — and get back ranked similarity matches plus blocklist and duplicate warnings.\n\n**How it works.** The largest detected face in `user_image` is embedded and searched against the index, scoped to your application. Up to **5 matches** above the similarity floor are returned in `matches`, each with `similarity_percentage`, the originating session (`session_id`, `session_number`, `status`, `vendor_data`), `user_details` extracted during that session's document verification, `is_blocklisted` / `is_allowlisted` flags, and a `source` discriminator (`session`, `imported`, or `list_entry`).\n\n- `search_type=most_similar` (default) ranks every enrolled face by similarity — use it for deduplication and fraud-ring investigation.\n- `search_type=blocklisted_or_approved` restricts candidates to blocklisted faces, allowlisted faces, faces from approved sessions, and imported user-profile faces, ranking blocklisted entries first — use it when the primary goal is blocklist screening.\n\n`status` is `Declined` only when a `FACE_IN_BLOCKLIST` or `POSSIBLE_FACE_IN_BLOCKLIST` warning fires; `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` (`information`) and `MULTIPLE_FACES_DETECTED` (`warning`) never decline. Unlike Passive Liveness, this endpoint does **not** exclude prior sessions with the same `vendor_data` from matching. A passive-liveness analysis also runs internally and is stored with the persisted session, but its result is not part of this response.\n\n**Billing.** Each `200` response consumes one Face Search API credit (standalone APIs have no free tier). Insufficient balance returns `403` before any image processing.\n\n**Session persistence and face enrollment (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session (Business Console, `GET /v3/session/{sessionId}/decision/` via the returned `request_id`, `status.updated` webhook) and the searched face is enrolled into your face search index. Faces enrolled by Face Search calls are excluded from future Face Search results, so repeated searches never match each other. When `false`, the call is one-shot: nothing is stored, the face is not enrolled, and `match_image_url` values are internal storage paths rather than downloadable URLs.\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", - "operationId": "post_v3face-search", - "tags": [ - "Standalone APIs" + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/{note_uuid}/": { + "patch": { + "operationId": "root_partial_update", + "x-didit-sdk": { + "resource": "cases.notes", + "method": "update", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "case_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "note_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + } ], - "parameters": [], "requestBody": { - "required": true, "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CaseCollaborationPatchedCaseNoteUpdateRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseCollaborationPatchedCaseNoteUpdateRequest" + } + }, "multipart/form-data": { "schema": { - "type": "object", - "required": [ - "user_image" - ], - "properties": { - "user_image": { - "type": "string", - "format": "binary", - "description": "Front-facing face image to search with. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy. The image must contain at least one detectable face — otherwise the endpoint returns `400`. When several faces are present, the largest one is searched and a `MULTIPLE_FACES_DETECTED` warning is added." - }, - "search_type": { - "type": "string", - "default": "most_similar", - "enum": [ - "most_similar", - "blocklisted_or_approved" - ], - "description": "Search policy. `most_similar` (default) ranks every enrolled face in your application by similarity. `blocklisted_or_approved` restricts candidates to blocklisted faces, allowlisted faces, faces from approved sessions, and imported user-profile faces — with blocklisted entries ranked first.", - "example": "most_similar" - }, - "rotate_image": { - "type": "boolean", - "default": false, - "description": "When `true`, the service tries 90-degree rotations of the input and uses the orientation that yields the best face detection. Useful when EXIF orientation is missing. Adds latency.", - "example": false - }, - "save_api_request": { - "type": "boolean", - "default": true, - "description": "When `true` (default), persists the call as an API-type session, emits a `status.updated` webhook, presigns `match_image_url` values, and enrolls the searched face into your face search index (Face Search enrollments are excluded from future searches). When `false`, the search is one-shot — no session is stored and the face is not enrolled.", - "example": true - }, - "vendor_data": { + "$ref": "#/components/schemas/CaseCollaborationPatchedCaseNoteUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseCollaborationCaseNote" + } + } + }, + "description": "" + }, + "403": { + "description": "The caller lacks the required cases permission." + }, + "404": { + "description": "The case or note is unavailable for this caller." + } + }, + "tags": [ + "Case Management" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Pin or unpin an existing note. Pinned notes appear first. Pin changes are recorded in case activity and do not change evidence or source decisions." + }, + "delete": { + "operationId": "delete_case_note", + "x-didit-sdk": { + "resource": "cases.notes", + "method": "delete", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "case_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "note_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + } + ], + "responses": { + "204": { + "description": "The note is permanently deleted. Replies remain, without the deleted parent excerpt. Attachments are deleted unless another note still uses them." + }, + "403": { + "description": "The caller lacks the required cases permission." + }, + "404": { + "description": "The case or note is unavailable for this caller." + } + }, + "tags": [ + "Case Management" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/presign/": { + "post": { + "summary": "Upload note attachment", + "description": "Upload a file (JPEG, PNG, WebP, PDF, plain text, or CSV; max 5 MB) to attach to a note. Returns the s3_key to pass in the note's attachments array.", + "operationId": "upload_case_note_attachment", + "x-didit-sdk": { + "resource": "cases.notes", + "method": "create_attachment_upload", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "file" + ], + "properties": { + "file": { "type": "string", - "description": "Optional opaque identifier (your user id, email, UUID…) stored on the persisted session and echoed back. It does not exclude same-user faces from the search results.", - "example": "user-123" - }, - "metadata": { - "type": "object", - "additionalProperties": true, - "description": "Optional JSON object stored with the session (when `save_api_request=true`) and echoed back. In multipart requests, send it as a JSON-encoded string field — it is parsed into an object.", - "example": { - "flow": "dedup_check" - } + "format": "binary" } } - }, - "example": { - "user_image": "(binary JPEG/PNG selfie)", - "search_type": "most_similar", - "rotate_image": false, - "save_api_request": true, - "vendor_data": "user-123", - "metadata": { - "flow": "dedup_check" - } } } } }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/notes/presign/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'file=@evidence.pdf'" + } + ], "responses": { - "200": { - "description": "Face search completed. `face_search.matches` holds up to 5 similar faces ordered by similarity; an empty array means no enrolled face exceeded the similarity floor. `status` is `Declined` only on blocklist hits — duplicate matches alone return `Approved`, so inspect `matches` and `warnings`, not just `status`. When `save_api_request=true`, `request_id` is the persisted session id. Each match carries `source` (`session`, `imported`, `list_entry`, or `retained_template`), `vendor_user_id`, and `biometric_template_id`; a `retained_template` match comes from an image-free biometric template kept after the source session was deleted and has `null` session fields, `match_image_url`, and `user_details`.", + "201": { + "description": "File uploaded.", "content": { "application/json": { - "examples": { - "Blocklist hit (Declined)": { - "summary": "The face matches a blocklisted face", - "value": { - "request_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", - "face_search": { - "status": "Declined", - "total_matches": 1, - "matches": [ - { - "session_id": "882c42d5-8a4d-4d20-8080-a22f57822c86", - "session_number": 323442, - "similarity_percentage": 99.99, - "source": "session", - "vendor_data": "user-1", - "verification_date": "2025-01-01T00:00:00Z", - "user_details": { - "full_name": "Jane Marie Doe", - "document_type": "ID", - "document_number": "X1234567" - }, - "match_image_url": "https:///face/3f6a1c2e/reference.jpg", - "status": "Approved", - "is_blocklisted": true, - "is_allowlisted": false, - "api_service": null, - "vendor_user_id": "2f7c1a9e-8b4d-4c6a-9e1f-3d5b7a9c1e2f", - "biometric_template_id": null - } - ], - "user_image": { - "entities": [ - { - "bbox": [ - 40, - 40, - 120, - 120 - ], - "confidence": 0.732973 - } - ], - "best_angle": 0 - }, - "warnings": [ - { - "risk": "FACE_IN_BLOCKLIST", - "feature": "LIVENESS", - "additional_data": { - "blocklisted_session_id": "882c42d5-8a4d-4d20-8080-a22f57822c86", - "blocklisted_session_number": 323442, - "api_service": null - }, - "log_type": "error", - "short_description": "Face in blocklist", - "long_description": "The system identified a face in the blocklist, which means the face is not allowed to be verified." - } - ] - }, - "vendor_data": "user-123", - "metadata": null, - "created_at": "2026-06-12T01:04:42.763237+00:00" - } - }, - "Duplicate found (Approved)": { - "summary": "The face already appeared in another approved session — informational, no decline", - "value": { - "request_id": "5e0c3a1f-7b2d-4c8e-9f10-2a3b4c5d6e7f", - "face_search": { - "status": "Approved", - "total_matches": 1, - "matches": [ - { - "session_id": "1f2e3d4c-5b6a-7980-1122-334455667788", - "session_number": 1024, - "similarity_percentage": 87.42, - "source": "session", - "vendor_data": "user-9", - "verification_date": "2025-11-20T09:15:00Z", - "user_details": null, - "match_image_url": "https:///face/9c8b7a6d/reference.jpg", - "status": "Approved", - "is_blocklisted": false, - "is_allowlisted": false, - "api_service": "PASSIVE_LIVENESS", - "vendor_user_id": "2f7c1a9e-8b4d-4c6a-9e1f-3d5b7a9c1e2f", - "biometric_template_id": null - } - ], - "user_image": { - "entities": [ - { - "bbox": [ - 40, - 40, - 120, - 120 - ], - "confidence": 0.732973 - } - ], - "best_angle": 0 - }, - "warnings": [ - { - "risk": "DUPLICATED_FACE", - "feature": "LIVENESS", - "additional_data": { - "duplicated_session_id": "1f2e3d4c-5b6a-7980-1122-334455667788", - "duplicated_session_number": 1024, - "api_service": "PASSIVE_LIVENESS" - }, - "log_type": "information", - "short_description": "Duplicated face from other approved session", - "long_description": "The system identified a duplicated face from another approved session, requiring further investigation." - } - ] - }, - "vendor_data": "user-123", - "metadata": null, - "created_at": "2026-06-12T01:04:42.763237+00:00" - } - }, - "No matches (Approved)": { - "summary": "No enrolled face exceeded the similarity floor", - "value": { - "request_id": "9d8c7b6a-5f4e-3d2c-1b0a-998877665544", - "face_search": { - "status": "Approved", - "total_matches": 0, - "matches": [], - "user_image": { - "entities": [ - { - "bbox": [ - 40, - 40, - 120, - 120 - ], - "confidence": 0.732973 - } - ], - "best_angle": 0 - }, - "warnings": [] - }, - "vendor_data": "user-123", - "metadata": null, - "created_at": "2026-06-12T01:04:42.763237+00:00" - } - } - }, "schema": { "type": "object", "properties": { - "request_id": { + "s3_key": { + "type": "string" + }, + "url": { "type": "string", - "format": "uuid", - "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." + "format": "uri" }, - "face_search": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "Approved", - "Declined" - ], - "description": "`Declined` only when a `FACE_IN_BLOCKLIST` or `POSSIBLE_FACE_IN_BLOCKLIST` warning fired. Duplicate matches and multiple-face warnings never decline." - }, - "total_matches": { - "type": "integer", - "description": "Number of entries in `matches` (0–5).", - "example": 1 - }, - "matches": { - "type": "array", - "maxItems": 5, - "description": "Up to 5 faces from your face search index that exceed the similarity floor, ordered by similarity (with blocklisted entries ranked first when `search_type=blocklisted_or_approved`). Empty when no enrolled face is similar enough.", - "items": { - "type": "object", - "properties": { - "session_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Id of the verification session the matched face belongs to. `null` when `source` is `imported`, `list_entry`, or `retained_template`.", - "example": "882c42d5-8a4d-4d20-8080-a22f57822c86" - }, - "session_number": { - "type": "integer", - "nullable": true, - "description": "Human-friendly session number shown in the Business Console. `null` for non-session matches.", - "example": 323442 - }, - "similarity_percentage": { - "type": "number", - "format": "float", - "minimum": 0, - "maximum": 100, - "description": "Similarity between the submitted face and the matched face (0–100).", - "example": 99.99 - }, - "source": { - "type": "string", - "enum": [ - "session", - "imported", - "list_entry", - "retained_template" - ], - "description": "Where the matched face was enrolled from: `session` — a verification session or a saved standalone API call; `imported` — a face uploaded directly to a User profile; `list_entry` — a face attached to one of your lists; `retained_template` — an image-free biometric template stored on a User, listed under `/v3/biometric-templates/`." - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "`vendor_data` of the matched session or linked User for imported and retained-template faces.", - "example": "user-1" - }, - "vendor_user_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Linked User id. `null` for list entries and session matches without a User.", - "example": "2f7c1a9e-4b28-4a6d-97f1-83576f80e1b4" - }, - "biometric_template_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Id of the retained biometric template. Set only when `source` is `retained_template`; `null` otherwise.", - "example": null - }, - "verification_date": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the matched face was originally captured, or when a retained template was created (`YYYY-MM-DDThh:mm:ssZ`). `null` for `list_entry` matches.", - "example": "2025-01-01T00:00:00Z" - }, - "user_details": { - "type": "object", - "nullable": true, - "description": "Identity data extracted during the matched session's document verification. `null` when the matched session has no document data (e.g. liveness-only sessions) and always `null` for `retained_template` matches. For `source: imported` matches it is populated from the User profile instead: `full_name` set, `document_type`/`document_number` null.", - "properties": { - "full_name": { - "type": "string", - "nullable": true, - "example": "Jane Marie Doe" - }, - "document_type": { - "type": "string", - "nullable": true, - "example": "ID" - }, - "document_number": { - "type": "string", - "nullable": true, - "example": "X1234567" - } - } - }, - "match_image_url": { - "type": "string", - "nullable": true, - "description": "Time-limited URL (`https:///...`) of the matched face's reference image when `save_api_request=true`. With `save_api_request=false` this is an internal storage path that is not directly downloadable. Always `null` for `retained_template` matches because retained templates contain no image.", - "example": "https:///face/3f6a1c2e/reference.jpg" - }, - "status": { - "type": "string", - "nullable": true, - "description": "Status of the matched session (`Approved`, `Declined`, `In Review`, …). `null` for `imported`/`list_entry` matches.", - "example": "Approved" - }, - "is_blocklisted": { - "type": "boolean", - "description": "`true` when the matched face is on your face blocklist. Any sufficiently similar blocklisted match declines the search." - }, - "is_allowlisted": { - "type": "boolean", - "description": "`true` when the matched face is on one of your allowlists. Allowlisted matches suppress duplicate warnings." - }, - "api_service": { - "type": "string", - "nullable": true, - "description": "Set when the matched face was enrolled by a standalone API call (e.g. `PASSIVE_LIVENESS`); `null` for regular verification sessions and imported faces.", - "example": null - } - } - } - }, - "user_image": { - "type": "object", - "description": "Face-detection results for the submitted image. Entries here carry only `bbox` and `confidence`.", - "properties": { - "entities": { - "type": "array", - "description": "One entry per detected face. The largest face is the one searched.", - "items": { - "type": "object", - "properties": { - "bbox": { - "type": "array", - "items": { - "type": "integer" - }, - "minItems": 4, - "maxItems": 4, - "description": "Bounding box `[x_min, y_min, x_max, y_max]` in pixels.", - "example": [ - 40, - 40, - 120, - 120 - ] - }, - "confidence": { - "type": "number", - "format": "float", - "minimum": 0, - "maximum": 1, - "description": "Face-detection confidence (0–1).", - "example": 0.732973 - } - } - } - }, - "best_angle": { - "type": "integer", - "description": "Rotation (degrees) that produced the best face detection; non-zero only when `rotate_image=true` corrected the orientation. Never null on this endpoint (defaults to 0 when the model omits it).", - "example": 0 - } - } - }, - "warnings": { - "type": "array", - "description": "Risk signals. `FACE_IN_BLOCKLIST` / `POSSIBLE_FACE_IN_BLOCKLIST` (`error`) decline the search; `MULTIPLE_FACES_DETECTED` (`warning`) and `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` (`information`) are advisory.", - "items": { - "type": "object", - "properties": { - "risk": { - "type": "string", - "enum": [ - "MULTIPLE_FACES_DETECTED", - "FACE_IN_BLOCKLIST", - "POSSIBLE_FACE_IN_BLOCKLIST", - "DUPLICATED_FACE", - "POSSIBLE_DUPLICATED_FACE" - ], - "description": "Machine-readable risk code." - }, - "feature": { - "type": "string", - "enum": [ - "LIVENESS" - ], - "description": "Feature that raised the warning. Always `LIVENESS` on this endpoint." - }, - "additional_data": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "`null` for `MULTIPLE_FACES_DETECTED`. Blocklist hits carry `{blocklisted_session_id, blocklisted_session_number, api_service}`; duplicate hits carry `{duplicated_session_id, duplicated_session_number, api_service}` pointing at the first matching session." - }, - "log_type": { - "type": "string", - "enum": [ - "error", - "warning", - "information" - ], - "description": "Severity. `error` warnings drive `status` to `Declined`; `warning` and `information` entries are advisory and never decline on their own." - }, - "short_description": { - "type": "string", - "description": "Human-readable one-line summary of the risk." - }, - "long_description": { - "type": "string", - "description": "Human-readable explanation of the risk." - } - } - } - } - } + "filename": { + "type": "string" + }, + "file_size": { + "type": "integer" + } + } + } + } + } + }, + "400": { + "description": "No file, unsupported content type, or file exceeds 5 MB.", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "get": { + "summary": "Get note attachment URL", + "description": "Mint a fresh presigned download URL for an attachment already stored on one of the case's notes.", + "operationId": "get_case_note_attachment_url", + "x-didit-sdk": { + "resource": "cases.notes", + "method": "retrieve_attachment_url", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + }, + { + "name": "s3_key", + "in": "query", + "schema": { + "type": "string" + }, + "description": "The attachment's s3_key, as returned by upload or listed on the note.", + "required": true + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/notes/presign/?s3_key=case-notes/c1a5e000-0000-4000-8000-000000000001/evidence.pdf' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Presigned URL.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "s3_key": { + "type": "string" }, - "vendor_data": { + "url": { "type": "string", - "nullable": true, - "description": "Echo of the `vendor_data` you sent, or `null`." - }, - "metadata": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Echo of the `metadata` object you sent, or `null`." + "format": "uri" }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 timestamp (UTC) of when the response was generated, e.g. `2026-06-12T01:04:42.763237+00:00`." + "filename": { + "type": "string" } } } } } }, - "400": { - "description": "Validation error. Returned when `user_image` is missing, exceeds 5 MB, has an unsupported extension, or when **no face is detected** in the submitted image. Field errors use DRF's `{field: [messages]}` envelope; face-detection failures use `{\"error\": ...}`.", + "404": { + "description": "s3_key missing or does not belong to this case.", "content": { "application/json": { - "examples": { - "No face detected": { - "summary": "The image decodes fine but contains no detectable face", - "value": { - "error": "No face detected in the image" - } - }, - "Missing image": { - "summary": "`user_image` not included in the form data", - "value": { - "user_image": [ - "No file was submitted." - ] - } - }, - "Unsupported file extension": { - "summary": "File extension outside tiff/jpg/jpeg/png/webp", - "value": { - "user_image": [ - "File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, webp." - ] - } - }, - "File too large": { - "summary": "Upload exceeds the 5 MB limit", - "value": { - "user_image": [ - "File size should not exceed 5 MB" - ] - } - } + "schema": { + "type": "object" } } } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/": { + "get": { + "summary": "List case links", + "description": "List sessions, business sessions, and transactions attached to the case.", + "operationId": "list_case_links", + "x-didit-sdk": { + "resource": "cases.links", + "method": "list", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, - "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any image processing happens.", + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/links/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Case links.", "content": { "application/json": { - "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", - "value": { - "detail": "You do not have permission to perform this action." - } + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseLink" + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "post": { + "summary": "Add case link", + "description": "Attach a session, business session, or transaction to the case. Rejected with 400 if the entity belongs to a different subject than the case.", + "operationId": "create_case_link", + "x-didit-sdk": { + "resource": "cases.links", + "method": "create", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "entity_type", + "entity_uuid" + ], + "properties": { + "entity_type": { + "type": "string", + "enum": [ + "session", + "business_session", + "transaction" + ] }, - "Not enough credits": { - "summary": "Organization balance cannot cover the call", - "value": { - "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" - } + "entity_uuid": { + "type": "string", + "format": "uuid" } } } } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/links/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"entity_type\": \"transaction\", \"entity_uuid\": \"77000000-0000-4000-8000-000000000077\"}'" + } + ], + "responses": { + "201": { + "description": "Link created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseLink" + } + } + } }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "400": { + "description": "Entity belongs to a different profile than the case subject.", "content": { "application/json": { - "examples": { - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." - } - } - }, "schema": { "type": "object", "properties": { @@ -16385,1214 +36815,1039 @@ { "ApiKeyAuth": [] } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl -X POST 'https://verification.didit.me/v3/face-search/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'user_image=@./selfie.jpg' \\\n -F 'search_type=most_similar' \\\n -F 'save_api_request=true' \\\n -F 'vendor_data=user-123'" - }, - { - "lang": "python", - "label": "Python", - "source": "import requests\n\nurl = 'https://verification.didit.me/v3/face-search/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('selfie.jpg', 'rb') as f:\n files = {'user_image': ('selfie.jpg', f, 'image/jpeg')}\n data = {\n 'search_type': 'most_similar',\n 'save_api_request': 'true',\n 'vendor_data': 'user-123',\n }\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=60)\n\nresp.raise_for_status()\nbody = resp.json()\nprint('status:', body['face_search']['status'])\nprint('matches:', body['face_search']['total_matches'])\nfor m in body['face_search']['matches']:\n print(f\" session={m['session_id']} similarity={m['similarity_percentage']} blocklisted={m['is_blocklisted']}\")" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('user_image', new Blob([fs.readFileSync('./selfie.jpg')]), 'selfie.jpg');\nform.append('search_type', 'most_similar');\nform.append('save_api_request', 'true');\nform.append('vendor_data', 'user-123');\n\nconst response = await fetch('https://verification.didit.me/v3/face-search/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n});\n\nif (!response.ok) throw new Error(`Face search failed: ${response.status}`);\nconst body = await response.json();\nconsole.log('status:', body.face_search.status, 'matches:', body.face_search.total_matches);\nbody.face_search.matches.forEach(m =>\n console.log(` session=${m.session_id} similarity=${m.similarity_percentage} blocklisted=${m.is_blocklisted}`)\n);" - } ] } }, - "/v3/sessions/": { - "get": { - "summary": "List verification sessions (KYC and/or KYB) with filters and pagination", - "description": "Returns a paginated list of verification sessions for the application identified by your API key, newest first (descending `session_number`).\n\n**Canonical URL:** the route is `/v3/sessions/` (trailing slash). Requests to `/v3/sessions` without the slash receive a `301` redirect to the slashed URL with the query string preserved; most HTTP clients follow it transparently on GET.\n\n**When to use this endpoint vs webhooks.** Use this endpoint for dashboards, back-office search, reconciliation, and batch exports. Do not poll it to track the progress of individual verifications - configure [webhooks](/integration/webhooks) instead, which push `status.updated` events the moment a session changes. For one session's full result, call `GET /v3/session/{sessionId}/decision/`.\n\n**KYC and KYB sessions.** `session_kind` selects which kinds are listed:\n- `user` (default) - User Verification (KYC) sessions only. Backward-compatible behavior.\n- `business` - Business Verification (KYB) sessions only. Rows have a different, smaller shape (see the response schema).\n- `all` - both kinds in one response: the KYC page and the KYB page for the same `limit`/`offset` are concatenated in `results` (KYC rows first), so a page can contain up to `2 x limit` rows. `count` is the sum of both kinds and `next`/`previous` reflect the KYC-side pagination. For strict pagination of mixed data, call `user` and `business` separately.\n\nEvery row carries a `session_kind` discriminator (`\"user\"` or `\"business\"`).\n\n**Pagination.** Standard limit/offset: `limit` (default 50) and `offset` (default 0), with `next`/`previous` ready-made page URLs. For KYC rows `count` is exact. For KYB rows `count` is computed with a bounded scan and is capped at 100 - when there are more than 100 matches it stays at 100, so iterate using `next` (or by checking whether a full page came back) rather than computing total pages from `count`.\n\n**Filter semantics.** All filters combine with AND and apply to KYC rows. When `session_kind` is `business` or `all`, the KYB side honors the shared parameters `vendor_data`, `status`, `workflow_id`, `country` (registry-company country code), `date_from`, `date_to`, `search` (company name, registration number — type registration numbers without separators, e.g. `DPC001` not `DPC-001` — vendor_data, session number, or session UUID), `warning`, and `organization_name`; the remaining KYC-only filters are ignored for KYB rows. Comma-separated parameters (`status`, `country`, `document_type`, `region`, `workflow_id`, `document_name`, `warning`, `organization_name`, `session_type`) match any of the listed values (OR within the parameter). `vendor_data` is an exact match - use `search` for partial matching. Unknown values in list filters simply return no rows (no error); malformed typed values (`date_from`, `date_to`, `didit_internal_id`, `api_service`, `session_kind`) return `400` with a per-field error object.\n\n**Search.** `search` adapts to the input: a full UUID looks up `session_id`; digits (with optional `+ - . space`) match session-number prefix, document/personal numbers, phone-number prefix, or `vendor_data`; values containing `@` match email prefixes; any other text matches names (accent-insensitive) and `vendor_data`.\n\nNo fixed per-endpoint rate limit is enforced, but keep list polling infrequent and rely on webhooks for real-time updates.", - "operationId": "get_v3_sessions", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/{link_uuid}/": { + "delete": { + "summary": "Remove case link", + "description": "Detach a linked session, business session, or transaction from the case.", + "operationId": "delete_case_link", + "x-didit-sdk": { + "resource": "cases.links", + "method": "delete", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + }, + { + "name": "link_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "11000000-0000-4000-8000-000000000004" + }, + "description": "Link UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X DELETE 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/links/11000000-0000-4000-8000-000000000004/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "204": { + "description": "Link removed." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/events/": { + "get": { + "summary": "List case events", + "description": "Read-only audit timeline of the case (created, status changes, assignment, resolution, escalation, transfers, tag/priority/due-date changes, entity actions, and more).", + "operationId": "list_case_events", + "x-didit-sdk": { + "resource": "cases", + "method": "list_events", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50, + "minimum": 1 + }, + "description": "Page size." + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + }, + "description": "Zero-based offset." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/events/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Paginated events, newest first.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "description": "Number of matching rows. Exact up to 100, capped at 100 beyond that for performance. Rely on `next` being null to detect the last page." + }, + "next": { + "type": "string", + "nullable": true, + "description": "Absolute URL of the next page, or null." + }, + "previous": { + "type": "string", + "nullable": true, + "description": "Absolute URL of the previous page, or null." + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseEvent" + } + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/checklist/": { + "get": { + "summary": "List case checklist", + "description": "List the case's checklist items, copied from the blueprint's content_config.checklist.items at creation, ordered.", + "operationId": "list_case_checklist", + "x-didit-sdk": { + "resource": "cases.checklist", + "method": "list", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/checklist/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Checklist items.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseChecklistItem" + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/checklist/{item_uuid}/": { + "patch": { + "summary": "Toggle checklist item", + "description": "Mark a checklist item complete or incomplete. Resolution is blocked while any item is incomplete, unless the blueprint marks the checklist optional.", + "operationId": "toggle_case_checklist_item", + "x-didit-sdk": { + "resource": "cases.checklist", + "method": "toggle_item", + "auth": "api_key", + "server": true, + "scope": "application" + }, "tags": [ - "Sessions" + "Cases" ], "parameters": [ { - "name": "session_kind", - "in": "query", - "required": false, - "description": "Which kind of sessions to list: `user` (KYC only, default), `business` (KYB only), or `all` (both, concatenated - see the description for merged-pagination semantics). Matching is case-insensitive (`BUSINESS` works); any value other than a case variant of `user`/`business`/`all` returns `400`.", + "name": "organization_id", + "in": "path", + "required": true, "schema": { "type": "string", - "enum": [ - "user", - "business", - "all" - ], - "default": "user" - }, - "example": "all" - }, - { - "name": "limit", - "in": "query", - "required": false, - "description": "Maximum number of rows per page (per kind when `session_kind=all`). Defaults to 50.", - "schema": { - "type": "integer", - "default": 50, - "minimum": 1 + "format": "uuid" }, - "example": 20 + "description": "Organization UUID." }, { - "name": "offset", - "in": "query", - "required": false, - "description": "Number of rows to skip before the first returned row. Defaults to 0. Prefer following the `next`/`previous` URLs from the response.", + "name": "application_id", + "in": "path", + "required": true, "schema": { - "type": "integer", - "default": 0, - "minimum": 0 + "type": "string", + "format": "uuid" }, - "example": 20 + "description": "Application UUID." }, { - "name": "status", - "in": "query", - "required": false, - "description": "Comma-separated list of session statuses to include, e.g. `Approved,In Review`. Values are case-sensitive; see [Verification Statuses](/integration/verification-statuses). Unknown values match nothing.", + "name": "case_uuid", + "in": "path", + "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" }, - "example": "Approved,In Review" + "description": "Case UUID." }, { - "name": "vendor_data", - "in": "query", - "required": false, - "description": "Exact-match filter on the `vendor_data` you sent at session creation (your own user/business identifier). For partial matches use `search` instead.", + "name": "item_uuid", + "in": "path", + "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid", + "example": "c4ec0000-0000-4000-8000-000000000005" }, - "example": "your-user-id-123" - }, + "description": "Checklist item UUID." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "is_completed" + ], + "properties": { + "is_completed": { + "type": "boolean" + } + } + } + } + } + }, + "x-codeSamples": [ { - "name": "search", - "in": "query", - "required": false, - "description": "Smart free-text search. A full UUID matches `session_id`; numeric input matches session-number prefix, document/personal numbers, phone prefix, or `vendor_data`; input containing `@` matches email prefixes; other text matches names (accent-insensitive) and `vendor_data`. Free text containing digits additionally matches document/personal-number search fields and session-number prefixes.", - "schema": { - "type": "string" - }, - "example": "jane doe" - }, + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/checklist/c4ec0000-0000-4000-8000-000000000005/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"is_completed\": true}'" + } + ], + "responses": { + "200": { + "description": "Updated checklist item.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseChecklistItem" + } + } + } + } + }, + "security": [ { - "name": "workflow_id", - "in": "query", - "required": false, - "description": "Comma-separated workflow UUIDs (find them on the Workflows page in the Console). Malformed UUIDs in the list are ignored; if none are valid the result is empty.", - "schema": { - "type": "string" - }, - "example": "9f9b1234-aaaa-bbbb-cccc-1234567890ab" - }, + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/aml/": { + "get": { + "summary": "List case AML hits", + "description": "List AML screening hits for the case's subject. Same shape as the hits returned by POST /v3/aml/.", + "operationId": "list_case_aml_hits", + "x-didit-sdk": { + "resource": "cases", + "method": "list_aml_hits", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ { - "name": "date_from", - "in": "query", - "required": false, - "description": "Only sessions created on or after this date (UTC), format `YYYY-MM-DD`. Malformed dates return `400`.", + "name": "organization_id", + "in": "path", + "required": true, "schema": { "type": "string", - "format": "date" + "format": "uuid" }, - "example": "2026-06-01" + "description": "Organization UUID." }, { - "name": "date_to", - "in": "query", - "required": false, - "description": "Only sessions created up to this date (UTC), format `YYYY-MM-DD`. For KYC rows the entire day is included; for KYB rows the comparison is `created_at <= date_to` (midnight), so use the next day's date to include a full day of KYB sessions.", + "name": "application_id", + "in": "path", + "required": true, "schema": { "type": "string", - "format": "date" + "format": "uuid" }, - "example": "2026-06-30" + "description": "Application UUID." }, { - "name": "country", - "in": "query", - "required": false, - "description": "Comma-separated country filter. For KYC rows: ISO 3166-1 alpha-3 codes of the ID document's issuing state (e.g. `ESP,USA`), which may differ from nationality. For KYB rows: the registry company's country code.", + "name": "case_uuid", + "in": "path", + "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" }, - "example": "ESP,USA" - }, + "description": "Case UUID." + } + ], + "x-codeSamples": [ { - "name": "document_type", - "in": "query", - "required": false, - "description": "Comma-separated ID-document type codes verified in the session (KYC rows only): `P` (Passport), `ID` (Identity Card), `DL` (Driver's License), `RP` (Residence Permit), `SSC` (Social Security Card), `HIC` (Health Insurance Card), `WP` (Work Permit), `TC` (Tax Card), `VISA`, `PSC` (Public Service Card), `BC` (Birth Certificate), `OTHER`. Note the response field `document_type` contains the display name, not the code.", - "schema": { - "type": "string" - }, - "example": "P,ID" - }, + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/aml/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "AML hits.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AMLScreening" + } + } + } + } + } + }, + "security": [ { - "name": "document_name", - "in": "query", - "required": false, - "description": "Exact document names; multiple names can be passed comma-separated, but the value is only split on commas when it contains more than one ` - ` separator (a lone name with an embedded comma is matched whole). Names are as recognized by document verification (front or back side), e.g. `Spain - Id Card (2021)`. KYC rows only.", - "schema": { - "type": "string" - }, - "example": "Spain - Id Card (2021)" - }, + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/statistics/": { + "get": { + "summary": "Case analytics", + "description": "Aggregate case analytics for the date range (defaults to the last 30 days; max 731 days). Charts, tables, totals, assignment events, and current queue counts use the same application-scoped filters. Filters use current case values. Dates apply to each metric event, so older cases assigned or resolved during the period remain included. Queue depth is independent of the date range. Invalid filters return 400; an unknown blueprint or assignee returns an empty result.", + "operationId": "get_case_analytics", + "x-didit-sdk": { + "resource": "cases", + "method": "retrieve_statistics", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ { - "name": "region", - "in": "query", - "required": false, - "description": "Comma-separated document region/state values (for documents issued per region, such as US driver's licenses). KYC rows only.", + "name": "organization_id", + "in": "path", + "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" }, - "example": "CA,TX" + "description": "Organization UUID." }, { - "name": "didit_internal_id", - "in": "query", - "required": false, - "description": "Filter by Didit's internal user identifier (the `didit_internal_id` response field) to list all sessions of one verified user. Must be a valid UUID; malformed values return `400`. KYC rows only.", + "name": "application_id", + "in": "path", + "required": true, "schema": { "type": "string", "format": "uuid" }, - "example": "d9e78474-6c8d-4f2a-bb90-6f5c2f3a1147" + "description": "Application UUID." }, { - "name": "session_type", + "name": "date_from", "in": "query", - "required": false, - "description": "Comma-separated session creation channels: `API`, `HOSTED`, `MIGRATED`. The special value `ALL` (or omitting the parameter) disables the filter. KYC rows only.", "schema": { - "type": "string" + "type": "string", + "format": "date" }, - "example": "HOSTED" + "description": "Range start (inclusive). Defaults to 30 days ago." }, { - "name": "api_service", + "name": "date_to", "in": "query", - "required": false, - "description": "Filter standalone API sessions by service. Invalid values return `400`. KYC rows only.", "schema": { "type": "string", - "enum": [ - "ID_VERIFICATION", - "FACE_MATCH", - "AGE_ESTIMATION", - "FACE_SEARCH", - "POA", - "AML", - "KYB_REGISTRY", - "PASSIVE_LIVENESS", - "DATABASE_VALIDATION", - "PHONE_VERIFICATION", - "EMAIL_VERIFICATION" - ] + "format": "date" }, - "example": "AML" + "description": "Range end (inclusive). Defaults to today." }, { - "name": "screened_full_name", + "name": "blueprint", "in": "query", "required": false, - "description": "Accent-insensitive name search inside AML and database-validation screened data. When combined with `api_service=AML` or `api_service=DATABASE_VALIDATION`, the search is restricted to that feature's screened data. KYC rows only.", "schema": { - "type": "string" + "type": "string", + "format": "uuid" }, - "example": "Jane Doe" + "description": "Filter by the current blueprint UUID." }, { - "name": "warning", + "name": "assigned_to", "in": "query", "required": false, - "description": "Comma-separated warning risk codes; returns sessions that raised any of them (e.g. `POSSIBLE_FRAUD`). Risk codes are listed per feature in the [warnings documentation](/integration/webhooks). Applies to KYC rows and, with business warning codes, to KYB rows.", "schema": { - "type": "string" + "type": "string", + "format": "uuid" }, - "example": "POSSIBLE_FRAUD" + "description": "Filter by the current assigned reviewer user UUID." }, { - "name": "status_initialized_as_in_review", + "name": "priority", "in": "query", "required": false, - "description": "When `true`, only sessions whose decision initially landed in `In Review` (useful to audit manual-review volume). KYC rows only.", "schema": { - "type": "boolean" + "type": "string", + "enum": [ + "LOW", + "MEDIUM", + "HIGH" + ] }, - "example": true + "description": "Filter by current case priority." }, { - "name": "is_kyc_to_be_reviewed", + "name": "source", "in": "query", "required": false, - "description": "`true` returns sessions whose document verification is flagged for manual review; `false` returns sessions with document verification explicitly not flagged. Omit to disable. KYC rows only.", "schema": { - "type": "boolean" + "type": "string", + "enum": [ + "MANUAL", + "AML_SCREENING", + "TRANSACTION_RULE", + "WORKFLOW_BUILDER" + ] }, - "example": true + "description": "Filter by case creation source." }, { - "name": "euuid", + "name": "status", "in": "query", "required": false, - "description": "Filter by the end user's euuid (UUID assigned to the verifying user, used by Console deep links). Must be a valid UUID; malformed values return a 400 field error. KYC rows only.", "schema": { "type": "string", - "format": "uuid" + "enum": [ + "OPEN", + "AWAITING_USER", + "RESOLVED" + ] }, - "example": "0f8fad5b-d9cb-469f-a165-70867728950e" - }, + "description": "Filter by current case status." + } + ], + "x-codeSamples": [ { - "name": "organization_name", - "in": "query", - "required": false, - "description": "Comma-separated organization names. Console-oriented; with an API key (already scoped to one application) it only matches your own organization's name.", - "schema": { - "type": "string" - }, - "example": "Acme Inc" + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/statistics/?date_from=2026-06-01&date_to=2026-06-30' \\\n -H 'x-api-key: YOUR_API_KEY'" } ], "responses": { "200": { - "description": "Paginated session list. `results` items are KYC rows, KYB rows, or both depending on `session_kind`; discriminate with each row's `session_kind` field.", + "description": "Analytics payload.", "content": { "application/json": { "schema": { "type": "object", - "required": [ - "count", - "next", - "previous", - "results" - ], "properties": { - "count": { - "type": "integer", - "description": "Number of sessions matching the filters. Exact for KYC rows; for KYB rows it is capped at 100 (use `next` to know whether more pages exist). For `session_kind=all` it is the sum of both kinds.", - "example": 2 - }, - "next": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "Absolute URL of the next page, or `null` on the last page.", - "example": "https://verification.didit.me/v3/sessions/?limit=20&offset=20" - }, - "previous": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "Absolute URL of the previous page, or `null` on the first page.", - "example": null - }, - "results": { + "series": { "type": "array", - "description": "Session rows, newest first. With `session_kind=all`, KYC rows come first, then KYB rows.", "items": { - "oneOf": [ - { - "title": "KYC session row (session_kind: user)", + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date" + }, + "created": { + "type": "integer" + }, + "reviewed": { + "type": "integer" + } + } + } + }, + "tables": { + "type": "object", + "properties": { + "officer": { + "type": "array", + "items": { "type": "object", - "description": "Summary row for a User Verification (KYC) session. Identity fields (`full_name`, `country`, `document_type`, `portrait_image`, contact fields) are populated progressively as the user completes verification steps, and are `null` until the relevant step finishes. Fields restricted by the workflow's response-attributes configuration are also returned as `null`.", - "required": [ - "session_id", - "session_kind", - "session_number", - "session_url", - "portrait_image", - "document_type", - "full_name", - "country", - "status", - "vendor_data", - "didit_internal_id", - "created_at", - "features", - "phone_number", - "email_address", - "additional_details" - ], "properties": { - "session_id": { + "key": { "type": "string", - "format": "uuid", - "description": "Unique identifier of the verification session. Use it with `GET /v3/session/{sessionId}/decision/` to fetch the full decision payload.", - "example": "5b3720ed-d429-42ef-b67f-37ea805f48ee" + "description": "Dimension value: officer email, \"blueprint_name:uuid\", source, or status. \"unassigned\"/\"none\" for missing dimensions." + }, + "created": { + "type": "integer" + }, + "assigned": { + "type": "integer" + }, + "resolved": { + "type": "integer" + }, + "threat_rate": { + "type": "number", + "format": "float" + }, + "false_positive_rate": { + "type": "number", + "format": "float" + }, + "unresolved": { + "type": "integer" + }, + "reassignment_rate": { + "type": "number", + "format": "float" + }, + "escalation_rate": { + "type": "number", + "format": "float" + }, + "avg_assign_hours": { + "type": "number", + "format": "float" + }, + "avg_resolve_hours": { + "type": "number", + "format": "float" + }, + "avg_handling_hours": { + "type": "number", + "format": "float" + } + } + } + }, + "blueprint": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Dimension value: officer email, \"blueprint_name:uuid\", source, or status. \"unassigned\"/\"none\" for missing dimensions." + }, + "created": { + "type": "integer" + }, + "assigned": { + "type": "integer" + }, + "resolved": { + "type": "integer" + }, + "threat_rate": { + "type": "number", + "format": "float" + }, + "false_positive_rate": { + "type": "number", + "format": "float" + }, + "unresolved": { + "type": "integer" }, - "session_kind": { - "type": "string", - "enum": [ - "user" - ], - "description": "Row discriminator. Always `\"user\"` for KYC rows; Business Verification (KYB) rows have `\"business\"` and a different shape.", - "example": "user" + "reassignment_rate": { + "type": "number", + "format": "float" }, - "session_number": { - "type": "integer", - "description": "Human-friendly sequential number of the session within your application. Also matched by the `search` parameter.", - "example": 720 + "escalation_rate": { + "type": "number", + "format": "float" }, - "session_url": { - "type": "string", - "nullable": true, - "description": "Hosted verification URL the end user opens to complete the flow. `null` when the session has no hosted URL.", - "example": "https://verify.didit.me/session/CuXoBYCYVO5Y" + "avg_assign_hours": { + "type": "number", + "format": "float" }, - "portrait_image": { - "type": "string", - "nullable": true, - "description": "Expiring (presigned) URL of the best available portrait: the NFC chip photo when available, otherwise the ID document portrait, otherwise the liveness reference image. `null` until one of those exists. Download promptly; the link expires.", - "example": "https:///portraits/5b3720ed.jpg?X-Amz-Expires=3600&X-Amz-Signature=..." + "avg_resolve_hours": { + "type": "number", + "format": "float" }, - "document_type": { + "avg_handling_hours": { + "type": "number", + "format": "float" + } + } + } + }, + "source": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { "type": "string", - "nullable": true, - "enum": [ - "Passport", - "Identity Card", - "Driver's License", - "Residence Permit", - "Health Insurance Card", - "Tax Card" - ], - "description": "Human-readable type of the verified ID document. `null` before document verification completes, and also `null` after completion when the verified code has no display mapping (`SSC`, `WP`, `VISA`, `PSC`, `BC`, `OTHER`). Note: the `document_type` query parameter filters by short codes (`P`, `ID`, `DL`, `RP`, ...), not by these display values.", - "example": "Passport" + "description": "Dimension value: officer email, \"blueprint_name:uuid\", source, or status. \"unassigned\"/\"none\" for missing dimensions." }, - "full_name": { - "type": "string", - "nullable": true, - "description": "Best available full name for the session, resolved in order: ID document OCR, AML screened data, database-validation screened data, proof-of-address document, then the expected details provided at session creation. `null` when none is available yet.", - "example": "Jane Marie Doe" + "created": { + "type": "integer" }, - "country": { - "type": "string", - "nullable": true, - "description": "ISO 3166-1 alpha-3 country associated with the session, resolved in order: ID document issuing state, database-validation issuing state, proof-of-address issuing state, expected details, then the verified phone's country. May differ from nationality.", - "example": "ESP" + "assigned": { + "type": "integer" }, - "status": { - "type": "string", - "enum": [ - "Not Started", - "In Progress", - "Approved", - "Declined", - "In Review", - "Expired", - "Abandoned", - "Kyc Expired", - "Resubmitted", - "Awaiting User" - ], - "description": "Overall session status. See the [Verification Statuses](/integration/verification-statuses) page for the lifecycle.", - "example": "Approved" + "resolved": { + "type": "integer" }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your own identifier for the end user, echoed back exactly as sent at session creation.", - "example": "your-user-id-123" + "threat_rate": { + "type": "number", + "format": "float" }, - "didit_internal_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Didit's internal identifier for the verified user profile behind this session. Stable across multiple sessions of the same user; filterable via the `didit_internal_id` query parameter.", - "example": "d9e78474-6c8d-4f2a-bb90-6f5c2f3a1147" + "false_positive_rate": { + "type": "number", + "format": "float" }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "UTC timestamp when the session was created.", - "example": "2026-06-09T12:26:54.328364Z" + "unresolved": { + "type": "integer" }, - "features": { - "type": "array", - "description": "Per-feature status for every verification step configured in the session's workflow, in workflow order.", - "items": { - "type": "object", - "required": [ - "feature", - "status" - ], - "properties": { - "feature": { - "type": "string", - "enum": [ - "ID_VERIFICATION", - "NFC", - "LIVENESS", - "FACE_MATCH", - "POA", - "QUESTIONNAIRE", - "EMAIL_VERIFICATION", - "PHONE", - "AML", - "IP_ANALYSIS", - "AGE_ESTIMATION", - "DATABASE_VALIDATION" - ], - "description": "Verification feature identifier.", - "example": "ID_VERIFICATION" - }, - "status": { - "type": "string", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review", - "Resub Requested" - ], - "description": "Status of this feature. `Not Finished` until the step completes.", - "example": "Approved" - } - } - } + "reassignment_rate": { + "type": "number", + "format": "float" }, - "phone_number": { - "type": "object", - "nullable": true, - "description": "Phone associated with the session: the verified phone when phone verification ran, otherwise the contact phone provided at session creation. `null` when neither exists.", - "required": [ - "number", - "is_verified" - ], - "properties": { - "number": { - "type": "string", - "description": "Phone number in E.164 format.", - "example": "+14155552671" - }, - "is_verified": { - "type": "boolean", - "description": "`true` only when the phone-verification feature approved this number.", - "example": true - } - } + "escalation_rate": { + "type": "number", + "format": "float" }, - "email_address": { - "type": "object", - "nullable": true, - "description": "Email associated with the session: the verified email when email verification ran, otherwise the contact email provided at session creation. `null` when neither exists.", - "required": [ - "email", - "is_verified" - ], - "properties": { - "email": { - "type": "string", - "format": "email", - "example": "jane@example.com" - }, - "is_verified": { - "type": "boolean", - "description": "`true` only when the email-verification feature approved this address.", - "example": true - } - } + "avg_assign_hours": { + "type": "number", + "format": "float" }, - "additional_details": { - "type": "object", - "nullable": true, - "description": "Compact per-feature extras. Only features that produced data appear as keys; `null` when no feature produced extras. Fields excluded by the workflow's response-attributes configuration are omitted.", - "properties": { - "aml": { - "type": "object", - "description": "Present when AML screening ran.", - "properties": { - "total_hits": { - "type": "integer", - "description": "Number of AML screening hits.", - "example": 0 - }, - "entity_type": { - "type": "string", - "description": "Screened entity type.", - "example": "person" - }, - "is_adverse_media_fetched": { - "type": "boolean", - "example": false - }, - "screened_data": { - "type": "object", - "description": "Subset of the data that was screened. Only non-empty keys are included.", - "properties": { - "nationality": { - "type": "string", - "example": "ESP" - }, - "document_number": { - "type": "string", - "example": "XX1234567" - }, - "date_of_birth": { - "type": "string", - "example": "1990-05-21" - } - } - } - } - }, - "database_validation": { - "type": "object", - "description": "Present when database validation ran.", - "properties": { - "validation_type": { - "type": "string", - "example": "DOCUMENT_VALIDATION" - }, - "match_type": { - "type": "string", - "example": "FULL_MATCH" - }, - "identification_number": { - "type": "string", - "description": "Tax, personal, or document number that was validated (first available).", - "example": "12345678Z" - } - } - }, - "poa": { - "type": "object", - "description": "Present when proof of address ran.", - "properties": { - "document_type": { - "type": "string", - "example": "UTILITY_BILL" - }, - "issuer": { - "type": "string", - "example": "Energy Co" - }, - "address": { - "type": "string", - "example": "Calle Mayor 1, 28013 Madrid" - }, - "name_on_document": { - "type": "string", - "example": "Jane Marie Doe" - } - } - }, - "liveness": { - "type": "object", - "description": "Present when a liveness check ran.", - "properties": { - "method": { - "type": "string", - "example": "PASSIVE" - }, - "score": { - "type": "number", - "description": "Liveness confidence score, rounded to 2 decimals.", - "example": 0.98 - } - } - }, - "face_match": { - "type": "object", - "description": "Present when face match ran.", - "properties": { - "score": { - "type": "number", - "description": "Face-match similarity score, rounded to 2 decimals.", - "example": 0.95 - }, - "source_image": { - "type": "string", - "description": "Expiring (presigned) URL of the source face image.", - "example": "https:///faces/source.jpg?X-Amz-Expires=3600&..." - }, - "target_image": { - "type": "string", - "description": "Expiring (presigned) URL of the target face image.", - "example": "https:///faces/target.jpg?X-Amz-Expires=3600&..." - } - } - }, - "phone_verification": { - "type": "object", - "description": "Present when phone verification ran.", - "properties": { - "country_code": { - "type": "string", - "description": "ISO 3166-1 alpha-2 country of the phone number.", - "example": "US" - }, - "is_temporary": { - "type": "boolean", - "example": false - }, - "is_ported": { - "type": "boolean", - "example": false - }, - "is_blocked": { - "type": "boolean", - "example": false - } - } - }, - "email_verification": { - "type": "object", - "description": "Present when email verification ran.", - "properties": { - "is_breached": { - "type": "boolean", - "example": false - }, - "is_disposable": { - "type": "boolean", - "example": false - }, - "is_undeliverable": { - "type": "boolean", - "example": false - }, - "is_blocklisted": { - "type": "boolean", - "example": false - } - } - } - } + "avg_resolve_hours": { + "type": "number", + "format": "float" + }, + "avg_handling_hours": { + "type": "number", + "format": "float" } } - }, - { - "title": "KYB session row (session_kind: business)", + } + }, + "status": { + "type": "array", + "items": { "type": "object", - "description": "Summary row for a Business Verification (KYB) session. Returned only when `session_kind` is `business` or `all`.", - "required": [ - "session_id", - "session_kind", - "session_number", - "status", - "vendor_data", - "created_at", - "workflow_id", - "workflow_type", - "workflow_label", - "company_name", - "registration_number", - "country" - ], "properties": { - "session_id": { + "key": { "type": "string", - "format": "uuid", - "description": "Unique identifier of the business verification session.", - "example": "8a1f63c2-1b9e-4f3a-9c41-2f6a1f0b7d52" + "description": "Dimension value: officer email, \"blueprint_name:uuid\", source, or status. \"unassigned\"/\"none\" for missing dimensions." }, - "session_kind": { - "type": "string", - "enum": [ - "business" - ], - "description": "Row discriminator. Always `\"business\"` for KYB rows.", - "example": "business" + "created": { + "type": "integer" }, - "session_number": { - "type": "integer", - "description": "Human-friendly sequential number of the business session within your application. Numbered independently from KYC sessions.", - "example": 42 + "assigned": { + "type": "integer" }, - "status": { - "type": "string", - "enum": [ - "Not Started", - "In Progress", - "Approved", - "Declined", - "In Review", - "Expired", - "Abandoned", - "Kyc Expired", - "Resubmitted", - "Awaiting User" - ], - "description": "Overall session status. See the [Verification Statuses](/integration/verification-statuses) page.", - "example": "Approved" + "resolved": { + "type": "integer" }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your own identifier for the business, echoed back exactly as sent at session creation.", - "example": "your-company-id-77" + "threat_rate": { + "type": "number", + "format": "float" }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "UTC timestamp when the session was created.", - "example": "2026-06-08T09:14:02.118450Z" + "false_positive_rate": { + "type": "number", + "format": "float" }, - "workflow_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Identifier of the workflow this session was created with.", - "example": "9f9b1234-aaaa-bbbb-cccc-1234567890ab" + "unresolved": { + "type": "integer" }, - "workflow_type": { - "type": "string", - "nullable": true, - "description": "Workflow type. `kyb` for business workflows.", - "example": "kyb" + "reassignment_rate": { + "type": "number", + "format": "float" }, - "workflow_label": { - "type": "string", - "nullable": true, - "description": "Display name of the workflow.", - "example": "Standard KYB" + "escalation_rate": { + "type": "number", + "format": "float" }, - "company_name": { - "type": "string", - "nullable": true, - "description": "Name of the company being verified, from the registry company linked to the session. `null` until a company is selected.", - "example": "Acme Holdings S.L." + "avg_assign_hours": { + "type": "number", + "format": "float" }, - "registration_number": { - "type": "string", - "nullable": true, - "description": "Company registration number from the registry company linked to the session.", - "example": "B12345678" + "avg_resolve_hours": { + "type": "number", + "format": "float" }, - "country": { - "type": "string", - "nullable": true, - "description": "Country code of the registry company linked to the session.", - "example": "ES" + "avg_handling_hours": { + "type": "number", + "format": "float" } } } - ] + } } - } - } - }, - "examples": { - "KYC sessions (default)": { - "summary": "session_kind=user - one completed and one not-started session", - "value": { - "count": 2, - "next": null, - "previous": null, - "results": [ - { - "session_id": "5b3720ed-d429-42ef-b67f-37ea805f48ee", - "session_kind": "user", - "session_number": 720, - "session_url": "https://verify.didit.me/session/CuXoBYCYVO5Y", - "portrait_image": "https:///portraits/5b3720ed.jpg?X-Amz-Expires=3600&X-Amz-Signature=...", - "document_type": "Passport", - "full_name": "Jane Marie Doe", - "country": "ESP", - "status": "Approved", - "vendor_data": "your-user-id-123", - "didit_internal_id": "d9e78474-6c8d-4f2a-bb90-6f5c2f3a1147", - "created_at": "2026-06-09T12:26:54.328364Z", - "features": [ - { - "feature": "ID_VERIFICATION", - "status": "Approved" - }, - { - "feature": "LIVENESS", - "status": "Approved" - }, - { - "feature": "FACE_MATCH", - "status": "Approved" - }, - { - "feature": "AML", - "status": "Approved" - }, - { - "feature": "IP_ANALYSIS", - "status": "Approved" - } - ], - "phone_number": { - "number": "+14155552671", - "is_verified": true - }, - "email_address": { - "email": "jane@example.com", - "is_verified": true - }, - "additional_details": { - "aml": { - "total_hits": 0, - "entity_type": "person", - "is_adverse_media_fetched": false - }, - "liveness": { - "method": "PASSIVE", - "score": 0.98 - }, - "face_match": { - "score": 0.95 - } - } + }, + "totals": { + "type": "object", + "properties": { + "created": { + "type": "integer" }, - { - "session_id": "038ad62f-5ebd-46c2-afe9-4b2956bf687f", - "session_kind": "user", - "session_number": 719, - "session_url": "https://verify.didit.me/session/7RGKiO6ioGH6", - "portrait_image": null, - "document_type": null, - "full_name": null, - "country": null, - "status": "Not Started", - "vendor_data": "your-user-id-124", - "didit_internal_id": "eb4e679d-ca4b-461f-9685-687bac29ebdb", - "created_at": "2026-06-09T08:02:23.343053Z", - "features": [ - { - "feature": "ID_VERIFICATION", - "status": "Not Finished" - }, - { - "feature": "LIVENESS", - "status": "Not Finished" - }, - { - "feature": "FACE_MATCH", - "status": "Not Finished" - }, - { - "feature": "AML", - "status": "Not Finished" - }, - { - "feature": "IP_ANALYSIS", - "status": "Not Finished" - } - ], - "phone_number": null, - "email_address": null, - "additional_details": null + "assigned": { + "type": "integer" + }, + "resolved": { + "type": "integer" + }, + "threat_rate": { + "type": "number", + "format": "float" + }, + "false_positive_rate": { + "type": "number", + "format": "float" + }, + "unresolved": { + "type": "integer" + }, + "reassignment_rate": { + "type": "number", + "format": "float" + }, + "escalation_rate": { + "type": "number", + "format": "float" + }, + "avg_assign_hours": { + "type": "number", + "format": "float" + }, + "avg_resolve_hours": { + "type": "number", + "format": "float" + }, + "avg_handling_hours": { + "type": "number", + "format": "float" } - ] - } - }, - "Mixed KYC + KYB (session_kind=all)": { - "summary": "session_kind=all - KYC row followed by a KYB row", - "value": { - "count": 2, - "next": null, - "previous": null, - "results": [ - { - "session_id": "5b3720ed-d429-42ef-b67f-37ea805f48ee", - "session_kind": "user", - "session_number": 720, - "session_url": "https://verify.didit.me/session/CuXoBYCYVO5Y", - "portrait_image": null, - "document_type": "Identity Card", - "full_name": "Jane Marie Doe", - "country": "ESP", - "status": "In Review", - "vendor_data": "your-user-id-123", - "didit_internal_id": "d9e78474-6c8d-4f2a-bb90-6f5c2f3a1147", - "created_at": "2026-06-09T12:26:54.328364Z", - "features": [ - { - "feature": "ID_VERIFICATION", - "status": "In Review" - }, - { - "feature": "LIVENESS", - "status": "Approved" - } - ], - "phone_number": null, - "email_address": null, - "additional_details": { - "liveness": { - "method": "PASSIVE", - "score": 0.98 - } - } + } + }, + "queue": { + "type": "object", + "description": "Live snapshot, not scoped to the date range.", + "properties": { + "open": { + "type": "integer" }, - { - "session_id": "8a1f63c2-1b9e-4f3a-9c41-2f6a1f0b7d52", - "session_kind": "business", - "session_number": 42, - "status": "Approved", - "vendor_data": "your-company-id-77", - "created_at": "2026-06-08T09:14:02.118450Z", - "workflow_id": "9f9b1234-aaaa-bbbb-cccc-1234567890ab", - "workflow_type": "kyb", - "workflow_label": "Standard KYB", - "company_name": "Acme Holdings S.L.", - "registration_number": "B12345678", - "country": "ES" + "overdue": { + "type": "integer" } - ] + } } } } } } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/bulk/status/": { + "post": { + "summary": "Bulk update case status", + "description": "Apply the same status transition to several cases in one call: OPEN reopens, AWAITING_USER requests info, RESOLVED resolves (resolution required). Each case is evaluated independently; gate failures (incomplete checklist, missing required note) are reported per case without aborting the batch.", + "operationId": "bulk_update_case_status", + "x-didit-sdk": { + "resource": "cases", + "method": "bulk_update_status", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, - "301": { - "description": "Redirect to the canonical trailing-slash URL. Returned when the request path is `/v3/sessions` (no trailing slash); the `Location` header points to `/v3/sessions/` with the query string preserved. GET clients typically follow it automatically." - }, - "400": { - "description": "Invalid query parameter. The body is an object keyed by the offending parameter.", - "content": { - "application/json": { - "examples": { - "Invalid session_kind": { - "summary": "session_kind not one of user/business/all", - "value": { - "session_kind": "Must be \"user\", \"business\", or \"all\"." - } + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "case_uuids", + "status" + ], + "properties": { + "case_uuids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "minItems": 1 }, - "Invalid date": { - "summary": "Malformed date_from/date_to", - "value": { - "date_from": [ - "Enter a valid date." - ] - } + "status": { + "type": "string", + "enum": [ + "OPEN", + "AWAITING_USER", + "RESOLVED" + ] }, - "Invalid didit_internal_id": { - "summary": "didit_internal_id is not a UUID", - "value": { - "didit_internal_id": [ - "Enter a valid UUID." - ] - } + "resolution": { + "type": "string", + "enum": [ + "FALSE_POSITIVE", + "VALID_THREAT", + "INCONCLUSIVE", + "REQUIREMENTS_NOT_MET", + "REQUIREMENTS_SATISFIED" + ], + "description": "Required when status is RESOLVED." }, - "Invalid api_service": { - "summary": "api_service not in the allowed choices", - "value": { - "api_service": [ - "Select a valid choice. FOO is not one of the available choices." - ] - } - } - } - } - } - }, - "403": { - "description": "Missing, malformed, or revoked API key. This API never returns `401`; all authentication failures are `403`.", - "content": { - "application/json": { - "examples": { - "Forbidden": { - "summary": "Missing or invalid x-api-key", - "value": { - "detail": "You do not have permission to perform this action." - } + "note": { + "type": "string", + "nullable": true } } } } } }, - "security": [ - { - "ApiKeyAuth": [] - } - ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -s 'https://verification.didit.me/v3/sessions/?status=Approved,In%20Review&date_from=2026-06-01&limit=20' \\\n -H 'x-api-key: YOUR_API_KEY'" - }, - { - "lang": "python", - "label": "Python", - "source": "import requests\n\nurl = \"https://verification.didit.me/v3/sessions/\"\nheaders = {\"x-api-key\": \"YOUR_API_KEY\"}\nparams = {\n \"session_kind\": \"user\", # \"user\" (default) | \"business\" | \"all\"\n \"status\": \"Approved,In Review\", # comma-separated, OR within the list\n \"date_from\": \"2026-06-01\",\n \"limit\": 20,\n}\n\nwhile url:\n response = requests.get(url, headers=headers, params=params)\n response.raise_for_status()\n page = response.json()\n for session in page[\"results\"]:\n print(session[\"session_kind\"], session[\"session_id\"], session[\"status\"])\n url, params = page[\"next\"], None # \"next\" already carries the query string" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const params = new URLSearchParams({\n session_kind: 'all',\n status: 'Approved,In Review',\n date_from: '2026-06-01',\n limit: '20',\n});\n\nconst response = await fetch(\n `https://verification.didit.me/v3/sessions/?${params}`,\n { headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\n\nconst { count, next, results } = await response.json();\nfor (const session of results) {\n // session.session_kind is \"user\" (KYC) or \"business\" (KYB)\n console.log(session.session_kind, session.session_id, session.status);\n}" + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/bulk/status/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"case_uuids\": [\"c1a5e000-0000-4000-8000-000000000001\"], \"status\": \"RESOLVED\", \"resolution\": \"FALSE_POSITIVE\"}'" } - ] - } - }, - "/v3/session/{sessionId}/share/": { - "post": { - "summary": "Mint a share token for a finished verification session", - "description": "Mint a short-lived JWT that lets a specific Didit application import this finished session. Pair with `POST /v3/session/import-shared/`.", - "operationId": "post_v3_session_share", - "tags": [ - "Sessions" ], "responses": { "200": { - "description": "Share token minted.", + "description": "Per-case results.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "share_token": { - "type": "string", - "description": "HS256-signed JWT. Pass this verbatim as `share_token` to [`POST /v3/session/import-shared/`](/sessions-api/import-shared-session) on the target application.", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - }, - "for_application_id": { - "type": "string", - "format": "uuid", - "description": "Echoes the target application UUID that the token is bound to.", - "example": "a5f3bca2-46e2-411e-90ef-a580900a57ee" - }, - "session_kind": { - "type": "string", - "enum": [ - "user", - "business" - ], - "description": "Whether the source session is a User Verification (KYC) or Business Verification (KYB) session." - } - } - }, - "examples": { - "User session": { - "summary": "Share a KYC session", - "value": { - "share_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uX2lkIjoiMjBiYjNjM2ItNjE0Ni00MzlmLTg0YTQtYmQzMGQwMGFjNmEyIiwiZnJvbV9hcHBsaWNhdGlvbl9pZCI6ImRiZDIwZTM0LTQyZTktNGYyYy1iYTkxLWNmMDc2MjAxNmY2NCIsImZvcl9hcHBsaWNhdGlvbl9pZCI6ImE1ZjNiY2EyLTQ2ZTItNDExZS05MGVmLWE1ODA5MDBhNTdlZSIsImlhdCI6MTc1MzYzMDY2NiwiZXhwIjoxNzUzNjM0MjY2fQ.JJ9pNE_hqZsOtbR0XYZIWw4JzidjdEl279iUrsIkhGE", - "for_application_id": "a5f3bca2-46e2-411e-90ef-a580900a57ee", - "session_kind": "user" - } - }, - "Business session": { - "summary": "Share a KYB session", - "value": { - "share_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "for_application_id": "a5f3bca2-46e2-411e-90ef-a580900a57ee", - "session_kind": "business" - } - } - } - } - } - }, - "401": { - "description": "Missing or invalid credentials. Unlike most v3 endpoints, this endpoint authenticates through the permission decorator and returns `401` when no valid token/key is presented.", - "content": { - "application/json": { - "examples": { - "Unauthorized": { - "summary": "Unauthorized", - "value": { - "detail": "Authentication credentials were not provided or are invalid." - } - } - } - } - } - }, - "404": { - "description": "No session with the given `session_id` exists in your application.", - "content": { - "application/json": { - "examples": { - "Not Found": { - "summary": "Session not found", - "value": { - "detail": "Not found." - } - } - } - } - } - }, - "400": { - "description": "Validation error. Field errors are keyed by field name; the not-finished error arrives under a `detail` key (as an array).", - "content": { - "application/json": { - "examples": { - "Wrong status": { - "summary": "Source session is not finished", - "value": { - "detail": [ - "Only finished sessions (\"Approved\", \"Declined\", \"In Review\") can be shared." - ] - } - }, - "Target missing": { - "summary": "Target application does not exist", - "value": { - "for_application_id": [ - "Target application does not exist." - ] - } - }, - "Self-share": { - "summary": "Target is the calling application", - "value": { - "for_application_id": [ - "Cannot share a session with the same application." - ] - } - }, - "TTL bounds": { - "summary": "ttl_in_seconds outside bounds", - "value": { - "ttl_in_seconds": [ - "Ensure this value is greater than or equal to 60." - ] - } - } - } - } - } - }, - "403": { - "description": "The credentials are valid but lack the `write:sessions` permission for this application.", - "content": { - "application/json": { - "examples": { - "No Permission": { - "value": { - "detail": "You do not have permission to perform this action." - }, - "summary": "No Permission" - } + "$ref": "#/components/schemas/CaseBulkUpdateResult" } } } } }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/bulk/assign/": { + "post": { + "summary": "Bulk assign cases", + "description": "Assign (or unassign, with assignee_user_id: null) several cases at once. All cases must share the same blueprint; mismatches are reported per case without aborting the batch.", + "operationId": "bulk_assign_cases", + "x-didit-sdk": { + "resource": "cases", + "method": "bulk_assign", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Cases" + ], "parameters": [ { + "name": "organization_id", "in": "path", - "name": "sessionId", "required": true, - "description": "UUID of the source verification session to mint a token for.", "schema": { "type": "string", - "format": "uuid", - "example": "11111111-2222-3333-4444-555555555555" - } - } - ], - "security": [ + "format": "uuid" + }, + "description": "Organization UUID." + }, { - "ApiKeyAuth": [] + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." } ], "requestBody": { @@ -17602,28 +37857,33 @@ "schema": { "type": "object", "required": [ - "for_application_id" + "case_uuids", + "assignee_user_id" ], "properties": { - "for_application_id": { + "case_uuids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "minItems": 1 + }, + "assignee_user_id": { "type": "string", "format": "uuid", - "description": "UUID of the Didit application that will redeem the token. Must exist, must not be deleted, and must differ from the calling application. Find it in the Business Console under **Settings → Application**.", - "example": "a5f3bca2-46e2-411e-90ef-a580900a57ee" + "nullable": true }, - "ttl_in_seconds": { - "type": "integer", - "description": "Token lifetime, in seconds. Minimum `60`, maximum `86400` (24 h). Defaults to `3600` (1 h).", - "minimum": 60, - "maximum": 86400, - "default": 3600, - "example": 3600 + "assignee_email": { + "type": "string", + "format": "email", + "nullable": true + }, + "assignee_name": { + "type": "string", + "nullable": true } } - }, - "example": { - "for_application_id": "a5f3bca2-46e2-411e-90ef-a580900a57ee", - "ttl_in_seconds": 7200 } } } @@ -17632,186 +37892,120 @@ { "lang": "curl", "label": "curl", - "source": "curl -X POST \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/share/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"for_application_id\": \"a5f3bca2-46e2-411e-90ef-a580900a57ee\",\n \"ttl_in_seconds\": 7200\n }'" - }, - { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/share/\",\n headers={\n 'x-api-key': 'YOUR_API_KEY',\n \"Content-Type\": \"application/json\",\n },\n json={\n \"for_application_id\": \"a5f3bca2-46e2-411e-90ef-a580900a57ee\",\n \"ttl_in_seconds\": 7200,\n },\n)\nresponse.raise_for_status()\nshare_token = response.json()[\"share_token\"]" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const response = await fetch(\n 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/share/',\n {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n for_application_id: 'a5f3bca2-46e2-411e-90ef-a580900a57ee',\n ttl_in_seconds: 7200,\n }),\n },\n);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst { share_token } = await response.json();" + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/bulk/assign/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"case_uuids\": [\"c1a5e000-0000-4000-8000-000000000001\"], \"assignee_user_id\": \"9a000000-0000-4000-8000-0000000000aa\"}'" } - ] - } - }, - "/v3/session/import-shared/": { - "post": { - "summary": "Redeem a share token to clone a verification session into your application", - "description": "Redeem a share token to clone a KYC or KYB session into the calling application with a fresh `session_id`. Not idempotent — a token redeems once per receiver.", - "operationId": "post_v3_session_import_shared", - "tags": [ - "Sessions" ], "responses": { - "201": { - "description": "Session cloned. The body is the V3 decision payload for both kinds, discriminated by `session_kind`: `\"user\"` (KYC) with list-based feature blocks such as `id_verifications`, `liveness_checks`, `aml_screenings`; `\"business\"` (KYB) with blocks like `registry_checks` and `key_people_checks`. Both include `shared_from_session` — the `session_id` of the original session in the source application. With `trust_review: false` the cloned session's `status` is forced to `In Review`.", + "200": { + "description": "Per-case results.", "content": { "application/json": { - "examples": { - "User session imported": { - "summary": "Cloned KYC session (truncated — full V3 decision payload in reality)", - "value": { - "session_id": "11111111-2222-3333-4444-555555555555", - "session_kind": "user", - "shared_from_session": "99999999-8888-7777-6666-555555555555", - "session_number": 43762, - "session_url": null, - "status": "In Review", - "workflow_id": "9f9b1234-aaaa-bbbb-cccc-1234567890ab", - "vendor_data": "user-1", - "created_at": "2026-05-17T08:42:11Z", - "expires_at": "2026-05-24T08:42:11Z" - } - }, - "Business session imported": { - "summary": "Cloned KYB session (truncated — full V3 KYB decision payload in reality)", - "value": { - "session_id": "22222222-3333-4444-5555-666666666666", - "session_kind": "business", - "shared_from_session": "88888888-7777-6666-5555-444444444444", - "session_number": 982, - "session_url": null, - "status": "Approved", - "workflow_id": "9f9b1234-aaaa-bbbb-cccc-1234567890ab", - "vendor_data": "company-1" - } - } + "schema": { + "$ref": "#/components/schemas/CaseBulkUpdateResult" } } } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/": { + "get": { + "summary": "List blueprints", + "description": "List case blueprints for the application.", + "operationId": "list_case_blueprints", + "x-didit-sdk": { + "resource": "cases.blueprints", + "method": "list", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Case Blueprints" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, - "400": { - "description": "Validation error (token invalid, expired, wrong target, or references a session that no longer exists).", - "content": { - "application/json": { - "examples": { - "Invalid Share Token": { - "summary": "Invalid JWT", - "value": { - "share_token": [ - "Invalid share token." - ] - } - }, - "Expired Share Token": { - "summary": "Token past `exp`", - "value": { - "share_token": [ - "Share token has expired." - ] - } - }, - "Wrong audience": { - "summary": "Token not bound to this application", - "value": { - "share_token": [ - "This token is not valid for this application." - ] - } - }, - "Original missing": { - "summary": "Source session was deleted", - "value": { - "share_token": [ - "Original session does not exist." - ] - } - } - } - } - } + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." }, - "403": { - "description": "Missing/invalid API key (this endpoint returns `403`, never `401`, for authentication failures), or this source session has already been imported into the calling application (one redeem per receiver).", - "content": { - "application/json": { - "examples": { - "Forbidden": { - "summary": "Forbidden", - "value": { - "detail": "You do not have permission to perform this action." - } - }, - "Already imported": { - "summary": "Source already imported once", - "value": { - "detail": "This session has already been shared with your application." - } - } - } - } - } + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50, + "minimum": 1 + }, + "description": "Page size." }, - "404": { - "description": "`workflow_id` does not belong to the calling application.", + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + }, + "description": "Zero-based offset." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/blueprints/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Paginated blueprint list.", "content": { "application/json": { - "examples": { - "Workflow not found": { - "summary": "Workflow unknown", - "value": { - "detail": "Workflow does not exist for this application." - } - } - } - } - } - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "share_token", - "trust_review", - "workflow_id" - ], - "properties": { - "share_token": { - "type": "string", - "description": "JWT share token issued by [`POST /v3/session/{sessionId}/share/`](/sessions-api/share-session).", - "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - }, - "trust_review": { - "type": "boolean", - "description": "If `true`, the cloned session keeps the source's final `status`. If `false`, it is forced into `In Review`.", - "example": false - }, - "workflow_id": { - "type": "string", - "format": "uuid", - "description": "UUID of a workflow in the calling application. Cross-application IDs are rejected with `404`.", - "example": "9f9b1234-aaaa-bbbb-cccc-1234567890ab" - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Optional override for the cloned session's `vendor_data`.", - "example": "user-1" + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "description": "Number of matching rows, capped at 100 for performance." + }, + "next": { + "type": "string", + "nullable": true + }, + "previous": { + "type": "string", + "nullable": true + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseBlueprintListItem" + } + } } } - }, - "example": { - "share_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "trust_review": false, - "workflow_id": "9f9b1234-aaaa-bbbb-cccc-1234567890ab", - "vendor_data": "user-1" } } } @@ -17820,40 +38014,44 @@ { "ApiKeyAuth": [] } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST \\\n https://verification.didit.me/v3/session/import-shared/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"share_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n \"trust_review\": false,\n \"workflow_id\": \"9f9b1234-aaaa-bbbb-cccc-1234567890ab\",\n \"vendor_data\": \"user-1\"\n }'" - }, - { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/session/import-shared/\",\n headers={\n 'x-api-key': 'YOUR_API_KEY',\n \"Content-Type\": \"application/json\",\n },\n json={\n \"share_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\",\n \"trust_review\": False,\n \"workflow_id\": \"9f9b1234-aaaa-bbbb-cccc-1234567890ab\",\n \"vendor_data\": \"user-1\",\n },\n)\nresponse.raise_for_status()\nimported = response.json()" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const response = await fetch(\n 'https://verification.didit.me/v3/session/import-shared/',\n {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n share_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',\n trust_review: false,\n workflow_id: '9f9b1234-aaaa-bbbb-cccc-1234567890ab',\n vendor_data: 'user-1',\n }),\n },\n);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst imported = await response.json();" - } ] - } - }, - "/v3/kyb/search/": { + }, "post": { - "summary": "KYB registry company search (standalone)", - "operationId": "post_v3_kyb_search", + "summary": "Create blueprint", + "description": "Create a case blueprint: general settings (assignment, escalation, transfer, 4-eyes pairing) plus content_config, the six sections that drive the case page. Set publish=false to create an unpublished draft; Publish creates its first live version. Discarding a never-published draft deletes it.", + "operationId": "create_case_blueprint", + "x-didit-sdk": { + "resource": "cases.blueprints", + "method": "create", + "auth": "api_key", + "server": true, + "scope": "application" + }, "tags": [ - "Standalone APIs" + "Case Blueprints" ], - "security": [ + "parameters": [ { - "ApiKeyAuth": [] + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." } ], - "parameters": [], - "description": "Search official company registries for candidate companies by name and/or registration number. This is step 1 of the two-step registry flow: **search** returns lightweight candidates, then `POST /v3/kyb/select/` retrieves the full profile of one candidate.\n\n**Search billing.** Successful searches using a paid company-data source cost USD 0.50 per request, regardless of candidate count. Empty results, failures and results served only by other sources are not charged. The applicable price is reserved before searching; insufficient balance returns 403 with code `insufficient_credits`. Contract rates apply where agreed, and an unconfigured contract search rate returns 403. Sandbox searches are free. Repeating a successful search can incur another charge; callback delivery does not add a charge. Profile retrieval is billed separately. This search fee also applies to v2. Candidates always come back with `fetch_status: \"pending\"`: the full company data is only fetched (and billed) on select.\n\n**`kyb_response_id` is ephemeral.** It is a per-search handle, not a stable company id. Select promptly after searching and re-search to get a fresh handle instead of storing it.\n\n**Two modes.**\n- *Synchronous (default):* the API polls the registry until the search resolves and returns the final candidate list (`search_resolved: true`).\n- *Asynchronous:* pass `webhook_url` and the API returns immediately — usually with `search_status: \"pending\"` and an empty candidate list — then POSTs a `kyb.registry_search.resolved` callback to your URL when the registry finishes. The callback body is `{\"event_id\", \"event_type\": \"kyb.registry_search.resolved\", \"request_id\", \"vendor_data\", \"metadata\", \"search_status\", \"search_resolved\", \"kyb_registry\", \"timestamp\", \"created_at\"}` — note `timestamp` and `created_at` in the callback are epoch integers, unlike this endpoint's ISO `created_at` — and is sent **unsigned** (header `X-Didit-Unsigned-Callback: true`, no `X-Signature`).\n\n**Country codes.** ISO 3166-1 alpha-2 (`GB`, `DE`), plus `XX-YY` subdivision codes where registries are regional — e.g. `US-CA` for California.\n\n**Sandbox.** Keys from sandbox applications return one static candidate without contacting any registry.", "requestBody": { "required": true, "content": { @@ -17861,50 +38059,85 @@ "schema": { "type": "object", "required": [ - "country_code" + "name", + "assignee_pool" ], "properties": { - "country_code": { - "type": "string", - "maxLength": 10, - "description": "ISO 3166-1 alpha-2 code of the country whose company registry to search. For example, `GB`. Where company registries operate at state or province level, append the ISO 3166-2 subdivision code as `XX-YY`: `US-CA` searches the California registry. United States searches always require the subdivision — a bare `US` is rejected. Case-insensitive; normalized to upper case.", - "example": "GB" - }, "name": { "type": "string", - "maxLength": 255, - "description": "Company name to search for. Either `name` or `registration_number` is required.", - "example": "Tesco" + "maxLength": 24, + "description": "Max 24 characters, e.g. \"AML investigation\"." }, - "registration_number": { + "description": { "type": "string", - "maxLength": 100, - "description": "Registry registration number to search for. Either `name` or `registration_number` is required." + "nullable": true }, - "search_type": { + "assignee_pool": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "minItems": 1, + "description": "List of User UUIDs. Empty arrays are rejected with \"Pick at least one assignee\"." + }, + "assignment_method": { "type": "string", "enum": [ - "contains", - "start_with", - "fuzzy" + "MANUAL", + "AUTOMATIC" ], - "default": "contains", - "description": "Name-matching strategy applied by the registry search." + "default": "MANUAL" }, - "vendor_data": { - "type": "string", - "description": "Your identifier for this lookup; echoed back and included in the webhook callback." + "max_active_cases": { + "type": "integer", + "nullable": true, + "minimum": 1, + "description": "Automatic-mode threshold: \"fewer than N cases of this blueprint assigned\"." }, - "metadata": { - "type": "object", + "escalation_enabled": { + "type": "boolean", + "default": false + }, + "escalation_blueprint": { + "type": "string", + "format": "uuid", "nullable": true, - "description": "Free-form JSON; echoed back and included in the webhook callback." + "description": "Target blueprint UUID. Not enforced at save time, but the escalate action fails with 400 until both escalation_enabled and escalation_blueprint are set." }, - "webhook_url": { + "transfer_enabled": { + "type": "boolean", + "default": false + }, + "four_eyes_checker_blueprint": { "type": "string", - "format": "uri", - "maxLength": 500, - "description": "Switches the search to asynchronous mode: the API returns immediately and POSTs an unsigned `kyb.registry_search.resolved` callback to this URL when the candidate list is ready." + "format": "uuid", + "nullable": true, + "description": "Setting this enables 4-eyes review: resolutions on this blueprint stage on the checker blueprint's queue." + }, + "require_resolution_note": { + "type": "boolean", + "default": false + }, + "content_config": { + "$ref": "#/components/schemas/CaseContentConfig" + }, + "is_active": { + "type": "boolean", + "default": true + }, + "default_deadline_hours": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 8760, + "description": "Default deadline for newly opened cases. Existing case deadlines are unchanged." + }, + "publish": { + "type": "boolean", + "writeOnly": true, + "default": true, + "description": "Set false to save a never-published draft. The record remains inactive at version 0 until the draft is explicitly published. Omitting this field preserves immediate creation for existing integrations." } } } @@ -17915,348 +38148,234 @@ { "lang": "curl", "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/kyb/search/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"country_code\": \"GB\",\n \"name\": \"Tesco\",\n \"search_type\": \"contains\",\n \"vendor_data\": \"company-1234\"\n }'" - }, - { - "lang": "python", - "label": "Python", - "source": "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/kyb/search/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"country_code\": \"GB\", \"name\": \"Tesco\"},\n timeout=60, # synchronous mode waits for the registry to resolve\n)\nresp.raise_for_status()\ncandidates = resp.json()[\"kyb_registry\"][\"companies\"]\nkyb_response_id = candidates[0][\"kyb_response_id\"] # pass to /v3/kyb/select/" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const res = await fetch('https://verification.didit.me/v3/kyb/search/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ country_code: 'GB', name: 'Tesco' }),\n});\nconst data = await res.json();\nconst kybResponseId = data.kyb_registry.companies[0].kyb_response_id;" + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/blueprints/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"name\": \"AML investigation\", \"assignee_pool\": [\"9a000000-0000-4000-8000-0000000000aa\"], \"assignment_method\": \"AUTOMATIC\", \"max_active_cases\": 15, \"require_resolution_note\": true, \"content_config\": {\"aml_control\": {\"enabled\": true, \"financial\": true, \"identity\": false}}}'" } ], "responses": { - "200": { - "description": "Search accepted. Synchronous mode returns the resolved candidate list; webhook mode returns immediately (often `search_status: \"pending\"`) and delivers the final list via the `kyb.registry_search.resolved` callback.", + "201": { + "description": "Blueprint created.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "request_id": { - "type": "string", - "format": "uuid", - "description": "With `webhook_url`: the persisted search-request id, echoed as `request_id` in the `kyb.registry_search.resolved` callback. Without `webhook_url`: a transient correlation UUID (the search is not stored)." - }, - "kyb_registry": { - "type": "object", - "properties": { - "companies": { - "type": "array", - "items": { - "type": "object", - "properties": { - "kyb_response_id": { - "type": "string", - "nullable": true, - "description": "Handle for this candidate — pass it to `POST /v3/kyb/select/` to retrieve the full profile. Ephemeral and scoped to this search: do not store it long-term; re-search to get a fresh one.", - "example": "69aeeb95febb0f1704042259" - }, - "name": { - "type": "string", - "nullable": true, - "description": "Registered company name.", - "example": "Tesco PLC" - }, - "registration_number": { - "type": "string", - "nullable": true, - "description": "Registry registration number.", - "example": "00445790" - }, - "status": { - "type": "string", - "nullable": true, - "description": "Normalized registry status of the company (e.g. `active`, `dissolved`, `inactive`, `struck off`).", - "example": "active" - }, - "type": { - "type": "string", - "nullable": true, - "description": "Legal form as reported by the registry.", - "example": "Public Limited Company" - }, - "risk_level": { - "type": "string", - "nullable": true, - "description": "Registry-reported risk level, when available. Usually `null` at search time." - }, - "fetch_status": { - "type": "string", - "nullable": true, - "description": "Always `pending` for search candidates — the full company profile is only fetched (and billed) when you select the candidate.", - "example": "pending" - } - } - }, - "description": "Deduplicated candidate list. Empty while `search_resolved=false` (webhook mode) or when nothing matched." - }, - "pagination": { - "type": "object", - "properties": { - "total": { - "type": "integer", - "description": "Number of candidates returned." - }, - "page": { - "type": "integer" - }, - "per_page": { - "type": "integer", - "example": 25 - } - } - }, - "search_status": { - "type": "string", - "nullable": true, - "enum": [ - "pending", - "resolved" - ], - "description": "Registry search lifecycle status." - }, - "search_resolved": { - "type": "boolean", - "description": "`true` once the candidate list is final." - } - } - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Echo of the `vendor_data` you sent." - }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Echo of the `metadata` you sent." - }, - "created_at": { - "type": "string", - "format": "date-time" - } - } - }, - "examples": { - "Resolved search": { - "summary": "Synchronous mode — final candidate list", - "value": { - "request_id": "0c7e9a40-3a13-4f7e-8a44-9d2e2f1c5b6a", - "kyb_registry": { - "companies": [ - { - "kyb_response_id": "69aeeb95febb0f1704042259", - "name": "Tesco PLC", - "registration_number": "00445790", - "status": "active", - "type": "Public Limited Company", - "risk_level": null, - "fetch_status": "pending" - }, - { - "kyb_response_id": "69aeeb95febb0f170404225a", - "name": "E-TESCO LTD", - "registration_number": "15168476", - "status": "active", - "type": "Private Limited Company", - "risk_level": null, - "fetch_status": "pending" - } - ], - "pagination": { - "total": 2, - "page": 1, - "per_page": 25 - }, - "search_status": "resolved", - "search_resolved": true - }, - "vendor_data": "company-1234", - "metadata": null, - "created_at": "2026-06-11T10:40:00.000000+00:00" - } - }, - "Pending search (webhook mode)": { - "summary": "`webhook_url` provided — candidates arrive via the kyb.registry_search.resolved callback", - "value": { - "request_id": "5f2d8c1b-7e4a-4b9c-9f0d-3a6e8c2b1d4f", - "kyb_registry": { - "companies": [], - "pagination": { - "total": 0, - "page": 1, - "per_page": 25 - }, - "search_status": "pending", - "search_resolved": false - }, - "vendor_data": "company-1234", - "metadata": { - "source": "onboarding" - }, - "created_at": "2026-06-11T10:41:00.000000+00:00" - } - } + "$ref": "#/components/schemas/CaseBlueprintDetail" } } } }, "400": { - "description": "Validation error. Field-level problems use DRF's standard envelope; the name/registration-number rule reports under `non_field_errors`.", - "content": { - "application/json": { - "examples": { - "Missing search criteria": { - "summary": "Neither `name` nor `registration_number` provided", - "value": { - "non_field_errors": [ - "Either 'name' or 'registration_number' is required." - ] - } - }, - "Invalid country_code": { - "summary": "Must be `XX` (alpha-2) or `XX-YY` subdivision", - "value": { - "country_code": [ - "country_code must be a 2-letter ISO code." - ] - } - }, - "Invalid search_type": { - "summary": "Only contains / start_with / fuzzy", - "value": { - "search_type": [ - "\"exact\" is not a valid choice." - ] - } - }, - "Invalid webhook_url": { - "summary": "`webhook_url` must be a valid URL", - "value": { - "webhook_url": [ - "Enter a valid URL." - ] - } - } - } - } - } - }, - "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Also returned before searching when the applicable search fee cannot be covered (`code: \"insufficient_credits\"`), or when a contract search rate is missing.", - "content": { - "application/json": { - "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", - "value": { - "detail": "You do not have permission to perform this action." - } - } - } - } - } - }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "description": "Name over 24 chars, empty assignee_pool, or a cross-referenced blueprint (escalation/checker) not found.", "content": { "application/json": { - "examples": { - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." - } - } - }, "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } + "type": "object" } } } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/": { + "get": { + "summary": "Get blueprint", + "description": "Retrieve a blueprint, including its connected creation sources (AML config, transaction rules, workflow nodes) and which blueprints escalate to it.", + "operationId": "get_case_blueprint", + "x-didit-sdk": { + "resource": "cases.blueprints", + "method": "retrieve", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Case Blueprints" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, - "502": { - "description": "The KYB registry provider is temporarily unavailable - a 5xx response, timeout, or connection failure from the upstream registry. The search is not billed. Retry later, or, in Didit's hosted verification flow, the user is automatically offered manual company entry instead of a retry-only error.", + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "blueprint_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "b1de0000-0000-4000-8000-000000000002" + }, + "description": "Blueprint UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/blueprints/b1de0000-0000-4000-8000-000000000002/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Blueprint detail.", "content": { "application/json": { - "examples": { - "Registry provider outage": { - "summary": "The registry provider did not answer the search", - "value": { - "detail": "The KYB registry provider is temporarily unavailable. Please try again shortly.", - "code": "kyb_registry_provider_unavailable" - } - } - }, "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - }, - "code": { - "type": "string" - } - } + "$ref": "#/components/schemas/CaseBlueprintDetail" } } } } - } - } - }, - "/v3/kyb/select/": { - "post": { - "summary": "KYB registry company select — Lite tier (standalone)", - "operationId": "post_v3_kyb_select", + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "patch": { + "summary": "Update blueprint", + "description": "Partially update a blueprint's general settings or content_config.", + "operationId": "update_case_blueprint", + "x-didit-sdk": { + "resource": "cases.blueprints", + "method": "update", + "auth": "api_key", + "server": true, + "scope": "application" + }, "tags": [ - "Standalone APIs" + "Case Blueprints" ], - "security": [ + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, { - "ApiKeyAuth": [] + "name": "blueprint_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "b1de0000-0000-4000-8000-000000000002" + }, + "description": "Blueprint UUID." } ], - "parameters": [], - "description": "Retrieve the **Lite** registry profile of one candidate returned by `POST /v3/kyb/search/` — company details, registered address, identifiers, status, activity and the officers, plus the raw registry payload. Shareholders and ultimate beneficial owners are sold by `POST /v3/kyb/shareholders/` and `POST /v3/kyb/ubo/`.\n\n**Pairing with search.** Pass the candidate's `kyb_response_id` exactly as returned by a search of the same application. The handle is ephemeral and scoped to that search — select promptly, and re-search for a fresh handle rather than storing it.\n\n**Billing.** Each select is billed at the country's Lite price (any search fee is separate); see [KYB registry pricing](/getting-started/kyb-registry-pricing). The balance is checked before any registry call (`403` when short) and the charge is recorded against the created business session.\n\n**Persistence is mandatory.** Every select creates a business session (its id is the `request_id`) so billing and retrieval can be tracked; it appears in the console. `save_api_request` is accepted for symmetry but must be `true` (the default) — sending `false` returns `400`.", "requestBody": { - "required": true, + "required": false, "content": { "application/json": { "schema": { "type": "object", - "required": [ - "kyb_response_id" - ], "properties": { - "kyb_response_id": { + "name": { "type": "string", - "maxLength": 255, - "description": "Candidate handle from `POST /v3/kyb/search/` (`kyb_registry.companies[].kyb_response_id`). Ephemeral — use it shortly after the search that produced it.", - "example": "69aeeb95febb0f1704042259" + "maxLength": 24, + "description": "Max 24 characters, e.g. \"AML investigation\"." }, - "vendor_data": { + "description": { "type": "string", - "description": "Your identifier for this company; echoed back and stored with the business session." + "nullable": true }, - "metadata": { - "type": "object", + "assignee_pool": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "minItems": 1, + "description": "List of User UUIDs. Empty arrays are rejected with \"Pick at least one assignee\"." + }, + "assignment_method": { + "type": "string", + "enum": [ + "MANUAL", + "AUTOMATIC" + ] + }, + "max_active_cases": { + "type": "integer", "nullable": true, - "description": "Free-form JSON stored with the business session and echoed back." + "minimum": 1, + "description": "Automatic-mode threshold: \"fewer than N cases of this blueprint assigned\"." }, - "save_api_request": { - "type": "boolean", - "default": true, - "description": "Must be `true` (the default). KYB registry selects are always persisted so billing and retrieval can be tracked; `false` returns `400`." + "escalation_enabled": { + "type": "boolean" + }, + "escalation_blueprint": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Target blueprint UUID. Not enforced at save time, but the escalate action fails with 400 until both escalation_enabled and escalation_blueprint are set." + }, + "transfer_enabled": { + "type": "boolean" + }, + "four_eyes_checker_blueprint": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Setting this enables 4-eyes review: resolutions on this blueprint stage on the checker blueprint's queue." + }, + "require_resolution_note": { + "type": "boolean" + }, + "content_config": { + "$ref": "#/components/schemas/CaseContentConfig" + }, + "is_active": { + "type": "boolean" + }, + "default_deadline_hours": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 8760, + "description": "Default deadline for newly opened cases. Existing case deadlines are unchanged." + }, + "expected_version": { + "type": "integer", + "minimum": 1, + "description": "The version opened by the editor. A stale version returns 409 without changing the review policy." } } } @@ -18267,2461 +38386,4206 @@ { "lang": "curl", "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/kyb/select/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"kyb_response_id\": \"69aeeb95febb0f1704042259\",\n \"vendor_data\": \"company-1234\"\n }'" - }, - { - "lang": "python", - "label": "Python", - "source": "import os, requests\n\nsearch = requests.post(\n \"https://verification.didit.me/v3/kyb/search/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"country_code\": \"GB\", \"name\": \"Tesco\"},\n timeout=60,\n).json()\nkyb_response_id = search[\"kyb_registry\"][\"companies\"][0][\"kyb_response_id\"]\n\nresp = requests.post(\n \"https://verification.didit.me/v3/kyb/select/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"kyb_response_id\": kyb_response_id, \"vendor_data\": \"company-1234\"},\n timeout=90, # waits for the registry to resolve the full profile\n)\nresp.raise_for_status()\ncompany = resp.json()[\"kyb_registry\"]\nprint(company[\"company_name\"], company[\"registry_status\"], company[\"status\"])" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const res = await fetch('https://verification.didit.me/v3/kyb/select/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n kyb_response_id: '69aeeb95febb0f1704042259',\n vendor_data: 'company-1234',\n }),\n});\nconst data = await res.json();\nconsole.log(data.kyb_registry.company_name, data.kyb_registry.registry_status);" + "source": "curl -X PATCH 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/blueprints/b1de0000-0000-4000-8000-000000000002/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"transfer_enabled\": true}'" } ], "responses": { "200": { - "description": "Company retrieved and billed. `kyb_registry` carries the full profile; check `data_resolved` — when `false` the registry was still processing and the stored record completes automatically once it resolves.", + "description": "Updated blueprint.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "request_id": { - "type": "string", - "format": "uuid", - "description": "Id of the business session created for this retrieval. The stored record (visible in the console) keeps updating if the registry resolves after the response." - }, - "registry_tier_requested": { - "type": "string", - "enum": [ - "basic", - "shareholders", - "ubo" - ], - "description": "The tier this endpoint sells: `basic` (Lite) for select, `shareholders`, `ubo`." - }, - "registry_tier_delivered": { - "type": "string", - "enum": [ - "basic", - "shareholders", - "ubo" - ], - "description": "The tier actually delivered and billed. Lower than requested when the country's registries hold no ownership data, or the registry has no ownership record for this company." - }, - "kyb_registry": { - "type": "object", - "description": "Full registry company profile.", - "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Didit's id for this company record." - }, - "node_id": { - "type": "string", - "example": "feature_kyb_registry" - }, - "status": { - "type": "string", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review" - ], - "description": "Didit's assessment of the registry check (e.g. `In Review` when the company is not active in the registry or ownership data could not be derived)." - }, - "registry_status": { - "type": "string", - "nullable": true, - "enum": [ - "active", - "dissolved", - "deregistered", - "see full details", - "authorised", - "appointed representative", - "unauthorised", - "inactive", - "no longer authorised", - "closed", - "struck off" - ], - "description": "Normalized company status as reported by the registry." - }, - "data_resolved": { - "type": "boolean", - "description": "`true` once the registry returned the full profile. `false` when the registry was still processing after polling — the stored record completes automatically once the registry resolves." - }, - "company_name": { - "type": "string", - "example": "Tesco PLC" - }, - "registration_number": { - "type": "string", - "nullable": true, - "example": "00445790" - }, - "country_code": { - "type": "string", - "description": "ISO 3166-1 alpha-2.", - "example": "GB" - }, - "region": { - "type": "string", - "nullable": true, - "description": "Subdivision code when the registry is regional (e.g. `CA` for `US-CA`); empty otherwise." - }, - "company_type": { - "type": "string", - "nullable": true, - "example": "Public Limited Company" - }, - "incorporation_date": { - "type": "string", - "format": "date", - "nullable": true, - "example": "1947-11-27" - }, - "registered_address": { - "type": "string", - "nullable": true, - "example": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA" - }, - "tax_number": { - "type": "string", - "nullable": true, - "example": "GB412 8925 93" - }, - "risk_level": { - "type": "string", - "nullable": true, - "description": "Registry-reported risk level, when available." - }, - "verification_status": { - "type": "string", - "nullable": true, - "description": "Registry verification status, when available." - }, - "is_from_registry": { - "type": "boolean", - "description": "Always `true` for this endpoint — the profile was retrieved from an official registry." - }, - "fetch_status": { - "type": "string", - "enum": [ - "pending", - "resolved" - ], - "description": "Registry fetch lifecycle. Mirrored by `data_resolved`." - }, - "alternative_names": { - "type": "string", - "nullable": true, - "description": "Alternative, trading, former or original-language company names as text, when reported." - }, - "nature_of_business": { - "type": "string", - "nullable": true, - "description": "Activity description or classification code, when reported." - }, - "registered_capital": { - "type": "string", - "nullable": true - }, - "registered_capital_amount": { - "type": "string", - "nullable": true, - "description": "Decimal amount as a string, when reported." - }, - "registered_capital_currency": { - "type": "string", - "nullable": true, - "description": "ISO 4217 code, when reported." - }, - "website": { - "type": "string", - "nullable": true, - "example": "https://www.tesco.com/" - }, - "email": { - "type": "string", - "nullable": true, - "example": "customer.service@tesco.co.uk" - }, - "phone": { - "type": "string", - "nullable": true, - "example": "03301231688" - }, - "legal_entity_identifier": { - "type": "string", - "nullable": true, - "description": "LEI, when reported." - }, - "location_of_registration": { - "type": "string", - "nullable": true, - "description": "Registry/jurisdiction the company is registered with, when reported." - }, - "vat_number": { - "type": "string", - "nullable": true, - "description": "EU VAT number, when collected during the hosted registry step or edited afterwards. At select time this is typically not yet set — VAT and tax ID validation runs when the end user submits the registry step in the hosted flow." - }, - "vat_validation_status": { - "type": "string", - "enum": [ - "valid", - "invalid", - "format_valid", - "invalid_format", - "could_not_validate", - "not_applicable" - ], - "description": "Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session." - }, - "vat_validated_number": { - "type": "string", - "nullable": true, - "description": "The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran." - }, - "vat_validation_source": { - "type": "string", - "nullable": true, - "enum": [ - "eu_vies", - "pl_vat_white_list", - "no_brreg", - "ch_uid_register", - "br_receita_federal", - "co_rues", - "cr_hacienda", - "ec_sri", - "py_dnit", - "format_rules", - null - ], - "description": "The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran." - }, - "vat_validated_name": { - "type": "string", - "nullable": true, - "description": "Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." - }, - "vat_validated_address": { - "type": "string", - "nullable": true, - "description": "Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." - }, - "vat_checked_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "Timestamp of the check. `null` when no check ran." - }, - "financial_summary": { - "type": "object", - "nullable": true, - "description": "Registry-reported share/capital details (currency, total shares, share classes), when available." - }, - "officers": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string", - "example": "MELISSA BETHELL" - }, - "designation": { - "type": "string", - "nullable": true, - "description": "Raw registry designation (e.g. `Director`, `Secretary`)." - }, - "role": { - "type": "string", - "description": "Normalized role derived from the designation. Full 17-value model set; registry parsing on this endpoint typically emits director, non_executive_director, secretary, chairman, or company_officer.", - "enum": [ - "director", - "non_executive_director", - "secretary", - "chairman", - "ubo", - "shareholder", - "representative", - "founder", - "legal_advisor", - "authorized_signatory", - "trustee", - "beneficiary", - "company_officer", - "settlor", - "protector", - "investor", - "other" - ] - }, - "nationality": { - "type": "string", - "nullable": true - }, - "is_active": { - "type": "boolean", - "description": "`false` when the registry marks the appointment as resigned." - }, - "kyc_status": { - "type": "string", - "nullable": true, - "enum": [ - "Approved", - "Declined", - "Pending" - ], - "description": "Status of an associated KYC session, when one was started for this officer. `null` otherwise." - }, - "kyc_session_url": { - "type": "string", - "nullable": true, - "description": "Verification URL of the associated KYC session, when one exists." - }, - "first_name": { - "type": "string", - "nullable": true, - "description": "Given name when explicitly reported by the registry; not inferred from the full name." - }, - "last_name": { - "type": "string", - "nullable": true, - "description": "Family name when explicitly reported by the registry." - }, - "address": { - "type": "string", - "nullable": true, - "description": "Officer address reported by the registry." - }, - "appointment_date": { - "type": "string", - "nullable": true, - "description": "Date the officer was appointed.", - "format": "date" - }, - "termination_date": { - "type": "string", - "nullable": true, - "description": "Date the appointment ended, when reported.", - "format": "date" - }, - "date_of_birth": { - "type": "string", - "nullable": true, - "description": "Complete registry-reported birth date. Null when only a birth year or month is known.", - "format": "date" - }, - "date_of_birth_year": { - "type": "integer", - "nullable": true, - "minimum": 1, - "maximum": 9999, - "description": "Registry-reported birth year, including when the full date is unavailable." - }, - "date_of_birth_month": { - "type": "integer", - "nullable": true, - "minimum": 1, - "maximum": 12, - "description": "Registry-reported birth month. Null when only the year is known." - } - } + "$ref": "#/components/schemas/CaseBlueprintDetail" + } + } + } + }, + "409": { + "description": "The review policy changed since expected_version. Review the latest version before retrying." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "delete": { + "summary": "Delete blueprint", + "description": "Delete a blueprint. Deletion always succeeds and does not touch the blueprint's cases: existing cases keep their blueprint reference and its settings continue to apply to them. A deleted blueprint no longer appears in blueprint lists and can no longer be chosen as a transfer target; if a maker blueprint is deleted while a resolution is pending 4-eyes approval, a rejected resolution leaves the case on the checker blueprint.", + "operationId": "delete_case_blueprint", + "x-didit-sdk": { + "resource": "cases.blueprints", + "method": "delete", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Case Blueprints" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "blueprint_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "b1de0000-0000-4000-8000-000000000002" + }, + "description": "Blueprint UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X DELETE 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/blueprints/b1de0000-0000-4000-8000-000000000002/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "204": { + "description": "Blueprint deleted." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/": { + "get": { + "summary": "List blueprint presets", + "description": "List the four built-in presets (Screening hit, AML investigation, Suspicious activity, KYC review) with their content_config and whether each is already installed on this application.", + "operationId": "list_case_blueprint_presets", + "x-didit-sdk": { + "resource": "cases.blueprints", + "method": "list_presets", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Case Blueprints" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/blueprints/presets/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Preset catalogue.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "preset_key": { + "type": "string", + "enum": [ + "screening_hit", + "aml_investigation", + "suspicious_activity", + "kyc" + ] }, - "description": "Directors, secretaries, and other officers extracted from the registry." - }, - "beneficial_owners": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string" - }, - "first_name": { - "type": "string", - "nullable": true - }, - "last_name": { - "type": "string", - "nullable": true - }, - "entity_type": { - "type": "string", - "enum": [ - "person", - "company" - ], - "description": "Whether the owner is a natural person or a corporate entity." - }, - "roles": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Normalized roles (e.g. `ubo`, `shareholder`)." - }, - "ownership_min_shares": { - "type": "string", - "nullable": true, - "description": "Lower bound of the ownership band reported by the registry (e.g. `25%`)." - }, - "ownership_max_shares": { - "type": "string", - "nullable": true, - "description": "Upper bound of the ownership band reported by the registry." - }, - "is_active": { - "type": "boolean" - }, - "kyc_status": { - "type": "string", - "nullable": true, - "enum": [ - "Approved", - "Declined", - "Pending" - ] - }, - "kyc_session_url": { - "type": "string", - "nullable": true - }, - "effective_ownership_percent": { - "type": "number", - "nullable": true, - "description": "Parsed ownership percentage (max bound, falling back to min bound)." - }, - "company_name": { - "type": "string", - "nullable": true - }, - "registration_number": { - "type": "string", - "nullable": true - }, - "country_of_birth": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string", - "nullable": true - }, - "nationality": { - "type": "string", - "nullable": true - }, - "designation": { - "type": "string", - "nullable": true - }, - "voting_min_shares": { - "type": "string", - "nullable": true - }, - "voting_max_shares": { - "type": "string", - "nullable": true - }, - "date_of_birth": { - "type": "string", - "nullable": true, - "description": "Complete birth date when reported.", - "format": "date" - }, - "appointment_date": { - "type": "string", - "nullable": true, - "description": "Appointment date when reported.", - "format": "date" - }, - "shareholding": { - "$ref": "#/components/schemas/KYBRegistryShareholding" - } - } + "name": { + "type": "string" }, - "description": "Beneficial owners / persons with significant control extracted from the registry. Can be empty when the registry does not expose ownership." - }, - "addresses": { - "type": "array", - "nullable": true, - "description": "Every address the registry states, one canonical shape. Null when it states none.", - "items": { - "$ref": "#/components/schemas/KYBRegistryAddress" - } - }, - "industries": { - "type": "array", - "nullable": true, - "description": "The activities the registry classifies the company under, one canonical shape. Null when it states none.", - "items": { - "$ref": "#/components/schemas/KYBRegistryIndustry" + "description": { + "type": "string" + }, + "content_config": { + "$ref": "#/components/schemas/CaseContentConfig" + }, + "installed": { + "type": "boolean" } - }, - "accounts": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts." - }, - "registry_data": { - "type": "object", - "description": "Raw record returned by the registry source, kept for reviewers. Its shape differs by country and source and is not a stable contract: read the canonical fields (`addresses`, `industries`, `ownership`, `beneficial_owners[].shareholding`) instead." - }, - "user_provided_data": { - "type": "object", - "nullable": true, - "description": "Always `null` on this endpoint (used by the hosted KYB flow when end users edit registry data)." - }, - "confirmed_by_user_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "Always `null` on this endpoint." - }, - "last_console_edit_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "Set when a console reviewer edits the record later." - }, - "is_editable": { - "type": "boolean", - "description": "Whether the registry-sourced data can still be edited in the hosted flow/console." - }, - "registry_tier_delivered": { - "type": "string", - "enum": [ - "basic", - "shareholders", - "ubo" - ], - "description": "Same as the top-level `registry_tier_delivered`." - }, - "registry_ownership_status": { - "type": "string", - "nullable": true, - "enum": [ - "not_requested", - "delivered", - "unavailable" - ], - "description": "`delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `not_requested` on the Lite tier." - }, - "ownership": { - "$ref": "#/components/schemas/KYBRegistryOwnership" - }, - "registry_tier": { - "type": "string", - "nullable": true, - "description": "The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`." } } - }, - "vendor_data": { + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/install/": { + "post": { + "summary": "Install blueprint presets", + "description": "Copy one or more presets into the application as editable blueprints. Idempotent per preset_key; omit preset_keys to install every preset not yet installed.", + "operationId": "install_case_blueprint_presets", + "x-didit-sdk": { + "resource": "cases.blueprints", + "method": "install_presets", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Case Blueprints" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "preset_keys": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "description": "Subset of screening_hit, aml_investigation, suspicious_activity, kyc. Omit to install all." + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/blueprints/presets/install/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"preset_keys\": [\"aml_investigation\", \"kyc\"]}'" + } + ], + "responses": { + "201": { + "description": "One or more presets installed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseBlueprintPresetInstallResult" + } + } + } + }, + "200": { + "description": "No new presets to install (all requested presets already exist).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseBlueprintPresetInstallResult" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/report-templates/": { + "get": { + "summary": "List report templates", + "description": "List FIU report templates for the application.", + "operationId": "list_report_templates", + "x-didit-sdk": { + "resource": "report_templates", + "method": "list", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Report Templates" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50, + "minimum": 1 + }, + "description": "Page size." + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + }, + "description": "Zero-based offset." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/report-templates/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Paginated templates.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "description": "Number of matching rows, capped at 100 for performance." + }, + "next": { + "type": "string", + "nullable": true + }, + "previous": { + "type": "string", + "nullable": true + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReportTemplateDetail" + } + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "post": { + "summary": "Create report template", + "description": "Create a reusable per-jurisdiction, per-report-type goAML header template. Rejected with 400 if any of the template-level obligatory fields (rentity_id, submission_code, currency_code_local) is missing from field_config; an invalid template cannot be saved or used to create a report. Per-filing header fields (action, submission_date, transactions) are enforced at report validate/finalize time instead.", + "operationId": "create_report_template", + "x-didit-sdk": { + "resource": "report_templates", + "method": "create", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Report Templates" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name", + "jurisdiction", + "report_type" + ], + "properties": { + "name": { + "type": "string" + }, + "jurisdiction": { + "type": "string", + "minLength": 2, + "maxLength": 2, + "description": "ISO 3166-1 alpha-2 country code, upper-cased on save." + }, + "report_type": { + "type": "string", + "enum": [ + "SAR", + "STR", + "CTR", + "TTR", + "UTR", + "IFT", + "CBR", + "TFR" + ] + }, + "field_config": { + "type": "object", + "description": "Reusable goAML header defaults. Template-level obligatory fields: rentity_id, submission_code, and currency_code_local (3-letter). Optional: enabled_fields (which optional report blocks to include) plus reporting_person, location, reason, and action defaults. Per-filing header fields (action, submission_date, transactions, jurisdiction) are enforced when the report is validated or finalized, not on the template." + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/report-templates/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"name\": \"US SAR default\", \"jurisdiction\": \"US\", \"report_type\": \"SAR\", \"field_config\": {\"rentity_id\": \"FIU-US-00123\", \"submission_code\": \"E\", \"currency_code_local\": \"USD\"}}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/report-templates/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={\n 'name': 'US SAR default',\n 'jurisdiction': 'US',\n 'report_type': 'SAR',\n 'field_config': {'rentity_id': 'FIU-US-00123', 'submission_code': 'E', 'currency_code_local': 'USD'},\n },\n timeout=10,\n)\nresp.raise_for_status()\nprint(resp.json()['uuid'])" + } + ], + "responses": { + "201": { + "description": "Template created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReportTemplateDetail" + } + } + } + }, + "400": { + "description": "Unrecognized jurisdiction, or field_config fails obligatory-field validation.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "errors": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/": { + "get": { + "summary": "Get report template", + "description": "Retrieve a report template.", + "operationId": "get_report_template", + "x-didit-sdk": { + "resource": "report_templates", + "method": "retrieve", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Report Templates" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "template_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "7ec00000-0000-4000-8000-000000000006" + }, + "description": "Report template UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/report-templates/7ec00000-0000-4000-8000-000000000006/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Template detail.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReportTemplateDetail" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "patch": { + "summary": "Update report template", + "description": "Partially update a report template. Re-validates obligatory goAML fields on save.", + "operationId": "update_report_template", + "x-didit-sdk": { + "resource": "report_templates", + "method": "update", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Report Templates" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "template_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "7ec00000-0000-4000-8000-000000000006" + }, + "description": "Report template UUID." + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "jurisdiction": { + "type": "string", + "minLength": 2, + "maxLength": 2, + "description": "ISO 3166-1 alpha-2 country code, upper-cased on save." + }, + "report_type": { + "type": "string", + "enum": [ + "SAR", + "STR", + "CTR", + "TTR", + "UTR", + "IFT", + "CBR", + "TFR" + ] + }, + "field_config": { + "type": "object", + "description": "Reusable goAML header defaults. Template-level obligatory fields: rentity_id, submission_code, and currency_code_local (3-letter). Optional: enabled_fields (which optional report blocks to include) plus reporting_person, location, reason, and action defaults. Per-filing header fields (action, submission_date, transactions, jurisdiction) are enforced when the report is validated or finalized, not on the template." + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/report-templates/7ec00000-0000-4000-8000-000000000006/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"field_config\": {\"rentity_id\": \"FIU-US-00124\"}}'" + } + ], + "responses": { + "200": { + "description": "Updated template.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReportTemplateDetail" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "delete": { + "summary": "Delete report template", + "description": "Delete a report template. Reports already created from it are unaffected.", + "operationId": "delete_report_template", + "x-didit-sdk": { + "resource": "report_templates", + "method": "delete", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Report Templates" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "template_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "7ec00000-0000-4000-8000-000000000006" + }, + "description": "Report template UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X DELETE 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/report-templates/7ec00000-0000-4000-8000-000000000006/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "204": { + "description": "Template deleted." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/regulatory-reports/": { + "get": { + "summary": "List reports for case", + "description": "List FIU reports generated for the case.", + "operationId": "list_case_regulatory_reports", + "x-didit-sdk": { + "resource": "cases.regulatory_reports", + "method": "list", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Regulatory Reports" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50, + "minimum": 1 + }, + "description": "Page size." + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + }, + "description": "Zero-based offset." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/regulatory-reports/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Paginated reports for the case.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegulatoryReportList" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "post": { + "summary": "Create report from case", + "description": "Create a draft FIU report for the case: DRAFT status, auto-populated from the optional template plus the case's subject, linked transactions, and narrative. Validate then finalize to produce the goAML XML and PDF.", + "operationId": "create_case_regulatory_report", + "x-didit-sdk": { + "resource": "cases.regulatory_reports", + "method": "create", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Regulatory Reports" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "case_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "c1a5e000-0000-4000-8000-000000000001" + }, + "description": "Case UUID." + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "report_type": { + "type": "string", + "enum": [ + "SAR", + "STR", + "CTR", + "TTR", + "UTR", + "IFT", + "CBR", + "TFR" + ], + "description": "Required unless a template is given; the template's report_type is used otherwise." + }, + "jurisdiction": { + "type": "string", + "minLength": 2, + "maxLength": 2, + "description": "Required unless a template is given." + }, + "template": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Report template UUID; its field_config seeds the draft before case-derived data is merged in." + }, + "transactions": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "nullable": true, + "description": "Transactions to attach; defaults to the case's linked transactions." + }, + "rentity_id": { + "type": "string", + "nullable": true + }, + "rentity_branch": { + "type": "string", + "nullable": true + }, + "submission_code": { + "type": "string", + "enum": [ + "E", + "M" + ], + "default": "E" + }, + "entity_reference": { + "type": "string", + "nullable": true + }, + "fiu_ref_number": { + "type": "string", + "nullable": true + }, + "currency_code_local": { + "type": "string", + "minLength": 3, + "maxLength": 3, + "nullable": true + }, + "submission_date": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "reporting_person": { + "type": "object" + }, + "location": { + "type": "object" + }, + "reason": { + "type": "string", + "nullable": true + }, + "action": { + "type": "string", + "nullable": true + }, + "activity": { + "type": "string", + "nullable": true + }, + "narrative": { + "type": "object", + "properties": { + "who": { + "type": "string" + }, + "what": { + "type": "string" + }, + "when": { + "type": "string" + }, + "where": { + "type": "string" + }, + "why": { + "type": "string" + }, + "how": { + "type": "string" + } + } + }, + "report_indicators": { + "type": "array", + "items": { + "type": "string" + } + }, + "included_fields": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/regulatory-reports/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"template\": \"7ec00000-0000-4000-8000-000000000006\"}'" + } + ], + "responses": { + "201": { + "description": "Draft report created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegulatoryReportDetail" + } + } + } + }, + "400": { + "description": "The referenced template was not found.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/": { + "get": { + "summary": "List reports", + "description": "Application-wide FIU report list, optionally filtered by status, report_type, or case.", + "operationId": "list_regulatory_reports", + "x-didit-sdk": { + "resource": "regulatory_reports", + "method": "list", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Regulatory Reports" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 50, + "minimum": 1 + }, + "description": "Page size." + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + }, + "description": "Zero-based offset." + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "DRAFT", + "VALIDATED", + "FINALIZED" + ] + }, + "description": "Filter by status." + }, + { + "name": "report_type", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "SAR", + "STR", + "CTR", + "TTR", + "UTR", + "IFT", + "CBR", + "TFR" + ] + }, + "description": "Filter by report type." + }, + { + "name": "case", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Filter to reports on this case." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Paginated reports across the application.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegulatoryReportList" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "post": { + "summary": "Create standalone report", + "description": "Create a draft FIU report from the standalone Reports page: both case and template are required (unlike the case-scoped create, which can omit template).", + "operationId": "create_standalone_regulatory_report", + "x-didit-sdk": { + "resource": "regulatory_reports", + "method": "create", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Regulatory Reports" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "case", + "template" + ], + "properties": { + "case": { + "type": "string", + "format": "uuid" + }, + "template": { + "type": "string", + "format": "uuid" + }, + "report_type": { + "type": "string", + "enum": [ + "SAR", + "STR", + "CTR", + "TTR", + "UTR", + "IFT", + "CBR", + "TFR" + ], + "description": "Required unless a template is given; the template's report_type is used otherwise." + }, + "jurisdiction": { + "type": "string", + "minLength": 2, + "maxLength": 2, + "description": "Required unless a template is given." + }, + "transactions": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "nullable": true, + "description": "Transactions to attach; defaults to the case's linked transactions." + }, + "rentity_id": { + "type": "string", + "nullable": true + }, + "rentity_branch": { + "type": "string", + "nullable": true + }, + "submission_code": { + "type": "string", + "enum": [ + "E", + "M" + ], + "default": "E" + }, + "entity_reference": { + "type": "string", + "nullable": true + }, + "fiu_ref_number": { + "type": "string", + "nullable": true + }, + "currency_code_local": { + "type": "string", + "minLength": 3, + "maxLength": 3, + "nullable": true + }, + "submission_date": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "reporting_person": { + "type": "object" + }, + "location": { + "type": "object" + }, + "reason": { + "type": "string", + "nullable": true + }, + "action": { + "type": "string", + "nullable": true + }, + "activity": { + "type": "string", + "nullable": true + }, + "narrative": { + "type": "object", + "properties": { + "who": { + "type": "string" + }, + "what": { + "type": "string" + }, + "when": { + "type": "string" + }, + "where": { + "type": "string" + }, + "why": { + "type": "string" + }, + "how": { + "type": "string" + } + } + }, + "report_indicators": { + "type": "array", + "items": { + "type": "string" + } + }, + "included_fields": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"case\": \"c1a5e000-0000-4000-8000-000000000001\", \"template\": \"7ec00000-0000-4000-8000-000000000006\"}'" + } + ], + "responses": { + "201": { + "description": "Draft report created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegulatoryReportDetail" + } + } + } + }, + "400": { + "description": "The referenced template was not found.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/": { + "get": { + "summary": "Get report", + "description": "Retrieve a regulatory report.", + "operationId": "get_regulatory_report", + "x-didit-sdk": { + "resource": "regulatory_reports", + "method": "retrieve", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Regulatory Reports" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "report_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "5ec00000-0000-4000-8000-000000000007" + }, + "description": "Report UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Report detail.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegulatoryReportDetail" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "patch": { + "summary": "Update report", + "description": "Edit a draft or validated report's fields. Finalized reports cannot be edited (409).", + "operationId": "update_regulatory_report", + "x-didit-sdk": { + "resource": "regulatory_reports", + "method": "update", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Regulatory Reports" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "report_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "5ec00000-0000-4000-8000-000000000007" + }, + "description": "Report UUID." + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "jurisdiction": { + "type": "string", + "minLength": 2, + "maxLength": 2 + }, + "rentity_id": { + "type": "string", + "nullable": true + }, + "rentity_branch": { + "type": "string", + "nullable": true + }, + "submission_code": { + "type": "string", + "enum": [ + "E", + "M" + ], + "default": "E" + }, + "entity_reference": { + "type": "string", + "nullable": true + }, + "fiu_ref_number": { + "type": "string", + "nullable": true + }, + "currency_code_local": { + "type": "string", + "minLength": 3, + "maxLength": 3, + "nullable": true + }, + "submission_date": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "reporting_person": { + "type": "object" + }, + "location": { + "type": "object" + }, + "reason": { + "type": "string", + "nullable": true + }, + "action": { + "type": "string", + "nullable": true + }, + "activity": { + "type": "string", + "nullable": true + }, + "narrative": { + "type": "object", + "properties": { + "who": { + "type": "string" + }, + "what": { + "type": "string" + }, + "when": { + "type": "string" + }, + "where": { + "type": "string" + }, + "why": { + "type": "string" + }, + "how": { + "type": "string" + } + } + }, + "report_indicators": { + "type": "array", + "items": { + "type": "string" + } + }, + "included_fields": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"rentity_id\": \"FIU-US-00123\", \"currency_code_local\": \"USD\"}'" + } + ], + "responses": { + "200": { + "description": "Updated report.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegulatoryReportDetail" + } + } + } + }, + "409": { + "description": "Report is already finalized.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "delete": { + "summary": "Delete report", + "description": "Delete a draft or validated report. Finalized reports cannot be deleted (409).", + "operationId": "delete_regulatory_report", + "x-didit-sdk": { + "resource": "regulatory_reports", + "method": "delete", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Regulatory Reports" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "report_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "5ec00000-0000-4000-8000-000000000007" + }, + "description": "Report UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X DELETE 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "204": { + "description": "Report deleted." + }, + "409": { + "description": "Report is already finalized.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/validate/": { + "post": { + "summary": "Validate report", + "description": "Check the report against the obligatory goAML header fields (reporting entity ID, submission code, local currency, action taken, submission date), plus at least one attached transaction and a 2-letter jurisdiction, without generating any files. Also stores the result on the report: validation_errors and validated_at are updated, and a non-finalized report's status moves to VALIDATED (no errors) or back to DRAFT (errors).", + "operationId": "validate_regulatory_report", + "x-didit-sdk": { + "resource": "regulatory_reports", + "method": "validate", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Regulatory Reports" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "report_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "5ec00000-0000-4000-8000-000000000007" + }, + "description": "Report UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/validate/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Validation result.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { "type": "string", - "nullable": true, - "description": "Echo of the `vendor_data` you sent." + "enum": [ + "DRAFT", + "VALIDATED", + "FINALIZED" + ] }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Echo of the `metadata` you sent." + "is_valid": { + "type": "boolean" }, - "created_at": { - "type": "string", - "format": "date-time" - } - } - }, - "examples": { - "Resolved company": { - "summary": "Full registry profile (officer list truncated for brevity)", - "value": { - "request_id": "9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e", - "kyb_registry": { - "uuid": "f1e2d3c4-b5a6-4789-9c0d-1e2f3a4b5c6d", - "node_id": "feature_kyb_registry", - "status": "In Review", - "registry_status": "active", - "data_resolved": true, - "company_name": "Tesco PLC", - "registration_number": "00445790", - "country_code": "GB", - "region": "", - "company_type": "Public Limited Company", - "incorporation_date": "1947-11-27", - "registered_address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", - "tax_number": "GB412 8925 93", - "risk_level": "low risk", - "verification_status": "verified", - "is_from_registry": true, - "fetch_status": "resolved", - "alternative_names": "TESCO STORES (HOLDINGS) PUBLIC LIMITED COMPANY", - "nature_of_business": null, - "registered_capital": null, - "registered_capital_amount": null, - "registered_capital_currency": null, - "website": "https://www.tesco.com/", - "email": "customer.service@tesco.co.uk", - "phone": "03301231688", - "legal_entity_identifier": null, - "location_of_registration": null, - "financial_summary": { - "currency": "Pound (£)", - "total_number_of_shares": "8174378721", - "meta_detail": { - "Class_Of_Share": "Ordinary" - } - }, - "officers": [ - { - "uuid": "a1b2c3d4-e5f6-4789-9a0b-1c2d3e4f5a6b", - "name": "CHRISTOPHER JON TAYLOR", - "designation": "Secretary", - "role": "secretary", - "nationality": null, - "is_active": true, - "kyc_status": null, - "kyc_session_url": null + "errors": { + "type": "array", + "description": "Empty when valid.", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string" }, - { - "uuid": "b2c3d4e5-f6a7-4890-8b1c-2d3e4f5a6b7c", - "name": "MELISSA BETHELL", - "designation": "Director", - "role": "director", - "nationality": "British", - "is_active": true, - "kyc_status": null, - "kyc_session_url": null - } - ], - "beneficial_owners": [], - "addresses": [ - { - "address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", - "type": "legal_entity_registry_address", - "description": "registered office address" + "message": { + "type": "string" } - ], - "industries": null, - "accounts": { - "last_account": "26 February 2025", - "next_account": "26 February 2026", - "due_by_date": "26 August 2026" - }, - "registry_data": { - "name": "Tesco PLC", - "registration_number": "00445790", - "status": "active", - "type": "Public Limited Company", - "incorporation_date": "1947-11-27", - "tax_number": "GB412 8925 93", - "country_code": "GB", - "fetch_status": "resolved", - "industries_detail": [ - { - "code": "47110", - "name": "Retail Sale In Non-specialised Stores With Food, Beverages Or Tobacco Predominating", - "description": null, - "meta_detail": [] - } - ] - }, - "user_provided_data": null, - "confirmed_by_user_at": null, - "last_console_edit_at": null, - "is_editable": true - }, - "vendor_data": "company-1234", - "metadata": null, - "created_at": "2026-06-11T10:45:00.000000+00:00" + } + } + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/finalize/": { + "post": { + "summary": "Finalize report", + "description": "Re-validate, then render the goAML XML and PDF and upload both to S3. On SAR/STR/TFR reports, this also moves the case's transaction alerts from Pending SAR to SAR filed.", + "operationId": "finalize_regulatory_report", + "x-didit-sdk": { + "resource": "regulatory_reports", + "method": "finalize", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Regulatory Reports" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "report_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "5ec00000-0000-4000-8000-000000000007" + }, + "description": "Report UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/finalize/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/finalize/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=30,\n)\nif resp.status_code == 400:\n print('Validation failed:', resp.json()['errors'])\nelse:\n resp.raise_for_status()\n report = resp.json()\n print(report['status'], report['report_number'])" + } + ], + "responses": { + "200": { + "description": "Report finalized; xml_s3_key and pdf_s3_key are set.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegulatoryReportDetail" + } + } + } + }, + "400": { + "description": "Report failed validation.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } } - }, - "Pending (registry still resolving)": { - "summary": "Select stayed pending after polling — sparse fields, auto-completes server-side", - "value": { - "request_id": "9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e", - "kyb_registry": { - "uuid": "7c0a4f7e-2b1d-4f6a-9c3e-8d5b2a1f0e9d", - "node_id": "feature_kyb_registry", - "status": "Not Finished", - "registry_status": null, - "data_resolved": false, - "company_name": "Tesco PLC", - "registration_number": "00445790", - "country_code": "GB", - "region": null, - "company_type": null, - "incorporation_date": null, - "registered_address": null, - "tax_number": null, - "risk_level": null, - "verification_status": null, - "is_from_registry": true, - "fetch_status": "pending", - "alternative_names": null, - "nature_of_business": null, - "registered_capital": null, - "registered_capital_amount": null, - "registered_capital_currency": null, - "website": null, - "email": null, - "phone": null, - "legal_entity_identifier": null, - "location_of_registration": null, - "financial_summary": {}, - "officers": [], - "beneficial_owners": [], - "addresses": null, - "industries": null, - "accounts": null, - "registry_data": {}, - "user_provided_data": null, - "confirmed_by_user_at": null, - "last_console_edit_at": null, - "is_editable": true - }, - "vendor_data": "company-1234", - "metadata": null, - "created_at": "2026-06-11T10:45:00.000000+00:00" + } + } + } + } + }, + "409": { + "description": "Report is not in a finalizable state.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" } } } } } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/download-url/": { + "get": { + "summary": "Get report download URL", + "description": "Mint a presigned URL for a finalized report's goAML XML or PDF.", + "operationId": "get_regulatory_report_download_url", + "x-didit-sdk": { + "resource": "regulatory_reports", + "method": "retrieve_download_url", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Regulatory Reports" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, - "400": { - "description": "Validation error. Field-level problems use DRF's standard envelope.", + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "report_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "5ec00000-0000-4000-8000-000000000007" + }, + "description": "Report UUID." + }, + { + "name": "file", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "pdf", + "xml" + ], + "default": "pdf" + }, + "description": "Which file to download." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/download-url/?file=xml' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Presigned download URL.", "content": { "application/json": { - "examples": { - "Missing kyb_response_id": { - "summary": "`kyb_response_id` is required", - "value": { - "kyb_response_id": [ - "This field is required." + "schema": { + "type": "object", + "properties": { + "download_url": { + "type": "string", + "format": "uri" + }, + "file": { + "type": "string", + "enum": [ + "pdf", + "xml" ] } - }, - "save_api_request=false rejected": { - "summary": "Selects are always persisted", - "value": { - "save_api_request": [ - "KYB registry select must be saved so billing and retrieval can be tracked." - ] + } + } + } + } + }, + "409": { + "description": "Report is not finalized yet.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "status": { + "type": "string" } } } } } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/transactions/rules/": { + "get": { + "summary": "List rules", + "description": "List the application's transaction-monitoring rules: your custom rules plus any library presets you have installed. Filter by source, category, mode, bundle or free-text search; order with `ordering` (run_count, approved_pct, reviewed_pct, declined_pct, latest_triggered_at, title, created_at, source, each with an optional `-` prefix).", + "operationId": "listTransactionRules", + "x-didit-sdk": { + "resource": "transactions.rules", + "method": "list", + "auth": "api_key", + "server": true + }, + "tags": [ + "Transactions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "source", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "PRESET", + "CUSTOM" + ] + }, + "description": "PRESET or CUSTOM" }, - "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any screening or provider call happens.", + { + "name": "category", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Filter by rule category" + }, + { + "name": "mode", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "ACTIVE", + "DISABLED", + "TEST" + ] + }, + "description": "ACTIVE, DISABLED or TEST" + }, + { + "name": "bundle", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Filter presets by bundle" + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Case-insensitive match against title and description" + }, + { + "name": "ordering", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Comma-separated ordering fields, default source,-run_count" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer" + }, + "description": "Page size" + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer" + }, + "description": "Page offset" + } + ], + "responses": { + "200": { + "description": "Paginated rules.", "content": { "application/json": { - "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", - "value": { - "detail": "You do not have permission to perform this action." + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "next": { + "type": "string", + "nullable": true + }, + "previous": { + "type": "string", + "nullable": true + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionRuleDetail" + } + } + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/transactions/rules/?source=CUSTOM&ordering=-run_count' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ] + }, + "post": { + "summary": "Create rule", + "description": "Create a CUSTOM transaction-monitoring rule. Titles are unique per application (duplicate -> 400). A `change_status` action targeting `AWAITING_USER` must carry `workflow_id`. Rules created in ACTIVE mode apply to new transactions immediately - the recommended workflow is create in TEST mode, backtest, then switch mode to ACTIVE.", + "operationId": "createTransactionRule", + "x-didit-sdk": { + "resource": "transactions.rules", + "method": "create", + "auth": "api_key", + "server": true + }, + "tags": [ + "Transactions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionRuleCreateRequest" + }, + "example": { + "title": "Structuring - inbound (custom)", + "description": "20+ inbound EUR transfers, all under 10,000 EUR, 30 days", + "category": "finance", + "mode": "TEST", + "evaluation_mode": "ALL", + "scope": { + "transaction_types": [ + "finance" + ], + "directions": [ + "INBOUND" + ] + }, + "conditions": [ + { + "field": "currency", + "operator": "eq", + "value": "EUR" + }, + { + "field": "amount", + "operator": "lt", + "value": 10000 + } + ], + "aggregation": [ + { + "metric": "count", + "operator": "gte", + "value": 20, + "window": "30d", + "filters": { + "direction": "INBOUND", + "transaction_type": "finance", + "subject_vendor_data": "__current__", + "currency": "EUR" } }, - "Not enough credits": { - "summary": "Organization balance cannot cover the call", - "value": { - "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + { + "metric": "max", + "field": "amount", + "operator": "lt", + "value": 10000, + "window": "30d", + "filters": { + "direction": "INBOUND", + "transaction_type": "finance", + "subject_vendor_data": "__current__", + "currency": "EUR" } } + ], + "actions": [ + { + "type": "add_score", + "value": 35 + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "The created rule.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionRuleDetail" + } + } + } + }, + "400": { + "description": "Validation error - duplicate title, unknown operator/metric, malformed window, invalid scope direction, or AWAITING_USER without workflow_id.", + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "title": [ + "A rule with this name already exists." + ] + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/transactions/rules/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"title\": \"Structuring - inbound (custom)\", \"description\": \"20+ inbound EUR transfers, all under 10,000 EUR, 30 days\", \"category\": \"finance\", \"mode\": \"TEST\", \"evaluation_mode\": \"ALL\", \"scope\": {\"transaction_types\": [\"finance\"], \"directions\": [\"INBOUND\"]}, \"conditions\": [{\"field\": \"currency\", \"operator\": \"eq\", \"value\": \"EUR\"}, {\"field\": \"amount\", \"operator\": \"lt\", \"value\": 10000}], \"aggregation\": [{\"metric\": \"count\", \"operator\": \"gte\", \"value\": 20, \"window\": \"30d\", \"filters\": {\"direction\": \"INBOUND\", \"transaction_type\": \"finance\", \"subject_vendor_data\": \"__current__\", \"currency\": \"EUR\"}}, {\"metric\": \"max\", \"field\": \"amount\", \"operator\": \"lt\", \"value\": 10000, \"window\": \"30d\", \"filters\": {\"direction\": \"INBOUND\", \"transaction_type\": \"finance\", \"subject_vendor_data\": \"__current__\", \"currency\": \"EUR\"}}], \"actions\": [{\"type\": \"add_score\", \"value\": 35}]}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nrule = {\n \"title\": \"Structuring - inbound (custom)\",\n \"description\": \"20+ inbound EUR transfers, all under 10,000 EUR, 30 days\",\n \"category\": \"finance\",\n \"mode\": \"TEST\",\n \"evaluation_mode\": \"ALL\",\n \"scope\": {\n \"transaction_types\": [\n \"finance\"\n ],\n \"directions\": [\n \"INBOUND\"\n ]\n },\n \"conditions\": [\n {\n \"field\": \"currency\",\n \"operator\": \"eq\",\n \"value\": \"EUR\"\n },\n {\n \"field\": \"amount\",\n \"operator\": \"lt\",\n \"value\": 10000\n }\n ],\n \"aggregation\": [\n {\n \"metric\": \"count\",\n \"operator\": \"gte\",\n \"value\": 20,\n \"window\": \"30d\",\n \"filters\": {\n \"direction\": \"INBOUND\",\n \"transaction_type\": \"finance\",\n \"subject_vendor_data\": \"__current__\",\n \"currency\": \"EUR\"\n }\n },\n {\n \"metric\": \"max\",\n \"field\": \"amount\",\n \"operator\": \"lt\",\n \"value\": 10000,\n \"window\": \"30d\",\n \"filters\": {\n \"direction\": \"INBOUND\",\n \"transaction_type\": \"finance\",\n \"subject_vendor_data\": \"__current__\",\n \"currency\": \"EUR\"\n }\n }\n ],\n \"actions\": [\n {\n \"type\": \"add_score\",\n \"value\": 35\n }\n ]\n}\n\nresp = requests.post(\n 'https://verification.didit.me/v3/transactions/rules/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json=rule,\n timeout=15,\n)\nresp.raise_for_status()\nprint(resp.json()['uuid'], resp.json()['mode'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const rule = {\n \"title\": \"Structuring - inbound (custom)\",\n \"description\": \"20+ inbound EUR transfers, all under 10,000 EUR, 30 days\",\n \"category\": \"finance\",\n \"mode\": \"TEST\",\n \"evaluation_mode\": \"ALL\",\n \"scope\": {\n \"transaction_types\": [\n \"finance\"\n ],\n \"directions\": [\n \"INBOUND\"\n ]\n },\n \"conditions\": [\n {\n \"field\": \"currency\",\n \"operator\": \"eq\",\n \"value\": \"EUR\"\n },\n {\n \"field\": \"amount\",\n \"operator\": \"lt\",\n \"value\": 10000\n }\n ],\n \"aggregation\": [\n {\n \"metric\": \"count\",\n \"operator\": \"gte\",\n \"value\": 20,\n \"window\": \"30d\",\n \"filters\": {\n \"direction\": \"INBOUND\",\n \"transaction_type\": \"finance\",\n \"subject_vendor_data\": \"__current__\",\n \"currency\": \"EUR\"\n }\n },\n {\n \"metric\": \"max\",\n \"field\": \"amount\",\n \"operator\": \"lt\",\n \"value\": 10000,\n \"window\": \"30d\",\n \"filters\": {\n \"direction\": \"INBOUND\",\n \"transaction_type\": \"finance\",\n \"subject_vendor_data\": \"__current__\",\n \"currency\": \"EUR\"\n }\n }\n ],\n \"actions\": [\n {\n \"type\": \"add_score\",\n \"value\": 35\n }\n ]\n};\n\nconst resp = await fetch('https://verification.didit.me/v3/transactions/rules/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(rule),\n});\nif (!resp.ok) throw new Error(await resp.text());\nconst created = await resp.json();\nconsole.log(created.uuid, created.mode);" + } + ] + } + }, + "/v3/transactions/rules/{rule_uuid}/": { + "parameters": [ + { + "name": "rule_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The rule's uuid from list or create." + } + ], + "get": { + "summary": "Get rule", + "description": "Retrieve one rule with its full configuration and execution counters.", + "operationId": "getTransactionRule", + "x-didit-sdk": { + "resource": "transactions.rules", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "tags": [ + "Transactions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "The rule.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionRuleDetail" } } } }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "404": { + "description": "No such rule on this application.", "content": { "application/json": { - "examples": { - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." - } - } - }, "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } + "type": "object" } } } } - } - } - }, - "/v3/passive-liveness/": { - "post": { - "summary": "Passive Liveness", - "description": "Detect presentation attacks — printed photos, screen replays, masks, deepfakes — from a single face image. No video, motion, or user interaction required.\n\n**How it works.** The image is analyzed by a biometric model; the largest detected face is evaluated and a liveness `score` (0–100) is computed. `status` is `Declined` when any `error`-severity warning fires:\n- `NO_FACE_DETECTED` — the liveness model found no face,\n- `LOW_LIVENESS_SCORE` — score at or below `face_liveness_score_decline_threshold` (default `30`),\n- `LIVENESS_FACE_ATTACK` — the model detected a presentation attack,\n- `FACE_IN_BLOCKLIST` / `POSSIBLE_FACE_IN_BLOCKLIST` — the face matches an entry on your face blocklist.\n\n`MULTIPLE_FACES_DETECTED` is a `warning`-severity signal (does not decline), and `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` are `information`-severity signals that the same face already appeared in another approved session (they never decline). `face_quality` and `face_luminance` (0–100, nullable) describe capture quality.\n\n**Blocklist and duplicate screening.** Runs on **every** call — with or without `save_api_request` — by matching the detected face against your application's face search index. Prior faces with the same `vendor_data` are excluded from the entire index search — so re-running liveness for the same user does not trigger `DUPLICATED_FACE`, and a blocklisted face originating from a session with the caller's own `vendor_data` is also NOT flagged (`FACE_IN_BLOCKLIST` screening is bypassed for same-`vendor_data` faces).\n\n**Billing.** Each `200` response consumes one Passive Liveness API credit (standalone APIs have no free tier). Insufficient balance returns `403` before any image processing.\n\n**Session persistence and face enrollment (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session (Business Console, `GET /v3/session/{sessionId}/decision/` via the returned `request_id`, `status.updated` webhook), the reference image and face crop are stored, and the detected face is **enrolled into your face search index** so later Face Search calls and duplicate checks can match it. When `false`, the call is one-shot: nothing is stored and the face is not enrolled (blocklist/duplicate screening still runs).\n\n**Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", - "operationId": "post_v3passive-liveness", + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/transactions/rules/RULE_UUID/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ] + }, + "patch": { + "summary": "Update rule", + "description": "Partially update a rule. Custom rules accept every configuration field. Library (PRESET) rules accept only `mode` - use it to activate, disable, or switch a preset to TEST; any other field returns 400. An installed copy of a withheld library rule is switched off: setting its `mode` to anything but `DISABLED` answers 400 (\"This library rule cannot run yet: its conditions read data the rules engine cannot evaluate.\").", + "operationId": "updateTransactionRule", + "x-didit-sdk": { + "resource": "transactions.rules", + "method": "update", + "auth": "api_key", + "server": true + }, "tags": [ - "Standalone APIs" + "Transactions" + ], + "security": [ + { + "ApiKeyAuth": [] + } ], - "parameters": [], "requestBody": { "required": true, "content": { - "multipart/form-data": { + "application/json": { "schema": { - "type": "object", - "required": [ - "user_image" - ], - "properties": { - "user_image": { - "type": "string", - "format": "binary", - "description": "Front-facing face image (e.g. a selfie). Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy. The image should contain a single subject — when several faces are present, the largest one is evaluated and a `MULTIPLE_FACES_DETECTED` warning is added." - }, - "face_liveness_score_decline_threshold": { - "type": "number", - "format": "float", - "default": 30, - "minimum": 0, - "maximum": 100, - "description": "Liveness score threshold (0–100). A computed score **at or below** this value emits a `LOW_LIVENESS_SCORE` warning and sets `status` to `Declined`. Default `30`; raise it for a stricter anti-spoofing posture. Values outside 0–100 return `400`.", - "example": 30 - }, - "rotate_image": { - "type": "boolean", - "default": false, - "description": "When `true`, the service tries 90-degree rotations of the input and uses the orientation that yields the best face detection. Useful when EXIF orientation is missing. Adds latency.", - "example": false - }, - "save_api_request": { - "type": "boolean", - "default": true, - "description": "When `true` (default), persists the call as an API-type session, stores the face images, emits a `status.updated` webhook, and enrolls the detected face into your face search index. When `false`, the call is one-shot — nothing is stored and the face is not enrolled. Blocklist and duplicate screening run either way.", - "example": true - }, - "vendor_data": { - "type": "string", - "description": "Optional opaque identifier (your user id, email, UUID…) stored on the persisted session and echoed back. Also used to exclude prior sessions with the same `vendor_data` from the duplicate-face check, so re-verifying the same user does not raise `DUPLICATED_FACE`.", - "example": "user-123" - }, - "metadata": { - "type": "object", - "additionalProperties": true, - "description": "Optional JSON object stored with the session (when `save_api_request=true`) and echoed back. In multipart requests, send it as a JSON-encoded string field — it is parsed into an object.", - "example": { - "flow": "withdrawal" - } - } - } + "$ref": "#/components/schemas/TransactionRuleUpdateRequest" }, "example": { - "user_image": "(binary JPEG/PNG selfie)", - "face_liveness_score_decline_threshold": 30, - "rotate_image": false, - "save_api_request": true, - "vendor_data": "user-123", - "metadata": { - "flow": "withdrawal" - } + "mode": "ACTIVE" } } } }, "responses": { "200": { - "description": "Passive liveness completed. `liveness.score` is the model's 0–100 confidence that the image shows a live person; `status` is `Approved` unless an `error`-severity warning fired. A spoofed or blocklisted face still returns `200` — inspect `liveness.status` and `liveness.warnings`, not just the HTTP code. When `save_api_request=true`, `request_id` is the persisted session id.", + "description": "The updated rule.", "content": { "application/json": { - "examples": { - "Approved": { - "summary": "Live face — no warnings", - "value": { - "request_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", - "liveness": { - "status": "Approved", - "method": "PASSIVE", - "score": 95.0, - "user_image": { - "entities": [ - { - "bbox": [ - 661, - 728, - 1688, - 2188 - ], - "confidence": 0.732973, - "age": 26.91, - "gender": "male", - "race": null - } - ], - "best_angle": 0 - }, - "warnings": [], - "face_quality": 84.21, - "face_luminance": 50.33 - }, - "vendor_data": "user-123", - "metadata": null, - "created_at": "2026-06-12T01:04:42.763237+00:00" - } - }, - "Declined - presentation attack": { - "summary": "The model flagged a spoof (print, replay, mask…)", - "value": { - "request_id": "f1c8c9b2-d3e4-4f5a-9b8c-7d6e5f4a3b2c", - "liveness": { - "status": "Declined", - "method": "PASSIVE", - "score": 12.5, - "user_image": { - "entities": [ - { - "bbox": [ - 100, - 120, - 420, - 540 - ], - "confidence": 0.81, - "age": 30.1, - "gender": "female", - "race": null - } - ], - "best_angle": 0 - }, - "warnings": [ - { - "risk": "LOW_LIVENESS_SCORE", - "feature": "LIVENESS", - "additional_data": null, - "log_type": "error", - "short_description": "Low liveness score", - "long_description": "The liveness check resulted in a low score, indicating potential use of non-live facial representations or poor-quality biometric data." - }, - { - "risk": "LIVENESS_FACE_ATTACK", - "feature": "LIVENESS", - "additional_data": null, - "log_type": "error", - "short_description": "Liveness Face Attack", - "long_description": "The system detected a potential attempt to bypass the liveness check." - } - ], - "face_quality": 72.4, - "face_luminance": 48.2 - }, - "vendor_data": "user-123", - "metadata": null, - "created_at": "2026-06-12T01:04:42.763237+00:00" - } - }, - "Declined - face on blocklist": { - "summary": "Live face, but it matches a blocklist entry", - "value": { - "request_id": "0b54a1de-22aa-4e0f-9c84-5a2b1f3c4d5e", - "liveness": { - "status": "Declined", - "method": "PASSIVE", - "score": 93.75, - "user_image": { - "entities": [ - { - "bbox": [ - 40, - 40, - 100, - 100 - ], - "confidence": 0.722082, - "age": 27.0, - "gender": "male", - "race": null - } - ], - "best_angle": 0 - }, - "warnings": [ - { - "risk": "FACE_IN_BLOCKLIST", - "feature": "LIVENESS", - "additional_data": { - "blocklisted_session_id": "882c42d5-8a4d-4d20-8080-a22f57822c86", - "blocklisted_session_number": 3242, - "api_service": null - }, - "log_type": "error", - "short_description": "Face in blocklist", - "long_description": "The system identified a face in the blocklist, which means the face is not allowed to be verified." - } - ], - "face_quality": null, - "face_luminance": null - }, - "vendor_data": "user-123", - "metadata": null, - "created_at": "2026-06-12T01:04:42.763237+00:00" - } - } - }, "schema": { - "type": "object", - "properties": { - "request_id": { - "type": "string", - "format": "uuid", - "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." - }, - "liveness": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "Approved", - "Declined" - ], - "description": "`Declined` when at least one `error`-severity warning fired (no face, low score, presentation attack, or a blocklist hit); `Approved` otherwise. `warning`/`information` entries (multiple faces, duplicates) never decline on their own." - }, - "method": { - "type": "string", - "enum": [ - "PASSIVE" - ], - "description": "Always `PASSIVE` for this endpoint." - }, - "score": { - "type": "number", - "format": "float", - "nullable": true, - "minimum": 0, - "maximum": 100, - "description": "Liveness confidence (0–100, two decimals) that the image depicts a live person rather than a spoof. `null` when the model returned no confidence.", - "example": 95.0 - }, - "user_image": { - "type": "object", - "description": "Face-detection results for the submitted image.", - "properties": { - "entities": { - "type": "array", - "items": { - "type": "object", - "properties": { - "bbox": { - "type": "array", - "items": { - "type": "integer" - }, - "minItems": 4, - "maxItems": 4, - "description": "Bounding box of the detected face as `[x_min, y_min, x_max, y_max]` pixel coordinates in the processed image.", - "example": [ - 661, - 728, - 1688, - 2188 - ] - }, - "confidence": { - "type": "number", - "format": "float", - "minimum": 0, - "maximum": 1, - "description": "Face-detection confidence (0–1).", - "example": 0.732973 - }, - "age": { - "type": "number", - "format": "float", - "description": "Model-estimated age of the detected face, in years. Informational only — it does not affect the liveness decision.", - "example": 26.91 - }, - "gender": { - "type": "string", - "description": "Model-predicted gender of the detected face (`male` or `female`). Informational only.", - "example": "male" - }, - "race": { - "type": "string", - "nullable": true, - "description": "Reserved field — always `null` in responses from this endpoint.", - "example": null - } - } - }, - "description": "One entry per detected face. The largest face is the one evaluated." - }, - "best_angle": { - "type": "integer", - "nullable": true, - "description": "Rotation (degrees) that produced the best face detection; non-zero only when `rotate_image=true` corrected the orientation.", - "example": 0 - } - } - }, - "warnings": { - "type": "array", - "description": "Risk signals. `error` severity (declines): `NO_FACE_DETECTED`, `LOW_LIVENESS_SCORE` (score at or below the threshold), `LIVENESS_FACE_ATTACK`, `FACE_IN_BLOCKLIST`, `POSSIBLE_FACE_IN_BLOCKLIST`. `warning` severity (review, no decline): `MULTIPLE_FACES_DETECTED`. `information` severity (no decline): `DUPLICATED_FACE`, `POSSIBLE_DUPLICATED_FACE` — the same face already appeared in another approved session.", - "items": { - "type": "object", - "properties": { - "risk": { - "type": "string", - "enum": [ - "NO_FACE_DETECTED", - "LOW_LIVENESS_SCORE", - "LIVENESS_FACE_ATTACK", - "MULTIPLE_FACES_DETECTED", - "FACE_IN_BLOCKLIST", - "POSSIBLE_FACE_IN_BLOCKLIST", - "DUPLICATED_FACE", - "POSSIBLE_DUPLICATED_FACE" - ], - "description": "Machine-readable risk code." - }, - "feature": { - "type": "string", - "enum": [ - "LIVENESS" - ], - "description": "Feature that raised the warning. Always `LIVENESS` on this endpoint." - }, - "additional_data": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "`null` for most warnings. Blocklist hits carry `{blocklisted_session_id, blocklisted_session_number, api_service}`; duplicate hits carry `{duplicated_session_id, duplicated_session_number, api_service}` pointing at the matching session." - }, - "log_type": { - "type": "string", - "enum": [ - "error", - "warning", - "information" - ], - "description": "Severity. `error` warnings drive `status` to `Declined`; `warning` and `information` entries are advisory and never decline on their own." - }, - "short_description": { - "type": "string", - "description": "Human-readable one-line summary of the risk." - }, - "long_description": { - "type": "string", - "description": "Human-readable explanation of the risk." - } - } - } - }, - "face_quality": { - "type": "number", - "format": "float", - "nullable": true, - "minimum": 0, - "maximum": 100, - "description": "Capture-quality score of the detected face, normalized to 0–100. `null` when the model could not produce a reliable value.", - "example": 84.21 - }, - "face_luminance": { - "type": "number", - "format": "float", - "nullable": true, - "minimum": 0, - "maximum": 100, - "description": "Brightness of the detected face, normalized to 0–100. `null` when unavailable.", - "example": 50.33 - } - } - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Echo of the `vendor_data` you sent, or `null`." - }, - "metadata": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Echo of the `metadata` object you sent, or `null`." - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 timestamp (UTC) of when the response was generated, e.g. `2026-06-12T01:04:42.763237+00:00`." - } - } + "$ref": "#/components/schemas/TransactionRuleDetail" } } } }, "400": { - "description": "Validation error. Returned when `user_image` is missing, exceeds 5 MB, has an unsupported extension, fails server-side image decoding, or when an option is out of range. Field errors use DRF's `{field: [messages]}` envelope; image-decoding failures use `{\"error\": ...}`.", + "description": "Validation error, a non-mode field on a library rule, or a mode other than DISABLED on a withheld library rule.", "content": { "application/json": { - "examples": { - "Missing image": { - "summary": "`user_image` not included in the form data", - "value": { - "user_image": [ - "No file was submitted." - ] - } - }, - "Invalid image format": { - "summary": "Allowed extension but undecodable image content", - "value": { - "error": "Invalid user image format." - } - }, - "Unsupported file extension": { - "summary": "File extension outside tiff/jpg/jpeg/png/webp", - "value": { - "user_image": [ - "File extension “txt” is not allowed. Allowed extensions are: tiff, jpg, jpeg, png, webp." - ] - } - }, - "File too large": { - "summary": "Upload exceeds the 5 MB limit", - "value": { - "user_image": [ - "File size should not exceed 5 MB" - ] - } - } + "schema": { + "type": "object" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH 'https://verification.didit.me/v3/transactions/rules/RULE_UUID/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"mode\": \"ACTIVE\"}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.patch(\n 'https://verification.didit.me/v3/transactions/rules/RULE_UUID/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={'mode': 'ACTIVE'},\n timeout=15,\n)\nresp.raise_for_status()\nprint(resp.json()['mode'])" + } + ] + }, + "delete": { + "summary": "Delete rule", + "description": "Delete a CUSTOM rule. Deletion is immediate and permanent: the rule stops evaluating for new transactions and disappears from listings, while past rule runs on already-evaluated transactions keep their recorded results. Library rules cannot be deleted - uninstall them via the install endpoint instead (they can always be reinstalled from the library).", + "operationId": "deleteTransactionRule", + "x-didit-sdk": { + "resource": "transactions.rules", + "method": "delete", + "auth": "api_key", + "server": true + }, + "tags": [ + "Transactions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "204": { + "description": "Deleted." + }, + "400": { + "description": "The rule is a library preset - uninstall instead.", + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "detail": "Library rules cannot be deleted. Uninstall them via the rules install endpoint instead." } } } }, - "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any image processing happens.", + "404": { + "description": "No such rule on this application.", "content": { "application/json": { - "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", - "value": { - "detail": "You do not have permission to perform this action." + "schema": { + "type": "object" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X DELETE 'https://verification.didit.me/v3/transactions/rules/RULE_UUID/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ] + } + }, + "/v3/transactions/rules/backtest/": { + "post": { + "summary": "Backtest rule configuration", + "description": "Evaluate a hypothetical rule configuration against your recent transactions (most recent first, up to the backtest cap) without creating a rule or touching any transaction. Use it before activating a rule to see how often it would have matched and how many subjects it would have affected.", + "operationId": "backtestTransactionRules", + "x-didit-sdk": { + "resource": "transactions.rules", + "method": "backtest", + "auth": "api_key", + "server": true + }, + "tags": [ + "Transactions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionRuleBacktestRequest" + }, + "example": { + "conditions": [ + { + "field": "currency", + "operator": "eq", + "value": "EUR" + }, + { + "field": "amount", + "operator": "lt", + "value": 10000 + } + ], + "aggregation": [ + { + "metric": "count", + "operator": "gte", + "value": 20, + "window": "30d", + "filters": { + "direction": "INBOUND", + "transaction_type": "finance", + "subject_vendor_data": "__current__", + "currency": "EUR" } }, - "Not enough credits": { - "summary": "Organization balance cannot cover the call", - "value": { - "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + { + "metric": "max", + "field": "amount", + "operator": "lt", + "value": 10000, + "window": "30d", + "filters": { + "direction": "INBOUND", + "transaction_type": "finance", + "subject_vendor_data": "__current__", + "currency": "EUR" } } - } + ], + "evaluation_mode": "ALL", + "scope": { + "transaction_types": [ + "finance" + ], + "directions": [ + "INBOUND" + ] + }, + "period_days": 90 } } - }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + } + }, + "responses": { + "200": { + "description": "Backtest statistics.", "content": { "application/json": { - "examples": { - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." - } - } - }, "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } + "$ref": "#/components/schemas/TransactionRuleBacktestResult" + }, + "example": { + "evaluated": 4820, + "matched": 12, + "affected_entities": 4, + "period_days": 90 } } } } }, - "security": [ - { - "ApiKeyAuth": [] - } - ], "x-codeSamples": [ { "lang": "curl", - "label": "cURL", - "source": "curl -X POST 'https://verification.didit.me/v3/passive-liveness/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'user_image=@./selfie.jpg' \\\n -F 'face_liveness_score_decline_threshold=30' \\\n -F 'save_api_request=true' \\\n -F 'vendor_data=user-123'" + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/transactions/rules/backtest/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"conditions\": [{\"field\": \"currency\", \"operator\": \"eq\", \"value\": \"EUR\"}, {\"field\": \"amount\", \"operator\": \"lt\", \"value\": 10000}], \"aggregation\": [{\"metric\": \"count\", \"operator\": \"gte\", \"value\": 20, \"window\": \"30d\", \"filters\": {\"direction\": \"INBOUND\", \"transaction_type\": \"finance\", \"subject_vendor_data\": \"__current__\", \"currency\": \"EUR\"}}, {\"metric\": \"max\", \"field\": \"amount\", \"operator\": \"lt\", \"value\": 10000, \"window\": \"30d\", \"filters\": {\"direction\": \"INBOUND\", \"transaction_type\": \"finance\", \"subject_vendor_data\": \"__current__\", \"currency\": \"EUR\"}}], \"evaluation_mode\": \"ALL\", \"scope\": {\"transaction_types\": [\"finance\"], \"directions\": [\"INBOUND\"]}, \"period_days\": 90}'" }, { "lang": "python", "label": "Python", - "source": "import requests\n\nurl = 'https://verification.didit.me/v3/passive-liveness/'\nheaders = {'x-api-key': 'YOUR_API_KEY'}\n\nwith open('selfie.jpg', 'rb') as f:\n files = {'user_image': ('selfie.jpg', f, 'image/jpeg')}\n data = {\n 'face_liveness_score_decline_threshold': 30,\n 'save_api_request': 'true',\n 'vendor_data': 'user-123',\n }\n resp = requests.post(url, headers=headers, files=files, data=data, timeout=60)\n\nresp.raise_for_status()\nbody = resp.json()\nprint('status:', body['liveness']['status'])\nprint('score:', body['liveness']['score'])\nfor w in body['liveness']['warnings']:\n print('warning:', w['risk'], '-', w['log_type'])" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "import fs from 'node:fs';\n\nconst form = new FormData();\nform.append('user_image', new Blob([fs.readFileSync('./selfie.jpg')]), 'selfie.jpg');\nform.append('face_liveness_score_decline_threshold', '30');\nform.append('save_api_request', 'true');\nform.append('vendor_data', 'user-123');\n\nconst response = await fetch('https://verification.didit.me/v3/passive-liveness/', {\n method: 'POST',\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n body: form,\n});\n\nif (!response.ok) throw new Error(`Passive liveness failed: ${response.status}`);\nconst body = await response.json();\nconsole.log('status:', body.liveness.status, 'score:', body.liveness.score);\nbody.liveness.warnings.forEach(w => console.log('warning:', w.risk, '-', w.log_type));" + "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/transactions/rules/backtest/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={\n \"conditions\": [\n {\n \"field\": \"currency\",\n \"operator\": \"eq\",\n \"value\": \"EUR\"\n },\n {\n \"field\": \"amount\",\n \"operator\": \"lt\",\n \"value\": 10000\n }\n ],\n \"aggregation\": [\n {\n \"metric\": \"count\",\n \"operator\": \"gte\",\n \"value\": 20,\n \"window\": \"30d\",\n \"filters\": {\n \"direction\": \"INBOUND\",\n \"transaction_type\": \"finance\",\n \"subject_vendor_data\": \"__current__\",\n \"currency\": \"EUR\"\n }\n },\n {\n \"metric\": \"max\",\n \"field\": \"amount\",\n \"operator\": \"lt\",\n \"value\": 10000,\n \"window\": \"30d\",\n \"filters\": {\n \"direction\": \"INBOUND\",\n \"transaction_type\": \"finance\",\n \"subject_vendor_data\": \"__current__\",\n \"currency\": \"EUR\"\n }\n }\n ],\n \"evaluation_mode\": \"ALL\",\n \"scope\": {\n \"transaction_types\": [\n \"finance\"\n ],\n \"directions\": [\n \"INBOUND\"\n ]\n },\n \"period_days\": 90\n},\n timeout=30,\n)\nresp.raise_for_status()\nstats = resp.json()\nprint(stats['matched'], 'matches across', stats['affected_entities'], 'subjects')" } ] } }, - "/v3/database-validation/": { - "post": { - "summary": "Database Validation (standalone)", - "operationId": "post_v3database-validation", + "/v3/transactions/rules/library/": { + "get": { + "summary": "List rule library", + "description": "Browse the full preset rule library (150+ rules) with each entry's complete configuration and an `is_installed` flag for this application. Filter by bundle, category, or free-text search. A library rule whose conditions read data the rules engine cannot evaluate yet is withheld until it can run. Withheld rules are not listed.", + "operationId": "listTransactionRuleLibrary", + "x-didit-sdk": { + "resource": "transactions.rules", + "method": "list_library", + "auth": "api_key", + "server": true + }, "tags": [ - "Standalone APIs" + "Transactions" ], "security": [ { "ApiKeyAuth": [] } ], - "parameters": [], - "description": "Validate a person's identity data against official government and registry sources — CPF in Brazil, RENAPER in Argentina, DNI registries in Spain and Peru, INE in Mexico, and 100+ more services across 60+ countries.\n\n**Service selection.** Each country exposes one or more catalog services identified by a `service_id` (e.g. `bra_cpf`, `arg_renaper`, `pan_cedula_sib`). Pin the services you want with the `services` array — if you omit it, exactly **one** default service runs (the longest-established live service for the country). Sending `services` also switches the response to the extended shape with `services_used` and `match_score`. Discover services, their required fields, and prices via `GET /v1/organization/database-validation-countries/` (a catalog endpoint not documented in this spec) or the Business Console workflow editor. Some services are flagged `requires_consent=true` in the catalog and demand `consent=true`; others require onboarding for your organization before they can be called.\n\n**Input fields.** `identification_number` is a universal field that maps to the right country-specific field automatically (see its description). Country format rules are enforced before any provider is called (e.g. BRA CPF must be exactly 11 digits) and format failures are never billed. Biometric services need a `selfie` upload (multipart only); Argentina RENAPER additionally requires `gender`.\n\n**Results.** Each service returns a field-by-field `validation` map plus a vendor-neutral `outcome_code` (`MATCH`, `PARTIAL_MATCH`, `NO_MATCH`, `DOCUMENT_NOT_FOUND`, `INVALID_DOCUMENT_FORMAT`, `INVALID_INPUT`, `MINOR_BLOCKED`, `DECEASED`, `BIOMETRIC_NO_MATCH`, `BIOMETRIC_IMAGE_UNUSABLE`, `INCONCLUSIVE`, `DOCUMENT_SUPERSEDED`, `REGISTRY_UNAVAILABLE`, `REGISTRY_ERROR`). Note the distinctions: `NO_MATCH` is a definitive mismatch, `INCONCLUSIVE` means the registry could not confirm either way (review, not decline), and `BIOMETRIC_IMAGE_UNUSABLE` is a technical selfie problem (retake), unlike the definitive `BIOMETRIC_NO_MATCH`. The overall `match_type` aggregates across services (any full → `full_match`, else any partial → `partial_match`, else `no_match`) and `no_match_action`/`partial_match_action` translate it into the final `status`. `validation_type` is derived: `two_by_two` when two or more distinct services produced a full match (on the identification number, on full name + date of birth together, or on the address without contradictions), else `one_by_one`.\n\n**Persistence.** `save_api_request` defaults to **true**: the result is stored as a session (`request_id` works with `GET /v3/session/{sessionId}/decision/`), appears in the console, and fires a `status.updated` webhook. It also controls the `validations` shape — per-service objects when saved, a merged `{field: match}` map when not.\n\n**Billing.** Per-service: each service that returns a billable result is charged its own catalog price. Failed or skipped services are not billed, and requests rejected with `400` cost nothing. The pre-flight balance check covers the sum of the selected services' prices (`403` when short).\n\n**Failures.** When no selected service returns a usable result, the API returns `400` with `validation_errors` if every unanswered attempt explicitly refused the input (`provider_rejected_input`, `retryable: false`). Correct the input before trying again. Otherwise it returns `502` with `validation_errors` (`empty_provider_response`, `retryable: true`), including mixed input rejection and unavailable fallback attempts; nothing is billed (a session is still recorded with status `Not Finished` when `save_api_request=true`). When only some services fail, the `200` response carries their failures in `database_validation.errors`.\n\n**Sandbox.** Keys from sandbox applications skip the registry calls and billing and return a static `full_match` response — the sandbox payload always includes `services_used` and `match_score: 100` regardless of how `services` was sent, so don't validate the live extended-shape gating or the 0.0–1.0 `match_score` fraction against sandbox responses.\n\nSend the request as `application/json`, or as `multipart/form-data` when uploading a `selfie`.", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "issuing_state" - ], - "properties": { - "issuing_state": { - "type": "string", - "description": "ISO 3166-1 **alpha-3** country code of the registry to validate against (e.g. `BRA`, `ESP`, `COL`). Determines which services are available. Unsupported codes return `400` with the full list of valid options.", - "example": "BRA" - }, - "services": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Catalog `service_id`s to run for this country (e.g. `[\"bra_cpf\"]`). Also accepts a single string or a comma-separated/JSON-encoded string. **If omitted or empty, exactly one default service runs** — the longest-established live service for the country; newer, biometric, and pay-as-you-go services must be named explicitly. Sending `services` switches the response to the extended shape (adds `services_used` and `match_score`). Every id must exist and be live for the `issuing_state`; services that require onboarding return `400` until activated for your organization. Discover available services per country via `GET /v1/organization/database-validation-countries/` (a catalog endpoint not documented in this spec) or the Business Console workflow editor.", - "example": [ - "bra_cpf" - ] - }, - "validation_type": { - "type": "string", - "deprecated": true, - "description": "DEPRECATED. Accepted for backward compatibility but ignored — the response `validation_type` is now derived from how many services full-matched. Use `services` to pin specific services." - }, - "consent": { - "type": "boolean", - "default": false, - "description": "Set to `true` when the end user has explicitly consented to the selected validation services. Required for services flagged `requires_consent=true` in the catalog — without it those services return `400` with `services_requiring_consent`." - }, - "identification_number": { - "type": "string", - "description": "Universal identification number — automatically mapped to the correct country-specific field, so you can use it instead of `personal_number`/`tax_number`/`document_number`: ARG→document_number (DNI), BOL→document_number (CI), BRA→tax_number (CPF, 11 digits), CHL→personal_number (RUT), COL→personal_number (Cédula), CRI→personal_number (Cédula), DOM→personal_number (Cédula, 11 digits), ECU→personal_number (Cédula, 10 digits), ESP→personal_number (DNI/NIE), GTM→document_number (DPI), HND→document_number (DNI), MEX→personal_number (CURP, 18 chars), PAN→personal_number (Cédula), PER→personal_number (DNI, 8 digits), PRY→document_number (CI), SLV→document_number (DUI), URY→personal_number (CI), VEN→document_number (Cédula). It never overrides an explicitly provided country-specific field." - }, - "first_name": { - "type": "string", - "description": "The individual's first name. Required by some services/countries.", - "example": "John" - }, - "last_name": { - "type": "string", - "description": "The individual's last name. Required by some services/countries.", - "example": "Doe" - }, - "middle_name": { - "type": "string", - "description": "Middle name, used by some country services (AUS, NZL, …)." - }, - "full_name": { - "type": "string", - "description": "Full name — some services accept this in lieu of first/last name (e.g. CHN services, which match the native-script name)." - }, - "date_of_birth": { - "type": "string", - "format": "date", - "description": "Date of birth, `YYYY-MM-DD`. Required by many services.", - "example": "1980-01-01" - }, - "personal_number": { - "type": "string", - "description": "Government-issued unique personal identifier. Used by: CHL (RUT), COL (Cédula), CRI, DOM, ECU, ESP (DNI/NIE), MEX (CURP), PAN (Cédula), PER (DNI), URY. Consider `identification_number` instead." - }, - "tax_number": { - "type": "string", - "description": "Tax identification number. Used by: BRA (CPF, 11 digits). Consider `identification_number` instead." - }, - "document_number": { - "type": "string", - "description": "Document number. Used by: ARG (DNI), BOL (CI), GTM (DPI), HND (DNI), PRY (CI), SLV (DUI), VEN (Cédula). Consider `identification_number` instead." - }, - "document_type": { - "type": "string", - "enum": [ - "P", - "DL", - "ID", - "RP", - "SSC", - "HIC", - "WP", - "TC", - "VISA", - "PSC", - "BC", - "OTHER" - ], - "description": "Type of document being validated: `P` passport, `DL` driver license, `ID` national ID, `RP` residence permit (plus `SSC`, `HIC`, `WP`, `TC`, `VISA`, `PSC`, `BC`, `OTHER`). Required by some services (e.g. ESP)." - }, - "expiration_date": { - "type": "string", - "format": "date", - "description": "Document expiration date, `YYYY-MM-DD`. Required for ESP (Spain) validation.", - "example": "2030-01-15" - }, - "date_of_issue": { - "type": "string", - "format": "date", - "description": "Document issue date, `YYYY-MM-DD` (fecha de expedición). Required for COL (Colombia) cédula validation.", - "example": "2015-06-20" - }, - "nationality": { - "type": "string", - "description": "Nationality as ISO 3166-1 alpha-3. Required by some services." - }, - "gender": { - "type": "string", - "enum": [ - "M", - "F", - "X" - ], - "description": "Gender: `M`, `F`, or `X` (other/unknown). Required for Argentina's RENAPER validation (`arg_renaper`)." - }, - "address": { - "description": "Residential address. Prefer the structured object `{\"street_1\":\"123 Main St\",\"street_2\":\"Apt 4B\",\"city\":\"Springfield\",\"region\":\"IL\",\"postal_code\":\"62701\",\"country\":\"US\"}`; a complete single-line string is still accepted. Required by address-verification services; when a service defines address requirements, missing parts return `400` with `address_fields_required_by`.", - "oneOf": [ - { - "type": "object", - "properties": { - "street_1": { - "type": "string" - }, - "street_2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "region": { - "type": "string" - }, - "postal_code": { - "type": "string" - }, - "country": { - "type": "string" - } - } - }, - { - "type": "string", - "description": "Complete single-line address (legacy)." - } - ] - }, - "address_element_1": { - "type": "string", - "description": "Street address including street number and type. If omitted, derived from `address`." - }, - "address_element_2": { - "type": "string", - "description": "Apartment, unit, building, floor, or extra address line. Only send when you have it explicitly." - }, - "address_element_3": { - "type": "string", - "description": "City, suburb, district, locality, or neighborhood. If omitted, derived from `address`." - }, - "address_element_4": { - "type": "string", - "description": "State, province, region, or town. If omitted, derived from `address`." - }, - "address_element_5": { - "type": "string", - "description": "Postcode or postal code. `postal_code` is accepted as an alias." - }, - "postal_code": { - "type": "string", - "description": "Postal code for address-based services (alias of `address_element_5`)." - }, - "driver_license_number": { - "type": "string", - "description": "Driver licence number for government document-verification services (AUS, NZL, IND)." - }, - "driver_license_card_number": { - "type": "string", - "description": "Physical driver licence card number, where required separately from the licence number (AUS)." - }, - "driver_license_state": { - "type": "string", - "description": "Driver licence state or territory of issue (AUS)." - }, - "driver_license_version": { - "type": "string", - "description": "Driver licence version code (NZL)." - }, - "passport_number": { - "type": "string", - "description": "Passport number for government passport-verification services (AUS, NZL, IND, CHN)." - }, - "passport_expiration_date": { - "type": "string", - "format": "date", - "description": "Passport expiry date, `YYYY-MM-DD`, for passport-verification services." - }, - "passport_file_number": { - "type": "string", - "description": "Passport file number (IND)." - }, - "passport_issue_country": { - "type": "string", - "description": "Issuing country of the passport (ISO 3166-1 alpha-3) for passport-verification services." - }, - "medicare_card_number": { - "type": "string", - "description": "Medicare card number (AUS)." - }, - "immi_card_number": { - "type": "string", - "description": "Australian ImmiCard number." - }, - "immi_card_expiry_date": { - "type": "string", - "format": "date", - "description": "Australian ImmiCard expiry date." - }, - "citizenship_certificate_number": { - "type": "string", - "description": "Citizenship certificate number (AUS)." - }, - "birth_registration_number": { - "type": "string", - "description": "Birth-certificate registration number (AUS)." - }, - "birth_registration_date": { - "type": "string", - "format": "date", - "description": "Birth-certificate registration date (AUS)." - }, - "birth_registration_state": { - "type": "string", - "description": "Birth-certificate registration state (AUS)." - }, - "marriage_certificate_number": { - "type": "string", - "description": "Marriage certificate number (AUS)." - }, - "change_of_name_certificate_number": { - "type": "string", - "description": "Change-of-name certificate number (AUS)." - }, - "first_partner_name": { - "type": "string", - "description": "First name of the partner on a marriage certificate (AUS)." - }, - "last_partner_name": { - "type": "string", - "description": "Last name of the partner on a marriage certificate (AUS)." - }, - "voter_id": { - "type": "string", - "description": "Voter registration number (IND EPIC, IRL)." - }, - "epic_card": { - "type": "string", - "description": "India EPIC voter card number." - }, - "pan": { - "type": "string", - "description": "India PAN (Permanent Account Number)." - }, - "national_id": { - "type": "string", - "description": "National ID number (CHN, MYS, KEN, KHM, NGA, ZAF, …)." - }, - "cic": { - "type": "string", - "description": "Mexican INE/IFE Código de Identificación de Credencial (CIC), 9 digits. Used by the MEX INE credential-validity service (`mex_ine_vigencia`)." - }, - "identificador_ciudadano": { - "type": "string", - "description": "Mexican INE Identificador del Ciudadano (9 digits). Used with `cic` for modern INE models (E/F/G/H) in `mex_ine_vigencia`." - }, - "ocr": { - "type": "string", - "description": "Mexican INE OCR number (13 digits, back of card). Used with `cic` for Model D in `mex_ine_vigencia`." - }, - "voter_number": { - "type": "string", - "description": "Mexican INE Clave de Elector (18 chars). Used with `emission_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`." - }, - "emission_number": { - "type": "string", - "description": "Mexican INE Número de Emisión. Used with `voter_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`." - }, - "bvn": { - "type": "string", - "description": "Nigerian Bank Verification Number." - }, - "bank_card_number": { - "type": "string", - "description": "Bank card number (CHN bank-card verification)." - }, - "ssn": { - "type": "string", - "description": "Social Security Number (USA)." - }, - "phone": { - "type": "string", - "description": "Phone number for phone-verification services." - }, - "landline": { - "type": "string", - "description": "Landline number for phone-verification services." - }, - "email": { - "type": "string", - "format": "email", - "description": "Email address for identity-verification services." - }, - "country_of_residence": { - "type": "string", - "description": "ISO 3166-1 alpha-2 country of residence. Used by global identity-enrichment services (e.g. `glb_identity_enrichment`) to focus the lookup.", - "example": "US" - }, - "partial_match_action": { - "type": "string", - "enum": [ - "DECLINE", - "NO_ACTION" - ], - "default": "NO_ACTION", - "description": "What a `partial_match` does to `database_validation.status`. Defaults to `NO_ACTION` (status stays `Approved`, with a warning)." - }, - "no_match_action": { - "type": "string", - "enum": [ - "DECLINE", - "NO_ACTION" - ], - "default": "DECLINE", - "description": "What a `no_match` does to `database_validation.status`. Defaults to `DECLINE`." - }, - "save_api_request": { - "type": "boolean", - "default": true, - "description": "Persist the validation as a session (console visibility, decision endpoint, `status.updated` webhook). Also changes the `validations` response shape: per-service objects when `true` (default), a merged field map when `false`." - }, - "vendor_data": { - "type": "string", - "description": "Your identifier for the validated user; echoed back and stored with the session." - }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Free-form JSON stored with the request and echoed back." + "parameters": [ + { + "name": "bundle", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Filter by bundle, e.g. finance, aml_ctf, crypto_monitoring, travel_rule" + }, + { + "name": "category", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Filter by category" + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Case-insensitive match against title and description" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer" + }, + "description": "Page size" + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer" + }, + "description": "Page offset" + } + ], + "responses": { + "200": { + "description": "Paginated library entries.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "next": { + "type": "string", + "nullable": true + }, + "previous": { + "type": "string", + "nullable": true + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionRuleLibraryEntry" + } + } } } } - }, - "multipart/form-data": { + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/transactions/rules/library/?bundle=finance' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ] + } + }, + "/v3/transactions/rules/install/": { + "post": { + "summary": "Install library rules", + "description": "Install one or more presets onto the application, by `library_keys` or a whole `bundle`. An installed preset is active at once while transaction monitoring is on, and disabled otherwise; already-installed keys are skipped. A library rule whose conditions read data the rules engine cannot evaluate yet is withheld until it can run. A request that names one in `library_keys` answers 400 and installs nothing (\"These library rules cannot run yet: their conditions read data the rules engine cannot evaluate.\" followed by the keys), and a `bundle` install skips it. A withheld rule comes back on its own once its conditions can be evaluated. A `library_keys` entry that is not in the library is skipped, not refused.", + "operationId": "installTransactionRuleLibrary", + "x-didit-sdk": { + "resource": "transactions.rules", + "method": "install_library", + "auth": "api_key", + "server": true + }, + "tags": [ + "Transactions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { "schema": { - "type": "object", - "required": [ - "issuing_state" - ], - "properties": { - "issuing_state": { - "type": "string", - "description": "ISO 3166-1 **alpha-3** country code of the registry to validate against (e.g. `BRA`, `ESP`, `COL`). Determines which services are available. Unsupported codes return `400` with the full list of valid options.", - "example": "BRA" - }, - "services": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Catalog `service_id`s to run for this country (e.g. `[\"bra_cpf\"]`). Also accepts a single string or a comma-separated/JSON-encoded string. **If omitted or empty, exactly one default service runs** — the longest-established live service for the country; newer, biometric, and pay-as-you-go services must be named explicitly. Sending `services` switches the response to the extended shape (adds `services_used` and `match_score`). Every id must exist and be live for the `issuing_state`; services that require onboarding return `400` until activated for your organization. Discover available services per country via `GET /v1/organization/database-validation-countries/` (a catalog endpoint not documented in this spec) or the Business Console workflow editor.", - "example": [ - "bra_cpf" - ] - }, - "validation_type": { - "type": "string", - "deprecated": true, - "description": "DEPRECATED. Accepted for backward compatibility but ignored — the response `validation_type` is now derived from how many services full-matched. Use `services` to pin specific services." - }, - "consent": { - "type": "boolean", - "default": false, - "description": "Set to `true` when the end user has explicitly consented to the selected validation services. Required for services flagged `requires_consent=true` in the catalog — without it those services return `400` with `services_requiring_consent`." - }, - "identification_number": { - "type": "string", - "description": "Universal identification number — automatically mapped to the correct country-specific field, so you can use it instead of `personal_number`/`tax_number`/`document_number`: ARG→document_number (DNI), BOL→document_number (CI), BRA→tax_number (CPF, 11 digits), CHL→personal_number (RUT), COL→personal_number (Cédula), CRI→personal_number (Cédula), DOM→personal_number (Cédula, 11 digits), ECU→personal_number (Cédula, 10 digits), ESP→personal_number (DNI/NIE), GTM→document_number (DPI), HND→document_number (DNI), MEX→personal_number (CURP, 18 chars), PAN→personal_number (Cédula), PER→personal_number (DNI, 8 digits), PRY→document_number (CI), SLV→document_number (DUI), URY→personal_number (CI), VEN→document_number (Cédula). It never overrides an explicitly provided country-specific field." - }, - "first_name": { - "type": "string", - "description": "The individual's first name. Required by some services/countries.", - "example": "John" - }, - "last_name": { - "type": "string", - "description": "The individual's last name. Required by some services/countries.", - "example": "Doe" - }, - "middle_name": { - "type": "string", - "description": "Middle name, used by some country services (AUS, NZL, …)." - }, - "full_name": { - "type": "string", - "description": "Full name — some services accept this in lieu of first/last name (e.g. CHN services, which match the native-script name)." - }, - "date_of_birth": { - "type": "string", - "format": "date", - "description": "Date of birth, `YYYY-MM-DD`. Required by many services.", - "example": "1980-01-01" - }, - "personal_number": { - "type": "string", - "description": "Government-issued unique personal identifier. Used by: CHL (RUT), COL (Cédula), CRI, DOM, ECU, ESP (DNI/NIE), MEX (CURP), PAN (Cédula), PER (DNI), URY. Consider `identification_number` instead." - }, - "tax_number": { - "type": "string", - "description": "Tax identification number. Used by: BRA (CPF, 11 digits). Consider `identification_number` instead." - }, - "document_number": { - "type": "string", - "description": "Document number. Used by: ARG (DNI), BOL (CI), GTM (DPI), HND (DNI), PRY (CI), SLV (DUI), VEN (Cédula). Consider `identification_number` instead." - }, - "document_type": { - "type": "string", - "enum": [ - "P", - "DL", - "ID", - "RP", - "SSC", - "HIC", - "WP", - "TC", - "VISA", - "PSC", - "BC", - "OTHER" - ], - "description": "Type of document being validated: `P` passport, `DL` driver license, `ID` national ID, `RP` residence permit (plus `SSC`, `HIC`, `WP`, `TC`, `VISA`, `PSC`, `BC`, `OTHER`). Required by some services (e.g. ESP)." - }, - "expiration_date": { - "type": "string", - "format": "date", - "description": "Document expiration date, `YYYY-MM-DD`. Required for ESP (Spain) validation.", - "example": "2030-01-15" - }, - "date_of_issue": { - "type": "string", - "format": "date", - "description": "Document issue date, `YYYY-MM-DD` (fecha de expedición). Required for COL (Colombia) cédula validation.", - "example": "2015-06-20" - }, - "nationality": { - "type": "string", - "description": "Nationality as ISO 3166-1 alpha-3. Required by some services." - }, - "gender": { - "type": "string", - "enum": [ - "M", - "F", - "X" - ], - "description": "Gender: `M`, `F`, or `X` (other/unknown). Required for Argentina's RENAPER validation (`arg_renaper`)." - }, - "address": { - "description": "Residential address. Prefer the structured object `{\"street_1\":\"123 Main St\",\"street_2\":\"Apt 4B\",\"city\":\"Springfield\",\"region\":\"IL\",\"postal_code\":\"62701\",\"country\":\"US\"}`; a complete single-line string is still accepted. Required by address-verification services; when a service defines address requirements, missing parts return `400` with `address_fields_required_by`.", - "oneOf": [ - { - "type": "object", - "properties": { - "street_1": { - "type": "string" - }, - "street_2": { - "type": "string" - }, - "city": { - "type": "string" - }, - "region": { - "type": "string" - }, - "postal_code": { - "type": "string" - }, - "country": { - "type": "string" - } + "$ref": "#/components/schemas/TransactionRuleInstallRequest" + }, + "example": { + "library_keys": [ + "structuring-inbound", + "structuring-outbound" + ] + } + } + } + }, + "responses": { + "200": { + "description": "What was installed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionRuleInstallResult" + }, + "example": { + "installed_library_keys": [ + "structuring-inbound", + "structuring-outbound" + ], + "installed_count": 2 + } + } + } + }, + "400": { + "description": "Neither library_keys nor bundle provided, `library_keys` is not a list, or a `library_keys` entry is withheld. An unknown entry is skipped and answers 200 without it.", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/transactions/rules/install/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"library_keys\": [\"structuring-inbound\", \"structuring-outbound\"]}'" + } + ] + }, + "delete": { + "summary": "Uninstall library rules", + "description": "Remove installed presets from the application, by `library_keys` or a whole `bundle`. Uninstalled presets stop evaluating immediately and can be reinstalled from the library at any time.", + "operationId": "uninstallTransactionRuleLibrary", + "x-didit-sdk": { + "resource": "transactions.rules", + "method": "uninstall_library", + "auth": "api_key", + "server": true + }, + "tags": [ + "Transactions" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionRuleInstallRequest" + }, + "example": { + "bundle": "responsible_gaming" + } + } + } + }, + "responses": { + "200": { + "description": "How many presets were removed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionRuleInstallResult" + }, + "example": { + "uninstalled_count": 5 + } + } + } + }, + "400": { + "description": "Neither library_keys nor bundle provided.", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X DELETE 'https://verification.didit.me/v3/transactions/rules/install/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"bundle\": \"responsible_gaming\"}'" + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/networks/": { + "get": { + "summary": "List networks", + "description": "List fraud networks detected within this application's organization, highest risk first by default. By default dismissed networks are excluded; pass status to include them. Sandbox applications never have networks: clustering excludes sandbox data entirely. Every response carries the organization's `sharing_mode` and whether cross-organization signals apply to it (`cross_org_available`, `cross_org_unavailable_reason`). `kpis` aggregate the networks that match your filters and `counts` aggregate every network the application can see; pass `summary=false` to skip both when you only need the page. Requires a full-access API key, or `list:networks` for a console member.", + "operationId": "list_networks", + "x-didit-sdk": { + "resource": "networks", + "method": "list", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Networks" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Comma-separated statuses to include: active,in_review,resolved,dismissed. Omit to exclude dismissed." + }, + { + "name": "scope", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "identity", + "transactions" + ] + }, + "description": "`transactions` keeps networks whose members are all transaction parties; `identity` keeps every other network. Omit for both. It also narrows `counts.views` and `counts.facets`. Any other value returns 400." + }, + { + "name": "signal_type", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Comma-separated signal types: ip_address,device,face,document_number,phone,email,address." + }, + { + "name": "pattern_type", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Comma-separated pattern types, e.g. exact_same_device,same_email." + }, + { + "name": "risk_band", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Comma-separated bands: low,medium,high." + }, + { + "name": "min_size", + "in": "query", + "schema": { + "type": "integer" + }, + "description": "Minimum member count." + }, + { + "name": "max_size", + "in": "query", + "schema": { + "type": "integer" + }, + "description": "Maximum member count." + }, + { + "name": "assignee", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Comma-separated assignees: `unassigned`, member emails (matched case-insensitively), or `me` for the calling console member. A network matches any value. `me` needs a console member token: an API key gets 400 `{\"assignee\": [\"\\\"me\\\" needs a user token; an API key has no assignee identity.\"]}`." + }, + { + "name": "priority", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Comma-separated priorities: `low`, `medium`, `high`, or `none` for networks without a priority. Any other value returns 400 `{\"priority\": \"Unknown priority: .\"}`." + }, + { + "name": "risk_min", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "description": "Minimum risk score, a whole number from 0 to 100. A value that is not a whole number returns 400 `{\"risk_min\": \"Use a whole number from 0 to 100.\"}`." + }, + { + "name": "has_case", + "in": "query", + "schema": { + "type": "boolean" + }, + "description": "`true` keeps the networks your application opened a case for, `false` the others. Any other value is ignored." + }, + { + "name": "date_from", + "in": "query", + "schema": { + "type": "string", + "format": "date" + }, + "description": "Lower bound on last_activity_at (ISO 8601 date or datetime)." + }, + { + "name": "date_to", + "in": "query", + "schema": { + "type": "string", + "format": "date" + }, + "description": "Upper bound on first_activity_at (ISO 8601 date or datetime)." + }, + { + "name": "q", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Free-text search across network name, member names, the network_id (N-0001), and exact pattern_type tokens." + }, + { + "name": "ordering", + "in": "query", + "schema": { + "type": "string" + }, + "description": "One of risk_score, -risk_score, last_activity_at, -last_activity_at, first_activity_at, -first_activity_at, network_number, -network_number. Defaults to -risk_score,-last_activity_at." + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 100, + "minimum": 1, + "maximum": 500 + }, + "description": "Page size, 1-500. Defaults to 100." + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0, + "minimum": 0 + }, + "description": "Zero-based offset of the first record." + }, + { + "name": "summary", + "in": "query", + "schema": { + "type": "boolean", + "default": true + }, + "description": "`false` returns the page and its `count` only, without `kpis` and `counts`. Defaults to `true`. Any value other than `true` or `false` returns 400." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/?status=active,in_review&ordering=-risk_score' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Paginated network list, with the organization's sharing mode and cross-organization availability, plus `kpis` and `counts` unless `summary=false`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "next": { + "type": "integer", + "nullable": true, + "description": "Offset of the next page, or null on the last page." + }, + "previous": { + "type": "integer", + "nullable": true + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkListItem" + } + }, + "kpis": { + "type": "object", + "description": "Each key is {value, delta} over the results matching the current filters. Omitted when `summary=false`.", + "properties": { + "active": { + "type": "object" + }, + "in_review": { + "type": "object" + }, + "applicants_involved": { + "type": "object" + }, + "detected_this_period": { + "type": "object" + }, + "false_positive_rate": { + "type": "object" } - }, - { - "type": "string", - "description": "Complete single-line address (legacy)." } - ] - }, - "address_element_1": { - "type": "string", - "description": "Street address including street number and type. If omitted, derived from `address`." - }, - "address_element_2": { - "type": "string", - "description": "Apartment, unit, building, floor, or extra address line. Only send when you have it explicitly." - }, - "address_element_3": { - "type": "string", - "description": "City, suburb, district, locality, or neighborhood. If omitted, derived from `address`." - }, - "address_element_4": { - "type": "string", - "description": "State, province, region, or town. If omitted, derived from `address`." - }, - "address_element_5": { - "type": "string", - "description": "Postcode or postal code. `postal_code` is accepted as an alias." - }, - "postal_code": { - "type": "string", - "description": "Postal code for address-based services (alias of `address_element_5`)." - }, - "driver_license_number": { - "type": "string", - "description": "Driver licence number for government document-verification services (AUS, NZL, IND)." - }, - "driver_license_card_number": { - "type": "string", - "description": "Physical driver licence card number, where required separately from the licence number (AUS)." - }, - "driver_license_state": { - "type": "string", - "description": "Driver licence state or territory of issue (AUS)." - }, - "driver_license_version": { - "type": "string", - "description": "Driver licence version code (NZL)." - }, - "passport_number": { - "type": "string", - "description": "Passport number for government passport-verification services (AUS, NZL, IND, CHN)." - }, - "passport_expiration_date": { - "type": "string", - "format": "date", - "description": "Passport expiry date, `YYYY-MM-DD`, for passport-verification services." - }, - "passport_file_number": { - "type": "string", - "description": "Passport file number (IND)." - }, - "passport_issue_country": { - "type": "string", - "description": "Issuing country of the passport (ISO 3166-1 alpha-3) for passport-verification services." - }, - "medicare_card_number": { - "type": "string", - "description": "Medicare card number (AUS)." - }, - "immi_card_number": { - "type": "string", - "description": "Australian ImmiCard number." - }, - "immi_card_expiry_date": { - "type": "string", - "format": "date", - "description": "Australian ImmiCard expiry date." - }, - "citizenship_certificate_number": { - "type": "string", - "description": "Citizenship certificate number (AUS)." - }, - "birth_registration_number": { - "type": "string", - "description": "Birth-certificate registration number (AUS)." - }, - "birth_registration_date": { - "type": "string", - "format": "date", - "description": "Birth-certificate registration date (AUS)." - }, - "birth_registration_state": { - "type": "string", - "description": "Birth-certificate registration state (AUS)." - }, - "marriage_certificate_number": { - "type": "string", - "description": "Marriage certificate number (AUS)." - }, - "change_of_name_certificate_number": { - "type": "string", - "description": "Change-of-name certificate number (AUS)." - }, - "first_partner_name": { - "type": "string", - "description": "First name of the partner on a marriage certificate (AUS)." - }, - "last_partner_name": { - "type": "string", - "description": "Last name of the partner on a marriage certificate (AUS)." - }, - "voter_id": { - "type": "string", - "description": "Voter registration number (IND EPIC, IRL)." - }, - "epic_card": { - "type": "string", - "description": "India EPIC voter card number." - }, - "pan": { - "type": "string", - "description": "India PAN (Permanent Account Number)." - }, - "national_id": { - "type": "string", - "description": "National ID number (CHN, MYS, KEN, KHM, NGA, ZAF, …)." - }, - "cic": { - "type": "string", - "description": "Mexican INE/IFE Código de Identificación de Credencial (CIC), 9 digits. Used by the MEX INE credential-validity service (`mex_ine_vigencia`)." - }, - "identificador_ciudadano": { - "type": "string", - "description": "Mexican INE Identificador del Ciudadano (9 digits). Used with `cic` for modern INE models (E/F/G/H) in `mex_ine_vigencia`." - }, - "ocr": { - "type": "string", - "description": "Mexican INE OCR number (13 digits, back of card). Used with `cic` for Model D in `mex_ine_vigencia`." - }, - "voter_number": { - "type": "string", - "description": "Mexican INE Clave de Elector (18 chars). Used with `emission_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`." - }, - "emission_number": { - "type": "string", - "description": "Mexican INE Número de Emisión. Used with `voter_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`." - }, - "bvn": { - "type": "string", - "description": "Nigerian Bank Verification Number." - }, - "bank_card_number": { - "type": "string", - "description": "Bank card number (CHN bank-card verification)." - }, - "ssn": { - "type": "string", - "description": "Social Security Number (USA)." + }, + "counts": { + "$ref": "#/components/schemas/NetworkListCounts" + }, + "sharing_mode": { + "type": "string", + "enum": [ + "off", + "private", + "global" + ], + "description": "The organization's cross-organization sharing mode. Only `global` compares networks across organizations; `private` and `off` keep them to the organization's own history.", + "example": "global" + }, + "cross_org_available": { + "type": "boolean", + "description": "Whether cross-organization signals apply to this organization. They need both the Identity Enterprise plan and the `global` sharing mode.", + "example": true + }, + "cross_org_unavailable_reason": { + "type": "string", + "enum": [ + "plan", + "sharing_mode" + ], + "nullable": true, + "description": "Why cross-organization signals do not apply, checked in this order. `plan`: the organization's Identity plan does not include the cross-organization fraud network (it needs Identity Enterprise), whatever the sharing mode. `sharing_mode`: the plan includes it, but the sharing mode is `private` or `off`. Null when `cross_org_available` is `true`.", + "example": null + } }, - "phone": { - "type": "string", - "description": "Phone number for phone-verification services." + "required": [ + "count", + "next", + "previous", + "results", + "sharing_mode", + "cross_org_available", + "cross_org_unavailable_reason" + ] + } + } + } + }, + "400": { + "description": "An invalid filter, ordering, pagination or `summary` value.", + "content": { + "application/json": { + "examples": { + "Invalid summary": { + "summary": "Invalid summary", + "value": { + "summary": [ + "Must be \"true\" or \"false\"." + ] + } }, - "landline": { - "type": "string", - "description": "Landline number for phone-verification services." + "Invalid scope": { + "summary": "Invalid scope", + "value": { + "scope": [ + "Use identity or transactions." + ] + } }, - "email": { - "type": "string", - "format": "email", - "description": "Email address for identity-verification services." + "Unsupported ordering": { + "summary": "Unsupported ordering", + "value": [ + "Unsupported ordering." + ] + } + } + } + } + }, + "401": { + "description": "Missing or invalid credentials.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "description": "Present on coded errors, for example `networks_not_entitled`." + } + } + }, + "examples": { + "Missing or invalid credentials": { + "summary": "Missing or invalid credentials", + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + } + } + } + } + }, + "403": { + "description": "The API key belongs to another application, or it is a key with restricted access: Networks endpoints need a full-access key. A console member token needs `list:networks`. Also returned with `code: networks_not_entitled` when Networks is not enabled for the organization.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "description": "Present on coded errors, for example `networks_not_entitled`." + } + } + }, + "examples": { + "Networks not enabled": { + "summary": "Networks not enabled", + "value": { + "detail": "Networks is disabled for this organization. Contact Didit support if you believe this is an error.", + "code": "networks_not_entitled" + } }, - "country_of_residence": { - "type": "string", - "description": "ISO 3166-1 alpha-2 country of residence. Used by global identity-enrichment services (e.g. `glb_identity_enrichment`) to focus the lookup.", - "example": "US" + "Restricted API key": { + "summary": "Restricted API key", + "value": { + "detail": "This API key has no access to this endpoint." + } }, - "partial_match_action": { - "type": "string", - "enum": [ - "DECLINE", - "NO_ACTION" - ], - "default": "NO_ACTION", - "description": "What a `partial_match` does to `database_validation.status`. Defaults to `NO_ACTION` (status stays `Approved`, with a warning)." + "No permission": { + "summary": "No permission", + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/": { + "get": { + "summary": "Get network", + "description": "Retrieve a single network's full detail: stats, fraud tags and risk signals, plus the organization's `sharing_mode` and cross-organization availability, your application's `tags` on the network, member `outcomes` and pattern scores (`patterns`). Requires a full-access API key, or `read:networks` for a console member.", + "operationId": "get_network", + "x-didit-sdk": { + "resource": "networks", + "method": "retrieve", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Networks" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "network_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "n3a5e000-0000-4000-8000-000000000001" + }, + "description": "Network UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/n3a5e000-0000-4000-8000-000000000001/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Network detail.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NetworkDetailResponse" + }, + "example": { + "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "network_id": "N-0042", + "name": null, + "status": "in_review", + "risk": { + "score": 82, + "band": "high", + "bar": 0.82 + }, + "pattern_types": [ + "exact_same_device", + "same_email" + ], + "signal_types": [ + "device", + "email" + ], + "member_counts": { + "total": 3, + "users": 3, + "businesses": 0, + "transaction_parties": 0 + }, + "member_status": { + "approved": 1, + "declined": 1, + "in_review": 0, + "not_completed": 1 + }, + "signal_coverage": { + "device": { + "signals": 1, + "subjects": 3, + "coverage": 1.0 + }, + "email": { + "signals": 1, + "subjects": 2, + "coverage": 0.6667 + } + }, + "first_activity_at": "2026-10-01T09:12:44Z", + "last_activity_at": "2026-10-12T16:03:10Z", + "stats": { + "members": { + "total": 3, + "users": 3, + "businesses": 0, + "transaction_parties": 0 + }, + "member_status": { + "approved": 1, + "declined": 1, + "in_review": 0, + "not_completed": 1 + }, + "signals": { + "total": 2, + "by_type": { + "device": { + "signals": 1, + "subjects": 3, + "coverage": 1.0 + }, + "email": { + "signals": 1, + "subjects": 2, + "coverage": 0.6667 + } + } + } }, - "no_match_action": { - "type": "string", - "enum": [ - "DECLINE", - "NO_ACTION" - ], - "default": "DECLINE", - "description": "What a `no_match` does to `database_validation.status`. Defaults to `DECLINE`." + "fraud_tags": [], + "risk_signals": { + "residential_proxy_reuse": { + "detected": false, + "signal_count": 0, + "max_subject_count": 0 + }, + "email_intelligence": { + "enriched_signal_count": 1, + "high_risk_count": 0, + "social_profile_count": 0 + }, + "phone_intelligence": { + "enriched_signal_count": 0, + "high_risk_count": 0 + } }, - "save_api_request": { - "type": "boolean", - "default": true, - "description": "Persist the validation as a session (console visibility, decision endpoint, `status.updated` webhook). Also changes the `validations` response shape: per-service objects when `true` (default), a merged field map when `false`." + "reviewed_at": "2026-10-12T16:30:00Z", + "reviewed_by": "reviewer@example.com", + "review_reason": "Shared device across three new accounts.", + "notes_count": 0, + "sharing_mode": "global", + "cross_org_available": true, + "cross_org_unavailable_reason": null, + "tags": [ + { + "uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "name": "SUSPICIOUS", + "color": "#6366F1", + "source": "custom", + "added_by": null, + "added_at": "2026-10-13T08:15:00Z" + } + ], + "outcomes": { + "approved": 1, + "declined": 1, + "in_review": 0, + "total": 3 }, - "vendor_data": { - "type": "string", - "description": "Your identifier for the validated user; echoed back and stored with the session." + "patterns": [ + { + "type": "background", + "score": 0.8125, + "members": 3, + "reason": null, + "computed_at": "2026-10-13T08:00:00Z" + } + ] + } + } + } + }, + "404": { + "description": "Network not found (or not visible to this application).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + }, + "401": { + "description": "Missing or invalid credentials.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "description": "Present on coded errors, for example `networks_not_entitled`." + } + } + }, + "examples": { + "Missing or invalid credentials": { + "summary": "Missing or invalid credentials", + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + } + } + } + } + }, + "403": { + "description": "The API key belongs to another application, or it is a key with restricted access: Networks endpoints need a full-access key. A console member token needs `read:networks`. Also returned with `code: networks_not_entitled` when Networks is not enabled for the organization.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "description": "Present on coded errors, for example `networks_not_entitled`." + } + } + }, + "examples": { + "Networks not enabled": { + "summary": "Networks not enabled", + "value": { + "detail": "Networks is disabled for this organization. Contact Didit support if you believe this is an error.", + "code": "networks_not_entitled" + } }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Free-form JSON stored with the request and echoed back." + "Restricted API key": { + "summary": "Restricted API key", + "value": { + "detail": "This API key has no access to this endpoint." + } }, - "selfie": { - "type": "string", - "format": "binary", - "description": "Selfie image (`jpg`, `jpeg`, `png`, `webp`; max 2 MB — images are compressed to ~1.5 MB before upload to the registry) for biometric validation services, e.g. Argentina RENAPER (`arg_renaper`) and Panama SIB (`pan_cedula_sib`, `pan_cedula_sib_plus`). Required whenever a selected service lists it as a required field. Only available via `multipart/form-data`." + "No permission": { + "summary": "No permission", + "value": { + "detail": "You do not have permission to perform this action." + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/graph/": { + "get": { + "summary": "Get network graph", + "description": "Retrieve the network as a graph of subject nodes (vendor users, vendor businesses, transaction parties) and signal nodes (the shared device/IP/face/document/phone/email/address values linking them), with edges representing memberships.", + "operationId": "get_network_graph", + "x-didit-sdk": { + "resource": "networks", + "method": "retrieve_graph", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Networks" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "network_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "n3a5e000-0000-4000-8000-000000000001" + }, + "description": "Network UUID." + }, + { + "name": "depth", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "1", + "2", + "3", + "all" + ] + }, + "description": "Hops from the focus subject to include. Defaults to all (unbounded)." + }, + { + "name": "focus_kind", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "vendor_user", + "vendor_business", + "transaction_party" + ] + }, + "description": "Subject type to center the graph on. Must be supplied together with focus_id; defaults to an arbitrary member when depth is bounded and neither is supplied." + }, + { + "name": "focus_id", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Subject UUID to center the graph on." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/n3a5e000-0000-4000-8000-000000000001/graph/?depth=2' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Network graph.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NetworkGraph" + } + } + } + }, + "404": { + "description": "Network not found (or not visible to this application).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } } } } } } }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/members/": { + "get": { + "summary": "List network members", + "description": "List the subjects (vendor users, vendor businesses, or transaction parties) that belong to this network.", + "operationId": "list_network_members", + "x-didit-sdk": { + "resource": "networks", + "method": "list_members", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Networks" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "network_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "n3a5e000-0000-4000-8000-000000000001" + }, + "description": "Network UUID." + } + ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/database-validation/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"issuing_state\": \"BRA\",\n \"services\": [\"bra_cpf\"],\n \"identification_number\": \"12345678900\",\n \"first_name\": \"John\",\n \"last_name\": \"Doe\",\n \"date_of_birth\": \"1980-01-01\",\n \"vendor_data\": \"user-1234\"\n }'" + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/n3a5e000-0000-4000-8000-000000000001/members/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Network members. Not paginated.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkMember" + } + } + } + } + } + } + }, + "404": { + "description": "Network not found (or not visible to this application).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/signals/": { + "get": { + "summary": "List network signals", + "description": "List the signals (shared device, IP, face, document number, phone, email, or address values) that link this network's members.", + "operationId": "list_network_signals", + "x-didit-sdk": { + "resource": "networks", + "method": "list_signals", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Networks" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, { - "lang": "python", - "label": "Python", - "source": "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/database-validation/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\n \"issuing_state\": \"BRA\",\n \"services\": [\"bra_cpf\"],\n \"identification_number\": \"12345678900\",\n \"first_name\": \"John\",\n \"last_name\": \"Doe\",\n \"date_of_birth\": \"1980-01-01\",\n \"vendor_data\": \"user-1234\",\n },\n timeout=45,\n)\nresp.raise_for_status()\ndv = resp.json()[\"database_validation\"]\nprint(dv[\"status\"], dv[\"match_type\"]) # e.g. Approved full_match\n\n# Biometric services need a selfie via multipart/form-data:\n# requests.post(..., data={\"issuing_state\": \"ARG\", \"services\": '[\"arg_renaper\"]',\n# \"identification_number\": \"12345678\", \"gender\": \"M\", ...},\n# files={\"selfie\": open(\"selfie.jpg\", \"rb\")})" + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const res = await fetch('https://verification.didit.me/v3/database-validation/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n issuing_state: 'BRA',\n services: ['bra_cpf'],\n identification_number: '12345678900',\n first_name: 'John',\n last_name: 'Doe',\n date_of_birth: '1980-01-01',\n vendor_data: 'user-1234',\n }),\n});\nconst data = await res.json();\nconsole.log(data.database_validation.status, data.database_validation.match_type);" + "name": "network_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "n3a5e000-0000-4000-8000-000000000001" + }, + "description": "Network UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/n3a5e000-0000-4000-8000-000000000001/signals/' \\\n -H 'x-api-key: YOUR_API_KEY'" } ], "responses": { "200": { - "description": "Validation completed — at least one selected service returned a usable result. Inspect `database_validation.match_type` for the aggregate outcome, `validations` for the per-service field comparisons and `outcome_code`s, and `errors` (when present) for services that failed.", + "description": "Network signals. Not paginated.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "request_id": { - "type": "string", - "format": "uuid", - "description": "Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID." - }, - "database_validation": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "Approved", - "Declined", - "In Review" - ], - "description": "`Declined` when the overall `match_type` triggers a `DECLINE` action (`no_match_action` defaults to `DECLINE`, `partial_match_action` to `NO_ACTION`); `In Review` when no service produced a usable comparison, for example when the only selected service came back `REGISTRY_UNAVAILABLE` or `INCONCLUSIVE`; `Approved` otherwise. A request that selected several services can be `Approved` on one service's match while another was unavailable, so check `errors` and the per-service `outcome_code`s too." - }, - "issuing_state": { - "type": "string", - "description": "Echo of the ISO 3166-1 alpha-3 country validated against.", - "example": "BRA" - }, - "validation_type": { - "type": "string", - "enum": [ - "one_by_one", - "two_by_two" - ], - "description": "Derived from the results: `two_by_two` when ≥2 distinct services produced a full match (on the identification number, on full name + date of birth together, or on the address without contradictions); `one_by_one` otherwise. (The request field of the same name is deprecated and ignored.)" - }, - "screened_data": { - "type": "object", - "description": "Echo of the input fields that were sent to the selected services (dates normalized to `YYYY-MM-DD`; a `selfie`, when sent, is returned as a pre-signed URL; `consent_obtained: true` is added when consent was given)." - }, - "match_type": { - "type": "string", - "nullable": true, - "enum": [ - "full_match", - "partial_match", - "no_match" - ], - "description": "Overall result aggregated across services with field-level comparisons: any `full_match` → `full_match`; else any `partial_match` → `partial_match`; else `no_match`. Outcome-only services, such as biometric criminal screening, can return `null`; inspect each item in `validations` for its `outcome_code`." - }, - "validations": { - "description": "Shape depends on `save_api_request`. **`true` (default):** an array of per-service objects (`service_id`, `service_name`, `validation`, `outcome_code`, `outcome_detail`, `source_data`). **`false`:** a single flat object merging the field-level results of all services (`{\"full_name\": \"full_match\", ...}`).", - "oneOf": [ - { - "type": "array", - "items": { - "type": "object", - "description": "Per-service validation result (returned when `save_api_request=true`, the default).", - "properties": { - "service_id": { - "type": "string", - "description": "Catalog id of the service that produced this result (e.g. `bra_cpf`).", - "example": "bra_cpf" - }, - "service_name": { - "type": "string", - "description": "Human-readable catalog name of the service.", - "example": "Brazil - CPF status check" - }, - "validation": { - "type": "object", - "description": "Field-by-field comparison between your input and the registry record. Keys are canonical field names (`full_name`, `date_of_birth`, `identification_number`, `address`, …); values are `full_match`, `partial_match`, or `no_match`.", - "additionalProperties": { - "type": "string", - "enum": [ - "full_match", - "partial_match", - "no_match" - ] - } - }, - "outcome_code": { - "type": "string", - "enum": [ - "MATCH", - "PARTIAL_MATCH", - "NO_MATCH", - "DOCUMENT_NOT_FOUND", - "INVALID_DOCUMENT_FORMAT", - "INVALID_INPUT", - "MINOR_BLOCKED", - "DECEASED", - "BIOMETRIC_NO_MATCH", - "BIOMETRIC_IMAGE_UNUSABLE", - "INCONCLUSIVE", - "DOCUMENT_SUPERSEDED", - "REGISTRY_UNAVAILABLE", - "REGISTRY_ERROR" - ], - "description": "Vendor-neutral outcome of the registry lookup. `MATCH`/`PARTIAL_MATCH`/`NO_MATCH` describe the comparison; `DOCUMENT_NOT_FOUND` means the identifier is not in the registry; `INCONCLUSIVE` means the registry could not confirm either way (routed to review — NOT a no-match); `BIOMETRIC_NO_MATCH` is a definitive face mismatch while `BIOMETRIC_IMAGE_UNUSABLE` is a technical image problem (retake the selfie); `DECEASED`/`MINOR_BLOCKED` are registry-policy outcomes; `DOCUMENT_SUPERSEDED` means the identity matched but the edition of the document presented is not the one on file (a newer edition has been issued); `INVALID_DOCUMENT_FORMAT`/`INVALID_INPUT` mean the registry itself rejected the data it was sent; `REGISTRY_UNAVAILABLE`/`REGISTRY_ERROR` indicate upstream failures." - }, - "outcome_detail": { - "type": "string", - "nullable": true, - "description": "Raw upstream status detail backing the outcome code (e.g. an HTTP or registry status code)." - }, - "source_data": { - "type": "object", - "nullable": true, - "description": "Cleaned, normalized view of the registry record using canonical field names (`identification_number`, `first_name`, `last_name`, `date_of_birth`, plus registry-specific flags). Image fields, when present, are returned as pre-signed URLs." - } - } - } - }, - { - "type": "object", - "additionalProperties": { - "type": "string", - "enum": [ - "full_match", - "partial_match", - "no_match" - ] - }, - "description": "Merged field-level results (stateless `save_api_request=false` shape)." - } - ] - }, - "warnings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "description": "Feature that raised the warning." - }, - "risk": { - "type": "string", - "description": "Machine-readable risk identifier." - }, - "additional_data": { - "type": "object", - "nullable": true, - "description": "Extra context for the warning, when available." - }, - "log_type": { - "type": "string", - "description": "`warning`, `information`, or `error` — how the risk affected the outcome." - }, - "short_description": { - "type": "string", - "description": "One-line human-readable summary." - }, - "long_description": { - "type": "string", - "description": "Full human-readable explanation." - } - } - }, - "description": "`DATABASE_VALIDATION_NO_MATCH` or `DATABASE_VALIDATION_PARTIAL_MATCH` when applicable; empty on a full match." - }, - "errors": { - "type": "array", - "description": "Present only when some (but not all) selected services failed. Each entry carries `service_id`, `code`, `message`, and `retryable`. A service reported here is absent from `validations` and `services_used`, and is not billed. This array is the only place it appears, so inspect it even when `status` is `Approved`.", - "items": { - "type": "object", - "properties": { - "service_id": { - "type": "string" - }, - "code": { - "type": "string", - "example": "empty_provider_response" - }, - "message": { - "type": "string" - }, - "retryable": { - "type": "boolean", - "description": "`true` when the service produced no answer and retrying it later can succeed; `false` when the source refused the data it was sent and the input must be corrected first.", - "example": true - } - } - } - }, - "services_used": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Catalog ids of the services that returned a billable result. **Only present when the request explicitly sent `services`.**" - }, - "match_score": { - "type": "number", - "description": "Fraction (0.0–1.0) of attempted services that produced a full match (on the identification number, on full name + date of birth together, or on the address without contradictions). **Only present when the request explicitly sent `services`.**" - } + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkSignal" } + } + } + } + } + } + }, + "404": { + "description": "Network not found (or not visible to this application).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/timeline/": { + "get": { + "summary": "Get network timeline", + "description": "Without `contract`: the network's own events (detection, each signal's latest observation and its review), oldest first and not paginated. `limit`, `cursor` and `event_type` are ignored. With `contract=2`: the typed timeline, newest first and paginated by cursor. It adds what the member accounts did (verification attempts, transactions, sign-ins you post through the events API and the live rules they hit) and the cases opened for the network. `note_added` events are returned to console members only, never to an API key. Requires a full-access API key, or `read:networks` for a console member.", + "operationId": "get_network_timeline", + "x-didit-sdk": { + "resource": "networks", + "method": "retrieve_timeline", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Networks" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "network_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "n3a5e000-0000-4000-8000-000000000001" + }, + "description": "Network UUID." + }, + { + "name": "contract", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "2" + ] + }, + "description": "`2` returns the typed timeline. Omit it for the default timeline. Any other value returns 400." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 100 + }, + "description": "Typed timeline only. Events per page, 1 to 500. Defaults to 100." + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Typed timeline only. The previous page's `next_cursor`. An invalid or empty cursor returns 400." + }, + { + "name": "event_type", + "in": "query", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "array", + "maxItems": 20, + "items": { + "type": "string", + "enum": [ + "network_detected", + "signal_observed", + "network_active", + "network_in_review", + "network_resolved", + "network_dismissed", + "verification_attempt", + "transaction", + "login", + "rule_hit", + "case_linked", + "note_added" + ] + } + }, + "description": "Typed timeline only. Event types to keep, repeated or comma-separated, at most 20 values. Defaults to every type." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/n3a5e000-0000-4000-8000-000000000001/timeline/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "curl", + "label": "curl (typed timeline)", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/3fa85f64-5717-4562-b3fc-2c963f66afa6/timeline/?contract=2&limit=50&event_type=verification_attempt,login' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Without `contract`: `{results}`, oldest first, not paginated. With `contract=2`: `{results, next_cursor}`, newest first.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/NetworkTimeline" }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Echo of the `vendor_data` you sent." - }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Echo of the `metadata` you sent." - }, - "created_at": { - "type": "string", - "format": "date-time" + { + "$ref": "#/components/schemas/NetworkTimelinePage" } - } + ] }, "examples": { - "Full match (Approved)": { - "summary": "Default save_api_request=true — per-service `validations` objects", + "Default timeline": { + "summary": "Without contract", "value": { - "request_id": "7f9a1c0e-1f2b-4f6e-9d3a-2b1c0e7f9a1c", - "database_validation": { - "status": "Approved", - "issuing_state": "BRA", - "validation_type": "one_by_one", - "screened_data": { - "tax_number": "12345678900", - "first_name": "John", - "last_name": "Doe", - "date_of_birth": "1980-01-01" - }, - "match_type": "full_match", - "validations": [ - { - "validation": { - "full_name": "full_match", - "date_of_birth": "full_match", - "identification_number": "full_match" - }, - "outcome_code": "MATCH", - "outcome_detail": "200", - "service_id": "bra_cpf", - "service_name": "Brazil - CPF status check", - "source_data": { - "identification_number": "12345678900", - "first_name": "JOHN", - "last_name": "DOE", - "date_of_birth": "1980-01-01", - "lgpd_minor": false, - "minor_under_16": false, - "minor_under_18": false - } + "results": [ + { + "event_type": "network_detected", + "occurred_at": "2026-10-01T09:12:44Z", + "label": "Network detected", + "metadata": { + "network_id": "N-0042" } - ], - "warnings": [] - }, - "vendor_data": "user-1234", - "metadata": null, - "created_at": "2026-06-11T10:30:00.000000+00:00" - } - }, - "Explicit services (extended shape)": { - "summary": "Request sent `services` — response adds `services_used` and `match_score`", - "value": { - "request_id": "b3e7c1a2-9d4f-4f0b-8a6c-5e2d1f0a9b3e", - "database_validation": { - "status": "Approved", - "issuing_state": "BRA", - "validation_type": "one_by_one", - "screened_data": { - "tax_number": "12345678900", - "first_name": "John", - "last_name": "Doe", - "date_of_birth": "1980-01-01" }, - "match_type": "full_match", - "validations": [ - { - "validation": { - "full_name": "full_match", - "date_of_birth": "full_match", - "identification_number": "full_match" - }, - "outcome_code": "MATCH", - "outcome_detail": "200", - "service_id": "bra_cpf", - "service_name": "Brazil - CPF status check", - "source_data": { - "identification_number": "12345678900", - "first_name": "JOHN", - "last_name": "DOE", - "date_of_birth": "1980-01-01", - "lgpd_minor": false, - "minor_under_16": false, - "minor_under_18": false - } + { + "event_type": "signal_observed", + "occurred_at": "2026-10-12T16:03:10Z", + "label": "device observed", + "metadata": { + "signal_type": "device", + "subject_count": 3, + "is_hub": false } - ], - "warnings": [], - "services_used": [ - "bra_cpf" - ], - "match_score": 1.0 - }, - "vendor_data": "user-1234", - "metadata": null, - "created_at": "2026-06-11T10:30:00.000000+00:00" - } - }, - "No match (Declined)": { - "summary": "Registry record does not match — default `no_match_action=DECLINE`", - "value": { - "request_id": "0a1b2c3d-4e5f-6789-abcd-ef0123456789", - "database_validation": { - "status": "Declined", - "issuing_state": "BRA", - "validation_type": "one_by_one", - "screened_data": { - "tax_number": "12345678900", - "first_name": "John", - "last_name": "Doe", - "date_of_birth": "1980-01-01" }, - "match_type": "no_match", - "validations": [ - { - "validation": { - "full_name": "no_match", - "date_of_birth": "no_match", - "identification_number": "no_match" - }, - "outcome_code": "NO_MATCH", - "outcome_detail": "200", - "service_id": "bra_cpf", - "service_name": "Brazil - CPF status check", - "source_data": { - "identification_number": "00987654321", - "first_name": "TOTALLY", - "last_name": "DIFFERENT NAME", - "date_of_birth": "1999-01-01", - "lgpd_minor": false, - "minor_under_16": false, - "minor_under_18": false - } - } - ], - "warnings": [ - { - "feature": "DATABASE_VALIDATION", - "risk": "DATABASE_VALIDATION_NO_MATCH", - "additional_data": null, - "log_type": "error", - "short_description": "Database validation no match", - "long_description": "The system identified a no match in the database validation, requiring further investigation." + { + "event_type": "network_in_review", + "occurred_at": "2026-10-12T16:30:00Z", + "label": "Network in_review", + "metadata": { + "reason": "Shared device across three new accounts." } - ] - }, - "vendor_data": null, - "metadata": null, - "created_at": "2026-06-11T10:31:00.000000+00:00" + } + ] } }, - "Stateless (save_api_request=false)": { - "summary": "`validations` collapses to a merged field map; nothing is stored", + "Typed timeline": { + "summary": "With contract=2", "value": { - "request_id": "c4d5e6f7-8901-2345-6789-abcdef012345", - "database_validation": { - "status": "Approved", - "issuing_state": "BRA", - "validation_type": "one_by_one", - "screened_data": { - "tax_number": "12345678900", - "first_name": "John", - "last_name": "Doe", - "date_of_birth": "1980-01-01" + "results": [ + { + "id": "monitoring_event:9b2f1c3e-4d5a-4b6c-8d7e-0f1a2b3c4d5e", + "event_type": "login", + "occurred_at": "2026-10-12T18:41:02Z", + "ingested_at": "2026-10-12T18:41:03Z", + "label": "Login", + "metadata": {}, + "account": { + "node_id": "vendor_user:5f6e7d8c-9b0a-4c1d-8e2f-3a4b5c6d7e8f", + "node_type": "account", + "uuid": "5f6e7d8c-9b0a-4c1d-8e2f-3a4b5c6d7e8f", + "vendor_data": "user-1042", + "display_name": "Alex Example", + "ownership": "owned", + "route": { + "kind": "user", + "id": "5f6e7d8c-9b0a-4c1d-8e2f-3a4b5c6d7e8f", + "application_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + } + }, + "source_ref": { + "kind": "monitoring_event", + "id": "9b2f1c3e-4d5a-4b6c-8d7e-0f1a2b3c4d5e", + "ownership": "owned" + }, + "verification_attempt": null, + "transaction": null, + "login": { + "action": "auth.login", + "succeeded": true, + "status": "APPROVED", + "ip": { + "address": "203.0.113.24", + "country_code": "ES", + "city": "Madrid", + "connection_type": null, + "is_vpn_or_tor": false, + "is_data_center": false + }, + "device": { + "fingerprint": "fp_0a1b2c3d", + "label": "Chrome · Android" + }, + "is_first_login": false, + "is_new_device": true + }, + "rule_hit": null, + "case_linked": null, + "note_added": null }, - "match_type": "full_match", - "validations": { - "full_name": "full_match", - "date_of_birth": "full_match", - "identification_number": "full_match" + { + "id": "session:1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f", + "event_type": "verification_attempt", + "occurred_at": "2026-10-12T16:03:10Z", + "ingested_at": "2026-10-12T16:03:10Z", + "label": "Verification attempt 2 of 2", + "metadata": {}, + "account": { + "node_id": "vendor_user:5f6e7d8c-9b0a-4c1d-8e2f-3a4b5c6d7e8f", + "node_type": "account", + "uuid": "5f6e7d8c-9b0a-4c1d-8e2f-3a4b5c6d7e8f", + "vendor_data": "user-1042", + "display_name": "Alex Example", + "ownership": "owned", + "route": { + "kind": "user", + "id": "5f6e7d8c-9b0a-4c1d-8e2f-3a4b5c6d7e8f", + "application_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + } + }, + "source_ref": { + "kind": "session", + "id": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f", + "ownership": "owned", + "route": { + "kind": "session", + "id": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f", + "application_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + } + }, + "verification_attempt": { + "session_id": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f", + "session_number": 1287, + "attempt": 2, + "attempt_count": 2, + "status": "Approved", + "status_changed_at": "2026-10-12T16:05:41Z", + "decline_reasons": [] + }, + "transaction": null, + "login": null, + "rule_hit": null, + "case_linked": null, + "note_added": null }, - "warnings": [] - }, - "vendor_data": null, - "metadata": null, - "created_at": "2026-06-11T10:32:00.000000+00:00" + { + "id": "signal_observed:8d7c6b5a-4e3f-4a2b-9c1d-0e9f8a7b6c5d", + "event_type": "signal_observed", + "occurred_at": "2026-10-12T16:03:10Z", + "ingested_at": "2026-10-01T09:12:44Z", + "label": "device observed", + "metadata": { + "signal_type": "device", + "subject_count": 3, + "is_hub": false + }, + "account": null, + "source_ref": null, + "verification_attempt": null, + "transaction": null, + "login": null, + "rule_hit": null, + "case_linked": null, + "note_added": null + } + ], + "next_cursor": "eyJ0IjoiMjAyNi0xMC0xMlQxNjowMzoxMCswMDowMCIsImMiOiIyMDI2LTEwLTAxVDA5OjEyOjQ0KzAwOjAwIiwiciI6MSwidSI6IjhkN2M2YjVhLTRlM2YtNGEyYi05YzFkLTBlOWY4YTdiNmM1ZCJ9" } } } @@ -20729,183 +42593,384 @@ } }, "400": { - "description": "Validation error - nothing is billed. Preflight field errors occur before any external call. If every unanswered attempt explicitly refuses the input, the response instead includes `validation_errors` with `code: provider_rejected_input` and `retryable: false`; correct the input before retrying. Field-level problems use DRF's standard envelope; catalog problems return a `services` array (plus `requires_onboarding` or `services_requiring_consent` when relevant); missing-field errors include `fields_required_by` mapping each missing field to the services that demand it; services that need at least one extra identifier (e.g. `usa_states_credit_bureau`, which cannot verify a name alone) return `any_field_required_by` mapping each service to the fields of which at least one must be present; format problems checked per service return `invalid_fields_by_service`.", + "description": "With `contract` present: an invalid `contract`, `limit`, `cursor` or `event_type` value.", "content": { "application/json": { "examples": { - "Invalid issuing_state": { - "summary": "Country not supported — message lists every valid alpha-3 code", + "Unsupported contract": { + "summary": "Unsupported contract", "value": { - "error": [ - "Invalid issuing state. Valid options are: ARE, ARG, AUS, AUT, BEL, BGD, BOL, BRA, CAN, CHE, CHL, CHN, CIV, COL, CRI, CZE, DEU, DNK, DOM, ECU, ESP, FIN, FRA, GBR, GHA, GLB, GRC, GTM, HKG, HND, IDN, IND, IRL, ITA, KEN, KHM, MAR, MEX, MYS, NGA, NLD, NOR, NZL, OMN, PAN, PER, PHL, POL, PRT, PRY, QAT, SGP, SLV, SVK, SWE, THA, UGA, URY, USA, VEN, ZAF, ZMB, ZWE" + "contract": [ + "\"3\" is not a valid choice." ] } }, - "Missing required fields": { - "summary": "Union of required fields across selected services, with the universal-field hint", - "value": { - "error": [ - "Missing required fields: tax_number (or use 'identification_number' which maps to 'tax_number' for BRA)" - ], - "fields_required_by": { - "tax_number": [ - "bra_cpf" - ] - } - } - }, - "Unknown service id": { - "summary": "`services` contains an id that is not in the catalog", + "Invalid cursor": { + "summary": "Invalid cursor", "value": { - "services": [ - "Unknown service_id(s): ['bogus_service']" + "cursor": [ + "This cursor is not valid. Reload the feed." ] } }, - "Service not live for country": { - "summary": "`services` names a service from another country (or a stub entry)", + "Limit too large": { + "summary": "Limit too large", "value": { - "services": [ - "Services not currently live for PER: ['bra_cpf']. Available services: ['per_dni', 'per_residential', 'per_tax_registration']" + "limit": [ + "Ensure this value is less than or equal to 500." ] } }, - "Service requires onboarding": { - "summary": "Service exists but is not activated for your organization yet", + "Unknown event type": { + "summary": "Unknown event type", "value": { - "services": [ - "Services require onboarding before they can be enabled for AUS: ['aus_australia_driver_licence']. Contact Didit support to activate them." - ], - "requires_onboarding": [ - "aus_australia_driver_licence" - ] + "event_type": { + "0": [ + "\"logout\" is not a valid choice." + ] + } } - }, - "Consent missing": { - "summary": "A selected service is flagged requires_consent=true and `consent` was not `true`", + } + } + } + } + }, + "401": { + "description": "Missing or invalid credentials.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "description": "Present on coded errors, for example `networks_not_entitled`." + } + } + }, + "examples": { + "Missing or invalid credentials": { + "summary": "Missing or invalid credentials", "value": { - "consent": [ - "Explicit end-user consent is required for these database validation services: ['chn_national_id']. Send `consent=true`." - ], - "services_requiring_consent": [ - "chn_national_id" - ] + "detail": "Authentication credentials were not provided or are invalid." } - }, - "Country format rule violation": { - "summary": "Identifier fails the country's format rule (length/digits/regex)", + } + } + } + } + }, + "403": { + "description": "The API key belongs to another application, or it is a key with restricted access: Networks endpoints need a full-access key. A console member token needs `read:networks`. Also returned with `code: networks_not_entitled` when Networks is not enabled for the organization.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "description": "Present on coded errors, for example `networks_not_entitled`." + } + } + }, + "examples": { + "Networks not enabled": { + "summary": "Networks not enabled", "value": { - "tax_number": [ - "Invalid tax number for BRA: Brazilian CPF (exactly 11 digits). Must be exactly 11 characters long (got 10)." - ] + "detail": "Networks is disabled for this organization. Contact Didit support if you believe this is an error.", + "code": "networks_not_entitled" } }, - "Service-specific format violation": { - "summary": "A field fails the selected service's own format requirements", + "Restricted API key": { + "summary": "Restricted API key", "value": { - "error": [ - "One or more fields do not match the format required by the selected service." - ], - "invalid_fields_by_service": { - "chn_national_id": [ - "national_id_format" - ] - } + "detail": "This API key has no access to this endpoint." } }, - "Input refused by every attempt": { - "summary": "Correct the input before retrying", + "No permission": { + "summary": "No permission", "value": { - "error": "The source refused to run this query with the supplied input.", - "validation_errors": [ - { - "service_id": "bra_cpf", - "code": "provider_rejected_input", - "message": "The source refused to run this query with the supplied input.", - "retryable": false - } - ] + "detail": "You do not have permission to perform this action." } } - }, + } + } + } + }, + "404": { + "description": "Network not found (or not visible to this application).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/map/": { + "get": { + "summary": "Get network map", + "description": "Geographic points where this network's members' addresses/locations converge, sourced from the sessions, business sessions, and transactions backing its memberships.", + "operationId": "get_network_map", + "x-didit-sdk": { + "resource": "networks", + "method": "retrieve_map", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Networks" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "network_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "example": "n3a5e000-0000-4000-8000-000000000001" + }, + "description": "Network UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/n3a5e000-0000-4000-8000-000000000001/map/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Network map.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NetworkMap" + } + } + } + }, + "404": { + "description": "Network not found (or not visible to this application).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/sessions/{session_id}/networks/": { + "get": { + "summary": "List networks for a session", + "description": "List the networks the session's vendor user belongs to. Resolves through the session's vendor_data-linked user.", + "operationId": "list_session_network_memberships", + "x-didit-sdk": { + "resource": "sessions", + "method": "list_networks", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Networks" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Session UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/sessions/aaaaaaaa-0000-4000-8000-000000000001/networks/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Networks this entity belongs to. Not paginated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EntityNetworkMembershipList" + } + } + } + }, + "404": { + "description": "Entity not found in this application, or has no linked subject to look up.", + "content": { + "application/json": { "schema": { "type": "object", - "additionalProperties": true, "properties": { - "error": { - "description": "Request-level explanation or preflight field errors.", - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "validation_errors": { - "type": "array", - "description": "Present when every unanswered attempt refused the input after preflight.", - "items": { - "type": "object", - "properties": { - "service_id": { - "type": "string" - }, - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "retryable": { - "type": "boolean", - "description": "Whether retrying later can resolve this service failure. False means correct the supplied input first." - } - } - } + "detail": { + "type": "string" } } } } } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/business-sessions/{business_session_id}/networks/": { + "get": { + "summary": "List networks for a business session", + "description": "List the networks the business session's vendor business belongs to.", + "operationId": "list_business_session_network_memberships", + "x-didit-sdk": { + "resource": "business_sessions", + "method": "list_networks", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Networks" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, - "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any screening or provider call happens.", + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "business_session_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Business session UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/business-sessions/aaaaaaaa-0000-4000-8000-000000000002/networks/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Networks this entity belongs to. Not paginated.", "content": { "application/json": { - "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", - "value": { - "detail": "You do not have permission to perform this action." - } - }, - "Not enough credits": { - "summary": "Organization balance cannot cover the call", - "value": { - "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" - } - } + "schema": { + "$ref": "#/components/schemas/EntityNetworkMembershipList" } } } }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "404": { + "description": "Entity not found in this application, or has no linked subject to look up.", "content": { "application/json": { - "examples": { - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." - } - } - }, "schema": { "type": "object", "properties": { @@ -20916,451 +42981,701 @@ } } } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/users/{vendor_user_uuid}/networks/": { + "get": { + "summary": "List networks for a user", + "description": "List the networks a vendor user belongs to.", + "operationId": "list_user_network_memberships", + "x-didit-sdk": { + "resource": "users", + "method": "list_networks", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Networks" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, - "502": { - "description": "No selected service returned a usable result. Inspect each validation_errors entry before retrying; HTTP 502 alone does not establish a transient failure. An empty_provider_response with retryable: true indicates unavailable or unexplained results, including a refused primary followed by an unavailable fallback. Legacy provider_invalid_input and provider_validation_error entries instead require corrected input and may omit retryable. Nothing is billed. When save_api_request=true, the failed attempt is recorded as a session with status Not Finished. Partial failures return 200 with database_validation.errors.", + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "vendor_user_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Vendor user UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/users/aaaaaaaa-0000-4000-8000-000000000003/networks/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Networks this entity belongs to. Not paginated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EntityNetworkMembershipList" + } + } + } + }, + "404": { + "description": "Entity not found in this application, or has no linked subject to look up.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "error": { + "detail": { "type": "string" - }, - "validation_errors": { - "type": "array", - "items": { - "type": "object", - "properties": { - "service_id": { - "type": "string" - }, - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "retryable": { - "type": "boolean", - "description": "Whether retrying later can resolve this service failure. False means correct the supplied input first." - } - } - } - } - } - }, - "examples": { - "Provider returned no result": { - "summary": "Every selected service failed", - "value": { - "error": "Database validation could not be performed. The external validation service did not return a result. Please try again later.", - "validation_errors": [ - { - "service_id": "bra_cpf", - "code": "empty_provider_response", - "message": "The provider did not return a database validation result.", - "retryable": true - } - ] } } } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/phone/send/": { - "post": { - "summary": "Send Phone Code", - "description": "Send a one-time passcode (OTP) to a phone number over WhatsApp, SMS, or another supported channel, then verify it with [`POST /v3/phone/check/`](/standalone-apis/phone-check).\n\n**How send and check pair up.** Verification state is keyed by your application plus the E.164 `phone_number` — the check call does not take `request_id`. Each new send creates a pending verification that lives for **5 minutes**: call the check with the same number and the code the user received before the window closes. Calling send again for the same number while a verification is pending re-sends a code for that same verification (`status: \"Retry\"`, same `request_id`). At most one retry is attached this way (two sends total); a further send starts a fresh verification with a new `request_id`. The 5-minute window is measured from the first send and is not extended by retries.\n\n**Delivery channels.** `options.preferred_channel` selects the channel (`whatsapp` by default; `sms`, `telegram`, `voice`, `rcs`, `viber`, and `zalo` are also supported). When the preferred channel is unavailable for the destination country or number, delivery automatically falls back to SMS — the send still reports `Success`, and the channel actually used is reported by the check response (`phone.verification_method` and the `lifecycle` events).\n\n**Branding.** SMS codes carry your application's SMS sender name, set in the Business Console, at no extra cost. Pass `options.sms_sender_name` to sign one code with another name, or `options.use_white_label_customization: true` to fall back to your white-label public name when no sender name is set. WhatsApp codes use a fixed message and are not branded.\n\n**Send statuses.** `Success` — OTP sent for a new verification; `Retry` — OTP re-sent for the pending verification; `Blocked` — the anti-fraud layer refused to send (see `reason`, e.g. `repeated_attempts`, `suspicious`, `spam`). A `Blocked` send immediately finalizes the verification as `Declined` with a high-risk warning, so a follow-up check returns `Expired or Not Found`.\n\n**Billing.** The price depends on the destination country and the channel that actually delivers the message — see [Phone Verification Pricing](/getting-started/phone-verification-pricing). Only `Blocked` sends are guaranteed free: any other unbilled send attempt is charged at verification finalization or expiration, even if delivery was never confirmed or the delivery provider reported the message undeliverable. Checks are always free. As an anti-abuse measure, phone verification is disabled until your organization's first top-up (`403`).\n\n**Session persistence.** Every new verification is persisted as an API-type session: `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the verification appears in the Business Console, and `status.updated` webhooks fire as it progresses.\n\n**Sandbox.** Sandbox API keys skip delivery and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Success` payload with a random `request_id`; no message is sent and nothing is persisted. Use code `123456` on the sandbox check.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limits.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints, plus an anti-abuse cap of 4 send attempts per phone number per hour; exceeding either returns `429`.", - "operationId": "post_v3phone_send", + "/v3/organization/{organization_id}/application/{application_id}/businesses/{vendor_business_uuid}/networks/": { + "get": { + "summary": "List networks for a business", + "description": "List the networks a vendor business belongs to.", + "operationId": "list_business_network_memberships", + "x-didit-sdk": { + "resource": "businesses", + "method": "list_networks", + "auth": "api_key", + "server": true, + "scope": "application" + }, "tags": [ - "Standalone APIs" + "Networks" ], - "x-codeSamples": [ + "parameters": [ { - "lang": "curl", - "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/phone/send/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"phone_number\": \"+14155552671\",\n \"options\": {\n \"code_size\": 6,\n \"preferred_channel\": \"whatsapp\",\n \"locale\": \"en-US\"\n },\n \"vendor_data\": \"user-1234\"\n }'" + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, { - "lang": "python", - "label": "Python", - "source": "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/phone/send/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n },\n json={\n \"phone_number\": \"+14155552671\",\n \"options\": {\n \"code_size\": 6,\n \"preferred_channel\": \"whatsapp\",\n \"locale\": \"en-US\",\n },\n \"vendor_data\": \"user-1234\",\n },\n timeout=15,\n)\nresp.raise_for_status()\nprint(resp.json()) # {request_id, status, reason, vendor_data, metadata}\n# Then verify with POST /v3/phone/check/ using the same phone_number" + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const res = await fetch('https://verification.didit.me/v3/phone/send/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n phone_number: '+14155552671',\n options: { code_size: 6, preferred_channel: 'whatsapp', locale: 'en-US' },\n vendor_data: 'user-1234',\n }),\n});\nif (!res.ok) throw new Error(`Phone send failed: ${res.status}`);\nconst data = await res.json();\nconsole.log(data); // { request_id, status, reason, vendor_data, metadata }\n// Then verify with POST /v3/phone/check/ using the same phone_number" + "name": "vendor_business_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Vendor business UUID." } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "phone_number": { - "type": "string", - "maxLength": 20, - "description": "Recipient phone number in E.164 format — leading `+` and country code (e.g., `+14155552671`). Numbers are validated and normalized to E.164; unparseable or invalid numbers return `400` with a `phone_number` field error.", - "example": "+14155552671" - }, - "options": { - "type": "object", - "description": "OTP delivery options. All fields are optional.", - "properties": { - "code_size": { - "type": "integer", - "minimum": 4, - "maximum": 8, - "default": 6, - "description": "Number of digits in the OTP." - }, - "locale": { - "type": "string", - "maxLength": 5, - "pattern": "^[a-z]{2,3}(-[A-Z]{2,3})?$", - "description": "BCP-47 locale used to localize the verification message (e.g., `en-US`, `fr`, `es`). Invalid formats return `400`.", - "example": "en-US" - }, - "preferred_channel": { - "type": "string", - "enum": [ - "whatsapp", - "sms", - "telegram", - "voice", - "rcs", - "viber", - "zalo" - ], - "default": "whatsapp", - "description": "Preferred delivery channel. If the channel is unavailable for the destination country or number, the message automatically falls back to SMS — the send still reports `Success`." - }, - "use_white_label_customization": { - "type": "boolean", - "default": false, - "description": "When `true`, the send is white-labelled: if your application has no SMS sender name, the SMS names your white-label public name instead (\"123456 is your verification code for Acme Bank\"). Your SMS sender name, when set, is used with or without this flag. Applies to SMS messages, including an SMS fallback from another channel; WhatsApp codes are sent with a fixed message and are not branded. Defaults to `false`." - }, - "sms_sender_name": { - "type": "string", - "nullable": true, - "maxLength": 30, - "example": "Acme Bank", - "description": "The name this code's SMS carries (\"123456 is your verification code for Acme Bank\"), instead of your application's SMS sender name. Free. 1 to 30 characters: letters, digits, spaces and ' & - ! only, not only digits, with single spaces between words; any other value returns `400` and no code is sent. Omitted, `null` or blank, your application's SMS sender name is used. Each application can start using up to two new names per day (UTC); past that, the code is sent with your application's name. Applies to SMS messages, including an SMS fallback from another channel; WhatsApp codes are not branded.", - "pattern": "^$|^(?![\\d ]+$)[\\p{L}\\p{N}'&!-]+( [\\p{L}\\p{N}'&!-]+)*$" - } - } - }, - "signals": { - "type": "object", - "description": "Optional device and network signals about the end user, forwarded to the anti-fraud layer to improve detection of abusive or automated traffic. All fields are optional.", - "properties": { - "ip": { - "type": "string", - "description": "IP address of the end user's device. IPv4 or IPv6.", - "example": "192.0.2.1" - }, - "device_id": { - "type": "string", - "maxLength": 255, - "description": "Unique identifier of the end user's device. Android: `ANDROID_ID`; iOS: `identifierForVendor`.", - "example": "8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2" - }, - "device_platform": { - "type": "string", - "enum": [ - "android", - "ios", - "ipados", - "tvos", - "web" - ], - "description": "Platform of the end user's device." - }, - "device_model": { - "type": "string", - "maxLength": 255, - "description": "Model of the end user's device.", - "example": "iPhone17,2" - }, - "os_version": { - "type": "string", - "maxLength": 64, - "description": "Operating-system version of the end user's device.", - "example": "18.0.1" - }, - "app_version": { - "type": "string", - "maxLength": 64, - "description": "Version of your application.", - "example": "1.2.34" - }, - "user_agent": { - "type": "string", - "maxLength": 512, - "description": "User agent of the end user's browser or app.", - "example": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1" - } - } - }, - "vendor_data": { - "type": "string", - "description": "Optional caller-controlled identifier (your internal user id, an email, a UUID, etc.) persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. Use it to correlate Didit's `request_id` with your user record." - }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Optional free-form JSON object persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console." - } - }, - "required": [ - "phone_number" - ] - }, - "examples": { - "WhatsApp (default)": { - "summary": "6-digit OTP via WhatsApp, falling back to SMS automatically", - "value": { - "phone_number": "+14155552671", - "options": { - "preferred_channel": "whatsapp", - "locale": "en-US" - }, - "vendor_data": "user-1234" - } - }, - "SMS with custom code length": { - "summary": "Force SMS delivery with a 4-digit code", - "value": { - "phone_number": "+34699999999", - "options": { - "preferred_channel": "sms", - "code_size": 4, - "locale": "es" + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/businesses/aaaaaaaa-0000-4000-8000-000000000004/networks/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Networks this entity belongs to. Not paginated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EntityNetworkMembershipList" + } + } + } + }, + "404": { + "description": "Entity not found in this application, or has no linked subject to look up.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" } } - }, - "With fraud signals": { - "summary": "Forward device signals to strengthen anti-fraud screening", - "value": { - "phone_number": "+14155552671", - "signals": { - "ip": "192.0.2.1", - "device_id": "8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2", - "device_platform": "ios", - "device_model": "iPhone17,2", - "os_version": "18.0.1", - "app_version": "1.2.34" - }, - "vendor_data": "user-1234" - } } } } } }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/networks/": { + "get": { + "summary": "List networks for a transaction", + "description": "List the networks the transaction's backing KYC/KYB subject belongs to (resolved through the transaction's session or business session).", + "operationId": "list_transaction_network_memberships", + "x-didit-sdk": { + "resource": "transactions", + "method": "list_networks", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Networks" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "transaction_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Transaction UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/transactions/aaaaaaaa-0000-4000-8000-000000000005/networks/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], "responses": { "200": { - "description": "Send acknowledged. Inspect `status`: `Success` and `Retry` mean a code is on its way; `Blocked` means the anti-fraud layer refused and the verification is already finalized as `Declined`. `request_id` is the persisted session id (same id on a `Retry`).", + "description": "Networks this entity belongs to. Not paginated.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PhoneVerificationSendResponse" - }, - "examples": { - "Success": { - "summary": "OTP sent for a new verification", - "value": { - "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", - "status": "Success", - "reason": null, - "vendor_data": "user-1234", - "metadata": null - } - }, - "Retry": { - "summary": "Second send for the same number inside the 5-minute window — same request_id", - "value": { - "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", - "status": "Retry", - "reason": null, - "vendor_data": "user-1234", - "metadata": null - } - }, - "Blocked": { - "summary": "Anti-fraud refusal — verification finalized as Declined, nothing billed", - "value": { - "request_id": "0d8af1f4-0c43-46d7-b75c-6e4e22c0a7b3", - "status": "Blocked", - "reason": "repeated_attempts", - "vendor_data": "user-1234", - "metadata": null - } - } + "$ref": "#/components/schemas/EntityNetworkMembershipList" } } } }, - "400": { - "description": "Validation error. Malformed input returns DRF's field-error envelope (one array of messages per offending field, nested under `options` for option errors). A number that parses but is rejected at send time (unreachable, or an ineligible line type such as a short code) returns a `{\"detail\": ...}` envelope instead.", + "404": { + "description": "Entity not found in this application, or has no linked subject to look up.", "content": { "application/json": { - "examples": { - "Invalid phone number (field error)": { - "summary": "Number is not valid E.164", - "value": { - "phone_number": [ - "Invalid phone number provided." - ] - } - }, - "Missing phone number": { - "summary": "`phone_number` not included in the body", - "value": { - "phone_number": [ - "This field is required." - ] - } - }, - "Invalid options": { - "summary": "Out-of-range `code_size`, malformed `locale`, unknown `preferred_channel`", - "value": { - "options": { - "code_size": [ - "Ensure this value is less than or equal to 8." - ], - "locale": [ - "Ensure this field has no more than 5 characters." - ], - "preferred_channel": [ - "\"carrier_pigeon\" is not a valid choice." - ] - } - } - }, - "Number rejected at send time": { - "summary": "Valid E.164 shape but the number cannot receive an OTP", - "value": { - "detail": "Invalid phone number provided." - } - }, - "Ineligible line type": { - "summary": "Line type cannot receive an OTP (e.g., short code)", - "value": { - "detail": "Invalid phone line type provided." + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" } } } } } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/parties/{transaction_party_uuid}/networks/": { + "get": { + "summary": "List networks for a transaction party", + "description": "List the networks a transaction party (remitter, beneficiary, or counterparty) belongs to, resolved through the party itself rather than through the transaction's backing session.", + "operationId": "list_transaction_party_network_memberships", + "x-didit-sdk": { + "resource": "transactions", + "method": "list_party_networks", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Networks" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, - "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment (`{\"detail\": ...}`) — this API never returns `401`. Also returned before any send when the organization has never topped up (anti-abuse gate) or when its balance cannot cover the destination's send price (`{\"error\": ...}`).", + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "transaction_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Transaction UUID." + }, + { + "name": "transaction_party_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Transaction party UUID." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/transactions/aaaaaaaa-0000-4000-8000-000000000005/parties/bbbbbbbb-0000-4000-8000-000000000006/networks/' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "Networks this entity belongs to. Not paginated.", "content": { "application/json": { - "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", - "value": { - "detail": "You do not have permission to perform this action." - } - }, - "Top-up required": { - "summary": "Phone verification stays disabled until the organization's first top-up", - "value": { - "detail": "To protect against abuse, phone verification is disabled until your first top-up." - } - }, - "Not enough credits": { - "summary": "Organization balance cannot cover the send price", - "value": { - "error": "You don't have enough credits to perform this request." + "schema": { + "$ref": "#/components/schemas/EntityNetworkMembershipList" + } + } + } + }, + "404": { + "description": "Entity not found in this application, or has no linked subject to look up.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" } } } } } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/biometric-templates/": { + "get": { + "summary": "List retained biometric templates", + "description": "An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`.\n\nReturns a paginated list of the templates the application currently holds. Purged templates are not listed.", + "operationId": "list_biometric_templates", + "x-didit-sdk": { + "resource": "biometric_templates", + "method": "list", + "auth": "api_key", + "server": true + }, + "tags": [ + "Biometric Templates" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "vendor_data", + "required": false, + "description": "Case-insensitive substring match on the owning User's current `vendor_data`.", + "schema": { + "type": "string" + } }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. Additionally, an anti-abuse cap of 4 verification attempts per phone number per hour applies upstream. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. Note: the `X-RateLimit-*`/`Retry-After` headers accompany only the 300/min write-limit 429; the per-number cap 429 carries no rate-limit headers.", + { + "in": "query", + "name": "vendor_user_uuid", + "required": false, + "description": "Exact match on the owning User.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "query", + "name": "status", + "required": false, + "description": "Lifecycle status. Purged templates are never listed.", + "schema": { + "type": "string", + "enum": [ + "pending", + "active", + "expired", + "held_out_of_use", + "purge_pending", + "purge_failed" + ] + } + }, + { + "in": "query", + "name": "source_type", + "required": false, + "description": "How the template was created.", + "schema": { + "type": "string", + "enum": [ + "session_delete", + "automatic_retention", + "backfill" + ] + } + }, + { + "in": "query", + "name": "retained_from", + "required": false, + "description": "Templates retained at or after this time.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "retained_to", + "required": false, + "description": "Templates retained at or before this time. Must not be earlier than `retained_from` (`400`).", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "ordering", + "required": false, + "description": "Sort order.", + "schema": { + "type": "string", + "enum": [ + "retained_at", + "-retained_at", + "expires_at", + "-expires_at" + ], + "default": "-retained_at" + } + }, + { + "in": "query", + "name": "limit", + "required": false, + "description": "Page size.", + "schema": { + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "required": false, + "description": "Pagination offset.", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Paginated templates.", "content": { "application/json": { - "examples": { - "Per-number cap": { - "summary": "More than 4 verification attempts for the same number in the last hour", - "value": { - "detail": "Maximum verification attempts reached for this phone number. Only 4 authentication attempts are allowed per hour. Try again later or use a different number." - } - }, - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "next": { + "type": "string", + "nullable": true + }, + "previous": { + "type": "string", + "nullable": true + }, + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Template id. Randomly generated; not derived from the deleted session or its face." + }, + "vendor_user_uuid": { + "type": "string", + "format": "uuid", + "description": "The owning User." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "The User's current `vendor_data`, resolved when you read the template (never copied)." + }, + "source_type": { + "type": "string", + "enum": [ + "session_delete", + "automatic_retention", + "backfill" + ], + "description": "How the template was created: a session deletion (API or Console), the application's automatic data-retention window, or a Didit-operated backfill you instructed." + }, + "provenance_reference": { + "type": "string", + "format": "uuid", + "description": "Opaque reference tying the template to its audit trail. It is not the deleted session's id and cannot be resolved to it." + }, + "retention_policy": { + "type": "string", + "enum": [ + "delete_with_session", + "retain_until_user_deleted" + ], + "description": "The application policy at retention time." + }, + "retention_override": { + "type": "boolean", + "nullable": true, + "description": "The `retain_face_embeddings` value sent on the deletion, or `null` when the policy applied." + }, + "retained_at": { + "type": "string", + "format": "date-time" + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "When the template expires and is purged automatically. Always set." + }, + "retained_by": { + "type": "string", + "nullable": true, + "description": "Principal that gave the instruction: the application id for API keys, the Console member id for Console actions, or a `service:` identifier for automatic retention. Never an email address." + }, + "retained_by_type": { + "type": "string", + "enum": [ + "API_KEY", + "CONSOLE_USER", + "SYSTEM" + ] + }, + "instruction_class": { + "type": "string", + "enum": [ + "operational_session_delete", + "privacy_erasure" + ] + }, + "instruction_source": { + "type": "string", + "enum": [ + "api", + "console", + "automatic_retention", + "backfill", + "privacy_erasure", + "explicit_template_purge", + "vendor_user_delete", + "expiry", + "reconciliation", + "migration", + "system" + ] + }, + "instruction_id": { + "type": "string", + "description": "The `instruction_id` sent on the deletion, or a Didit-generated id." + }, + "held_out_of_use": { + "type": "boolean", + "description": "`true` only while a legal hold applies. A held template is excluded from all matching." + }, + "status": { + "type": "string", + "enum": [ + "pending", + "active", + "expired", + "held_out_of_use", + "purge_pending", + "purge_failed", + "purged" + ], + "description": "Only `active` templates that are not held, not past `expires_at`, and whose User still exists take part in matching." + }, + "purged_at": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + } } } }, - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" + "examples": { + "One active template": { + "value": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", + "vendor_user_uuid": "2f7c1a9e-8b4d-4c6a-9e1f-3d5b7a9c1e2f", + "vendor_data": "user-42", + "source_type": "session_delete", + "provenance_reference": "6b1e8d2f-4a3c-4f9e-8c7d-1a2b3c4d5e6f", + "retention_policy": "retain_until_user_deleted", + "retention_override": null, + "retained_at": "2026-08-28T10:15:42.318402Z", + "expires_at": "2027-08-28T10:15:42.318402Z", + "retained_by": "3c9e2b1f-7d4a-4e8b-9c1d-5f6a7b8c9d0e", + "retained_by_type": "API_KEY", + "instruction_class": "operational_session_delete", + "instruction_source": "api", + "instruction_id": "post-approval-cleanup-2026-08", + "held_out_of_use": false, + "status": "active", + "purged_at": null + } + ] } } } } } }, - "502": { - "description": "The phone verification provider is temporarily unavailable - a 5xx response, timeout, or connection failure that survived the built-in retries. Safe to retry; nothing is billed and no attempt is consumed.", + "400": { + "description": "Invalid filter value, or `retained_to` earlier than `retained_from`. Field-keyed validation errors.", "content": { "application/json": { "examples": { - "Provider outage": { - "summary": "The provider did not answer the send request", + "Bad range": { "value": { - "detail": "Phone verification is temporarily unavailable. Please try again shortly.", - "code": "phone_provider_unavailable" - } - } - }, - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - }, - "code": { - "type": "string" + "retained_to": [ + "Must be on or after retained_from." + ] } } } } } }, - "500": { - "description": "Unexpected failure while creating the verification that is not a known provider outage. Safe to retry.", + "403": { + "description": "Console user token, or the API key is not allowed. These endpoints accept application API keys only.", "content": { "application/json": { "examples": { - "Provider error": { - "summary": "OTP delivery could not be initiated", + "Forbidden": { "value": { - "detail": "Error creating phone verification" - } - } - }, - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" + "detail": "You do not have permission to perform this action." } } } @@ -21368,381 +43683,124 @@ } } }, - "security": [ + "x-codeSamples": [ { - "ApiKeyAuth": [] + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/biometric-templates/?vendor_data=user-42&status=active' \\\n -H 'x-api-key: YOUR_API_KEY'" } ] } }, - "/v3/phone/check/": { - "post": { - "summary": "Check Phone Code", - "description": "Verify the OTP delivered by [`POST /v3/phone/send/`](/standalone-apis/phone-send) and get the final verification result plus phone intelligence: carrier name and line type, virtual (VoIP) and disposable flags, and duplicate usage of the number across your sessions.\n\n**How the check finds the verification.** Matching is by your application plus the E.164 `phone_number` — `request_id` is not an input. The most recent pending verification created within the last **5 minutes** is checked. If there is none (never sent, already finalized, or older than 5 minutes) the endpoint returns `200` with `status: \"Expired or Not Found\"`.\n\n**Attempt budget.** Each verification allows **3 code attempts**. The first two wrong codes return `status: \"Failed\"` with the attempts remaining and `phone: null`; the third wrong code finalizes the verification as `Declined` with a `VERIFICATION_CODE_ATTEMPTS_EXCEEDED` warning and returns the full phone report.\n\n**Outcomes.** `Approved` — correct code and no declining risk. `Declined` — terminal: the code was correct but a declining risk matched (a `DECLINE` action below, a blocklisted or high-risk number), or the attempt budget was exhausted. `Failed` — wrong code, attempts remaining. `Expired or Not Found` — nothing to check. On `Approved`/`Declined` the `request_id` equals the send's `request_id` (the session id) and `phone` carries the full report (`carrier`, `is_virtual`, `is_disposable`, `warnings`, `lifecycle`, `matches`); on `Failed` and `Expired or Not Found` the `request_id` is a one-off random UUID.\n\n**Risk actions.** `duplicated_phone_number_action`, `disposable_number_action`, and `voip_number_action` decide what happens when the corresponding risk is detected on a correct code: `DECLINE` flips the final status to `Declined`; `NO_ACTION` (default) records the risk in `phone.warnings` without affecting the status.\n\n**Billing.** Checks are free — delivery is billed on the send side (see [Phone Verification Pricing](/getting-started/phone-verification-pricing)).\n\n**Session persistence.** A finalized check updates the session created by the send (visible in the Business Console, queryable via `GET /v3/session/{sessionId}/decision/`) and fires a `status.updated` webhook.\n\n**Sandbox.** Sandbox API keys skip all processing: any well-formed code (4–8 digits, `123456` included) returns a static `Approved` payload with a simplified flat `phone` object (`status`, `phone_number`, `country_code`, `carrier_name`, `line_type`, `flags`); malformed input still returns `400`. Nothing is persisted.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limits.** Shared write budget of 300 requests/min per API key, plus the upstream anti-abuse cap of 4 attempts per number per hour; exceeding either returns `429`.\n\n**Social footprint.** Set `enable_social` to `true` to add a `phone_social` block: the online platforms this number is registered on, with per-category counts and any profile details available. `profiles_registered: 0` means the platforms were checked and the number was found on none of them - a valid, billable answer. The block is absent when the add-on was not requested or the check could not be completed, and it is not billed in that case. Billing adds one `phone_social` unit per request on top of this endpoint's own price.", - "operationId": "post_v3phone_check", + "/v3/biometric-templates/count/": { + "get": { + "summary": "Count retained biometric templates", + "description": "An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`.\n\nReturns the number of templates matching the same filters as the list endpoint. Use it to check what a filtered bulk purge with `delete_all: true` would touch.", + "operationId": "count_biometric_templates", + "x-didit-sdk": { + "resource": "biometric_templates", + "method": "count", + "auth": "api_key", + "server": true + }, "tags": [ - "Standalone APIs" + "Biometric Templates" ], - "x-codeSamples": [ + "security": [ { - "lang": "curl", - "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/phone/check/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"phone_number\": \"+14155552671\",\n \"code\": \"123456\",\n \"voip_number_action\": \"DECLINE\"\n }'" + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "vendor_data", + "required": false, + "description": "Case-insensitive substring match on the owning User's current `vendor_data`.", + "schema": { + "type": "string" + } }, { - "lang": "python", - "label": "Python", - "source": "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/phone/check/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n },\n json={\n \"phone_number\": \"+14155552671\", # same number as the send call\n \"code\": \"123456\",\n \"voip_number_action\": \"DECLINE\",\n },\n timeout=15,\n)\nresp.raise_for_status()\nresult = resp.json()\nprint(result[\"status\"]) # Approved / Declined / Failed / Expired or Not Found\nif result[\"status\"] in (\"Approved\", \"Declined\"):\n print(result[\"phone\"][\"carrier\"], result[\"phone\"][\"is_virtual\"])" + "in": "query", + "name": "vendor_user_uuid", + "required": false, + "description": "Exact match on the owning User.", + "schema": { + "type": "string", + "format": "uuid" + } }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const res = await fetch('https://verification.didit.me/v3/phone/check/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n phone_number: '+14155552671', // same number as the send call\n code: '123456',\n voip_number_action: 'DECLINE',\n }),\n});\nconst data = await res.json();\nif (data.status === 'Approved') {\n // full phone report is in data.phone (carrier, is_virtual, matches, ...)\n}" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "phone_number": { - "type": "string", - "maxLength": 20, - "description": "The same phone number used in the matching [`POST /v3/phone/send/`](/standalone-apis/phone-send) call, in E.164 format. This is what links the check to the send.", - "example": "+14155552671" - }, - "code": { - "type": "string", - "minLength": 4, - "maxLength": 8, - "pattern": "^[0-9]{4,8}$", - "description": "The OTP the end user received. Must be 4–8 numeric digits — anything else returns `400` without consuming an attempt. A well-formed but wrong code returns `200` with `status: \"Failed\"` and consumes one of the 3 attempts.", - "example": "123456" - }, - "duplicated_phone_number_action": { - "type": "string", - "enum": [ - "NO_ACTION", - "DECLINE" - ], - "default": "NO_ACTION", - "description": "What to do when the same number was already used by a different user (different `vendor_data`) of your application. `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings` and fills `phone.matches`." - }, - "disposable_number_action": { - "type": "string", - "enum": [ - "NO_ACTION", - "DECLINE" - ], - "default": "NO_ACTION", - "description": "What to do when the carrier lookup flags the number as a temporary/burner line (`phone.is_disposable`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings`." - }, - "voip_number_action": { - "type": "string", - "enum": [ - "NO_ACTION", - "DECLINE" - ], - "default": "NO_ACTION", - "description": "What to do when the carrier lookup reports a virtual line type — `voip`, `isp`, or `vpn` (`phone.is_virtual`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings`." - }, - "enable_social": { - "type": "boolean", - "default": false, - "description": "When true, also look up the social footprint of the number - the messaging, social, e-commerce and professional platforms it is registered on. Billed as one `phone_social` unit per request in addition to this endpoint's own price.", - "example": true - }, - "phone_no_social_presence_action": { - "type": "string", - "enum": [ - "NO_ACTION", - "DECLINE" - ], - "default": "NO_ACTION", - "description": "Action when the social footprint check finds the number registered on none of the platforms it covers. Only NO_ACTION and DECLINE are accepted on this endpoint; REVIEW is available on the workflow configuration key of the same name." - } - }, - "required": [ - "phone_number", - "code" - ] - }, - "examples": { - "Default": { - "summary": "Record any risks but never auto-decline", - "value": { - "phone_number": "+14155552671", - "code": "123456" - } - }, - "Strict risk policy": { - "summary": "Auto-decline VoIP and disposable lines", - "value": { - "phone_number": "+14155552671", - "code": "123456", - "voip_number_action": "DECLINE", - "disposable_number_action": "DECLINE" - } - } - } + "in": "query", + "name": "status", + "required": false, + "description": "Lifecycle status. Purged templates are never listed.", + "schema": { + "type": "string", + "enum": [ + "pending", + "active", + "expired", + "held_out_of_use", + "purge_pending", + "purge_failed" + ] + } + }, + { + "in": "query", + "name": "source_type", + "required": false, + "description": "How the template was created.", + "schema": { + "type": "string", + "enum": [ + "session_delete", + "automatic_retention", + "backfill" + ] + } + }, + { + "in": "query", + "name": "retained_from", + "required": false, + "description": "Templates retained at or after this time.", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "retained_to", + "required": false, + "description": "Templates retained at or before this time. Must not be earlier than `retained_from` (`400`).", + "schema": { + "type": "string", + "format": "date-time" } } - }, + ], "responses": { "200": { - "description": "Check completed — wrong codes and missing verifications also return `200`; inspect `status`, not the HTTP code. `phone` is populated only on finalized outcomes (`Approved`/`Declined`), `null` on `Failed`, and absent on `Expired or Not Found`.", + "description": "Count.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PhoneVerificationCheckResponse" - }, - "examples": { - "Approved": { - "summary": "Correct code — full phone report with carrier, lifecycle, and matches", - "value": { - "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", - "status": "Approved", - "message": "The verification code is correct.", - "phone": { - "status": "Approved", - "phone_number_prefix": "+1", - "phone_number": "4155552671", - "full_number": "+14155552671", - "country_code": "US", - "country_name": "United States", - "carrier": { - "name": "AT&T", - "type": "mobile" - }, - "is_disposable": false, - "is_virtual": false, - "verification_method": "whatsapp", - "verification_attempts": 1, - "verified_at": "2026-06-12T01:24:47.311323Z", - "warnings": [], - "lifecycle": [ - { - "type": "PHONE_VERIFICATION_MESSAGE_SENT", - "timestamp": "2026-06-12T01:23:39.580554+00:00", - "details": { - "status": "Success", - "reason": null, - "channel": "whatsapp", - "actual_channel": "whatsapp" - }, - "fee": 0.0709 - }, - { - "type": "PHONE_DELIVERY_DELIVERED", - "timestamp": "2026-06-12T01:23:43.118220+00:00", - "details": { - "channel": "whatsapp", - "status": "delivered" - }, - "fee": 0 - }, - { - "type": "VALID_CODE_ENTERED", - "timestamp": "2026-06-12T01:24:47.311201+00:00", - "details": { - "code_tried": "123456", - "status": "Approved" - }, - "fee": 0 - }, - { - "type": "PHONE_VERIFICATION_APPROVED", - "timestamp": "2026-06-12T01:24:47.384292+00:00", - "details": null, - "fee": 0 - } - ], - "matches": [] - }, - "vendor_data": "user-1234", - "metadata": null, - "created_at": "2026-06-12T01:24:47.401719+00:00" - } - }, - "Failed (attempts remaining)": { - "summary": "Wrong code — the user can retry; request_id here is a one-off UUID", - "value": { - "request_id": "11111111-2222-3333-4444-555555555555", - "status": "Failed", - "message": "The verification code is incorrect. Attempts remaining: 2", - "phone": null, - "vendor_data": "user-1234", - "metadata": null, - "created_at": "2026-06-12T01:24:21.512340+00:00" - } - }, - "Declined (max attempts)": { - "summary": "Third wrong code — verification finalized as Declined", - "value": { - "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", - "status": "Declined", - "message": "The verification code is incorrect. Maximum attempts reached.", - "phone": { - "status": "Declined", - "phone_number_prefix": "+1", - "phone_number": "4155552671", - "full_number": "+14155552671", - "country_code": "US", - "country_name": "United States", - "carrier": { - "name": "AT&T", - "type": "mobile" - }, - "is_disposable": false, - "is_virtual": false, - "verification_method": "whatsapp", - "verification_attempts": 1, - "verified_at": null, - "warnings": [ - { - "feature": "PHONE", - "risk": "VERIFICATION_CODE_ATTEMPTS_EXCEEDED", - "additional_data": null, - "log_type": "error", - "short_description": "Verification code attempts exceeded", - "long_description": "The system detected that the phone number has exceeded the maximum number of verification code attempts." - } - ], - "lifecycle": [ - { - "type": "PHONE_VERIFICATION_MESSAGE_SENT", - "timestamp": "2026-06-12T01:23:39.580554+00:00", - "details": { - "status": "Success", - "reason": null, - "channel": "whatsapp", - "actual_channel": "whatsapp" - }, - "fee": 0.0709 - }, - { - "type": "INVALID_CODE_ENTERED", - "timestamp": "2026-06-12T01:24:01.118220+00:00", - "details": { - "code_tried": "111111", - "status": "Failed" - }, - "fee": 0 - }, - { - "type": "INVALID_CODE_ENTERED", - "timestamp": "2026-06-12T01:24:21.512340+00:00", - "details": { - "code_tried": "222222", - "status": "Failed" - }, - "fee": 0 - }, - { - "type": "INVALID_CODE_ENTERED", - "timestamp": "2026-06-12T01:24:47.311201+00:00", - "details": { - "code_tried": "333333", - "status": "Failed" - }, - "fee": 0 - }, - { - "type": "PHONE_VERIFICATION_DECLINED", - "timestamp": "2026-06-12T01:24:47.384292+00:00", - "details": { - "reason": "VERIFICATION_CODE_ATTEMPTS_EXCEEDED" - }, - "fee": 0 - } - ], - "matches": [] - }, - "vendor_data": "user-1234", - "metadata": null, - "created_at": "2026-06-12T01:24:47.401719+00:00" - } - }, - "Declined (risk policy)": { - "summary": "Correct code, but the line is VoIP and voip_number_action=DECLINE", - "value": { - "request_id": "7f1c9d2e-44a1-4b6e-9a3e-5b8e6f1c2d3a", - "status": "Declined", - "message": "The verification code is correct.", - "phone": { - "status": "Declined", - "phone_number_prefix": "+1", - "phone_number": "5005550006", - "full_number": "+15005550006", - "country_code": "US", - "country_name": "United States", - "carrier": { - "name": "CLEC LLC", - "type": "voip" - }, - "is_disposable": false, - "is_virtual": true, - "verification_method": "sms", - "verification_attempts": 1, - "verified_at": "2026-06-12T01:24:47.311323Z", - "warnings": [ - { - "feature": "PHONE", - "risk": "VOIP_NUMBER_DETECTED", - "additional_data": null, - "log_type": "error", - "short_description": "VoIP number detected", - "long_description": "The system detected that the phone number is a VoIP number, which is not allowed." - } - ], - "lifecycle": [ - { - "type": "PHONE_VERIFICATION_MESSAGE_SENT", - "timestamp": "2026-06-12T01:23:39.580554+00:00", - "details": { - "status": "Success", - "reason": null, - "channel": "whatsapp", - "actual_channel": "sms" - }, - "fee": 0.0709 - }, - { - "type": "VALID_CODE_ENTERED", - "timestamp": "2026-06-12T01:24:47.311201+00:00", - "details": { - "code_tried": "123456", - "status": "Approved" - }, - "fee": 0 - }, - { - "type": "PHONE_VERIFICATION_DECLINED", - "timestamp": "2026-06-12T01:24:47.384292+00:00", - "details": { - "reason": "VOIP_NUMBER_DETECTED" - }, - "fee": 0 - } - ], - "matches": [] - }, - "vendor_data": "user-1234", - "metadata": null, - "created_at": "2026-06-12T01:24:47.401719+00:00" + "type": "object", + "properties": { + "count": { + "type": "integer", + "minimum": 0 } - }, - "Expired or Not Found": { - "summary": "No pending verification for this number in the last 5 minutes", + } + }, + "examples": { + "Count": { "value": { - "request_id": "69cc35c6-280a-47a6-ab8c-4f32abc02ed0", - "status": "Expired or Not Found", - "message": "No pending phone verification found in the last 5 minutes.", - "vendor_data": null, - "metadata": null, - "created_at": "2026-06-12T01:24:47.311323+00:00" + "count": 128 } } } @@ -21750,31 +43808,14 @@ } }, "400": { - "description": "Validation error — DRF's field-error envelope: one array of messages per offending field.", + "description": "Invalid filter value, or `retained_to` earlier than `retained_from`. Field-keyed validation errors.", "content": { "application/json": { "examples": { - "Invalid code format": { - "summary": "`code` is not 4–8 numeric digits", - "value": { - "code": [ - "Invalid code format. Enter 4 to 8 numeric digits." - ] - } - }, - "Invalid phone number": { - "summary": "`phone_number` is not valid E.164", - "value": { - "phone_number": [ - "Invalid phone number provided." - ] - } - }, - "Invalid action": { - "summary": "Risk actions accept only NO_ACTION or DECLINE", + "Bad range": { "value": { - "voip_number_action": [ - "\"REVIEW\" is not a valid choice." + "retained_to": [ + "Must be on or after retained_from." ] } } @@ -21783,12 +43824,11 @@ } }, "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — this API never returns `401`. Checks are free, so there is no credit check here.", + "description": "Console user token, or the API key is not allowed. These endpoints accept application API keys only.", "content": { "application/json": { "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", + "Forbidden": { "value": { "detail": "You do not have permission to perform this action." } @@ -21796,433 +43836,336 @@ } } } - }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. Additionally, an anti-abuse cap of 4 verification attempts per phone number per hour applies upstream. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers. Note: the `X-RateLimit-*`/`Retry-After` headers accompany only the 300/min write-limit 429; the per-number cap 429 carries no rate-limit headers.", + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/biometric-templates/count/?status=active' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ] + } + }, + "/v3/biometric-templates/{template_uuid}/": { + "get": { + "summary": "Get a retained biometric template", + "description": "An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`.\n\nReturns one template. The response never includes an image, a vector, the deleted session's id, or identity data.", + "operationId": "get_biometric_template", + "x-didit-sdk": { + "resource": "biometric_templates", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "tags": [ + "Biometric Templates" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "template_uuid", + "required": true, + "description": "Template id, as returned in `biometric_template_uuid` by the session deletion endpoints, `biometric_template_id` on a Face Search match, or the list endpoint.", + "schema": { + "type": "string", + "format": "uuid", + "example": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b" + } + } + ], + "responses": { + "200": { + "description": "The template.", "content": { "application/json": { - "examples": { - "Per-number cap": { - "summary": "More than 4 verification attempts for the same number in the last hour", - "value": { - "detail": "Maximum verification attempts reached for this phone number. Only 4 authentication attempts are allowed per hour. Try again later or use a different number." - } - }, - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." - } - } - }, "schema": { "type": "object", "properties": { - "detail": { - "type": "string" + "uuid": { + "type": "string", + "format": "uuid", + "description": "Template id. Randomly generated; not derived from the deleted session or its face." + }, + "vendor_user_uuid": { + "type": "string", + "format": "uuid", + "description": "The owning User." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "The User's current `vendor_data`, resolved when you read the template (never copied)." + }, + "source_type": { + "type": "string", + "enum": [ + "session_delete", + "automatic_retention", + "backfill" + ], + "description": "How the template was created: a session deletion (API or Console), the application's automatic data-retention window, or a Didit-operated backfill you instructed." + }, + "provenance_reference": { + "type": "string", + "format": "uuid", + "description": "Opaque reference tying the template to its audit trail. It is not the deleted session's id and cannot be resolved to it." + }, + "retention_policy": { + "type": "string", + "enum": [ + "delete_with_session", + "retain_until_user_deleted" + ], + "description": "The application policy at retention time." + }, + "retention_override": { + "type": "boolean", + "nullable": true, + "description": "The `retain_face_embeddings` value sent on the deletion, or `null` when the policy applied." + }, + "retained_at": { + "type": "string", + "format": "date-time" + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "When the template expires and is purged automatically. Always set." + }, + "retained_by": { + "type": "string", + "nullable": true, + "description": "Principal that gave the instruction: the application id for API keys, the Console member id for Console actions, or a `service:` identifier for automatic retention. Never an email address." + }, + "retained_by_type": { + "type": "string", + "enum": [ + "API_KEY", + "CONSOLE_USER", + "SYSTEM" + ] + }, + "instruction_class": { + "type": "string", + "enum": [ + "operational_session_delete", + "privacy_erasure" + ] + }, + "instruction_source": { + "type": "string", + "enum": [ + "api", + "console", + "automatic_retention", + "backfill", + "privacy_erasure", + "explicit_template_purge", + "vendor_user_delete", + "expiry", + "reconciliation", + "migration", + "system" + ] + }, + "instruction_id": { + "type": "string", + "description": "The `instruction_id` sent on the deletion, or a Didit-generated id." + }, + "held_out_of_use": { + "type": "boolean", + "description": "`true` only while a legal hold applies. A held template is excluded from all matching." + }, + "status": { + "type": "string", + "enum": [ + "pending", + "active", + "expired", + "held_out_of_use", + "purge_pending", + "purge_failed", + "purged" + ], + "description": "Only `active` templates that are not held, not past `expires_at`, and whose User still exists take part in matching." + }, + "purged_at": { + "type": "string", + "format": "date-time", + "nullable": true } } - } - } - } - }, - "502": { - "description": "The phone verification provider is temporarily unavailable - a 5xx response, timeout, or connection failure that survived the built-in retries. Safe to retry; the attempt budget is not consumed.", - "content": { - "application/json": { + }, "examples": { - "Provider outage": { - "summary": "The provider did not answer the check request", + "Active template": { "value": { - "detail": "Phone verification is temporarily unavailable. Please try again shortly.", - "code": "phone_provider_unavailable" - } - } - }, - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - }, - "code": { - "type": "string" + "uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", + "vendor_user_uuid": "2f7c1a9e-8b4d-4c6a-9e1f-3d5b7a9c1e2f", + "vendor_data": "user-42", + "source_type": "session_delete", + "provenance_reference": "6b1e8d2f-4a3c-4f9e-8c7d-1a2b3c4d5e6f", + "retention_policy": "retain_until_user_deleted", + "retention_override": null, + "retained_at": "2026-08-28T10:15:42.318402Z", + "expires_at": "2027-08-28T10:15:42.318402Z", + "retained_by": "3c9e2b1f-7d4a-4e8b-9c1d-5f6a7b8c9d0e", + "retained_by_type": "API_KEY", + "instruction_class": "operational_session_delete", + "instruction_source": "api", + "instruction_id": "post-approval-cleanup-2026-08", + "held_out_of_use": false, + "status": "active", + "purged_at": null } } } } } }, - "500": { - "description": "Unexpected failure while verifying the code upstream that is not a known provider outage. Safe to retry; the attempt budget is only consumed by completed checks.", + "403": { + "description": "Console user token, or the API key is not allowed. These endpoints accept application API keys only.", "content": { "application/json": { "examples": { - "Provider error": { - "summary": "Code verification could not be completed", + "Forbidden": { "value": { - "detail": "Error checking phone verification" - } - } - }, - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" + "detail": "You do not have permission to perform this action." } } } } } - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - } - }, - "/v3/email/send/": { - "post": { - "summary": "Send Email Code", - "description": "Send a one-time passcode (OTP) to an email address, then verify it with [`POST /v3/email/check/`](/standalone-apis/email-check).\n\n**How send and check pair up.** Verification state is keyed by your application plus the `email` address — the check call does not take `request_id`. Each new send creates a pending verification that lives for **5 minutes**: call the check with the same address and the code the user received before the window closes. Calling send again for the same address while a verification is pending generates a fresh code for that same verification (`status: \"Retry\"`, same `request_id`); the previous code stops working. At most one retry is attached this way (two sends total); a further send starts a fresh verification with a new `request_id`. The 5-minute window is measured from the first send and is not extended by retries.\n\n**Deliverability pre-check.** Before anything is sent, the address goes through syntax and DNS/MX validation. Addresses that cannot receive mail return `200` with `status: \"Undeliverable\"` and `reason: \"email_can_not_be_delivered\"` — no email is sent, nothing is billed, and the verification is immediately finalized as `Declined` (a follow-up check returns `Expired or Not Found`). A downstream send failure reports the same way.\n\n**Code format and branding.** Codes are 4–8 characters (`options.code_size`, default 6), numeric by default; set `options.alphanumeric_code: true` for uppercase letters and digits (the check comparison is case-insensitive). The email template is localized via `options.locale` (54 supported languages) and can use your application's white-label branding via `options.use_white_label_customization`.\n\n**Billing.** One Email Verification API credit per successful send (`status: \"Success\"`), charged at send time. `Retry` and `Undeliverable` sends are free, and checks are free.\n\n**Session persistence.** Every new verification is persisted as an API-type session: `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the verification appears in the Business Console, and `status.updated` webhooks fire as it progresses.\n\n**Sandbox.** Sandbox API keys skip delivery and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Success` payload with a random `request_id`; no email is sent and nothing is persisted. Use code `123456` on the sandbox check.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.", - "operationId": "post_v3email_send", - "tags": [ - "Standalone APIs" - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/email/send/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"email\": \"alice@example.com\",\n \"options\": { \"code_size\": 6, \"locale\": \"en\" },\n \"vendor_data\": \"user-1234\"\n }'" - }, - { - "lang": "python", - "label": "Python", - "source": "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/email/send/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n },\n json={\n \"email\": \"alice@example.com\",\n \"options\": {\"code_size\": 6, \"locale\": \"en\"},\n \"vendor_data\": \"user-1234\",\n },\n timeout=15,\n)\nresp.raise_for_status()\nprint(resp.json()) # {request_id, status, reason, vendor_data, metadata}\n# Then verify with POST /v3/email/check/ using the same email" }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const res = await fetch('https://verification.didit.me/v3/email/send/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n email: 'alice@example.com',\n options: { code_size: 6, locale: 'en' },\n vendor_data: 'user-1234',\n }),\n});\nif (!res.ok) throw new Error(`Email send failed: ${res.status}`);\nconst data = await res.json();\nconsole.log(data); // { request_id, status, reason, vendor_data, metadata }\n// Then verify with POST /v3/email/check/ using the same email" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "type": "string", - "format": "email", - "description": "Recipient email address. Malformed addresses return `400`; syntactically valid addresses that cannot receive mail (failed DNS/MX validation) return `200` with `status: \"Undeliverable\"`.", - "example": "alice@example.com" - }, - "options": { - "type": "object", - "description": "OTP format, localization, and branding options. All fields are optional.", - "properties": { - "code_size": { - "type": "integer", - "minimum": 4, - "maximum": 8, - "default": 6, - "description": "Number of characters in the OTP (digits by default; uppercase letters and digits when `alphanumeric_code` is `true`)." - }, - "alphanumeric_code": { - "type": "boolean", - "default": false, - "description": "When `true`, the OTP is composed of uppercase letters `A–Z` and digits `0–9` instead of digits only. The `/v3/email/check/` comparison is case-insensitive." - }, - "locale": { - "type": "string", - "maxLength": 5, - "enum": [ - "en", - "ar", - "bn", - "bg", - "bs", - "ca", - "cs", - "da", - "de", - "el", - "es", - "et", - "fa", - "fi", - "fr", - "he", - "hi", - "hr", - "hu", - "hy", - "id", - "it", - "ja", - "ka", - "kk", - "ko", - "ky", - "lt", - "lv", - "cnr", - "mk", - "mn", - "ms", - "nl", - "no", - "pl", - "pt-BR", - "pt", - "ro", - "ru", - "sk", - "sl", - "so", - "sq", - "sr", - "sv", - "th", - "tr", - "uk", - "uz", - "vi", - "zh-CN", - "zh-TW", - "zh" - ], - "description": "Language of the OTP email template. Defaults to `en`. Unsupported values return `400` listing all supported locales.", - "example": "en" - }, - "use_white_label_customization": { - "type": "boolean", - "default": false, - "description": "When `true`, the OTP email uses your application's white-label customization (logo, colors, sender branding) instead of the default Didit template." - } - } - }, - "signals": { - "type": "object", - "description": "Optional device and network signals about the end user, forwarded to the anti-fraud layer to improve detection of abusive or automated traffic. All fields are optional.", - "properties": { - "ip": { - "type": "string", - "description": "IP address of the end user's device. IPv4 or IPv6.", - "example": "192.0.2.1" - }, - "device_id": { - "type": "string", - "maxLength": 255, - "description": "Unique identifier of the end user's device. Android: `ANDROID_ID`; iOS: `identifierForVendor`.", - "example": "8F0B8FDD-C2CB-4387-B20A-56E9B2E5A0D2" - }, - "device_platform": { - "type": "string", - "enum": [ - "android", - "ios", - "ipados", - "tvos", - "web" - ], - "description": "Platform of the end user's device." - }, - "device_model": { - "type": "string", - "maxLength": 255, - "description": "Model of the end user's device.", - "example": "iPhone17,2" - }, - "os_version": { - "type": "string", - "maxLength": 64, - "description": "Operating-system version of the end user's device.", - "example": "18.0.1" - }, - "app_version": { - "type": "string", - "maxLength": 64, - "description": "Version of your application.", - "example": "1.2.34" - }, - "user_agent": { - "type": "string", - "maxLength": 512, - "description": "User agent of the end user's browser or app.", - "example": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1" - } - } - }, - "vendor_data": { - "type": "string", - "description": "Optional caller-controlled identifier (your internal user id, an email, a UUID, etc.) persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. Use it to correlate Didit's `request_id` with your user record." - }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Optional free-form JSON object persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console." - } - }, - "required": [ - "email" - ] - }, - "examples": { - "Numeric 6-digit code": { - "summary": "Default — 6-digit numeric code in English", - "value": { - "email": "alice@example.com", - "options": { - "locale": "en" - }, - "vendor_data": "user-1234" - } - }, - "Alphanumeric 8-character code": { - "summary": "Uppercase letters + digits, Spanish template", - "value": { - "email": "bob@example.com", - "options": { - "code_size": 8, - "alphanumeric_code": true, - "locale": "es" - } - } - }, - "White-label branding": { - "summary": "Send the OTP email with your application's white-label customization", - "value": { - "email": "carol@example.com", - "options": { - "locale": "en", - "use_white_label_customization": true - }, - "vendor_data": "user-5678" + "404": { + "description": "Unknown template, a template of another application, or (on `GET`) a purged template.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "Biometric template not found." + } } } } } } }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET https://verification.didit.me/v3/biometric-templates/9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ] + }, + "delete": { + "summary": "Purge a retained biometric template", + "description": "An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`.\n\nPurges one template: it is removed from every kind of matching and the stored template is deleted. Irreversible and recorded on the audit trail with the acting principal.\n\n**Retry semantics:** Didit deletes the stored template first and only then marks the template purged. If a biometric store is unavailable the call returns `503`, the template moves to `status: purge_failed` and is already excluded from matching; repeat the same call to finish. Repeating a purge of an already purged template returns `already_purged`.\n\nTemplates are also purged by `POST /v3/users/delete/` for the owning User, by any deletion of that User's sessions with `deletion_instruction: privacy_erasure`, and automatically at `expires_at`.", + "operationId": "purge_biometric_template", + "x-didit-sdk": { + "resource": "biometric_templates", + "method": "delete", + "auth": "api_key", + "server": true + }, + "tags": [ + "Biometric Templates" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "template_uuid", + "required": true, + "description": "Template id, as returned in `biometric_template_uuid` by the session deletion endpoints, `biometric_template_id` on a Face Search match, or the list endpoint.", + "schema": { + "type": "string", + "format": "uuid", + "example": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b" + } + } + ], "responses": { "200": { - "description": "Send acknowledged. Inspect `status`: `Success` and `Retry` mean a code is on its way; `Undeliverable` means the address cannot receive mail and the verification is already finalized as `Declined`. `request_id` is the persisted session id (same id on a `Retry`).", + "description": "Purge outcome.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailVerificationSendResponse" + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "outcome": { + "type": "string", + "enum": [ + "purged", + "already_purged" + ] + } + } }, "examples": { - "Success": { - "summary": "OTP emailed for a new verification (billed)", - "value": { - "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", - "status": "Success", - "reason": null, - "vendor_data": "user-1234", - "metadata": null - } - }, - "Retry": { - "summary": "Second send inside the 5-minute window — fresh code, same request_id, free", + "Purged": { "value": { - "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", - "status": "Retry", - "reason": null, - "vendor_data": "user-1234", - "metadata": null + "uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", + "outcome": "purged" } }, - "Undeliverable": { - "summary": "Address failed DNS/MX validation — verification finalized as Declined, nothing billed", + "Already purged": { "value": { - "request_id": "cb1a5011-344d-444d-bdce-218904745d7f", - "status": "Undeliverable", - "reason": "email_can_not_be_delivered", - "vendor_data": "user-1234", - "metadata": null + "uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", + "outcome": "already_purged" } } } } } }, - "400": { - "description": "Validation error — DRF's field-error envelope: one array of messages per offending field, nested under `options` for option errors.", + "403": { + "description": "Console user token, or the API key is not allowed. These endpoints accept application API keys only.", "content": { "application/json": { "examples": { - "Invalid email": { - "summary": "Malformed email address", - "value": { - "email": [ - "Enter a valid email address." - ] - } - }, - "Unsupported locale": { - "summary": "`options.locale` outside the supported set", - "value": { - "options": { - "locale": [ - "Invalid locale. Supported locales are en, ar, bn, bg, bs, ca, cs, da, de, el, es, et, fa, fi, fr, he, hi, hr, hu, hy, id, it, ja, ka, kk, ko, ky, lt, lv, cnr, mk, mn, ms, nl, no, pl, pt-BR, pt, ro, ru, sk, sl, so, sq, sr, sv, th, tr, uk, uz, vi, zh-CN, zh-TW, zh." - ] - } - } - }, - "Code size out of range": { - "summary": "`options.code_size` outside 4–8", + "Forbidden": { "value": { - "options": { - "code_size": [ - "Ensure this value is less than or equal to 8." - ] - } + "detail": "You do not have permission to perform this action." } } } } } }, - "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment (`{\"detail\": ...}`) — this API never returns `401`. Also returned before any send when the organization's balance cannot cover one Email Verification API credit (`{\"error\": ...}`).", + "404": { + "description": "Unknown template, a template of another application, or (on `GET`) a purged template.", "content": { "application/json": { "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", - "value": { - "detail": "You do not have permission to perform this action." - } - }, - "Not enough credits": { - "summary": "Organization balance cannot cover the send price", + "Not found": { "value": { - "error": "You don't have enough credits to perform this request." + "detail": "Biometric template not found." } } } } } }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "503": { + "description": "A biometric store was unavailable while retaining or purging a biometric template. Nothing is deleted; repeat the same request. Retries are idempotent and converge on a single template.", "content": { "application/json": { - "examples": { - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." - } - } - }, "schema": { "type": "object", "properties": { @@ -22230,41 +44173,44 @@ "type": "string" } } + }, + "examples": { + "Retryable": { + "value": { + "detail": "The biometric-template operation could not finish because an external store is unavailable. The operation is retryable." + } + } } } } } }, - "security": [ + "x-codeSamples": [ { - "ApiKeyAuth": [] + "lang": "curl", + "label": "curl", + "source": "curl -X DELETE https://verification.didit.me/v3/biometric-templates/9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b/ \\\n -H 'x-api-key: YOUR_API_KEY'" } ] } }, - "/v3/email/check/": { + "/v3/biometric-templates/delete/": { "post": { - "summary": "Check Email Code", - "description": "Verify the OTP delivered by [`POST /v3/email/send/`](/standalone-apis/email-send) and get the final verification result plus email intelligence: deliverability, breach exposure (`is_breached` and the `breaches` list from a breach-intelligence database), disposable-provider detection, and duplicate usage of the address across your sessions.\n\n**How the check finds the verification.** Matching is by your application plus the `email` address — `request_id` is not an input. The most recent pending verification created within the last **5 minutes** is checked. If there is none (never sent, already finalized, undeliverable at send time, or older than 5 minutes) the endpoint returns `200` with `status: \"Expired or Not Found\"`.\n\n**Attempt budget.** Each verification allows **3 code attempts**. The first two wrong codes return `status: \"Failed\"` with the attempts remaining and `email: null`; the third wrong code finalizes the verification as `Declined` with an `EMAIL_CODE_ATTEMPTS_EXCEEDED` warning and returns the full email report. Codes are compared case-insensitively (relevant for `alphanumeric_code` sends); only the most recently sent code is valid.\n\n**Outcomes.** `Approved` — correct code and no declining risk. `Declined` — terminal: the code was correct but a declining risk matched (a `DECLINE` action below, a blocklisted address, or the address found undeliverable at finalization), or the attempt budget was exhausted. `Failed` — wrong code, attempts remaining. `Expired or Not Found` — nothing to check. On `Approved`/`Declined` the `request_id` equals the send's `request_id` (the session id) and `email` carries the full report (`is_breached`, `breaches`, `is_disposable`, `is_undeliverable`, `warnings`, `lifecycle`, `matches`); on `Failed` and `Expired or Not Found` the `request_id` is a one-off random UUID.\n\n**Risk actions.** `duplicated_email_action`, `breached_email_action`, and `disposable_email_action` decide what happens when the corresponding risk is detected on a correct code: `DECLINE` flips the final status to `Declined`; `NO_ACTION` (default) records the risk in `email.warnings` without affecting the status.\n\n**Billing.** Checks are free — the credit is consumed by the successful send.\n\n**Session persistence.** A finalized check updates the session created by the send (visible in the Business Console, queryable via `GET /v3/session/{sessionId}/decision/`) and fires a `status.updated` webhook.\n\n**Sandbox.** Sandbox API keys skip all processing: any well-formed code (`123456` included) returns a static `Approved` payload with a simplified `email` object (`status`, `email`, `is_breached`, `is_disposable`, `is_undeliverable`); malformed input still returns `400`. Nothing is persisted.\n\n**Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`) — this API never returns `401`.\n\n**Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.\n\n**Social footprint.** Set `enable_social` to `true` to add a `email_social` block: the online platforms this address is registered on, with per-category counts and any profile details available. `profiles_registered: 0` means the platforms were checked and the address was found on none of them - a valid, billable answer. The block is absent when the add-on was not requested or the check could not be completed, and it is not billed in that case. Billing adds one `email_social` unit per request on top of this endpoint's own price.", - "operationId": "post_v3email_check", + "summary": "Bulk purge retained biometric templates", + "description": "An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`.\n\nPurges many templates in one call, by explicit id list or with `delete_all: true` optionally narrowed by the same filters as the list endpoint. Templates are purged one by one and each result reports `purged` or `already_purged`. If a biometric store is unavailable the call returns `503` at the first failing template: templates purged earlier in the request stay purged, the failing one moves to `purge_failed` (already excluded from matching), and repeating the request finishes the job.\n\n**`delete_all: true` without filters purges every biometric template in the application and cannot be undone.** Run the count endpoint with the same filters first.", + "operationId": "bulk_purge_biometric_templates", + "x-didit-sdk": { + "resource": "biometric_templates", + "method": "batch_delete", + "auth": "api_key", + "server": true + }, "tags": [ - "Standalone APIs" + "Biometric Templates" ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/email/check/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"email\": \"alice@example.com\",\n \"code\": \"123456\",\n \"disposable_email_action\": \"DECLINE\"\n }'" - }, - { - "lang": "python", - "label": "Python", - "source": "import os, requests\n\nresp = requests.post(\n \"https://verification.didit.me/v3/email/check/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n },\n json={\n \"email\": \"alice@example.com\", # same address as the send call\n \"code\": \"123456\",\n \"disposable_email_action\": \"DECLINE\",\n },\n timeout=15,\n)\nresp.raise_for_status()\nresult = resp.json()\nprint(result[\"status\"]) # Approved / Declined / Failed / Expired or Not Found\nif result[\"status\"] in (\"Approved\", \"Declined\"):\n print(result[\"email\"][\"is_breached\"], result[\"email\"][\"is_disposable\"])" - }, + "security": [ { - "lang": "javascript", - "label": "JavaScript", - "source": "const res = await fetch('https://verification.didit.me/v3/email/check/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n email: 'alice@example.com', // same address as the send call\n code: '123456',\n disposable_email_action: 'DECLINE',\n }),\n});\nconst data = await res.json();\nif (data.status === 'Approved') {\n // full email report is in data.email (breaches, is_disposable, matches, ...)\n}" + "ApiKeyAuth": [] } ], "requestBody": { @@ -22274,81 +44220,80 @@ "schema": { "type": "object", "properties": { - "email": { - "type": "string", - "format": "email", - "description": "The same email address used in the matching [`POST /v3/email/send/`](/standalone-apis/email-send) call. This is what links the check to the send.", - "example": "alice@example.com" + "template_uuids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "minItems": 1, + "description": "Templates to purge. Required unless `delete_all` is `true`. Already purged ids are reported as `already_purged`." }, - "code": { + "delete_all": { + "type": "boolean", + "default": false, + "description": "When `true`, purges every template matching the filters below (all of the application's templates when no filter is given). `template_uuids` is ignored. Irreversible." + }, + "vendor_data": { "type": "string", - "maxLength": 10, - "description": "The OTP the end user received: 4–8 digits, or 4–8 letters/digits when `alphanumeric_code: true` was used at send time. Comparison is case-insensitive.", - "example": "123456" + "description": "Only with `delete_all`: case-insensitive substring match on the owning User's `vendor_data`." }, - "duplicated_email_action": { + "vendor_user_uuid": { "type": "string", - "enum": [ - "NO_ACTION", - "DECLINE" - ], - "default": "NO_ACTION", - "description": "What to do when the same address was already used and approved by a different user (only previously Approved verifications count) (different `vendor_data`) of your application. `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings` and fills `email.matches`." + "format": "uuid", + "description": "Only with `delete_all`: exact match on the owning User." }, - "breached_email_action": { + "status": { "type": "string", "enum": [ - "NO_ACTION", - "DECLINE" + "pending", + "active", + "expired", + "held_out_of_use", + "purge_pending", + "purge_failed" ], - "default": "NO_ACTION", - "description": "What to do when the address appears in known data breaches (`email.is_breached`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings`." + "description": "Only with `delete_all`." }, - "disposable_email_action": { + "source_type": { "type": "string", "enum": [ - "NO_ACTION", - "DECLINE" + "session_delete", + "automatic_retention", + "backfill" ], - "default": "NO_ACTION", - "description": "What to do when the domain belongs to a disposable/temporary-mail provider (`email.is_disposable`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings`." + "description": "Only with `delete_all`." }, - "enable_social": { - "type": "boolean", - "default": false, - "description": "When true, also look up the social footprint of the address - the email, social, professional, e-commerce and entertainment platforms it is registered on. Billed as one `email_social` unit per request in addition to this endpoint's own price.", - "example": true + "retained_from": { + "type": "string", + "format": "date-time", + "description": "Only with `delete_all`." }, - "email_no_social_presence_action": { + "retained_to": { "type": "string", - "enum": [ - "NO_ACTION", - "DECLINE" - ], - "default": "NO_ACTION", - "description": "Action when the social footprint check finds the address registered on none of the platforms it covers. Only NO_ACTION and DECLINE are accepted on this endpoint; REVIEW is available on the workflow configuration key of the same name." + "format": "date-time", + "description": "Only with `delete_all`. Must not be earlier than `retained_from`." } - }, - "required": [ - "email", - "code" - ] + } }, "examples": { - "Default": { - "summary": "Record any risks but never auto-decline", + "By id": { "value": { - "email": "alice@example.com", - "code": "123456" + "template_uuids": [ + "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", + "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d" + ] } }, - "Strict risk policy": { - "summary": "Auto-decline disposable and breached inboxes", + "Every template of one User": { "value": { - "email": "alice@example.com", - "code": "123456", - "disposable_email_action": "DECLINE", - "breached_email_action": "DECLINE" + "delete_all": true, + "vendor_user_uuid": "2f7c1a9e-8b4d-4c6a-9e1f-3d5b7a9c1e2f" + } + }, + "Every template in the application": { + "value": { + "delete_all": true } } } @@ -22357,1339 +44302,1724 @@ }, "responses": { "200": { - "description": "Check completed — wrong codes and missing verifications also return `200`; inspect `status`, not the HTTP code. `email` is populated only on finalized outcomes (`Approved`/`Declined`), `null` on `Failed`, and absent on `Expired or Not Found`.", + "description": "Per-template purge outcomes.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmailVerificationCheckResponse" + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "outcome": { + "type": "string", + "enum": [ + "purged", + "already_purged" + ] + } + } + } + } + } }, "examples": { - "Approved": { - "summary": "Correct code — clean address, full email report", + "Mixed": { "value": { - "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", - "status": "Approved", - "message": "The verification code is correct.", - "email": { - "status": "Approved", - "email": "alice@example.com", - "is_breached": false, - "breaches": [], - "is_disposable": false, - "is_undeliverable": false, - "verification_attempts": 1, - "verified_at": "2026-06-12T01:24:47.311323Z", - "warnings": [], - "lifecycle": [ - { - "type": "EMAIL_VERIFICATION_MESSAGE_SENT", - "timestamp": "2026-06-12T01:23:39.580554+00:00", - "details": { - "status": "Success", - "reason": null - }, - "fee": 0.03 - }, - { - "type": "VALID_CODE_ENTERED", - "timestamp": "2026-06-12T01:24:47.311201+00:00", - "details": { - "code_tried": "123456", - "status": "Approved" - }, - "fee": 0 - }, - { - "type": "EMAIL_VERIFICATION_APPROVED", - "timestamp": "2026-06-12T01:24:47.384292+00:00", - "details": null, - "fee": 0 - } - ], - "matches": [] - }, - "vendor_data": "user-1234", - "metadata": null, - "created_at": "2026-06-12T01:24:47.401719+00:00" + "results": [ + { + "uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", + "outcome": "purged" + }, + { + "uuid": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + "outcome": "already_purged" + } + ] } - }, - "Approved with breach exposure": { - "summary": "Correct code; breach exposure recorded as a warning (breached_email_action=NO_ACTION)", + } + } + } + } + }, + "400": { + "description": "Validation error — nothing is purged.", + "content": { + "application/json": { + "examples": { + "Missing selector": { "value": { - "request_id": "3a1f2b6c-9d4e-4f7a-8b2c-1e5d6f7a8b9c", - "status": "Approved", - "message": "The verification code is correct.", - "email": { - "status": "Approved", - "email": "bob@example.com", - "is_breached": true, - "breaches": [ - { - "name": "ExampleApp", - "domain": "exampleapp.com", - "breach_date": "2019-03-21", - "breach_emails_count": 763117, - "description": "In March 2019, the social platform ExampleApp suffered a data breach that exposed email addresses and passwords.", - "logo_path": "https:///logos/ExampleApp.png", - "data_classes": [ - "email_addresses", - "passwords" - ], - "is_verified": true - } - ], - "is_disposable": false, - "is_undeliverable": false, - "verification_attempts": 1, - "verified_at": "2026-06-12T01:24:47.311323Z", - "warnings": [ - { - "feature": "EMAIL", - "risk": "BREACHED_EMAIL_DETECTED", - "additional_data": null, - "log_type": "information", - "short_description": "Breached email detected", - "long_description": "This email address was found in one or more known data breaches." - } - ], - "lifecycle": [ - { - "type": "EMAIL_VERIFICATION_MESSAGE_SENT", - "timestamp": "2026-06-12T01:23:39.580554+00:00", - "details": { - "status": "Success", - "reason": null - }, - "fee": 0.03 - }, - { - "type": "VALID_CODE_ENTERED", - "timestamp": "2026-06-12T01:24:47.311201+00:00", - "details": { - "code_tried": "835126", - "status": "Approved" - }, - "fee": 0 - }, - { - "type": "EMAIL_VERIFICATION_APPROVED", - "timestamp": "2026-06-12T01:24:47.384292+00:00", - "details": null, - "fee": 0 - } - ], - "matches": [] - }, - "vendor_data": "user-5678", - "metadata": null, - "created_at": "2026-06-12T01:24:47.401719+00:00" + "template_uuids": [ + "This field is required unless delete_all is true." + ] } }, - "Failed (attempts remaining)": { - "summary": "Wrong code — the user can retry; request_id here is a one-off UUID", + "Bad range": { "value": { - "request_id": "11111111-2222-3333-4444-555555555555", - "status": "Failed", - "message": "The verification code is incorrect. Attempts remaining: 2", - "email": null, - "vendor_data": "user-1234", - "metadata": null, - "created_at": "2026-06-12T01:24:21.512340+00:00" + "retained_to": [ + "Must be on or after retained_from." + ] } - }, - "Declined (risk policy)": { - "summary": "Correct code, but the address is disposable and disposable_email_action=DECLINE", + } + } + } + } + }, + "403": { + "description": "Console user token, or the API key is not allowed. These endpoints accept application API keys only.", + "content": { + "application/json": { + "examples": { + "Forbidden": { "value": { - "request_id": "8c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f", - "status": "Declined", - "message": "The verification code is correct.", - "email": { - "status": "Declined", - "email": "tempuser42@mailinator.com", - "is_breached": false, - "breaches": [], - "is_disposable": true, - "is_undeliverable": false, - "verification_attempts": 1, - "verified_at": "2026-06-12T01:24:47.311323Z", - "warnings": [ - { - "feature": "EMAIL", - "risk": "DISPOSABLE_EMAIL_DETECTED", - "additional_data": null, - "log_type": "error", - "short_description": "Disposable email detected", - "long_description": "The system detected that the email is disposable, which is not allowed." - } - ], - "lifecycle": [ - { - "type": "EMAIL_VERIFICATION_MESSAGE_SENT", - "timestamp": "2026-06-12T01:23:39.580554+00:00", - "details": { - "status": "Success", - "reason": null - }, - "fee": 0.03 + "detail": "You do not have permission to perform this action." + } + } + } + } + } + }, + "503": { + "description": "A biometric store was unavailable while retaining or purging a biometric template. Nothing is deleted; repeat the same request. Retries are idempotent and converge on a single template.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Retryable": { + "value": { + "detail": "The biometric-template operation could not finish because an external store is unavailable. The operation is retryable." + } + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/biometric-templates/delete/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"delete_all\": true, \"vendor_user_uuid\": \"2f7c1a9e-8b4d-4c6a-9e1f-3d5b7a9c1e2f\"}'" + } + ] + } + }, + "/v3/kyb/shareholders/": { + "post": { + "summary": "KYB registry company with shareholders (standalone)", + "operationId": "post_v3_kyb_shareholders", + "x-didit-sdk": { + "resource": "kyb", + "method": "list_shareholders", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [], + "description": "Retrieve the Lite profile **plus the direct shareholders** (natural persons and corporate entities, with their ownership bands) of one candidate returned by `POST /v3/kyb/search/`.\n\n**Pairing with search.** Pass the candidate's `kyb_response_id` exactly as returned by a search of the same application. The search runs on the Lite registry chain; when the country's ownership data lives at another registry Didit re-finds the company there by registration number, at no extra cost.\n\n**Billing.** Billed at the country's Shareholders price - see [KYB registry pricing](/getting-started/kyb-registry-pricing). When the registry holds no shareholder record for the company (or the country offers no ownership data at all) the Lite profile is delivered and billed instead: read `registry_tier_delivered` and `registry_ownership_status`. The balance is checked against the Shareholders price before any registry call (`403` when short).\n\n**Availability.** v3 only. Every call creates a business session (its id is the `request_id`).\n\n**Pending reports.** When a report is still being prepared, or the ownership section has not been retrieved from the registry yet, the endpoint returns 202 Accepted, kyb_registry.fetch_status=pending, kyb_registry.registry_ownership_status=pending, and registry_tier_delivered=null. Keep request_id and retrieve GET /v3/session/{request_id}/decision/ until registry_checks[0].company.fetch_status is resolved. Didit completes the report automatically and charges the delivered tier once it is ready. Do not repeat the purchase while it is pending; every new POST creates a separate request.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KYBRegistryCandidateRequest" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/kyb/shareholders/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"kyb_response_id\": \"69aeeb95febb0f1704042259\",\n \"vendor_data\": \"company-1234\"\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os, time, requests\n\nsearch = requests.post(\n \"https://verification.didit.me/v3/kyb/search/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"country_code\": \"GB\", \"name\": \"Tesco\"},\n timeout=60,\n).json()\nkyb_response_id = search[\"kyb_registry\"][\"companies\"][0][\"kyb_response_id\"]\n\nresp = requests.post(\n \"https://verification.didit.me/v3/kyb/shareholders/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"kyb_response_id\": kyb_response_id, \"vendor_data\": \"company-1234\"},\n timeout=90,\n)\nresp.raise_for_status()\ndata = resp.json()\nrequest_id = data[\"request_id\"]\nprint(\"Keep this request ID:\", request_id)\ncompany = data[\"kyb_registry\"]\nif resp.status_code == 202 or company.get(\"fetch_status\") == \"pending\":\n # This client wait limit does not cancel the report or create a new purchase.\n deadline = time.monotonic() + 300\n while company.get(\"fetch_status\") != \"resolved\" and time.monotonic() < deadline:\n time.sleep(5)\n decision = requests.get(\n f\"https://verification.didit.me/v3/session/{request_id}/decision/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n timeout=30,\n )\n decision.raise_for_status()\n checks = decision.json().get(\"registry_checks\") or []\n if checks:\n company = checks[0].get(\"company\") or company\nif company.get(\"fetch_status\") == \"resolved\":\n print(company[\"company_name\"], company[\"registry_tier_delivered\"])\nelse:\n print(\"Still processing; retrieve this request later:\", request_id)" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const res = await fetch('https://verification.didit.me/v3/kyb/shareholders/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n kyb_response_id: '69aeeb95febb0f1704042259',\n vendor_data: 'company-1234',\n }),\n});\nif (!res.ok) throw new Error(`Request failed: ${res.status}`);\nconst data = await res.json();\nconst requestId = data.request_id;\nconsole.log('Keep this request ID:', requestId);\nlet company = data.kyb_registry;\nif (res.status === 202 || company.fetch_status === 'pending') {\n // This client wait limit does not cancel the report or create a new purchase.\n const deadline = Date.now() + 300000;\n while (company.fetch_status !== 'resolved' && Date.now() < deadline) {\n await new Promise(resolve => setTimeout(resolve, 5000));\n const decisionResponse = await fetch(\n `https://verification.didit.me/v3/session/${requestId}/decision/`,\n { headers: { 'x-api-key': 'YOUR_API_KEY' }, signal: AbortSignal.timeout(30000) },\n );\n if (!decisionResponse.ok) throw new Error(`Retrieval failed: ${decisionResponse.status}`);\n const decision = await decisionResponse.json();\n company = decision.registry_checks?.[0]?.company ?? company;\n }\n}\nif (company.fetch_status === 'resolved') {\n console.log(company.company_name, company.registry_tier_delivered);\n} else {\n console.log('Still processing; retrieve this request later:', requestId);\n}" + } + ], + "responses": { + "200": { + "description": "Company report returned and billed at the delivered tier. Ownership reports still being prepared can return 202; keep request_id to retrieve their saved decision.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Id of the business session created for this retrieval. The stored record (visible in the console) keeps updating if the registry resolves after the response." + }, + "registry_tier_requested": { + "type": "string", + "enum": [ + "basic", + "shareholders", + "ubo" + ], + "description": "The tier this endpoint sells: `basic` (Lite) for select, `shareholders`, `ubo`." + }, + "registry_tier_delivered": { + "type": "string", + "enum": [ + "basic", + "shareholders", + "ubo", + null + ], + "description": "The tier actually delivered and billed. Lower than requested when the country's registries hold no ownership data, or the registry has no ownership record for this company.", + "nullable": true + }, + "kyb_registry": { + "type": "object", + "description": "Full registry company profile.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Didit's id for this company record." + }, + "node_id": { + "type": "string", + "example": "feature_kyb_registry" + }, + "status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review" + ], + "description": "Didit's assessment of the registry check (e.g. `In Review` when the company is not active in the registry or ownership data could not be derived)." + }, + "registry_status": { + "type": "string", + "nullable": true, + "enum": [ + "active", + "dissolved", + "deregistered", + "see full details", + "authorised", + "appointed representative", + "unauthorised", + "inactive", + "no longer authorised", + "closed", + "struck off", + null + ], + "description": "Normalized company status as reported by the registry." + }, + "data_resolved": { + "type": "boolean", + "description": "`true` once the registry returned the full profile. `false` when the registry was still processing after polling — the stored record completes automatically once the registry resolves." + }, + "company_name": { + "type": "string", + "example": "Tesco PLC" + }, + "registration_number": { + "type": "string", + "nullable": true, + "example": "00445790" + }, + "country_code": { + "type": "string", + "description": "ISO 3166-1 alpha-2.", + "example": "GB" + }, + "region": { + "type": "string", + "nullable": true, + "description": "Subdivision code when the registry is regional (e.g. `CA` for `US-CA`); empty otherwise." + }, + "company_type": { + "type": "string", + "nullable": true, + "example": "Public Limited Company" + }, + "incorporation_date": { + "type": "string", + "format": "date", + "nullable": true, + "example": "1947-11-27" + }, + "registered_address": { + "type": "string", + "nullable": true, + "example": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA" + }, + "tax_number": { + "type": "string", + "nullable": true, + "example": "GB412 8925 93" + }, + "risk_level": { + "type": "string", + "nullable": true, + "description": "Registry-reported risk level, when available." + }, + "verification_status": { + "type": "string", + "nullable": true, + "description": "Registry verification status, when available." + }, + "is_from_registry": { + "type": "boolean", + "description": "Always `true` for this endpoint — the profile was retrieved from an official registry." + }, + "fetch_status": { + "type": "string", + "enum": [ + "pending", + "resolved" + ], + "description": "Registry fetch lifecycle. Mirrored by `data_resolved`." + }, + "alternative_names": { + "type": "string", + "nullable": true, + "description": "Alternative, trading, former or original-language company names as text, when reported." + }, + "nature_of_business": { + "type": "string", + "nullable": true, + "description": "Activity description or classification code, when reported." + }, + "registered_capital": { + "type": "string", + "nullable": true + }, + "registered_capital_amount": { + "type": "string", + "nullable": true, + "description": "Decimal amount as a string, when reported." + }, + "registered_capital_currency": { + "type": "string", + "nullable": true, + "description": "ISO 4217 code, when reported." + }, + "website": { + "type": "string", + "nullable": true, + "example": "https://www.tesco.com/" + }, + "email": { + "type": "string", + "nullable": true, + "example": "customer.service@tesco.co.uk" + }, + "phone": { + "type": "string", + "nullable": true, + "example": "03301231688" + }, + "legal_entity_identifier": { + "type": "string", + "nullable": true, + "description": "LEI, when reported." + }, + "location_of_registration": { + "type": "string", + "nullable": true, + "description": "Registry/jurisdiction the company is registered with, when reported." + }, + "vat_number": { + "type": "string", + "nullable": true, + "description": "EU VAT number, when collected during the hosted registry step or edited afterwards. At select time this is typically not yet set — VAT and tax ID validation runs when the end user submits the registry step in the hosted flow." + }, + "vat_validation_status": { + "type": "string", + "enum": [ + "valid", + "invalid", + "format_valid", + "invalid_format", + "could_not_validate", + "not_applicable" + ], + "description": "Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session." + }, + "vat_validated_number": { + "type": "string", + "nullable": true, + "description": "The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran." + }, + "vat_validation_source": { + "type": "string", + "nullable": true, + "enum": [ + "eu_vies", + "pl_vat_white_list", + "no_brreg", + "ch_uid_register", + "br_receita_federal", + "co_rues", + "cr_hacienda", + "ec_sri", + "py_dnit", + "format_rules", + null + ], + "description": "The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran." + }, + "vat_validated_name": { + "type": "string", + "nullable": true, + "description": "Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." + }, + "vat_validated_address": { + "type": "string", + "nullable": true, + "description": "Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." + }, + "vat_checked_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Timestamp of the check. `null` when no check ran." + }, + "financial_summary": { + "type": "object", + "nullable": true, + "description": "Registry-reported share/capital details (currency, total shares, share classes), when available." + }, + "officers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "example": "MELISSA BETHELL" + }, + "designation": { + "type": "string", + "nullable": true, + "description": "Raw registry designation (e.g. `Director`, `Secretary`)." + }, + "role": { + "type": "string", + "description": "Normalized role derived from the designation. Full 17-value model set; registry parsing on this endpoint typically emits director, non_executive_director, secretary, chairman, or company_officer.", + "enum": [ + "director", + "non_executive_director", + "secretary", + "chairman", + "ubo", + "shareholder", + "representative", + "founder", + "legal_advisor", + "authorized_signatory", + "trustee", + "beneficiary", + "company_officer", + "settlor", + "protector", + "investor", + "other" + ] + }, + "nationality": { + "type": "string", + "nullable": true + }, + "is_active": { + "type": "boolean", + "description": "`false` when the registry marks the appointment as resigned." + }, + "kyc_status": { + "type": "string", + "nullable": true, + "enum": [ + "Approved", + "Declined", + "Pending", + null + ], + "description": "Status of an associated KYC session, when one was started for this officer. `null` otherwise." + }, + "kyc_session_url": { + "type": "string", + "nullable": true, + "description": "Verification URL of the associated KYC session, when one exists." + }, + "first_name": { + "type": "string", + "nullable": true, + "description": "Given name when explicitly reported by the registry; not inferred from the full name." + }, + "last_name": { + "type": "string", + "nullable": true, + "description": "Family name when explicitly reported by the registry." + }, + "address": { + "type": "string", + "nullable": true, + "description": "Officer address reported by the registry." + }, + "appointment_date": { + "type": "string", + "nullable": true, + "description": "Date the officer was appointed.", + "format": "date" + }, + "termination_date": { + "type": "string", + "nullable": true, + "description": "Date the appointment ended, when reported.", + "format": "date" + }, + "date_of_birth": { + "type": "string", + "nullable": true, + "description": "Complete registry-reported birth date. Null when only a birth year or month is known.", + "format": "date" + }, + "date_of_birth_year": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 9999, + "description": "Registry-reported birth year, including when the full date is unavailable." + }, + "date_of_birth_month": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 12, + "description": "Registry-reported birth month. Null when only the year is known." + } + } }, - { - "type": "VALID_CODE_ENTERED", - "timestamp": "2026-06-12T01:24:47.311201+00:00", - "details": { - "code_tried": "123456", - "status": "Approved" - }, - "fee": 0 + "description": "Directors, secretaries, and other officers extracted from the registry." + }, + "beneficial_owners": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "first_name": { + "type": "string", + "nullable": true + }, + "last_name": { + "type": "string", + "nullable": true + }, + "entity_type": { + "type": "string", + "enum": [ + "person", + "company" + ], + "description": "Whether the owner is a natural person or a corporate entity." + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Normalized roles (e.g. `ubo`, `shareholder`)." + }, + "ownership_min_shares": { + "type": "string", + "nullable": true, + "description": "Lower bound of the ownership band reported by the registry (e.g. `25%`)." + }, + "ownership_max_shares": { + "type": "string", + "nullable": true, + "description": "Upper bound of the ownership band reported by the registry." + }, + "is_active": { + "type": "boolean" + }, + "kyc_status": { + "type": "string", + "nullable": true, + "enum": [ + "Approved", + "Declined", + "Pending", + null + ] + }, + "kyc_session_url": { + "type": "string", + "nullable": true + }, + "effective_ownership_percent": { + "type": "number", + "nullable": true, + "description": "Parsed ownership percentage (max bound, falling back to min bound)." + }, + "company_name": { + "type": "string", + "nullable": true + }, + "registration_number": { + "type": "string", + "nullable": true + }, + "country_of_birth": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "nullable": true + }, + "nationality": { + "type": "string", + "nullable": true + }, + "designation": { + "type": "string", + "nullable": true + }, + "voting_min_shares": { + "type": "string", + "nullable": true + }, + "voting_max_shares": { + "type": "string", + "nullable": true + }, + "date_of_birth": { + "type": "string", + "nullable": true, + "description": "Complete birth date when reported.", + "format": "date" + }, + "appointment_date": { + "type": "string", + "nullable": true, + "description": "Appointment date when reported.", + "format": "date" + }, + "shareholding": { + "$ref": "#/components/schemas/KYBRegistryShareholding" + } + } }, - { - "type": "EMAIL_VERIFICATION_DECLINED", - "timestamp": "2026-06-12T01:24:47.384292+00:00", - "details": { - "reason": "DISPOSABLE_EMAIL_DETECTED" - }, - "fee": 0 + "description": "Beneficial owners / persons with significant control extracted from the registry. Can be empty when the registry does not expose ownership." + }, + "addresses": { + "type": "array", + "nullable": true, + "description": "Every address the registry states, one canonical shape. Null when it states none.", + "items": { + "$ref": "#/components/schemas/KYBRegistryAddress" } - ], - "matches": [] - }, - "vendor_data": "user-1234", - "metadata": null, - "created_at": "2026-06-12T01:24:47.401719+00:00" - } - }, - "Expired or Not Found": { - "summary": "No pending verification for this address in the last 5 minutes", - "value": { - "request_id": "88808a77-06e9-4cb0-85f5-9c052ddfd987", - "status": "Expired or Not Found", - "message": "No pending email verification found in the last 5 minutes.", - "vendor_data": null, - "metadata": null, - "created_at": "2026-06-12T01:24:59.887904+00:00" - } - } - } - } - } - }, - "400": { - "description": "Validation error — DRF's field-error envelope: one array of messages per offending field.", - "content": { - "application/json": { - "examples": { - "Invalid email": { - "summary": "Malformed email address", - "value": { - "email": [ - "Enter a valid email address." - ] - } - }, - "Missing code": { - "summary": "`code` not included in the body", - "value": { - "code": [ - "This field is required." - ] - } - } - } - } - } - }, - "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — this API never returns `401`. Checks are free, so there is no credit check here.", - "content": { - "application/json": { - "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", - "value": { - "detail": "You do not have permission to perform this action." - } - } - } - } - } - }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", - "content": { - "application/json": { - "examples": { - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." - } - } - }, - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } - } - } - } - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - } - }, - "/v3/workflows/": { - "get": { - "summary": "List workflows", - "description": "List your workflows, newest first, in a limit/offset pagination envelope (`{count, next, previous, results}`, default page size 50). Each row is one version; `workflow_id` is the stable group ID and new sessions use the latest `published` version.", - "operationId": "list_workflows", - "tags": [ - "Workflows" - ], - "parameters": [ - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "default": 50 - }, - "description": "Page size (number of workflows per page)." - }, - { - "name": "offset", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "default": 0 - }, - "description": "Number of rows to skip before the first returned workflow." - } - ], - "responses": { - "200": { - "description": "One page of workflows ordered by `created_at` descending, wrapped in the pagination envelope. Each item in `results` is a `WorkflowListItem`.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "count": { - "type": "integer", - "description": "Total number of workflow versions for the application." + }, + "industries": { + "type": "array", + "nullable": true, + "description": "The activities the registry classifies the company under, one canonical shape. Null when it states none.", + "items": { + "$ref": "#/components/schemas/KYBRegistryIndustry" + } + }, + "accounts": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts." + }, + "registry_data": { + "type": "object", + "description": "Raw record returned by the registry source, kept for reviewers. Its shape differs by country and source and is not a stable contract: read the canonical fields (`addresses`, `industries`, `ownership`, `beneficial_owners[].shareholding`) instead." + }, + "user_provided_data": { + "type": "object", + "nullable": true, + "description": "Always `null` on this endpoint (used by the hosted KYB flow when end users edit registry data)." + }, + "confirmed_by_user_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Always `null` on this endpoint." + }, + "last_console_edit_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Set when a console reviewer edits the record later." + }, + "is_editable": { + "type": "boolean", + "description": "Whether the registry-sourced data can still be edited in the hosted flow/console." + }, + "registry_tier_delivered": { + "type": "string", + "enum": [ + "basic", + "shareholders", + "ubo", + null + ], + "description": "Same as the top-level `registry_tier_delivered`.", + "nullable": true + }, + "registry_ownership_status": { + "type": "string", + "nullable": true, + "enum": [ + "not_requested", + "delivered", + "unavailable", + "pending", + null + ], + "description": "`delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier." + }, + "ownership": { + "$ref": "#/components/schemas/KYBRegistryOwnership" + }, + "registry_tier": { + "type": "string", + "nullable": true, + "description": "The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`." + } + } }, - "next": { + "vendor_data": { "type": "string", - "format": "uri", "nullable": true, - "description": "URL of the next page, or `null` on the last page." + "description": "Echo of the `vendor_data` you sent." }, - "previous": { - "type": "string", - "format": "uri", + "metadata": { + "type": "object", "nullable": true, - "description": "URL of the previous page, or `null` on the first page." + "description": "Echo of the `metadata` you sent." }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WorkflowListItem" - } + "created_at": { + "type": "string", + "format": "date-time" } } }, "examples": { - "Two workflows": { - "summary": "A default KYC workflow and a draft KYC + AML workflow", + "Resolved company": { + "summary": "Full registry profile (officer list truncated for brevity)", "value": { - "count": 2, - "next": null, - "previous": null, - "results": [ - { - "uuid": "a1b2c3d4-5678-90ab-cdef-111111111111", - "workflow_id": "a1b2c3d4-5678-90ab-cdef-111111111111", - "workflow_label": "Standard KYC", - "workflow_type": "kyc", - "is_default": true, - "is_archived": false, - "is_white_label_enabled": false, - "total_price": 0.15, - "min_price": 0.15, - "max_price": 0.15, - "features": "OCR + LIVENESS + FACE_MATCH", - "is_simple_workflow": true, - "is_editable": false, - "workflow_url": "https://verify.didit.me/u/_yljPOx8RWG3UAz5ZRQGVQ", - "max_retry_attempts": 3, - "retry_window_days": 7, - "session_expiration_time": 604800, - "version": 2, - "status": "published", - "has_draft": false, - "created_by": { - "name": "Alex Mateo", - "email": "alex@example.com", - "type": "user" - }, - "updated_at": "2026-04-12T10:30:00Z" + "request_id": "9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e", + "kyb_registry": { + "uuid": "f1e2d3c4-b5a6-4789-9c0d-1e2f3a4b5c6d", + "node_id": "feature_kyb_registry", + "status": "In Review", + "registry_status": "active", + "data_resolved": true, + "company_name": "Tesco PLC", + "registration_number": "00445790", + "country_code": "GB", + "region": "", + "company_type": "Public Limited Company", + "incorporation_date": "1947-11-27", + "registered_address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", + "tax_number": "GB412 8925 93", + "risk_level": "low risk", + "verification_status": "verified", + "is_from_registry": true, + "fetch_status": "resolved", + "alternative_names": "TESCO STORES (HOLDINGS) PUBLIC LIMITED COMPANY", + "nature_of_business": null, + "registered_capital": null, + "registered_capital_amount": null, + "registered_capital_currency": null, + "website": "https://www.tesco.com/", + "email": "customer.service@tesco.co.uk", + "phone": "03301231688", + "legal_entity_identifier": null, + "location_of_registration": null, + "financial_summary": { + "currency": "Pound (£)", + "total_number_of_shares": "8174378721", + "meta_detail": { + "Class_Of_Share": "Ordinary" + } }, - { - "uuid": "b2c3d4e5-6789-01bc-defa-222222222222", - "workflow_id": "b2c3d4e5-6789-01bc-defa-222222222222", - "workflow_label": "Full Verification + AML", - "workflow_type": "kyc", - "is_default": false, - "is_archived": false, - "is_white_label_enabled": false, - "total_price": 0.25, - "min_price": 0.25, - "max_price": 0.25, - "features": "OCR + LIVENESS + FACE_MATCH + AML", - "is_simple_workflow": true, - "is_editable": true, - "workflow_url": "https://verify.didit.me/u/Aq3kPOx8RWG3UAz5ZRQGVQ", - "max_retry_attempts": 3, - "retry_window_days": 7, - "session_expiration_time": 604800, - "version": 1, - "status": "draft", - "has_draft": true, - "created_by": null, - "updated_at": "2026-04-15T18:05:21Z" - } - ] - } - } - } - } - } - }, - "403": { - "description": "The API key is missing, malformed, expired, or belongs to a different application. Body: `{\"detail\": \"You do not have permission to perform this action.\"}`. Refresh the token via the [Auth API](/auth-api/get-credentials) and retry.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } - }, - "examples": { - "Invalid token": { - "value": { - "detail": "You do not have permission to perform this action." - } - } - } - } - } - }, - "429": { - "description": "Rate limit exceeded. Retry with exponential backoff and honour any `Retry-After` header." - }, - "500": { - "description": "Unexpected server error while building the list. The request can be safely retried." - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl -X GET 'https://verification.didit.me/v3/workflows/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Accept: application/json'" - }, - { - "lang": "Python", - "label": "Python (requests)", - "source": "import requests\n\nresp = requests.get(\n \"https://verification.didit.me/v3/workflows/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n timeout=10,\n)\nresp.raise_for_status()\npage = resp.json()\nfor workflow in page[\"results\"]:\n print(workflow[\"uuid\"], workflow[\"workflow_label\"], workflow[\"status\"])" - }, - { - "lang": "JavaScript", - "label": "Node.js (fetch)", - "source": "const res = await fetch(\"https://verification.didit.me/v3/workflows/\", {\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n});\nif (!res.ok) throw new Error(`Didit ${res.status}`);\nconst page = await res.json();\npage.results.forEach((w) => console.log(w.uuid, w.workflow_label, w.status));" - } - ] - }, - "post": { - "summary": "Create workflow", - "description": "Create a simple workflow. Send `features` in execution order (ordering rules apply); defaults to `published` v1. The body accepts ONLY the fields listed in `SimpleWorkflowRequest` — any other key (including `workflow_type`) is rejected with a 400. Not idempotent — each call creates a new workflow. Maximum 50 workflows per application.", - "operationId": "create_workflow", - "tags": [ - "Workflows" - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SimpleWorkflowRequest" - }, - "examples": { - "Simple KYC": { - "summary": "Linear ID, liveness, and face match workflow", - "value": { - "workflow_label": "Standard KYC", - "features": [ - { - "feature": "OCR", - "config": { - "duplicated_user_action": "review" - } - }, - { - "feature": "LIVENESS", - "config": { - "face_liveness_method": "passive" - } - }, - { - "feature": "FACE_MATCH", - "config": { - "face_match_score_decline_threshold": 40, - "face_match_score_review_threshold": 60 - } - } - ] - } - }, - "KYC + AML": { - "summary": "Run AML after ID verification", - "value": { - "workflow_label": "KYC with AML", - "features": [ - { - "feature": "OCR" + "officers": [ + { + "uuid": "a1b2c3d4-e5f6-4789-9a0b-1c2d3e4f5a6b", + "name": "CHRISTOPHER JON TAYLOR", + "designation": "Secretary", + "role": "secretary", + "nationality": null, + "is_active": true, + "kyc_status": null, + "kyc_session_url": null + }, + { + "uuid": "b2c3d4e5-f6a7-4890-8b1c-2d3e4f5a6b7c", + "name": "MELISSA BETHELL", + "designation": "Director", + "role": "director", + "nationality": "British", + "is_active": true, + "kyc_status": null, + "kyc_session_url": null + } + ], + "beneficial_owners": [], + "addresses": [ + { + "address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", + "type": "legal_entity_registry_address", + "description": "registered office address" + } + ], + "industries": null, + "accounts": { + "last_account": "26 February 2025", + "next_account": "26 February 2026", + "due_by_date": "26 August 2026" + }, + "registry_data": { + "name": "Tesco PLC", + "registration_number": "00445790", + "status": "active", + "type": "Public Limited Company", + "incorporation_date": "1947-11-27", + "tax_number": "GB412 8925 93", + "country_code": "GB", + "fetch_status": "resolved", + "industries_detail": [ + { + "code": "47110", + "name": "Retail Sale In Non-specialised Stores With Food, Beverages Or Tobacco Predominating", + "description": null, + "meta_detail": [] + } + ] + }, + "user_provided_data": null, + "confirmed_by_user_at": null, + "last_console_edit_at": null, + "is_editable": true }, - { - "feature": "AML", - "config": { - "aml_score_review_threshold": 70, - "aml_score_approve_threshold": 30 - } - } - ] - } - }, - "Questionnaire workflow": { - "summary": "Run a questionnaire created with POST /v3/questionnaires/", - "value": { - "workflow_label": "Source of Funds", - "features": [ - { - "feature": "QUESTIONNAIRE", - "config": { - "questionnaire_uuid": "11111111-2222-3333-4444-555555555555", - "review_questionnaire_manually": true - } - } - ] + "vendor_data": "company-1234", + "metadata": null, + "created_at": "2026-06-11T10:45:00.000000+00:00" + } } } } } - } - }, - "responses": { - "201": { - "description": "Workflow created. The response body is the FULL workflow configuration — the same raw `VerificationSettings` serialization returned by `GET /v3/workflows/{settings_uuid}/` (~190 fields: every feature toggle, threshold, action and the generated `workflow_graph`), not the compact list item. Store the `uuid` (also exposed as `workflow_id`) so you can reference it from later session-creation calls. Note: `features` is a `\" + \"`-joined string and prices are JSON numbers.", + }, + "400": { + "description": "Validation error. Field-level problems use DRF's standard envelope.", "content": { "application/json": { - "schema": { - "type": "object", - "description": "Full workflow configuration (abbreviated here — see `GET /v3/workflows/{settings_uuid}/`).", - "additionalProperties": true - }, "examples": { - "Created (abbreviated)": { - "summary": "Key fields of the full configuration object", + "Missing kyb_response_id": { + "summary": "`kyb_response_id` is required", "value": { - "uuid": "c3d4e5f6-7890-12cd-ef34-333333333333", - "workflow_id": "c3d4e5f6-7890-12cd-ef34-333333333333", - "workflow_label": "Standard KYC", - "workflow_type": null, - "is_default": false, - "is_archived": false, - "total_price": 0.15, - "min_price": 0.15, - "max_price": 0.15, - "features": "OCR + LIVENESS + FACE_MATCH", - "is_simple_workflow": false, - "is_editable": false, - "workflow_url": "https://verify.didit.me/u/w9TkPOx8RWG3UAz5ZRQGVQ", - "workflow_graph": { - "start_node": "ocr", - "nodes": "... one node per feature plus a final status node ..." - }, - "max_retry_attempts": 3, - "retry_window_days": 7, - "session_expiration_time": 604800, - "version": 1, - "status": "published", - "published_at": "2026-05-17T10:30:00Z", - "created_at": "2026-05-17T10:30:00Z", - "updated_at": "2026-05-17T10:30:00Z" + "kyb_response_id": [ + "This field is required." + ] + } + }, + "save_api_request=false rejected": { + "summary": "Selects are always persisted", + "value": { + "save_api_request": [ + "KYB registry select must be saved so billing and retrieval can be tracked." + ] } } } } } }, - "400": { - "description": "Validation error. Common causes: a top-level key outside the accepted whitelist (e.g. `workflow_type`), an unknown `feature` value, a feature placed before its dependency (e.g., `FACE_MATCH` before `OCR`), a passive feature (`AML`, `IP_ANALYSIS`, `DATABASE_VALIDATION`, `KYB_KEY_PEOPLE`) used as the first step, a `QUESTIONNAIRE` feature whose `config.questionnaire_uuid` does not exist for this application, or an out-of-range threshold. Body is a DRF validation error object — keys are field names, values are arrays of human-readable messages.", + "403": { + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any screening or provider call happens.", "content": { "application/json": { "examples": { - "Missing feature": { - "value": { - "features": [ - "At least one feature is required." - ] - } - }, - "Unknown top-level field": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", "value": { - "workflow_type": [ - "The v3 workflow API only accepts `workflow_label`, `is_default`, `status`, `features`, `is_white_label_enabled`, `is_desktop_allowed`, `max_retry_attempts`, `retry_window_days`, `session_expiration_time`, `face_liveness_max_attempts`, `face_match_max_attempts`." - ] + "detail": "You do not have permission to perform this action." } }, - "Unknown feature": { + "Not enough credits": { + "summary": "Organization balance cannot cover the call", "value": { - "features": [ - "Invalid feature 'FOO'. Allowed values: AGE_ESTIMATION, AML, DATABASE_VALIDATION, EMAIL_VERIFICATION, FACE_MATCH, IP_ANALYSIS, KYB_DOCUMENTS, KYB_KEY_PEOPLE, KYB_REGISTRY, LIVENESS, NFC, OCR, PHONE_VERIFICATION, PROOF_OF_ADDRESS, QUESTIONNAIRE." - ] + "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" } - }, - "Ordering": { + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "content": { + "application/json": { + "examples": { + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", "value": { - "features": [ - "Feature 'FACE_MATCH' must appear after one of: OCR." - ] + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." } - }, - "Questionnaire not found": { - "value": { - "features": "Questionnaire '11111111-2222-3333-4444-555555555555' was not found for this application. Create the questionnaire first, then use its questionnaire_id." + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" } } } } } }, - "403": { - "description": "The API key is missing, malformed, expired, or scoped to a different application. Refresh via the [Auth API](/auth-api/get-credentials) and retry.", + "202": { + "description": "Report accepted and still processing. When a report is still being prepared, the endpoint returns 202 Accepted, kyb_registry.fetch_status=pending, and registry_tier_delivered=null. Keep request_id and retrieve GET /v3/session/{request_id}/decision/ until registry_checks[0].company.fetch_status is resolved. Didit completes the report automatically and charges the delivered tier once it is ready. Do not repeat the purchase while it is pending; every new POST creates a separate request.", "content": { "application/json": { + "schema": { + "$ref": "#/paths/~1v3~1kyb~1shareholders~1/post/responses/200/content/application~1json/schema" + }, "examples": { - "Invalid token": { + "Pending (registry still resolving)": { + "summary": "Ownership report is still processing; retrieve the saved decision", "value": { - "detail": "You do not have permission to perform this action." + "request_id": "9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e", + "kyb_registry": { + "uuid": "7c0a4f7e-2b1d-4f6a-9c3e-8d5b2a1f0e9d", + "node_id": "feature_kyb_registry", + "status": "Not Finished", + "registry_status": null, + "data_resolved": false, + "company_name": "Example Holdings Limited", + "registration_number": "12345678", + "country_code": "HK", + "region": null, + "company_type": null, + "incorporation_date": null, + "registered_address": null, + "tax_number": null, + "risk_level": null, + "verification_status": null, + "is_from_registry": true, + "fetch_status": "pending", + "alternative_names": null, + "nature_of_business": null, + "registered_capital": null, + "registered_capital_amount": null, + "registered_capital_currency": null, + "website": null, + "email": null, + "phone": null, + "legal_entity_identifier": null, + "location_of_registration": null, + "financial_summary": {}, + "officers": [], + "beneficial_owners": [], + "addresses": null, + "industries": null, + "accounts": null, + "registry_data": {}, + "user_provided_data": null, + "confirmed_by_user_at": null, + "last_console_edit_at": null, + "is_editable": true, + "registry_tier_delivered": null, + "registry_ownership_status": "pending" + }, + "vendor_data": "company-1234", + "metadata": null, + "created_at": "2026-06-11T10:45:00.000000+00:00", + "registry_tier_delivered": null, + "registry_tier_requested": "shareholders" } } } } } - }, - "415": { - "description": "Unsupported media type. Set `Content-Type: application/json` and send a JSON body." - }, - "429": { - "description": "Rate limit exceeded. Retry with exponential backoff." - }, - "500": { - "description": "Unexpected server error while creating the workflow. Safe to retry." } + } + } + }, + "/v3/kyb/ubo/": { + "post": { + "summary": "KYB registry company with ultimate beneficial owners (standalone)", + "operationId": "post_v3_kyb_ubo", + "x-didit-sdk": { + "resource": "kyb", + "method": "list_ubos", + "auth": "api_key", + "server": true }, + "tags": [ + "Standalone APIs" + ], "security": [ { "ApiKeyAuth": [] } ], + "parameters": [], + "description": "Retrieve the Lite profile, the direct shareholders **and the ultimate beneficial owners** resolved through the ownership chain (`registry_data.ubo_resolution` carries the corporate parent chain) of one candidate returned by `POST /v3/kyb/search/`.\n\n**Pairing with search.** Pass the candidate's `kyb_response_id` exactly as returned by a search of the same application. The search runs on the Lite registry chain; when the country's ownership data lives at another registry Didit re-finds the company there by registration number, at no extra cost.\n\n**Billing.** Billed at the country's UBO price - see [KYB registry pricing](/getting-started/kyb-registry-pricing). The tier delivered is the highest the country and the registry can serve: Shareholders when the resolved report names no natural person as a beneficial owner, Lite when there is no ownership record at all; `registry_tier_delivered` names what was billed. The balance is checked against the UBO price before any registry call (`403` when short).\n\n**Availability.** v3 only. Every call creates a business session (its id is the `request_id`).\n\n**Pending reports.** When a report is still being prepared, or the ownership section has not been retrieved from the registry yet, the endpoint returns 202 Accepted, kyb_registry.fetch_status=pending, kyb_registry.registry_ownership_status=pending, and registry_tier_delivered=null. Keep request_id and retrieve GET /v3/session/{request_id}/decision/ until registry_checks[0].company.fetch_status is resolved. Didit completes the report automatically and charges the delivered tier once it is ready. Do not repeat the purchase while it is pending; every new POST creates a separate request.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KYBRegistryCandidateRequest" + } + } + } + }, "x-codeSamples": [ { "lang": "curl", - "label": "cURL", - "source": "curl -X POST 'https://verification.didit.me/v3/workflows/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"workflow_label\": \"Standard KYC\",\n \"features\": [\n {\"feature\": \"OCR\"},\n {\"feature\": \"LIVENESS\", \"config\": {\"face_liveness_method\": \"passive\"}},\n {\"feature\": \"FACE_MATCH\", \"config\": {\"face_match_score_decline_threshold\": 40, \"face_match_score_review_threshold\": 60}}\n ]\n }'" + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/kyb/ubo/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"kyb_response_id\": \"69aeeb95febb0f1704042259\",\n \"vendor_data\": \"company-1234\"\n }'" }, { - "lang": "Python", - "label": "Python (requests)", - "source": "import requests\n\npayload = {\n \"workflow_label\": \"Standard KYC\",\n \"features\": [\n {\"feature\": \"OCR\"},\n {\"feature\": \"LIVENESS\", \"config\": {\"face_liveness_method\": \"passive\"}},\n {\"feature\": \"FACE_MATCH\"},\n ],\n}\nresp = requests.post(\n \"https://verification.didit.me/v3/workflows/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n json=payload,\n timeout=10,\n)\nresp.raise_for_status()\nworkflow = resp.json()\nprint(\"Workflow id:\", workflow[\"uuid\"])" + "lang": "python", + "label": "Python", + "source": "import os, time, requests\n\nsearch = requests.post(\n \"https://verification.didit.me/v3/kyb/search/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"country_code\": \"GB\", \"name\": \"Tesco\"},\n timeout=60,\n).json()\nkyb_response_id = search[\"kyb_registry\"][\"companies\"][0][\"kyb_response_id\"]\n\nresp = requests.post(\n \"https://verification.didit.me/v3/kyb/ubo/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"kyb_response_id\": kyb_response_id, \"vendor_data\": \"company-1234\"},\n timeout=90,\n)\nresp.raise_for_status()\ndata = resp.json()\nrequest_id = data[\"request_id\"]\nprint(\"Keep this request ID:\", request_id)\ncompany = data[\"kyb_registry\"]\nif resp.status_code == 202 or company.get(\"fetch_status\") == \"pending\":\n # This client wait limit does not cancel the report or create a new purchase.\n deadline = time.monotonic() + 300\n while company.get(\"fetch_status\") != \"resolved\" and time.monotonic() < deadline:\n time.sleep(5)\n decision = requests.get(\n f\"https://verification.didit.me/v3/session/{request_id}/decision/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n timeout=30,\n )\n decision.raise_for_status()\n checks = decision.json().get(\"registry_checks\") or []\n if checks:\n company = checks[0].get(\"company\") or company\nif company.get(\"fetch_status\") == \"resolved\":\n print(company[\"company_name\"], company[\"registry_tier_delivered\"])\nelse:\n print(\"Still processing; retrieve this request later:\", request_id)" }, { - "lang": "JavaScript", - "label": "Node.js (fetch)", - "source": "const res = await fetch(\"https://verification.didit.me/v3/workflows/\", {\n method: \"POST\",\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n workflow_label: \"Standard KYC\",\n features: [\n { feature: \"OCR\" },\n { feature: \"LIVENESS\", config: { face_liveness_method: \"passive\" } },\n { feature: \"FACE_MATCH\" },\n ],\n }),\n});\nif (!res.ok) throw new Error(`Didit ${res.status}: ${await res.text()}`);\nconst workflow = await res.json();\nconsole.log(\"Workflow id:\", workflow.uuid);" - } - ] - } - }, - "/v3/workflows/{settings_uuid}/": { - "get": { - "summary": "Get workflow", - "description": "Fetch full configuration for one workflow version by `settings_uuid`: feature toggles, thresholds, allowed documents, graph, retry policy, callbacks, and pricing.", - "operationId": "get_workflow", - "tags": [ - "Workflows" - ], - "parameters": [ - { - "name": "settings_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Per-version UUID of the workflow (the `uuid` value returned by the list endpoint). Each version of a workflow has its own `settings_uuid`." + "lang": "javascript", + "label": "JavaScript", + "source": "const res = await fetch('https://verification.didit.me/v3/kyb/ubo/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n kyb_response_id: '69aeeb95febb0f1704042259',\n vendor_data: 'company-1234',\n }),\n});\nif (!res.ok) throw new Error(`Request failed: ${res.status}`);\nconst data = await res.json();\nconst requestId = data.request_id;\nconsole.log('Keep this request ID:', requestId);\nlet company = data.kyb_registry;\nif (res.status === 202 || company.fetch_status === 'pending') {\n // This client wait limit does not cancel the report or create a new purchase.\n const deadline = Date.now() + 300000;\n while (company.fetch_status !== 'resolved' && Date.now() < deadline) {\n await new Promise(resolve => setTimeout(resolve, 5000));\n const decisionResponse = await fetch(\n `https://verification.didit.me/v3/session/${requestId}/decision/`,\n { headers: { 'x-api-key': 'YOUR_API_KEY' }, signal: AbortSignal.timeout(30000) },\n );\n if (!decisionResponse.ok) throw new Error(`Retrieval failed: ${decisionResponse.status}`);\n const decision = await decisionResponse.json();\n company = decision.registry_checks?.[0]?.company ?? company;\n }\n}\nif (company.fetch_status === 'resolved') {\n console.log(company.company_name, company.registry_tier_delivered);\n} else {\n console.log('Still processing; retrieve this request later:', requestId);\n}" } ], "responses": { "200": { - "description": "Full workflow configuration. The shape is the raw `VerificationSettings` serialization (~190 fields: every feature toggle, threshold, warning action, allowed-documents config, the `workflow_graph`, retry policy, callbacks and pricing). The example below is abbreviated. Notes: `features` is a `\" + \"`-joined string (e.g. `\"OCR + LIVENESS\"`), prices are JSON numbers, and `is_editable` is `true` only for `draft` versions.", + "description": "Company report returned and billed at the delivered tier. Ownership reports still being prepared can return 202; keep request_id to retrieve their saved decision.", "content": { "application/json": { - "examples": { - "KYC workflow with face match (abbreviated)": { - "value": { - "uuid": "a1b2c3d4-5678-90ab-cdef-111111111111", - "workflow_id": "a1b2c3d4-5678-90ab-cdef-111111111111", - "workflow_label": "Standard KYC", - "workflow_type": "kyc", - "is_default": true, - "is_archived": false, - "is_liveness_enabled": true, - "face_liveness_method": "passive", - "face_liveness_score_decline_threshold": 50, - "face_liveness_score_review_threshold": 70, - "is_face_match_enabled": true, - "face_match_score_decline_threshold": 40, - "face_match_score_review_threshold": 60, - "is_aml_enabled": false, - "is_phone_verification_enabled": false, - "is_email_verification_enabled": false, - "is_database_validation_enabled": false, - "is_ip_analysis_enabled": false, - "is_nfc_enabled": false, - "duplicated_user_action": "no_action", - "documents_allowed": { - "ESP": { - "P": { - "sides": 1, - "enabled": 1, - "subtypes": [ - "EPASSPORT", - "PASSPORT_GENERIC" - ], - "preferred_characters": "latin", - "expiration_check_mode": "strict" + "schema": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Id of the business session created for this retrieval. The stored record (visible in the console) keeps updating if the registry resolves after the response." + }, + "registry_tier_requested": { + "type": "string", + "enum": [ + "basic", + "shareholders", + "ubo" + ], + "description": "The tier this endpoint sells: `basic` (Lite) for select, `shareholders`, `ubo`." + }, + "registry_tier_delivered": { + "type": "string", + "enum": [ + "basic", + "shareholders", + "ubo", + null + ], + "description": "The tier actually delivered and billed. Lower than requested when the country's registries hold no ownership data, when the registry has no ownership record for this company, or when the resolved report of a `ubo` request names no natural person as a beneficial owner.", + "nullable": true + }, + "kyb_registry": { + "type": "object", + "description": "Full registry company profile.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Didit's id for this company record." + }, + "node_id": { + "type": "string", + "example": "feature_kyb_registry" + }, + "status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review" + ], + "description": "Didit's assessment of the registry check (e.g. `In Review` when the company is not active in the registry or ownership data could not be derived)." + }, + "registry_status": { + "type": "string", + "nullable": true, + "enum": [ + "active", + "dissolved", + "deregistered", + "see full details", + "authorised", + "appointed representative", + "unauthorised", + "inactive", + "no longer authorised", + "closed", + "struck off", + null + ], + "description": "Normalized company status as reported by the registry." + }, + "data_resolved": { + "type": "boolean", + "description": "`true` once the registry returned the full profile. `false` when the registry was still processing after polling — the stored record completes automatically once the registry resolves." + }, + "company_name": { + "type": "string", + "example": "Tesco PLC" + }, + "registration_number": { + "type": "string", + "nullable": true, + "example": "00445790" + }, + "country_code": { + "type": "string", + "description": "ISO 3166-1 alpha-2.", + "example": "GB" + }, + "region": { + "type": "string", + "nullable": true, + "description": "Subdivision code when the registry is regional (e.g. `CA` for `US-CA`); empty otherwise." + }, + "company_type": { + "type": "string", + "nullable": true, + "example": "Public Limited Company" + }, + "incorporation_date": { + "type": "string", + "format": "date", + "nullable": true, + "example": "1947-11-27" + }, + "registered_address": { + "type": "string", + "nullable": true, + "example": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA" + }, + "tax_number": { + "type": "string", + "nullable": true, + "example": "GB412 8925 93" + }, + "risk_level": { + "type": "string", + "nullable": true, + "description": "Registry-reported risk level, when available." + }, + "verification_status": { + "type": "string", + "nullable": true, + "description": "Registry verification status, when available." + }, + "is_from_registry": { + "type": "boolean", + "description": "Always `true` for this endpoint — the profile was retrieved from an official registry." + }, + "fetch_status": { + "type": "string", + "enum": [ + "pending", + "resolved" + ], + "description": "Registry fetch lifecycle. Mirrored by `data_resolved`." + }, + "alternative_names": { + "type": "string", + "nullable": true, + "description": "Alternative, trading, former or original-language company names as text, when reported." + }, + "nature_of_business": { + "type": "string", + "nullable": true, + "description": "Activity description or classification code, when reported." + }, + "registered_capital": { + "type": "string", + "nullable": true + }, + "registered_capital_amount": { + "type": "string", + "nullable": true, + "description": "Decimal amount as a string, when reported." + }, + "registered_capital_currency": { + "type": "string", + "nullable": true, + "description": "ISO 4217 code, when reported." + }, + "website": { + "type": "string", + "nullable": true, + "example": "https://www.tesco.com/" + }, + "email": { + "type": "string", + "nullable": true, + "example": "customer.service@tesco.co.uk" + }, + "phone": { + "type": "string", + "nullable": true, + "example": "03301231688" + }, + "legal_entity_identifier": { + "type": "string", + "nullable": true, + "description": "LEI, when reported." + }, + "location_of_registration": { + "type": "string", + "nullable": true, + "description": "Registry/jurisdiction the company is registered with, when reported." + }, + "vat_number": { + "type": "string", + "nullable": true, + "description": "EU VAT number, when collected during the hosted registry step or edited afterwards. At select time this is typically not yet set — VAT and tax ID validation runs when the end user submits the registry step in the hosted flow." + }, + "vat_validation_status": { + "type": "string", + "enum": [ + "valid", + "invalid", + "format_valid", + "invalid_format", + "could_not_validate", + "not_applicable" + ], + "description": "Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session." + }, + "vat_validated_number": { + "type": "string", + "nullable": true, + "description": "The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran." + }, + "vat_validation_source": { + "type": "string", + "nullable": true, + "enum": [ + "eu_vies", + "pl_vat_white_list", + "no_brreg", + "ch_uid_register", + "br_receita_federal", + "co_rues", + "cr_hacienda", + "ec_sri", + "py_dnit", + "format_rules", + null + ], + "description": "The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran." + }, + "vat_validated_name": { + "type": "string", + "nullable": true, + "description": "Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." + }, + "vat_validated_address": { + "type": "string", + "nullable": true, + "description": "Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." + }, + "vat_checked_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Timestamp of the check. `null` when no check ran." + }, + "financial_summary": { + "type": "object", + "nullable": true, + "description": "Registry-reported share/capital details (currency, total shares, share classes), when available." + }, + "officers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "example": "MELISSA BETHELL" + }, + "designation": { + "type": "string", + "nullable": true, + "description": "Raw registry designation (e.g. `Director`, `Secretary`)." + }, + "role": { + "type": "string", + "description": "Normalized role derived from the designation. Full 17-value model set; registry parsing on this endpoint typically emits director, non_executive_director, secretary, chairman, or company_officer.", + "enum": [ + "director", + "non_executive_director", + "secretary", + "chairman", + "ubo", + "shareholder", + "representative", + "founder", + "legal_advisor", + "authorized_signatory", + "trustee", + "beneficiary", + "company_officer", + "settlor", + "protector", + "investor", + "other" + ] + }, + "nationality": { + "type": "string", + "nullable": true + }, + "is_active": { + "type": "boolean", + "description": "`false` when the registry marks the appointment as resigned." + }, + "kyc_status": { + "type": "string", + "nullable": true, + "enum": [ + "Approved", + "Declined", + "Pending", + null + ], + "description": "Status of an associated KYC session, when one was started for this officer. `null` otherwise." + }, + "kyc_session_url": { + "type": "string", + "nullable": true, + "description": "Verification URL of the associated KYC session, when one exists." + }, + "first_name": { + "type": "string", + "nullable": true, + "description": "Given name when explicitly reported by the registry; not inferred from the full name." + }, + "last_name": { + "type": "string", + "nullable": true, + "description": "Family name when explicitly reported by the registry." + }, + "address": { + "type": "string", + "nullable": true, + "description": "Officer address reported by the registry." + }, + "appointment_date": { + "type": "string", + "nullable": true, + "description": "Date the officer was appointed.", + "format": "date" + }, + "termination_date": { + "type": "string", + "nullable": true, + "description": "Date the appointment ended, when reported.", + "format": "date" + }, + "date_of_birth": { + "type": "string", + "nullable": true, + "description": "Complete registry-reported birth date. Null when only a birth year or month is known.", + "format": "date" + }, + "date_of_birth_year": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 9999, + "description": "Registry-reported birth year, including when the full date is unavailable." + }, + "date_of_birth_month": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 12, + "description": "Registry-reported birth month. Null when only the year is known." + } + } + }, + "description": "Directors, secretaries, and other officers extracted from the registry." + }, + "beneficial_owners": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "first_name": { + "type": "string", + "nullable": true + }, + "last_name": { + "type": "string", + "nullable": true + }, + "entity_type": { + "type": "string", + "enum": [ + "person", + "company" + ], + "description": "Whether the owner is a natural person or a corporate entity." + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Normalized roles (e.g. `ubo`, `shareholder`)." + }, + "ownership_min_shares": { + "type": "string", + "nullable": true, + "description": "Lower bound of the ownership band reported by the registry (e.g. `25%`)." + }, + "ownership_max_shares": { + "type": "string", + "nullable": true, + "description": "Upper bound of the ownership band reported by the registry." + }, + "is_active": { + "type": "boolean" + }, + "kyc_status": { + "type": "string", + "nullable": true, + "enum": [ + "Approved", + "Declined", + "Pending", + null + ] + }, + "kyc_session_url": { + "type": "string", + "nullable": true + }, + "effective_ownership_percent": { + "type": "number", + "nullable": true, + "description": "Parsed ownership percentage (max bound, falling back to min bound)." + }, + "company_name": { + "type": "string", + "nullable": true + }, + "registration_number": { + "type": "string", + "nullable": true + }, + "country_of_birth": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "nullable": true + }, + "nationality": { + "type": "string", + "nullable": true + }, + "designation": { + "type": "string", + "nullable": true + }, + "voting_min_shares": { + "type": "string", + "nullable": true + }, + "voting_max_shares": { + "type": "string", + "nullable": true + }, + "date_of_birth": { + "type": "string", + "nullable": true, + "description": "Complete birth date when reported.", + "format": "date" + }, + "appointment_date": { + "type": "string", + "nullable": true, + "description": "Appointment date when reported.", + "format": "date" + }, + "shareholding": { + "$ref": "#/components/schemas/KYBRegistryShareholding" + } + } + }, + "description": "Beneficial owners / persons with significant control extracted from the registry. Can be empty when the registry does not expose ownership." + }, + "addresses": { + "type": "array", + "nullable": true, + "description": "Every address the registry states, one canonical shape. Null when it states none.", + "items": { + "$ref": "#/components/schemas/KYBRegistryAddress" } - } - }, - "max_retry_attempts": 3, - "retry_window_days": 7, - "session_expiration_time": 604800, - "version": 2, - "status": "published", - "published_at": "2026-04-12T10:30:00Z", - "is_simple_workflow": false, - "is_editable": false, - "workflow_url": "https://verify.didit.me/u/_yljPOx8RWG3UAz5ZRQGVQ", - "total_price": 0.15, - "min_price": 0.15, - "max_price": 0.15, - "features": "OCR + LIVENESS + FACE_MATCH", - "created_at": "2025-01-15T10:30:00Z", - "updated_at": "2026-04-12T10:30:00Z" - } - } - } - } - } - }, - "403": { - "description": "API key is missing, malformed, expired, or scoped to a different application.", - "content": { - "application/json": { - "examples": { - "Invalid token": { - "value": { - "detail": "You do not have permission to perform this action." - } - } - } - } - } - }, - "404": { - "description": "No workflow with this `settings_uuid` exists for the authenticated application. Either the UUID is wrong or it belongs to another application — Didit deliberately does not distinguish the two cases to avoid leaking the existence of cross-application records.", - "content": { - "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Not found." - } - } - } - } - } - }, - "429": { - "description": "Rate limit exceeded. Retry with exponential backoff." - }, - "500": { - "description": "Unexpected server error. Safe to retry." - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl -X GET 'https://verification.didit.me/v3/workflows/a1b2c3d4-5678-90ab-cdef-111111111111/' \\\n -H 'x-api-key: YOUR_API_KEY'" - }, - { - "lang": "Python", - "label": "Python (requests)", - "source": "import requests\n\nresp = requests.get(\n f\"https://verification.didit.me/v3/workflows/{settings_uuid}/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n timeout=10,\n)\nresp.raise_for_status()\nworkflow = resp.json()\nprint(workflow[\"workflow_label\"], workflow[\"status\"])" - }, - { - "lang": "JavaScript", - "label": "Node.js (fetch)", - "source": "const res = await fetch(`https://verification.didit.me/v3/workflows/${settingsUuid}/`, {\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n});\nif (!res.ok) throw new Error(`Didit ${res.status}`);\nconst workflow = await res.json();\nconsole.log(workflow.workflow_label, workflow.status);" - } - ] - }, - "patch": { - "summary": "Update workflow", - "description": "Update a workflow. Versioning depends on the `status` field you send and the current state of the version: (1) no `status` on a **published** version → the published version is updated in place (same `uuid`, same `version`); (2) no `status` on a **draft** → the draft is updated and immediately published; (3) `status: \"published\"` on a **published** version → a NEW published version is created under the same `workflow_id` (new `uuid`, `version` + 1) and returned, and any existing draft in the group is deleted; (4) `status: \"draft\"` keeps a draft as a draft (autosave) but is rejected with 400 on a published version (`\"Cannot revert a published version to draft.\"`). The body accepts the same strict field whitelist as create — unknown keys are rejected with 400.", - "operationId": "update_workflow", - "tags": [ - "Workflows" - ], - "parameters": [ - { - "name": "settings_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Per-version UUID of the workflow to update." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SimpleWorkflowUpdateRequest" - }, - "examples": { - "Replace features": { - "summary": "Send the full desired feature order", - "value": { - "features": [ - { - "feature": "OCR" - }, - { - "feature": "AML" - } - ] - } - }, - "Rename": { - "summary": "Rename a workflow", - "value": { - "workflow_label": "Premium KYC" - } - }, - "Set as default": { - "summary": "Make this workflow the application's default", - "value": { - "is_default": true - } - }, - "Update config": { - "summary": "Change feature configuration", - "value": { - "features": [ - { - "feature": "OCR" - }, - { - "feature": "LIVENESS", - "config": { - "face_liveness_method": "passive" + }, + "industries": { + "type": "array", + "nullable": true, + "description": "The activities the registry classifies the company under, one canonical shape. Null when it states none.", + "items": { + "$ref": "#/components/schemas/KYBRegistryIndustry" + } + }, + "accounts": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts." + }, + "registry_data": { + "type": "object", + "description": "Raw record returned by the registry source, kept for reviewers. Its shape differs by country and source and is not a stable contract: read the canonical fields (`addresses`, `industries`, `ownership`, `beneficial_owners[].shareholding`) instead." + }, + "user_provided_data": { + "type": "object", + "nullable": true, + "description": "Always `null` on this endpoint (used by the hosted KYB flow when end users edit registry data)." + }, + "confirmed_by_user_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Always `null` on this endpoint." + }, + "last_console_edit_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Set when a console reviewer edits the record later." + }, + "is_editable": { + "type": "boolean", + "description": "Whether the registry-sourced data can still be edited in the hosted flow/console." + }, + "registry_tier_delivered": { + "type": "string", + "enum": [ + "basic", + "shareholders", + "ubo", + null + ], + "description": "Same as the top-level `registry_tier_delivered`.", + "nullable": true + }, + "registry_ownership_status": { + "type": "string", + "nullable": true, + "enum": [ + "not_requested", + "delivered", + "unavailable", + "pending", + null + ], + "description": "`delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier." + }, + "ownership": { + "$ref": "#/components/schemas/KYBRegistryOwnership" + }, + "registry_tier": { + "type": "string", + "nullable": true, + "description": "The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`." } } - ] - } - }, - "Publish a draft": { - "summary": "Promote a draft to published", - "value": { - "status": "published" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Workflow updated. The body is the FULL configuration of the resulting version (same raw `VerificationSettings` shape as `GET /v3/workflows/{settings_uuid}/`). When the update created a new version (`status: \"published\"` sent on a published row) the returned `uuid` and `version` differ from the ones you patched — use the returned `uuid` for subsequent calls.", - "content": { - "application/json": { - "schema": { - "type": "object", - "description": "Full workflow configuration (see `GET /v3/workflows/{settings_uuid}/`).", - "additionalProperties": true - } - } - } - }, - "400": { - "description": "Validation error — a top-level key outside the accepted whitelist, an unknown feature, invalid ordering, out-of-range threshold, referencing a non-existent questionnaire, or `status: \"draft\"` sent for a published version (`\"Cannot revert a published version to draft.\"`). The response body is a DRF validation error keyed by field name." - }, - "403": { - "description": "API key missing or invalid.", - "content": { - "application/json": { - "examples": { - "Invalid token": { - "value": { - "detail": "You do not have permission to perform this action." - } - } - } - } - } - }, - "404": { - "description": "No workflow with this `settings_uuid` exists for the authenticated application." - }, - "415": { - "description": "Unsupported media type. Set `Content-Type: application/json`." - }, - "429": { - "description": "Rate limit exceeded. Retry with exponential backoff." - }, - "500": { - "description": "Unexpected server error. Safe to retry." - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl -X PATCH 'https://verification.didit.me/v3/workflows/a1b2c3d4-5678-90ab-cdef-111111111111/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"workflow_label\": \"Premium KYC\"}'" - }, - { - "lang": "Python", - "label": "Python (requests)", - "source": "import requests\n\nresp = requests.patch(\n f\"https://verification.didit.me/v3/workflows/{settings_uuid}/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n json={\"workflow_label\": \"Premium KYC\"},\n timeout=10,\n)\nresp.raise_for_status()\nprint(resp.json()[\"workflow_label\"])" - }, - { - "lang": "JavaScript", - "label": "Node.js (fetch)", - "source": "const res = await fetch(`https://verification.didit.me/v3/workflows/${settingsUuid}/`, {\n method: \"PATCH\",\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ workflow_label: \"Premium KYC\" }),\n});\nif (!res.ok) throw new Error(`Didit ${res.status}`);\nconst workflow = await res.json();\nconsole.log(workflow.workflow_label);" - } - ] - }, - "delete": { - "summary": "Delete workflow", - "description": "Permanently delete one workflow version (no undo). Existing sessions keep their verification results but their workflow reference is cleared; other versions in the same `workflow_id` group are not deleted.", - "operationId": "delete_workflow", - "tags": [ - "Workflows" - ], - "parameters": [ - { - "name": "settings_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Per-version UUID of the workflow to delete." - } - ], - "responses": { - "204": { - "description": "Workflow deleted. The response body is empty." - }, - "403": { - "description": "API key missing or invalid.", - "content": { - "application/json": { - "examples": { - "Invalid token": { - "value": { - "detail": "You do not have permission to perform this action." - } - } - } - } - } - }, - "404": { - "description": "No workflow with this `settings_uuid` exists for the authenticated application.", - "content": { - "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Not found." - } - } - } - } - } - }, - "429": { - "description": "Rate limit exceeded. Retry with exponential backoff." - }, - "500": { - "description": "Unexpected server error. Safe to retry." - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl -X DELETE 'https://verification.didit.me/v3/workflows/a1b2c3d4-5678-90ab-cdef-111111111111/' \\\n -H 'x-api-key: YOUR_API_KEY'" - }, - { - "lang": "Python", - "label": "Python (requests)", - "source": "import requests\n\nresp = requests.delete(\n f\"https://verification.didit.me/v3/workflows/{settings_uuid}/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n timeout=10,\n)\nresp.raise_for_status() # raises on 4xx/5xx; 204 is success" - }, - { - "lang": "JavaScript", - "label": "Node.js (fetch)", - "source": "const res = await fetch(`https://verification.didit.me/v3/workflows/${settingsUuid}/`, {\n method: \"DELETE\",\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n});\nif (res.status !== 204) throw new Error(`Didit ${res.status}`);" - } - ] - } - }, - "/v3/questionnaires/": { - "get": { - "summary": "List questionnaires", - "description": "List your questionnaires, newest first, in a limit/offset pagination envelope (`{count, next, previous, results}`, default page size 50). Each row is one version; `questionnaire_group_id` is the stable group ID resolved to the latest `published` version.", - "operationId": "list_questionnaires", - "tags": [ - "Questionnaires" - ], - "parameters": [ - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "default": 50 - }, - "description": "Page size (number of questionnaires per page)." - }, - { - "name": "offset", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "default": 0 - }, - "description": "Number of rows to skip before the first returned questionnaire." - } - ], - "responses": { - "200": { - "description": "One page of questionnaires ordered by `created_at` descending, wrapped in the pagination envelope. Each item in `results` is a `QuestionnaireListItem`.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "count": { - "type": "integer", - "description": "Total number of questionnaire versions for the application." }, - "next": { + "vendor_data": { "type": "string", - "format": "uri", "nullable": true, - "description": "URL of the next page, or `null` on the last page." + "description": "Echo of the `vendor_data` you sent." }, - "previous": { - "type": "string", - "format": "uri", + "metadata": { + "type": "object", "nullable": true, - "description": "URL of the previous page, or `null` on the first page." + "description": "Echo of the `metadata` you sent." }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionnaireListItem" - } + "created_at": { + "type": "string", + "format": "date-time" } } }, "examples": { - "One questionnaire": { - "summary": "A simple bilingual questionnaire", - "value": { - "count": 1, - "next": null, - "previous": null, - "results": [ - { - "uuid": "11111111-2222-3333-4444-555555555555", - "title": "KYC Questionnaire", - "description": "Additional verification questions", - "languages": [ - "en", - "es" - ], - "default_language": "en", - "is_active": true, - "is_simple_questionnaire": true, - "created_at": "2026-04-23T09:55:00Z", - "updated_at": "2026-04-23T10:00:00Z", - "question_types": [ - "SHORT_TEXT", - "MULTIPLE_CHOICE", - "FILE_UPLOAD" - ], - "workflow_names": [], - "questionnaire_group_id": "11111111-2222-3333-4444-555555555555", - "version": 1, - "status": "published", - "published_at": "2026-04-23T10:00:00Z", - "is_editable": false - } - ] - } - } - } - } - } - }, - "403": { - "description": "API key missing, malformed, expired, or scoped to a different application.", - "content": { - "application/json": { - "examples": { - "Invalid token": { + "Resolved company": { + "summary": "Full registry profile (officer list truncated for brevity)", "value": { - "detail": "You do not have permission to perform this action." - } - } - } - } - } - }, - "429": { - "description": "Rate limit exceeded. Retry with exponential backoff." - }, - "500": { - "description": "Unexpected server error while building the list. Safe to retry." - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl -X GET 'https://verification.didit.me/v3/questionnaires/' \\\n -H 'x-api-key: YOUR_API_KEY'" - }, - { - "lang": "Python", - "label": "Python (requests)", - "source": "import requests\n\nresp = requests.get(\n \"https://verification.didit.me/v3/questionnaires/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n timeout=10,\n)\nresp.raise_for_status()\npage = resp.json()\nfor q in page[\"results\"]:\n print(q[\"uuid\"], q[\"title\"], q[\"status\"])" - }, - { - "lang": "JavaScript", - "label": "Node.js (fetch)", - "source": "const res = await fetch(\"https://verification.didit.me/v3/questionnaires/\", {\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n});\nif (!res.ok) throw new Error(`Didit ${res.status}`);\nconst page = await res.json();\npage.results.forEach((q) => console.log(q.uuid, q.title, q.status));" - } - ] - }, - "post": { - "summary": "Create questionnaire", - "description": "Create a simple linear questionnaire. Send `form_elements` in display order; branching and repeatable groups require the Console. Defaults to `published` v1.", - "operationId": "create_questionnaire", - "tags": [ - "Questionnaires" - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SimpleQuestionnaireRequest" - }, - "examples": { - "Simple": { - "summary": "Basic questionnaire", - "value": { - "title": "Customer Onboarding", - "description": "Additional information needed for verification", - "default_language": "en", - "languages": [ - "en" - ], - "is_active": true, - "form_elements": [ - { - "id": "occupation", - "element_type": "short_text", - "label": { - "en": "What is your occupation?" - }, - "is_required": true, - "placeholder": { - "en": "Software engineer" - } - }, - { - "id": "source_of_funds", - "element_type": "multiple_choice", - "label": { - "en": "Source of funds" + "request_id": "9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e", + "kyb_registry": { + "uuid": "f1e2d3c4-b5a6-4789-9c0d-1e2f3a4b5c6d", + "node_id": "feature_kyb_registry", + "status": "In Review", + "registry_status": "active", + "data_resolved": true, + "company_name": "Tesco PLC", + "registration_number": "00445790", + "country_code": "GB", + "region": "", + "company_type": "Public Limited Company", + "incorporation_date": "1947-11-27", + "registered_address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", + "tax_number": "GB412 8925 93", + "risk_level": "low risk", + "verification_status": "verified", + "is_from_registry": true, + "fetch_status": "resolved", + "alternative_names": "TESCO STORES (HOLDINGS) PUBLIC LIMITED COMPANY", + "nature_of_business": null, + "registered_capital": null, + "registered_capital_amount": null, + "registered_capital_currency": null, + "website": "https://www.tesco.com/", + "email": "customer.service@tesco.co.uk", + "phone": "03301231688", + "legal_entity_identifier": null, + "location_of_registration": null, + "financial_summary": { + "currency": "Pound (£)", + "total_number_of_shares": "8174378721", + "meta_detail": { + "Class_Of_Share": "Ordinary" + } }, - "is_required": true, - "options": [ - { - "value": "employment", - "label": { - "en": "Employment" - } - }, - { - "value": "business", - "label": { - "en": "Business" - } - }, + "officers": [ { - "value": "investments", - "label": { - "en": "Investments" - } + "uuid": "a1b2c3d4-e5f6-4789-9a0b-1c2d3e4f5a6b", + "name": "CHRISTOPHER JON TAYLOR", + "designation": "Secretary", + "role": "secretary", + "nationality": null, + "is_active": true, + "kyc_status": null, + "kyc_session_url": null }, { - "value": "other", - "label": { - "en": "Other" - } + "uuid": "b2c3d4e5-f6a7-4890-8b1c-2d3e4f5a6b7c", + "name": "MELISSA BETHELL", + "designation": "Director", + "role": "director", + "nationality": "British", + "is_active": true, + "kyc_status": null, + "kyc_session_url": null } - ] - } - ] - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Questionnaire created. The response includes the `questionnaire_id` — store it to reference this questionnaire from workflows or future update/delete calls.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/QuestionnaireDetail" - }, - "examples": { - "Created": { - "value": { - "questionnaire_id": "11111111-2222-3333-4444-555555555555", - "title": "Customer Onboarding", - "description": "Additional information needed for verification", - "languages": [ - "en" - ], - "default_language": "en", - "is_active": true, - "is_simple_questionnaire": true, - "graph": { - "start_node": "occupation", - "nodes": { - "occupation": { - "element_type": "SHORT_TEXT", - "is_required": true, - "title": { - "en": "What is your occupation?" - }, - "next": "source_of_funds" - }, - "source_of_funds": { - "element_type": "MULTIPLE_CHOICE", - "is_required": true, - "title": { - "en": "Source of funds" - }, - "choices": [ - { - "value": "employment", - "label": { - "en": "Employment" - } - }, - { - "value": "business", - "label": { - "en": "Business" - } - } - ], - "next": null + ], + "beneficial_owners": [], + "addresses": [ + { + "address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", + "type": "legal_entity_registry_address", + "description": "registered office address" } - } + ], + "industries": null, + "accounts": { + "last_account": "26 February 2025", + "next_account": "26 February 2026", + "due_by_date": "26 August 2026" + }, + "registry_data": { + "name": "Tesco PLC", + "registration_number": "00445790", + "status": "active", + "type": "Public Limited Company", + "incorporation_date": "1947-11-27", + "tax_number": "GB412 8925 93", + "country_code": "GB", + "fetch_status": "resolved", + "industries_detail": [ + { + "code": "47110", + "name": "Retail Sale In Non-specialised Stores With Food, Beverages Or Tobacco Predominating", + "description": null, + "meta_detail": [] + } + ] + }, + "user_provided_data": null, + "confirmed_by_user_at": null, + "last_console_edit_at": null, + "is_editable": true }, - "sections": [], - "questionnaire_group_id": "11111111-2222-3333-4444-555555555555", - "version": 1, - "status": "published", - "published_at": "2026-04-23T10:00:00Z", - "is_editable": false + "vendor_data": "company-1234", + "metadata": null, + "created_at": "2026-06-11T10:45:00.000000+00:00" } } } @@ -23697,23 +46027,23 @@ } }, "400": { - "description": "Validation error. Common causes: an unknown `element_type`, a translatable field missing a translation for one of `languages`, `languages` missing `en`, `default_language` not in `languages`, an option without a `label`, a question that has `branches`/`required_if`/`repeatable_config` (not supported by this endpoint), or sending a raw `graph` instead of `form_elements`. The body is a DRF validation error keyed by field name.", + "description": "Validation error. Field-level problems use DRF's standard envelope.", "content": { "application/json": { "examples": { - "Graph not accepted": { + "Missing kyb_response_id": { + "summary": "`kyb_response_id` is required", "value": { - "graph": "The v3 questionnaire API only accepts simple `form_elements`, not `graph`." + "kyb_response_id": [ + "This field is required." + ] } }, - "Missing label": { + "save_api_request=false rejected": { + "summary": "Selects are always persisted", "value": { - "form_elements": [ - { - "label": [ - "This field is required." - ] - } + "save_api_request": [ + "KYB registry select must be saved so billing and retrieval can be tracked." ] } } @@ -23722,7283 +46052,4850 @@ } }, "403": { - "description": "API key missing, malformed, expired, or scoped to a different application.", + "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any screening or provider call happens.", "content": { "application/json": { "examples": { - "Invalid token": { + "Missing or invalid API key": { + "summary": "No `x-api-key` header, or the key is invalid/revoked", "value": { "detail": "You do not have permission to perform this action." } + }, + "Not enough credits": { + "summary": "Organization balance cannot cover the call", + "value": { + "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + } } } } } }, - "415": { - "description": "Unsupported media type. Set `Content-Type: application/json` and send a JSON body." - }, "429": { - "description": "Rate limit exceeded. Retry with exponential backoff." + "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", + "content": { + "application/json": { + "examples": { + "Write rate limit exceeded": { + "summary": "More than 300 write requests in the current minute", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + }, + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + } }, - "500": { - "description": "Unexpected server error while creating the questionnaire. Safe to retry." + "202": { + "description": "Report accepted and still processing. When a report is still being prepared, the endpoint returns 202 Accepted, kyb_registry.fetch_status=pending, and registry_tier_delivered=null. Keep request_id and retrieve GET /v3/session/{request_id}/decision/ until registry_checks[0].company.fetch_status is resolved. Didit completes the report automatically and charges the delivered tier once it is ready. Do not repeat the purchase while it is pending; every new POST creates a separate request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/paths/~1v3~1kyb~1ubo~1/post/responses/200/content/application~1json/schema" + }, + "examples": { + "Pending (registry still resolving)": { + "summary": "Ownership report is still processing; retrieve the saved decision", + "value": { + "request_id": "9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e", + "kyb_registry": { + "uuid": "7c0a4f7e-2b1d-4f6a-9c3e-8d5b2a1f0e9d", + "node_id": "feature_kyb_registry", + "status": "Not Finished", + "registry_status": null, + "data_resolved": false, + "company_name": "Example Holdings Limited", + "registration_number": "12345678", + "country_code": "HK", + "region": null, + "company_type": null, + "incorporation_date": null, + "registered_address": null, + "tax_number": null, + "risk_level": null, + "verification_status": null, + "is_from_registry": true, + "fetch_status": "pending", + "alternative_names": null, + "nature_of_business": null, + "registered_capital": null, + "registered_capital_amount": null, + "registered_capital_currency": null, + "website": null, + "email": null, + "phone": null, + "legal_entity_identifier": null, + "location_of_registration": null, + "financial_summary": {}, + "officers": [], + "beneficial_owners": [], + "addresses": null, + "industries": null, + "accounts": null, + "registry_data": {}, + "user_provided_data": null, + "confirmed_by_user_at": null, + "last_console_edit_at": null, + "is_editable": true, + "registry_tier_delivered": null, + "registry_ownership_status": "pending" + }, + "vendor_data": "company-1234", + "metadata": null, + "created_at": "2026-06-11T10:45:00.000000+00:00", + "registry_tier_delivered": null, + "registry_tier_requested": "ubo" + } + } + } + } + } } + } + } + }, + "/v3/id-verification/wallets/": { + "get": { + "operationId": "list_wallets", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "list", + "auth": "api_key", + "server": true }, + "description": "Every Digital ID wallet with its start endpoint, countries, required fields and whether this application can start it in this environment now. Listed does not mean live.", + "summary": "List Digital ID wallets", + "tags": [ + "Digital ID wallets" + ], "security": [ { "ApiKeyAuth": [] + }, + { + "BearerAuth": [] } ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletCatalogResponse" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiClientCredentialsForbiddenErrorMessage" + } + } + }, + "description": "Missing or invalid credentials (x-api-key or Authorization: Bearer)." + } + }, "x-codeSamples": [ { "lang": "curl", "label": "cURL", - "source": "curl -X POST 'https://verification.didit.me/v3/questionnaires/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"title\": \"Customer Onboarding\",\n \"default_language\": \"en\",\n \"languages\": [\"en\"],\n \"form_elements\": [\n {\n \"id\": \"occupation\",\n \"element_type\": \"short_text\",\n \"label\": {\"en\": \"What is your occupation?\"},\n \"is_required\": true\n }\n ]\n }'" - }, - { - "lang": "Python", - "label": "Python (requests)", - "source": "import requests\n\npayload = {\n \"title\": \"Customer Onboarding\",\n \"default_language\": \"en\",\n \"languages\": [\"en\"],\n \"form_elements\": [\n {\n \"id\": \"occupation\",\n \"element_type\": \"short_text\",\n \"label\": {\"en\": \"What is your occupation?\"},\n \"is_required\": True,\n },\n ],\n}\nresp = requests.post(\n \"https://verification.didit.me/v3/questionnaires/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n json=payload,\n timeout=10,\n)\nresp.raise_for_status()\nquestionnaire = resp.json()\nprint(\"Questionnaire id:\", questionnaire[\"questionnaire_id\"])" + "source": "curl --request GET \\\n --url https://verification.didit.me/v3/id-verification/wallets/ \\\n --header 'x-api-key: YOUR_API_KEY'" }, { - "lang": "JavaScript", - "label": "Node.js (fetch)", - "source": "const res = await fetch(\"https://verification.didit.me/v3/questionnaires/\", {\n method: \"POST\",\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n title: \"Customer Onboarding\",\n default_language: \"en\",\n languages: [\"en\"],\n form_elements: [\n {\n id: \"occupation\",\n element_type: \"short_text\",\n label: { en: \"What is your occupation?\" },\n is_required: true,\n },\n ],\n }),\n});\nif (!res.ok) throw new Error(`Didit ${res.status}: ${await res.text()}`);\nconst questionnaire = await res.json();\nconsole.log(\"Questionnaire id:\", questionnaire.questionnaire_id);" + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"]\n}\n\nresponse = requests.get(\n \"https://verification.didit.me/v3/id-verification/wallets/\",\n headers=headers,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } ] } }, - "/v3/questionnaires/{questionnaire_uuid}/": { + "/v3/id-verification/wallets/verifications/{request_id}/": { "get": { - "summary": "Get questionnaire", - "description": "Fetch full questionnaire detail for one version by `questionnaire_uuid`: title, `languages`, canonical `graph`, derived `sections`, and version metadata.", - "operationId": "get_questionnaire", - "tags": [ - "Questionnaires" - ], + "operationId": "get_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "retrieve_verification", + "auth": "api_key", + "server": true + }, + "description": "Status and result of a wallet verification: the normalized identity and the wallet's own attributes once verified, the error code once declined, the next action while pending. A verification past expires_at is decided timeout.", + "summary": "Get a wallet verification", "parameters": [ { - "name": "questionnaire_uuid", "in": "path", - "required": true, + "name": "request_id", "schema": { "type": "string", "format": "uuid" }, - "description": "Per-version UUID of the questionnaire (the `uuid` value returned by the list endpoint, also returned as `questionnaire_id` on detail endpoints)." + "description": "The request_id the start endpoint returned.", + "required": true + } + ], + "tags": [ + "Digital ID wallets" + ], + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] } ], "responses": { "200": { - "description": "Full questionnaire detail including the canonical `graph`, the derived `sections` view, version metadata, and `is_editable`.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QuestionnaireDetail" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" }, - "examples": { - "Published questionnaire": { - "value": { - "questionnaire_id": "11111111-2222-3333-4444-555555555555", - "title": "Customer Onboarding", - "description": "Additional information needed for verification", - "languages": [ - "en" - ], - "default_language": "en", - "is_active": true, - "is_simple_questionnaire": true, - "graph": { - "start_node": "occupation", - "nodes": { - "occupation": { - "element_type": "SHORT_TEXT", - "is_required": true, - "title": { - "en": "What is your occupation?" - }, - "next": "source_of_funds" - }, - "source_of_funds": { - "element_type": "MULTIPLE_CHOICE", - "is_required": true, - "title": { - "en": "Source of funds" - }, - "choices": [ - { - "value": "employment", - "label": { - "en": "Employment" - } - }, - { - "value": "business", - "label": { - "en": "Business" - } - } - ], - "next": null - } - } - }, - "sections": [ - { - "title": {}, - "description": {}, - "items": [ - { - "uuid": "11111111-2222-3333-4444-666666666666", - "value": "occupation", - "element_type": "SHORT_TEXT", - "is_required": true, - "title": { - "en": "What is your occupation?" - } - } - ] - } - ], - "questionnaire_group_id": "11111111-2222-3333-4444-555555555555", - "version": 1, - "status": "published", - "published_at": "2026-04-23T10:00:00Z", - "is_editable": false - } - } + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "ftn", + "name": "Finnish Trust Network", + "issuing_authority": "Finnish banks and mobile operators (FTN)" + }, + "country": "FIN", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" }, "403": { - "description": "API key missing, malformed, expired, or scoped to a different application.", "content": { "application/json": { - "examples": { - "Invalid token": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiClientCredentialsForbiddenErrorMessage" } } - } + }, + "description": "Missing or invalid credentials (x-api-key or Authorization: Bearer)." }, "404": { - "description": "No questionnaire with this `questionnaire_uuid` exists for the authenticated application. Either the UUID is wrong or it belongs to another application — the API deliberately does not distinguish the two cases.", "content": { "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Not found." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } - }, - "429": { - "description": "Rate limit exceeded. Retry with exponential backoff." - }, - "500": { - "description": "Unexpected server error. Safe to retry." + }, + "description": "error not_found: no wallet verification of this application has this request_id." } }, - "security": [ - { - "ApiKeyAuth": [] - } - ], "x-codeSamples": [ { "lang": "curl", "label": "cURL", - "source": "curl -X GET 'https://verification.didit.me/v3/questionnaires/11111111-2222-3333-4444-555555555555/' \\\n -H 'x-api-key: YOUR_API_KEY'" - }, - { - "lang": "Python", - "label": "Python (requests)", - "source": "import requests\n\nresp = requests.get(\n f\"https://verification.didit.me/v3/questionnaires/{questionnaire_uuid}/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n timeout=10,\n)\nresp.raise_for_status()\nquestionnaire = resp.json()\nprint(questionnaire[\"title\"], questionnaire[\"status\"])" + "source": "curl --request GET \\\n --url https://verification.didit.me/v3/id-verification/wallets/verifications/REQUEST_ID/ \\\n --header 'x-api-key: YOUR_API_KEY'" }, { - "lang": "JavaScript", - "label": "Node.js (fetch)", - "source": "const res = await fetch(`https://verification.didit.me/v3/questionnaires/${questionnaireUuid}/`, {\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n});\nif (!res.ok) throw new Error(`Didit ${res.status}`);\nconst questionnaire = await res.json();\nconsole.log(questionnaire.title, questionnaire.status);" + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"]\n}\n\nresponse = requests.get(\n \"https://verification.didit.me/v3/id-verification/wallets/verifications/REQUEST_ID/\",\n headers=headers,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } ] - }, - "patch": { - "summary": "Update questionnaire", - "description": "Update a questionnaire. Versioning depends on the `status` field you send and the current state of the version: (1) no `status` on a **published** version → the published version is updated in place (same `questionnaire_id`, same `version`); (2) no `status` on a **draft** → the draft is updated and immediately published; (3) `status: \"published\"` on a **published** version → a NEW published version is created under the same `questionnaire_group_id` (new `questionnaire_id`, `version` + 1) and returned, and any existing draft in the group is deleted; (4) `status: \"draft\"` keeps a draft as a draft (autosave) but is rejected with 400 on a published version (`\"Cannot revert a published version to draft.\"`).", - "operationId": "update_questionnaire", - "tags": [ - "Questionnaires" - ], + } + }, + "/v3/id-verification/wallets/verifications/{request_id}/poll/": { + "post": { + "operationId": "poll_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "poll_verification", + "auth": "api_key", + "server": true + }, + "description": "Smart-ID and Mobile-ID: ask the provider whether the user confirmed, refused or let the prompt lapse, and verify the signed identity when they confirmed. Redirect wallets complete through the user's browser, so for them this only reads the verification back. Answers the verification as it is afterwards.", + "summary": "Poll a wallet verification", "parameters": [ { - "name": "questionnaire_uuid", "in": "path", - "required": true, + "name": "request_id", "schema": { "type": "string", "format": "uuid" }, - "description": "Per-version UUID of the questionnaire to update." + "description": "The request_id the start endpoint returned.", + "required": true } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SimpleQuestionnaireUpdateRequest" - }, - "examples": { - "Rename": { - "summary": "Rename a questionnaire", - "value": { - "title": "Updated Customer Onboarding" - } + "tags": [ + "Digital ID wallets" + ], + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" }, - "Replace questions": { - "summary": "Replace the full linear question list", - "value": { - "form_elements": [ - { - "id": "occupation", - "element_type": "short_text", - "label": { - "en": "What is your occupation?" - }, - "is_required": true - }, - { - "id": "source_of_funds", - "element_type": "multiple_choice", - "label": { - "en": "Source of funds" - }, - "options": [ - { - "value": "employment", - "label": { - "en": "Employment" - } - }, - { - "value": "business", - "label": { - "en": "Business" - } - } - ] - } - ] - } + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "smart_id", + "name": "Smart-ID", + "issuing_authority": "SK ID Solutions" + }, + "country": "EST", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "in_app", + "simulated": true, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "confirm_in_app", + "verification_code": "1234", + "poll_interval_seconds": 2 + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiClientCredentialsForbiddenErrorMessage" } } - } - } - }, - "responses": { - "200": { - "description": "Questionnaire updated. The body is the latest state of the resulting version — the in-place updated version, or the newly created published version when you sent `status: \"published\"` on a published row (in that case `questionnaire_id` and `version` differ from the ones you patched). `questionnaire_id` in the response is the per-version UUID; use `questionnaire_group_id` if you need the stable group identifier.", + }, + "description": "Missing or invalid credentials (x-api-key or Authorization: Bearer)." + }, + "404": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QuestionnaireDetail" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } - }, - "400": { - "description": "Validation error — unknown `element_type`, missing translation, unsupported branching/conditional fields, `languages` missing `en`, `default_language` not in `languages`, option without a label, sending a raw `graph`, or `status: \"draft\"` on a published version (`\"Cannot revert a published version to draft.\"`). Response body is a DRF validation error keyed by field name." + }, + "description": "error not_found: no wallet verification of this application has this request_id." }, - "403": { - "description": "API key missing or invalid.", + "503": { "content": { "application/json": { - "examples": { - "Invalid token": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } - }, - "404": { - "description": "No questionnaire with this `questionnaire_uuid` exists for the authenticated application." - }, - "415": { - "description": "Unsupported media type. Set `Content-Type: application/json`." - }, - "429": { - "description": "Rate limit exceeded. Retry with exponential backoff." - }, - "500": { - "description": "Unexpected server error. Safe to retry." + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ - { - "ApiKeyAuth": [] - } - ], "x-codeSamples": [ { "lang": "curl", "label": "cURL", - "source": "curl -X PATCH 'https://verification.didit.me/v3/questionnaires/11111111-2222-3333-4444-555555555555/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"title\": \"Updated Customer Onboarding\"}'" - }, - { - "lang": "Python", - "label": "Python (requests)", - "source": "import requests\n\nresp = requests.patch(\n f\"https://verification.didit.me/v3/questionnaires/{questionnaire_uuid}/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n json={\"title\": \"Updated Customer Onboarding\"},\n timeout=10,\n)\nresp.raise_for_status()\nprint(resp.json()[\"questionnaire_id\"], resp.json()[\"status\"])" + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/verifications/REQUEST_ID/poll/ \\\n --header 'x-api-key: YOUR_API_KEY'" }, { - "lang": "JavaScript", - "label": "Node.js (fetch)", - "source": "const res = await fetch(`https://verification.didit.me/v3/questionnaires/${questionnaireUuid}/`, {\n method: \"PATCH\",\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ title: \"Updated Customer Onboarding\" }),\n});\nif (!res.ok) throw new Error(`Didit ${res.status}`);\nconst q = await res.json();\nconsole.log(q.questionnaire_id, q.status);" + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"]\n}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/verifications/REQUEST_ID/poll/\",\n headers=headers,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } ] - }, - "delete": { - "summary": "Delete questionnaire", - "description": "Permanently delete one questionnaire version (no undo). Stored questionnaire responses linked to this version are deleted with it, and sessions referencing it lose the link; other versions in the same group are not deleted. Workflows that referenced it keep running but the questionnaire step can no longer load it.", - "operationId": "delete_questionnaire", - "tags": [ - "Questionnaires" - ], + } + }, + "/v3/id-verification/wallets/verifications/{request_id}/cancel/": { + "post": { + "operationId": "cancel_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "cancel_verification", + "auth": "api_key", + "server": true + }, + "description": "Withdraw a pending verification: it is declined as cancelled and never billed. A decided one is returned unchanged. Smart-ID and Mobile-ID: a cancel that arrives while a poll is checking the sign-in waits a few seconds for that poll, then declines it, or returns it verified if the user had already confirmed. A verification the user is completing right now cannot be cancelled (409 verification_in_progress with Retry-After): retry after that many seconds to read its outcome.", + "summary": "Cancel a wallet verification", "parameters": [ { - "name": "questionnaire_uuid", "in": "path", - "required": true, + "name": "request_id", "schema": { "type": "string", "format": "uuid" }, - "description": "Per-version UUID of the questionnaire to delete." + "description": "The request_id the start endpoint returned.", + "required": true + } + ], + "tags": [ + "Digital ID wallets" + ], + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] } ], "responses": { - "204": { - "description": "Questionnaire deleted. The response body is empty." + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + }, + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "ftn", + "name": "Finnish Trust Network", + "issuing_authority": "Finnish banks and mobile operators (FTN)" + }, + "country": "FIN", + "status": "Declined", + "outcome": "cancelled", + "error": "cancelled_by_customer", + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": null, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" + } + } + }, + "description": "" }, "403": { - "description": "API key missing or invalid.", "content": { "application/json": { - "examples": { - "Invalid token": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiClientCredentialsForbiddenErrorMessage" } } - } + }, + "description": "Missing or invalid credentials (x-api-key or Authorization: Bearer)." }, "404": { - "description": "No questionnaire with this `questionnaire_uuid` exists for the authenticated application.", "content": { "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Not found." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } - }, - "429": { - "description": "Rate limit exceeded. Retry with exponential backoff." + }, + "description": "error not_found: no wallet verification of this application has this request_id." }, - "500": { - "description": "Unexpected server error. Safe to retry." + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error verification_in_progress: the user is completing this sign-in right now. The Retry-After header says when to retry; the retry returns the decided verification." } }, - "security": [ - { - "ApiKeyAuth": [] - } - ], "x-codeSamples": [ { "lang": "curl", "label": "cURL", - "source": "curl -X DELETE 'https://verification.didit.me/v3/questionnaires/11111111-2222-3333-4444-555555555555/' \\\n -H 'x-api-key: YOUR_API_KEY'" - }, - { - "lang": "Python", - "label": "Python (requests)", - "source": "import requests\n\nresp = requests.delete(\n f\"https://verification.didit.me/v3/questionnaires/{questionnaire_uuid}/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n timeout=10,\n)\nresp.raise_for_status() # raises on 4xx/5xx; 204 is success" + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/verifications/REQUEST_ID/cancel/ \\\n --header 'x-api-key: YOUR_API_KEY'" }, { - "lang": "JavaScript", - "label": "Node.js (fetch)", - "source": "const res = await fetch(`https://verification.didit.me/v3/questionnaires/${questionnaireUuid}/`, {\n method: \"DELETE\",\n headers: { 'x-api-key': 'YOUR_API_KEY' },\n});\nif (res.status !== 204) throw new Error(`Didit ${res.status}`);" + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"]\n}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/verifications/REQUEST_ID/cancel/\",\n headers=headers,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } ] } }, - "/v3/users/": { - "get": { - "summary": "List users", - "description": "List [users](/entities/users) Didit assembles from KYC sessions sharing the same `vendor_data` (free-form string, NOT a UUID), plus users created via the API. Paginated, ordered by `last_session_at` desc. This endpoint does not support filtering or search parameters — fetch pages and filter client-side.", - "operationId": "list_users", - "tags": [ - "Users" - ], + "/v3/id-verification/wallets/mitid/": { + "post": { + "operationId": "start_mitid_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_mitid", + "auth": "api_key", + "server": true + }, + "description": "Start a MitID sign-in (Danish Agency for Digital Government; DNK) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start MitID verification", "parameters": [ { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "default": 50, - "minimum": 1 - }, - "description": "Page size. Defaults to 50 when omitted. There is no enforced maximum, but keep pages small for latency." - }, - { - "name": "offset", - "in": "query", + "in": "header", + "name": "Idempotency-Key", "schema": { - "type": "integer", - "default": 0, - "minimum": 0 + "type": "string" }, - "description": "Zero-based offset of the first record to return. Prefer following the `next` URL from the previous page over computing this by hand." + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/users/?limit=50' \\\n -H 'x-api-key: YOUR_API_KEY'" + "tags": [ + "Digital ID wallets" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiMitidWalletVerificationStartRequest" + }, + "example": { + "country": "DNK", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiMitidWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiMitidWalletVerificationStartRequest" + } + } }, + "required": true + }, + "security": [ { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/users/',\n headers={'x-api-key': 'YOUR_API_KEY'},\n params={'limit': 50},\n)\nresp.raise_for_status()\npage = resp.json()\nfor user in page['results']:\n print(user['vendor_data'], user['status'])" + "ApiKeyAuth": [] }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const url = new URL('https://verification.didit.me/v3/users/');\nurl.searchParams.set('limit', '50');\n\nconst page = await fetch(url, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n}).then((r) => r.json());\n\nconsole.log(page.count, page.results.length);" + "BearerAuth": [] } ], "responses": { - "200": { - "description": "Paginated list of users with verification status, session counts, and feature breakdown.", + "201": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "count": { - "type": "integer", - "description": "Number of matching rows — exact up to 100, capped at 100 beyond that for performance. Do not use it to detect the last page; rely on `next` being `null` instead." - }, - "next": { - "type": "string", - "nullable": true, - "description": "Absolute URL of the next page, or `null` on the last page." + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + }, + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "mitid", + "name": "MitID", + "issuing_authority": "Danish Agency for Digital Government" }, - "previous": { - "type": "string", - "nullable": true, - "description": "Absolute URL of the previous page, or `null` on the first page." + "country": "DNK", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UserListItem" - } - } - } - }, - "examples": { - "Example": { - "value": { - "count": 2, - "next": null, - "previous": null, - "results": [ - { - "didit_internal_id": "f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418", - "vendor_data": "user-abc-123", - "display_name": null, - "full_name": "John Michael Doe", - "date_of_birth": "1990-05-15", - "effective_name": "John Michael Doe", - "status": "ACTIVE", - "portrait_image_url": "https:///...", - "session_count": 3, - "approved_count": 2, - "declined_count": 0, - "in_review_count": 1, - "issuing_states": [ - "USA" - ], - "approved_emails": [ - "john@example.com" - ], - "approved_phones": [ - "+14155551234" - ], - "features": { - "ID_VERIFICATION": "Approved", - "LIVENESS": "Approved", - "FACE_MATCH": "Approved", - "AML": "Approved" - }, - "features_list": [ - { - "feature": "ID_VERIFICATION", - "status": "Approved" - }, - { - "feature": "LIVENESS", - "status": "Approved" - }, - { - "feature": "FACE_MATCH", - "status": "Approved" - }, - { - "feature": "AML", - "status": "Approved" - } - ], - "last_session_at": "2025-06-15T10:30:00Z", - "last_activity_at": "2025-06-15T10:30:00Z", - "first_session_at": "2025-06-01T08:00:00Z", - "tags": [ - { - "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", - "name": "VIP", - "color": "#2567FF" - } - ], - "created_at": "2025-06-01T08:00:00Z" - }, - { - "didit_internal_id": "6ed99d53-e8f5-4cf8-9ac1-4a506cb40f6b", - "vendor_data": null, - "display_name": "Jane S.", - "full_name": "Jane Elizabeth Smith", - "date_of_birth": "1985-11-22", - "effective_name": "Jane S.", - "status": "FLAGGED", - "portrait_image_url": null, - "session_count": 1, - "approved_count": 0, - "declined_count": 0, - "in_review_count": 1, - "issuing_states": [], - "approved_emails": [], - "approved_phones": [], - "features": { - "ID_VERIFICATION": "In Review" - }, - "features_list": [ - { - "feature": "ID_VERIFICATION", - "status": "In Review" - } - ], - "last_session_at": "2025-06-20T14:00:00Z", - "last_activity_at": "2025-06-20T14:05:00Z", - "first_session_at": "2025-06-20T14:00:00Z", - "tags": [], - "created_at": "2025-06-20T14:00:00Z" - } - ] - } - } + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" }, - "401": { - "description": "No credentials supplied. Requests to `/v3/users/*` paths without an `x-api-key` header (or `Authorization: Bearer` token) are rejected by the authentication middleware before reaching the API.", + "400": { "content": { "application/json": { - "examples": { - "Missing credentials": { - "value": { - "detail": "You must be authenticated with a valid access token to access this endpoint." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" } } - } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, "403": { - "description": "Invalid or revoked API key, or the key cannot list users for this application. Note: on this endpoint an *invalid* key returns `403` (not `401`).", "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "409": { "content": { "application/json": { - "examples": { - "Rate limited": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ + "x-codeSamples": [ { - "ApiKeyAuth": [] - } - ] - } - }, - "/v3/users/{vendor_data}/": { - "get": { - "summary": "Get user", - "description": "Fetch one [user](/entities/users) by `vendor_data` (exact match). Adds `metadata`, `comments`, and `updated_at` to the list view, and switches `tags` to the detailed tag-link shape.", - "operationId": "get_user", - "tags": [ - "Users" + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/mitid/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"DNK\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'DNK',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/mitid/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" + } ], + "x-didit-wallet-id": "mitid" + } + }, + "/v3/id-verification/wallets/bankid-se/": { + "post": { + "operationId": "start_bankid_se_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_bankid_se", + "auth": "api_key", + "server": true + }, + "description": "Start a BankID (SWE) sign-in (Finansiell ID-Teknik / bank consortium; SWE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start BankID (SWE) verification", "parameters": [ { - "name": "vendor_data", - "in": "path", - "required": true, + "in": "header", + "name": "Idempotency-Key", "schema": { "type": "string" }, - "description": "Your unique identifier for the user — a free-form string (NOT a UUID). This is the same value you passed as `vendor_data` when creating the session that first introduced this user, matched exactly as sent.", - "example": "user-abc-123" + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/users/user-abc-123/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "tags": [ + "Digital ID wallets" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiBankidSeWalletVerificationStartRequest" + }, + "example": { + "country": "SWE", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiBankidSeWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiBankidSeWalletVerificationStartRequest" + } + } }, + "required": true + }, + "security": [ { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/users/user-abc-123/',\n headers={'x-api-key': 'YOUR_API_KEY'},\n)\nresp.raise_for_status()\nuser = resp.json()\nprint(user['didit_internal_id'], user['status'], user['session_count'])" + "ApiKeyAuth": [] }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const vendorData = encodeURIComponent('user-abc-123');\nconst user = await fetch(\n `https://verification.didit.me/v3/users/${vendorData}/`,\n { headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n).then((r) => r.json());\n\nconsole.log(user.didit_internal_id, user.status);" + "BearerAuth": [] } ], "responses": { - "200": { - "description": "Full user detail including metadata, comments/activity log, and aggregated session data.", + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserDetailItem" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" }, - "examples": { - "Active user": { - "value": { - "didit_internal_id": "f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418", - "vendor_data": "user-abc-123", - "display_name": null, - "full_name": "John Michael Doe", - "date_of_birth": "1990-05-15", - "effective_name": "John Michael Doe", - "status": "ACTIVE", - "metadata": { - "tier": "premium", - "source": "website" - }, - "portrait_image_url": "https:///...", - "session_count": 3, - "approved_count": 2, - "declined_count": 0, - "in_review_count": 1, - "issuing_states": [ - "USA" - ], - "approved_emails": [ - "john@example.com" - ], - "approved_phones": [ - "+14155551234" - ], - "features": { - "ID_VERIFICATION": "Approved", - "LIVENESS": "Approved", - "FACE_MATCH": "Approved", - "AML": "Approved" - }, - "features_list": [ - { - "feature": "ID_VERIFICATION", - "status": "Approved" - }, - { - "feature": "LIVENESS", - "status": "Approved" - }, - { - "feature": "FACE_MATCH", - "status": "Approved" - }, - { - "feature": "AML", - "status": "Approved" - } - ], - "last_session_at": "2025-06-15T10:30:00Z", - "last_activity_at": "2025-06-15T10:30:00Z", - "first_session_at": "2025-06-01T08:00:00Z", - "tags": [ - { - "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", - "tag": { - "uuid": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b", - "name": "VIP", - "color": "#2567FF", - "description": null, - "source": "custom", - "created_at": "2025-05-01T09:00:00Z", - "updated_at": "2025-05-01T09:00:00Z" - }, - "added_by_email": "analyst@acme.com", - "added_by_name": "Jane Analyst", - "created_at": "2025-06-02T11:00:00Z" - } - ], - "comments": [ - { - "uuid": "c1111111-2222-4333-8444-555555555555", - "comment_type": "STATUS_CHANGED", - "comment": null, - "actor_email": "analyst@acme.com", - "actor_name": "Jane Analyst", - "previous_status": "FLAGGED", - "new_status": "ACTIVE", - "previous_value": null, - "new_value": null, - "changed_fields": [], - "metadata": null, - "mentioned_emails": [], - "created_at": "2025-06-01T10:30:00Z" - } - ], - "created_at": "2025-06-01T08:00:00Z", - "updated_at": "2025-06-15T10:30:00Z" - } - } + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "bankid_se", + "name": "BankID", + "issuing_authority": "Finansiell ID-Teknik / bank consortium" + }, + "country": "SWE", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" }, - "401": { - "description": "No credentials supplied. Requests to `/v3/users/*` paths without an `x-api-key` header (or `Authorization: Bearer` token) are rejected by the authentication middleware before reaching the API.", + "400": { "content": { "application/json": { - "examples": { - "Missing credentials": { - "value": { - "detail": "You must be authenticated with a valid access token to access this endpoint." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" } } - } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, "403": { - "description": "Invalid or revoked API key, or the key cannot read this application's users. Note: on this endpoint an *invalid* key returns `403` (not `401`).", "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "404": { - "description": "No user with the supplied `vendor_data` exists for this application.", + "409": { "content": { "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Not found." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "502": { "content": { "application/json": { - "examples": { - "Rate limited": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ + "x-codeSamples": [ { - "ApiKeyAuth": [] + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/bankid-se/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"SWE\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'SWE',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/bankid-se/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } - ] - }, - "patch": { - "summary": "Update user", - "description": "Partial update of a user: `display_name`, `full_name`, `date_of_birth`, `status`, `metadata`, `approved_emails`/`approved_phones`/`issuing_states`. `metadata` fully replaces. **Note:** unlike [Update User Status](#patch-/v3/users/-vendor_data-/update-status/), changing `status` here does NOT record a status-change activity and does NOT sync the system blocklist — use the dedicated update-status endpoint for `BLOCKED`/unblock flows.", - "operationId": "update_user", - "tags": [ - "Users" ], + "x-didit-wallet-id": "bankid_se" + } + }, + "/v3/id-verification/wallets/bankid-no/": { + "post": { + "operationId": "start_bankid_no_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_bankid_no", + "auth": "api_key", + "server": true + }, + "description": "Start a BankID (NOR) sign-in (BankID BankAxept AS; NOR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start BankID (NOR) verification", "parameters": [ { - "name": "vendor_data", - "in": "path", - "required": true, + "in": "header", + "name": "Idempotency-Key", "schema": { "type": "string" }, - "description": "Your unique identifier for the user (free-form string, NOT a UUID).", - "example": "user-abc-123" + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH 'https://verification.didit.me/v3/users/user-abc-123/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"display_name\": \"Jane S.\", \"status\": \"FLAGGED\"}'" + "tags": [ + "Digital ID wallets" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiBankidNoWalletVerificationStartRequest" + }, + "example": { + "country": "NOR", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiBankidNoWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiBankidNoWalletVerificationStartRequest" + } + } }, + "required": true + }, + "security": [ { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.patch(\n 'https://verification.didit.me/v3/users/user-abc-123/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'display_name': 'Jane S.', 'status': 'FLAGGED'},\n)\nresp.raise_for_status()\nprint(resp.json()['status'])" + "ApiKeyAuth": [] }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/users/user-abc-123/', {\n method: 'PATCH',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ display_name: 'Jane S.', status: 'FLAGGED' }),\n});\nconst user = await resp.json();\nconsole.log(user.status);" + "BearerAuth": [] } ], "responses": { - "200": { - "description": "User updated. The full updated user record (same shape as Get User) is returned.", + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserDetailItem" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" }, - "examples": { - "Updated": { - "value": { - "didit_internal_id": "f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418", - "vendor_data": "user-abc-123", - "display_name": "Jane S.", - "full_name": "Jane Elizabeth Smith", - "date_of_birth": "1985-11-22", - "effective_name": "Jane S.", - "status": "FLAGGED", - "metadata": { - "tier": "premium" - }, - "portrait_image_url": "https:///...", - "session_count": 3, - "approved_count": 2, - "declined_count": 0, - "in_review_count": 1, - "issuing_states": [ - "USA" - ], - "approved_emails": [ - "john@example.com" - ], - "approved_phones": [ - "+14155551234" - ], - "features": { - "ID_VERIFICATION": "Approved", - "LIVENESS": "Approved", - "FACE_MATCH": "Approved", - "AML": "Approved" - }, - "features_list": [ - { - "feature": "ID_VERIFICATION", - "status": "Approved" - }, - { - "feature": "LIVENESS", - "status": "Approved" - }, - { - "feature": "FACE_MATCH", - "status": "Approved" - }, - { - "feature": "AML", - "status": "Approved" - } - ], - "last_session_at": "2025-06-15T10:30:00Z", - "last_activity_at": "2025-06-15T10:30:00Z", - "first_session_at": "2025-06-01T08:00:00Z", - "tags": [ - { - "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", - "tag": { - "uuid": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b", - "name": "VIP", - "color": "#2567FF", - "description": null, - "source": "custom", - "created_at": "2025-05-01T09:00:00Z", - "updated_at": "2025-05-01T09:00:00Z" - }, - "added_by_email": "analyst@acme.com", - "added_by_name": "Jane Analyst", - "created_at": "2025-06-02T11:00:00Z" - } - ], - "comments": [], - "created_at": "2025-06-01T08:00:00Z", - "updated_at": "2025-06-15T10:30:00Z" - } - } + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "bankid_no", + "name": "BankID", + "issuing_authority": "BankID BankAxept AS" + }, + "country": "NOR", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" }, "400": { - "description": "Invalid body — typically an unknown `status` or a `date_of_birth` not in `YYYY-MM-DD`.", "content": { "application/json": { - "examples": { - "Bad status": { - "value": { - "status": [ - "\"INVALID\" is not a valid choice." - ] - } - }, - "Bad date_of_birth": { - "value": { - "date_of_birth": [ - "Date has wrong format. Use one of these formats instead: YYYY-MM-DD." - ] - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" } } - } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, - "401": { - "description": "No credentials supplied. Requests to `/v3/users/*` paths without an `x-api-key` header (or `Authorization: Bearer` token) are rejected by the authentication middleware before reaching the API.", + "403": { "content": { "application/json": { - "examples": { - "Missing credentials": { - "value": { - "detail": "You must be authenticated with a valid access token to access this endpoint." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "403": { - "description": "Invalid or revoked API key, or the key cannot update users for this application. Note: on this endpoint an *invalid* key returns `403` (not `401`).", + "409": { "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "404": { - "description": "No user with the supplied `vendor_data` exists for this application.", + "502": { "content": { "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Not found." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "503": { "content": { "application/json": { - "examples": { - "Rate limited": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ + "x-codeSamples": [ { - "ApiKeyAuth": [] + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/bankid-no/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"NOR\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'NOR',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/bankid-no/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "full_name": { - "type": "string", - "nullable": true, - "maxLength": 512, - "description": "Full name of the user" - }, - "display_name": { - "type": "string", - "nullable": true, - "description": "Custom display name for this user" - }, - "date_of_birth": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Date of birth in YYYY-MM-DD format" - }, - "status": { - "type": "string", - "enum": [ - "ACTIVE", - "FLAGGED", - "BLOCKED" - ], - "description": "User lifecycle status. Changing it via this generic PATCH does NOT sync the system blocklist or record a status-change activity; prefer the update-status endpoint." - }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Arbitrary JSON object you attach to the user. **Fully replaces** the existing metadata on update — merge client-side if you only want to add keys." - }, - "approved_emails": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Verified email addresses for this user, e.g. `[\"john@example.com\"]`. Fully replaces the stored list." - }, - "approved_phones": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Verified phone numbers for this user. Fully replaces the stored list." - }, - "issuing_states": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Issuing countries (ISO 3166-1 alpha-3), e.g. `[\"USA\"]`. Fully replaces the stored list." - } - } - }, - "examples": { - "Rename": { - "summary": "Set display name", - "value": { - "display_name": "Jane S." - } - }, - "Override status": { - "summary": "Flag the user for review", - "value": { - "status": "FLAGGED" - } - }, - "Add metadata": { - "summary": "Replace metadata JSON", - "value": { - "metadata": { - "tier": "premium", - "risk_level": "low" - } - } - }, - "Fix extracted DOB": { - "summary": "Correct extracted profile fields", - "value": { - "full_name": "Jane Elizabeth Smith", - "date_of_birth": "1985-11-22" - } - } - } - } - } - } + "x-didit-wallet-id": "bankid_no" } }, - "/v3/users/{vendor_data}/generate-pdf/": { - "get": { - "summary": "Download the full user history PDF report", - "description": "Render and download one compliance-ready PDF containing the full verification history of the user identified by `vendor_data`. It is the same document the Console produces from **User detail → Actions → Download PDF**, so periodic compliance exports no longer need a person clicking in the Console.\n\n**What the report contains.** A cover page with the profile summary (name, date of birth, issuing states, approved emails and phones), the latest status of every feature, the session counters and an index of the sessions included — followed by the standard report of every included session, in chronological order (oldest first). Each session report is the same document `GET /v3/session/{sessionId}/generate-pdf/` returns for that session.\n\n**Eligible sessions.** Only User Verification (KYC) sessions in `Approved`, `Declined`, `In Review` or `Kyc Expired` are included; sessions in any other status are skipped. If the user has no session in one of those statuses the endpoint returns `403` — see below.\n\n**Cap.** The report is capped at the **20 most recent** reportable sessions. When a user has more, the newest 20 are included and the cover page states how many older sessions were left out. Use `GET /v3/sessions/?vendor_data=…` for the complete list as JSON.\n\n**Branding.** If your application has white-label customization, the cover and every session report are rendered with your logo and privacy-policy URL; otherwise they carry Didit branding. Reports are rendered in English — there is no language parameter.\n\n**No caching.** Every call re-renders the PDF from the current data, so a report generated after a manual review reflects the reviewer's decision. Two calls for the same user can produce byte-different files — archive the downloaded file if you need an immutable copy.\n\n**Latency.** Rendering is synchronous and downloads every stored image of every included session, so a user with many media-heavy sessions can take tens of seconds. Use a generous client read timeout (120 s recommended) and stream the body to disk. Generation stops at a server-side time budget instead of running until your client gives up: if that budget is reached, the response is still a valid PDF containing the most recent sessions, and the cover states how many were omitted.\n\n**Scope.** The user is resolved inside the application that owns the API key. A key can never export a user of another application — such a user does not exist for it and the endpoint returns `404`.\n\n**Trailing slash.** The canonical route ends with a trailing slash (`…/generate-pdf/`). A request without it is served directly with the same response — there is no `301` redirect, so no client has to follow redirects and `curl` does not need `-L`. Use the slashed URL as in the samples.", - "operationId": "get_user_generate_pdf", - "tags": [ - "Users" - ], + "/v3/id-verification/wallets/vipps/": { + "post": { + "operationId": "start_vipps_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_vipps", + "auth": "api_key", + "server": true + }, + "description": "Start a Vipps sign-in (Vipps MobilePay / BankID NO; NOR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start Vipps verification", "parameters": [ { - "name": "vendor_data", - "in": "path", - "required": true, + "in": "header", + "name": "Idempotency-Key", "schema": { "type": "string" }, - "description": "Your unique identifier for the user — a free-form string (NOT a UUID). This is the same value you passed as `vendor_data` when creating the user's sessions, matched exactly as sent. URL-encode it; a value containing `/` cannot be addressed by this route.", - "example": "user-abc-123" + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], - "responses": { - "200": { - "description": "The rendered PDF document, returned directly as binary `application/pdf` — there is no JSON wrapper and no download-URL indirection. The body starts with the `%PDF` magic bytes. Save it to a `.pdf` file or stream it through to your caller.", - "headers": { - "Content-Disposition": { - "description": "`attachment; filename=user_{didit_internal_id}.pdf` — the file is named after the user's stable Didit identifier (`didit_internal_id` on `GET /v3/users/{vendor_data}/`), not after `vendor_data`.", - "schema": { - "type": "string", - "example": "attachment; filename=user_f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418.pdf" - } + "tags": [ + "Digital ID wallets" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiVippsWalletVerificationStartRequest" }, - "Content-Length": { - "description": "Total size of the PDF in bytes; the body is not chunked.", - "schema": { - "type": "integer", - "example": 148213 - } + "example": { + "country": "NOR", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "return_url": "https://app.example.com/identity/complete" } }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiVippsWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiVippsWalletVerificationStartRequest" + } + } + }, + "required": true + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "responses": { + "201": { "content": { - "application/pdf": { + "application/json": { "schema": { - "type": "string", - "format": "binary" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + }, + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "vipps", + "name": "Vipps", + "issuing_authority": "Vipps MobilePay / BankID NO" + }, + "country": "NOR", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" }, - "401": { - "description": "No credentials supplied. Requests to `/v3/users/*` paths without an `x-api-key` header (or `Authorization: Bearer` token) are rejected by the authentication middleware before reaching the API.", + "400": { "content": { "application/json": { - "examples": { - "Missing credentials": { - "value": { - "detail": "You must be authenticated with a valid access token to access this endpoint." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" } } - } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, "403": { - "description": "Authorization or eligibility failure. Distinguish the two by the `detail` string:\n\n- `\"You do not have permission to perform this action.\"` — the API key is invalid or revoked, or the request carries a Console user token instead of an application API key.\n- `\"This user has no sessions in review, declined, approved or kyc expired to report.\"` — the user exists in your application but none of its sessions is in a reportable status yet (for example every session is still `In Progress`, or the user was created through `POST /v3/users/create/` and has never verified). Wait for a `status.updated` webhook before requesting the report.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string", - "example": "This user has no sessions in review, declined, approved or kyc expired to report." - } - } - }, - "examples": { - "Invalid credentials": { - "summary": "Revoked or invalid API key, or a Console user token", - "value": { - "detail": "You do not have permission to perform this action." - } - }, - "No reportable sessions": { - "summary": "User exists but has no Approved / Declined / In Review / Kyc Expired session", - "value": { - "detail": "This user has no sessions in review, declined, approved or kyc expired to report." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "404": { - "description": "No user with the supplied `vendor_data` exists in the application behind the API key. Users of other applications are invisible to this key and return the same `404`.", + "409": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string", - "example": "User not found." - } - } - }, - "examples": { - "Unknown user": { - "summary": "vendor_data does not match any user of this application", - "value": { - "detail": "User not found." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "429": { - "description": "Rate limit exceeded. PDF generation has a dedicated limit of **50 requests per minute per credential** (API key, Bearer token, or source IP when unauthenticated), shared between this endpoint and `GET /v3/session/{sessionId}/generate-pdf/`, in addition to the global 600/min GET limit. Wait `Retry-After` seconds (also reflected in the `X-RateLimit-*` headers), then retry. Reports are not cached server-side, so download each report once and store the file instead of re-fetching it.", - "headers": { - "Retry-After": { - "description": "Seconds to wait before retrying the request.", - "schema": { - "type": "integer", - "minimum": 1, - "example": 30 - } - }, - "X-RateLimit-Limit": { - "description": "Maximum number of PDF generations allowed in the current window (50).", - "schema": { - "type": "integer", - "example": 50 - } - }, - "X-RateLimit-Remaining": { - "description": "Requests remaining in the current window.", - "schema": { - "type": "integer", - "example": 0 - } - }, - "X-RateLimit-Reset": { - "description": "Unix timestamp (seconds) at which the current window resets.", + "502": { + "content": { + "application/json": { "schema": { - "type": "integer", - "example": 1750000000 + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + }, + "503": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string", - "description": "Human-readable explanation of the rate-limit breach.", - "example": "Session PDF generation rate limit exceeded. You can make up to 50 requests per minute." - } - } - }, - "examples": { - "Throttled": { - "summary": "More than 50 PDF generations in one minute from the same credential", - "value": { - "detail": "Session PDF generation rate limit exceeded. You can make up to 50 requests per minute." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ - { - "ApiKeyAuth": [] - } - ], "x-codeSamples": [ { "lang": "curl", "label": "cURL", - "source": "curl --fail \\\n 'https://verification.didit.me/v3/users/user-abc-123/generate-pdf/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n --output user-history.pdf" + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/vipps/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"NOR\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" }, { "lang": "python", "label": "Python", - "source": "import requests\n\nresponse = requests.get(\n \"https://verification.didit.me/v3/users/user-abc-123/generate-pdf/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n stream=True,\n timeout=120,\n)\nresponse.raise_for_status()\nwith open(\"user-history.pdf\", \"wb\") as fh:\n for chunk in response.iter_content(chunk_size=8192):\n fh.write(chunk)" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "import { writeFile } from 'node:fs/promises';\n\nconst vendorData = encodeURIComponent('user-abc-123');\nconst response = await fetch(\n `https://verification.didit.me/v3/users/${vendorData}/generate-pdf/`,\n { headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n);\nif (!response.ok) throw new Error(`PDF generation failed: HTTP ${response.status}`);\nawait writeFile('user-history.pdf', Buffer.from(await response.arrayBuffer()));" + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'NOR',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/vipps/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } - ] + ], + "x-didit-wallet-id": "vipps" } }, - "/v3/users/delete/": { + "/v3/id-verification/wallets/buypass/": { "post": { - "summary": "Batch delete users", - "description": "**Permanently delete** users — the user records are removed permanently. For everyday blocking prefer [Update User Status](#patch-/v3/users/-vendor_data-/update-status/) with `BLOCKED`. Select users with `vendor_data_list`, with `didit_internal_id_list`, or with both at once; or pass `delete_all: true`. The Console's `filter_*` batch params are rejected with a `400` rather than ignored, so a narrowed intent can never turn into a full wipe.\n\n**Biometric templates:** every image-free biometric template retained for these users after session deletion (see `/v3/biometric-templates/`) is purged before the user is deleted, so the person stops matching in duplicate detection and Face Search. If a template cannot be purged because a biometric store is unavailable, the call returns `503`, no user in the request is deleted, and repeating the request finishes the job.", - "operationId": "batch_delete_users", + "operationId": "start_buypass_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_buypass", + "auth": "api_key", + "server": true + }, + "description": "Start a Buypass ID sign-in (Buypass AS; NOR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start Buypass ID verification", + "parameters": [ + { + "in": "header", + "name": "Idempotency-Key", + "schema": { + "type": "string" + }, + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." + } + ], "tags": [ - "Users" + "Digital ID wallets" ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "type": "object", - "description": "Provide `vendor_data_list`, `didit_internal_id_list`, or `delete_all: true`. The two lists can be combined and are OR'd. When `delete_all` is `true` it takes precedence and both lists are ignored.", - "properties": { - "vendor_data_list": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Your own identifiers to delete, matched exactly as sent." - }, - "didit_internal_id_list": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - }, - "description": "Didit internal user IDs to delete - the `didit_internal_id` returned by the create and get endpoints." - }, - "delete_all": { - "type": "boolean", - "default": false, - "description": "If `true`, deletes every user in the application." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiBuypassWalletVerificationStartRequest" }, - "examples": { - "Specific users": { - "summary": "Delete users by vendor_data", - "value": { - "vendor_data_list": [ - "user-123", - "user-456" - ] - } - }, - "By Didit internal ID": { - "summary": "Delete users by didit_internal_id", - "value": { - "didit_internal_id_list": [ - "3f1c0d3e-8f0a-4c2e-9a3d-9b1f5f4a2c11" - ] - } + "example": { + "country": "NOR", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" }, - "All users": { - "summary": "Wipe every user in this application", - "value": { - "delete_all": true - } - } + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiBuypassWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiBuypassWalletVerificationStartRequest" } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/users/delete/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"vendor_data_list\": [\"user-123\", \"user-456\"]}'" }, + "required": true + }, + "security": [ { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/users/delete/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'vendor_data_list': ['user-123', 'user-456']},\n)\nresp.raise_for_status()\nprint('deleted', resp.json()['deleted'])" + "ApiKeyAuth": [] }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/users/delete/', {\n method: 'POST',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ vendor_data_list: ['user-123', 'user-456'] }),\n});\nconst body = await resp.json();\nconsole.log('deleted', body.deleted);" + "BearerAuth": [] } ], "responses": { - "200": { - "description": "Users deleted. Returns the number of users actually deleted.", + "201": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "deleted": { - "type": "integer", - "description": "Number of users deleted (excludes vendor_data values that didn't match anything)." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" }, - "examples": { - "Deleted some": { - "value": { - "deleted": 2 - } + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "buypass", + "name": "Buypass ID", + "issuing_authority": "Buypass AS" + }, + "country": "NOR", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" }, - "Nothing matched": { - "value": { - "deleted": 0 - } - } + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" }, "400": { - "description": "No selector supplied (`vendor_data_list` missing/empty and `delete_all` not `true`). The error body is a JSON array.", "content": { "application/json": { - "examples": { - "Missing selector": { - "value": [ - "Provide vendor_data_list or set delete_all=true." - ] - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" } } - } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, - "401": { - "description": "No credentials supplied. Requests to `/v3/users/*` paths without an `x-api-key` header (or `Authorization: Bearer` token) are rejected by the authentication middleware before reaching the API.", + "403": { "content": { "application/json": { - "examples": { - "Missing credentials": { - "value": { - "detail": "You must be authenticated with a valid access token to access this endpoint." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "403": { - "description": "Invalid or revoked API key, or the key cannot delete users for this application. Note: on this endpoint an *invalid* key returns `403` (not `401`).", + "409": { "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "502": { "content": { "application/json": { - "examples": { - "Rate limited": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." }, "503": { - "description": "A biometric store was unavailable while retaining or purging a biometric template. Nothing is deleted; repeat the same request. Retries are idempotent and converge on a single template.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } - }, - "examples": { - "Retryable": { - "value": { - "detail": "The biometric-template operation could not finish because an external store is unavailable. The operation is retryable." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ + "x-codeSamples": [ { - "ApiKeyAuth": [] + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/buypass/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"NOR\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'NOR',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/buypass/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } - ] + ], + "x-didit-wallet-id": "buypass" } }, - "/v3/sessions/delete/": { + "/v3/id-verification/wallets/itsme/": { "post": { - "summary": "Bulk delete KYC sessions by session_number", - "description": "Bulk delete User Verification (KYC) sessions by their numeric `session_number`, or every KYC session in your application with `delete_all: true`.\n\n`session_number` is the human-friendly incrementing counter shown in the Console session list and returned as `session_number` by the create-session and decision endpoints; it is **not** the `session_id` UUID. To delete a session by UUID, or to delete a Business Verification (KYB) session, which this endpoint never touches, use `DELETE /v3/session/{sessionId}/delete/` instead.\n\n**Up to 20 sessions are deleted in the request, more run as a job.** The number of matching sessions decides, whether they are selected by `session_numbers` or with `delete_all`:\n- Up to 20 matching sessions: they are deleted before the response, which is `200` with one entry per session in `results[]`.\n- More than 20: the response is `202` with a deletion job, and the sessions are deleted in the background. Poll `GET /v3/sessions/delete/{job_id}/` until `status` is `COMPLETED` or `FAILED`. A job reports counts per outcome, never which sessions it deleted. A `delete_all` job deletes only the sessions created before the request; sessions started while it runs are not deleted.\n\nEach deleted session gets exactly the same treatment as the single-session delete endpoint: the session, every verification attempt it holds, its decision and extracted data, its related feature records (ID verification, face, liveness, face match, and so on) and all of its stored files are deleted, it disappears from every read endpoint, and previously issued media URLs stop resolving. Webhook deliveries still queued for a deleted session, and their retries, are dropped; webhooks already delivered are not affected. Blocklist entries are **not** removed, credits already consumed are **not** refunded, and the parent User entities are **not** deleted (use `POST /v3/users/delete/` for those); no webhook is emitted for the deletion. Deletion is permanent and irreversible.\n\n**Selection semantics:**\n- Entries in `session_numbers` that do not match a live KYC session in your application (unknown numbers, already-deleted sessions, or sessions owned by another application) are **silently skipped**: they do not appear in `results` and are not counted in a job's `total`.\n- Validation failures (a non-numeric entry, an empty list, neither `session_numbers` nor `delete_all` provided, or a template retention the application cannot honour) reject the **whole request** with `400` and delete nothing.\n- Sessions are deleted in groups. If a group cannot be deleted (for example because a storage service is unavailable), every session in that group is reported as `face_retention_outcome: failed_retryable` and stays in place; the other groups are unaffected. Repeat the request for those sessions. A job retries such a group by itself.\n- There is no documented cap on the list length; the shared write rate limit (below) is the practical bound.\n\n**Face biometric data:** the optional `retain_face_embeddings`, `face_retention_days`, `face_retention_deadline`, `deletion_instruction`, and `instruction_id` fields have the same meaning as on `DELETE /v3/session/{sessionId}/delete/` and apply to every matched session. Each result reports its own `face_retention_outcome`.\n\n**Response:** `200 OK` with `results[]` for up to 20 matching sessions, or `202 Accepted` with a deletion job for more. Every result confirms deletion except `face_retention_outcome: failed_retryable`, which reports a session that was NOT deleted and should be retried. Earlier versions of this endpoint returned `204 No Content`; update clients that assert on `204`.\n\n**Idempotency:** repeating the same request deletes nothing more: numbers that no longer match a session are skipped.\n\n**`delete_all: true` (destructive):** deletes **every** remaining KYC session in the application; `session_numbers` is ignored when set. There is no confirmation step and no undo.\n\n**Authentication:** API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{\"detail\": \"You do not have permission to perform this action.\"}`); this API never returns `401`.\n\n**Rate limit:** shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`.\n\nThis endpoint accepts API keys only. Business Console user Bearer tokens are rejected with `403` (unlike the single-session delete, which accepts both).", - "operationId": "batch_delete_sessions", - "tags": [ - "Sessions" - ], - "security": [ + "operationId": "start_itsme_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_itsme", + "auth": "api_key", + "server": true + }, + "description": "Start a itsme sign-in (Belgian Mobile ID; BEL, LUX, NLD) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start itsme verification", + "parameters": [ { - "ApiKeyAuth": [] + "in": "header", + "name": "Idempotency-Key", + "schema": { + "type": "string" + }, + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], + "tags": [ + "Digital ID wallets" + ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "session_numbers": { - "type": "array", - "description": "Numeric `session_number` values of the KYC sessions to delete, as digit-only strings (JSON numbers are also accepted and coerced). Required unless `delete_all` is `true`; an empty list returns `400`. Any non-digit entry rejects the whole request with `400` (unless `delete_all` is `true`, in which case validation is skipped and the entries are ignored). Numbers that do not match a live session in your application are silently skipped.", - "items": { - "type": "string", - "pattern": "^[0-9]+$", - "example": "1001" - }, - "minItems": 1, - "example": [ - "1001", - "1002", - "1003" - ] - }, - "delete_all": { - "type": "boolean", - "description": "When `true`, every remaining KYC session in the calling application is deleted and `session_numbers` is ignored. More than 20 matching sessions run as a deletion job (`202`) that covers only the sessions created before the request. Defaults to `false`. Irreversible; there is no confirmation step.", - "default": false, - "example": false - }, - "retain_face_embeddings": { - "type": "boolean", - "nullable": true, - "description": "Override the application's `face_retention_policy` for this deletion. `true` keeps one image-free face biometric template anchored to the session's User after the session is deleted; `false` deletes the face embedding with the session; omit or `null` to follow the application policy. Ignored for KYB sessions and for sessions without a face embedding.", - "example": true - }, - "face_retention_days": { - "type": "integer", - "minimum": 1, - "maximum": 3650, - "description": "Finite retention duration, in days, for a retained template. Required when a template is retained unless the application already sets `face_retention_days`. The template's `expires_at` is the earliest of this duration, the application's general data-retention window, and `face_retention_deadline`.", - "example": 365 - }, - "face_retention_deadline": { - "type": "string", - "format": "date-time", - "description": "Optional hard expiry for a retained template. Must be in the future.", - "example": "2027-08-28T00:00:00Z" - }, - "deletion_instruction": { - "type": "string", - "enum": [ - "operational_session_delete", - "privacy_erasure" - ], - "default": "operational_session_delete", - "description": "The class of instruction you are giving. `operational_session_delete` deletes the session and retains a template only when the policy or `retain_face_embeddings` says so. `privacy_erasure` purges every retained biometric template anchored to the session's User before deleting the session, cannot be overridden by the application policy, and cannot be combined with `retain_face_embeddings: true` (`400`).", - "example": "operational_session_delete" - }, - "instruction_id": { - "type": "string", - "maxLength": 255, - "description": "Your durable reference for this deletion instruction (for example an erasure-ticket id). Recorded on the audit trail and on any retained template. Generated by Didit when omitted.", - "example": "dsar-2026-0142" - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiItsmeWalletVerificationStartRequest" }, - "examples": { - "Specific sessions": { - "summary": "Delete specific sessions by number", - "value": { - "session_numbers": [ - "1001", - "1002", - "1003" - ] - } - }, - "All sessions": { - "summary": "Delete every KYC session in the application", - "value": { - "delete_all": true - } - }, - "Delete and retain templates": { - "summary": "Delete sessions but keep image-free biometric templates for 365 days", - "value": { - "session_numbers": [ - "1001", - "1002" - ], - "retain_face_embeddings": true, - "face_retention_days": 365, - "instruction_id": "post-approval-cleanup-2026-08" - } + "example": { + "country": "BEL", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" }, - "Privacy erasure": { - "summary": "Delete sessions and purge every retained template for their Users", - "value": { - "session_numbers": [ - "1001" - ], - "deletion_instruction": "privacy_erasure", - "instruction_id": "dsar-2026-0142" - } - } + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiItsmeWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiItsmeWalletVerificationStartRequest" } } - } + }, + "required": true }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], "responses": { - "200": { - "description": "Up to 20 sessions matched and were processed in the request. One result per matched session; numbers that matched nothing (unknown or already deleted) are skipped and do not appear. A result confirms deletion except when `face_retention_outcome` is `failed_retryable`: that session was NOT deleted and should be retried.", + "201": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "results": { - "type": "array", - "items": { - "type": "object", - "properties": { - "session_id": { - "type": "string", - "format": "uuid", - "description": "The deleted session." - }, - "session_number": { - "type": "integer", - "nullable": true, - "description": "The deleted session's number." - }, - "face_retention_outcome": { - "type": "string", - "enum": [ - "retained_with_user", - "deleted", - "none", - "ineligible_no_vendor_user", - "failed_retryable" - ], - "description": "What happened to the session's face biometric data. `deleted`: the face embedding was deleted with the session (default). `retained_with_user`: the session is deleted and one image-free biometric template stays anchored to the User; see `biometric_template_uuid`. `none`: the session had no face embedding (for example a document-only KYC session without a liveness selfie; KYB sessions are never selected by this endpoint). `ineligible_no_vendor_user`: retention was requested but the session has no linked User, so the embedding was deleted with the session. `failed_retryable` (batch endpoint only): the group this session was deleted with could not be deleted (a storage service, a biometric store, or a monitoring provider was unavailable); this session is still in place, repeat the request for it." - }, - "biometric_template_uuid": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Id of the retained biometric template when `face_retention_outcome` is `retained_with_user`; `null` otherwise. Manage it through `/v3/biometric-templates/{template_uuid}/`." - }, - "biometric_template_expires_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the retained biometric template is deleted, when `face_retention_outcome` is `retained_with_user`; `null` when no template is retained." - }, - "deleted_child_verifications": { - "type": "integer", - "description": "Always `0` on this endpoint, which deletes KYC sessions only. Deleting a Business Verification (KYB) session with `DELETE /v3/session/{sessionId}/delete/` reports the verifications it created that were deleted with it." - } - } - } - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" }, - "examples": { - "Mixed outcomes": { - "value": { - "results": [ - { - "session_id": "4c5c7f3a-3b1d-4b7e-9c39-2b4f7e6a1d02", - "session_number": 1001, - "face_retention_outcome": "deleted", - "biometric_template_uuid": null, - "biometric_template_expires_at": null, - "deleted_child_verifications": 0 - }, - { - "session_id": "b8e1d2c3-4f5a-4b6c-8d7e-9f0a1b2c3d4e", - "session_number": 1002, - "face_retention_outcome": "retained_with_user", - "biometric_template_uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", - "biometric_template_expires_at": "2027-10-09T10:15:00Z", - "deleted_child_verifications": 0 - }, - { - "session_id": "7a1e9c2d-5f4b-4c3a-8e2d-1b0a9f8e7d6c", - "session_number": 1003, - "face_retention_outcome": "ineligible_no_vendor_user", - "biometric_template_uuid": null, - "biometric_template_expires_at": null, - "deleted_child_verifications": 0 - }, - { - "session_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", - "session_number": 1004, - "face_retention_outcome": "failed_retryable", - "biometric_template_uuid": null, - "biometric_template_expires_at": null, - "deleted_child_verifications": 0 - } - ] - } - } - } - } - } - }, - "202": { - "description": "More than 20 sessions matched. They are deleted in the background; poll `GET /v3/sessions/delete/{job_id}/` until `status` is `COMPLETED` or `FAILED`.", - "content": { - "application/json": { - "schema": { - "type": "object", - "description": "A bulk session deletion running in the background: progress and counts per outcome, never which sessions it deleted.", - "properties": { - "job_id": { - "type": "string", - "format": "uuid", - "description": "Poll `GET /v3/sessions/delete/{job_id}/` with this id." - }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED" - ], - "description": "`PENDING`: accepted, not started yet. `PROCESSING`: deleting; `processed` and `outcomes` are updating. `COMPLETED`: every selected session is accounted for. `FAILED`: the job stopped early; see `error`." - }, - "total": { - "type": "integer", - "description": "Sessions selected when the deletion was requested." - }, - "processed": { - "type": "integer", - "description": "Sessions the job has finished with so far." - }, - "outcomes": { - "type": "object", - "description": "How many sessions ended in each outcome. Every key is always present.", - "properties": { - "deleted": { - "type": "integer", - "description": "Sessions deleted together with their face embedding." - }, - "none": { - "type": "integer", - "description": "Sessions deleted that had no face embedding." - }, - "retained_with_user": { - "type": "integer", - "description": "Sessions deleted while one image-free biometric template was retained for their User, as the request or the application policy asked." - }, - "ineligible_no_vendor_user": { - "type": "integer", - "description": "Sessions deleted, with their face embedding, because retention was requested but they had no linked User." - }, - "failed_retryable": { - "type": "integer", - "description": "Sessions reported as not deleted. A job retries a group it could not delete by itself instead of counting it here." - }, - "already_deleted": { - "type": "integer", - "description": "Selected sessions that were already deleted, for example by another request, when the job reached them." - } - } - }, - "deleted_child_verifications": { - "type": "integer", - "description": "Verifications that the deleted business verifications created, deleted with them. Always `0` for a job started by `POST /v3/sessions/delete/`, which deletes KYC sessions only." - }, - "error": { - "type": "string", - "nullable": true, - "description": "`null` unless `status` is `FAILED`; then why the deletion stopped. The sessions the job had not reached are still in place: repeat the request to delete them." + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "itsme", + "name": "itsme", + "issuing_authority": "Belgian Mobile ID" }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "When the deletion was requested." + "country": "BEL", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" }, - "completed_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the job finished; `null` while it runs." - } - } - }, - "examples": { - "Job accepted": { - "value": { - "job_id": "5b8f2c1e-7d3a-4f6b-9e2c-1a0d8f7e6c5b", - "status": "PENDING", - "total": 250, - "processed": 0, - "outcomes": { - "deleted": 0, - "none": 0, - "retained_with_user": 0, - "ineligible_no_vendor_user": 0, - "failed_retryable": 0, - "already_deleted": 0 - }, - "deleted_child_verifications": 0, - "error": null, - "created_at": "2026-10-09T10:15:00Z", - "completed_at": null - } - } + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" }, "400": { - "description": "Validation error — nothing was deleted. Error values are arrays of message strings keyed by field name.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "session_numbers": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "examples": { - "Missing selector": { - "summary": "Neither session_numbers nor delete_all provided", - "value": { - "session_numbers": [ - "This field is required unless delete_all is true." - ] - } - }, - "Empty list": { - "summary": "session_numbers is an empty array", - "value": { - "session_numbers": [ - "This list may not be empty." - ] - } - }, - "Non-numeric entry": { - "summary": "session_numbers contains a non-digit string", - "value": { - "session_numbers": [ - "All session_numbers must be numeric." - ] - } - }, - "Privacy erasure cannot retain": { - "summary": "deletion_instruction privacy_erasure combined with retain_face_embeddings true", - "value": { - "retain_face_embeddings": [ - "Privacy erasure cannot retain biometric templates." - ] - } - }, - "No finite retention duration": { - "summary": "Retention requested without face_retention_days on the request or the application", - "value": { - "detail": "A finite biometric-template retention duration is required." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" + } + } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "403": { - "description": "Missing or invalid API key. This API returns `403` for authentication failures — never `401`.", + "409": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } - }, - "examples": { - "Missing or invalid API key": { - "summary": "No x-api-key header, or the key is invalid", - "value": { - "detail": "You do not have permission to perform this action." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "429": { - "description": "Shared write rate limit exceeded (300 POST/PATCH/DELETE requests per minute per API key). Inspect `Retry-After` and the `X-RateLimit-*` response headers before retrying.", + "502": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } - }, - "examples": { - "Rate limited": { - "summary": "Write budget exhausted", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, "x-codeSamples": [ { "lang": "curl", - "label": "curl", - "source": "curl -X POST \\\n https://verification.didit.me/v3/sessions/delete/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"session_numbers\": [\"1001\", \"1002\", \"1003\"]\n }'" + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/itsme/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"BEL\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" }, { "lang": "python", "label": "Python", - "source": "import requests\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/sessions/delete/\",\n headers={\n \"x-api-key\": \"YOUR_API_KEY\",\n \"Content-Type\": \"application/json\",\n },\n json={\"session_numbers\": [\"1001\", \"1002\", \"1003\"]},\n)\nresponse.raise_for_status()\nif response.status_code == 202: # more than 20 sessions: poll the job\n job_id = response.json()[\"job_id\"]\nelse: # 200: one result per matched session\n print(response.json()[\"results\"])\n\n# Destructive: delete every KYC session in the application\n# requests.post(\n# \"https://verification.didit.me/v3/sessions/delete/\",\n# headers={\"x-api-key\": \"YOUR_API_KEY\"},\n# json={\"delete_all\": True},\n# )" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const response = await fetch(\n 'https://verification.didit.me/v3/sessions/delete/',\n {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ session_numbers: ['1001', '1002', '1003'] }),\n },\n);\nif (response.status !== 200 && response.status !== 202) throw new Error(`HTTP ${response.status}`);\nconst body = await response.json();\n// 200: body.results, one per matched session. 202: poll /v3/sessions/delete/${body.job_id}/" + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'BEL',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/itsme/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } - ] + ], + "x-didit-wallet-id": "itsme" } }, - "/v3/sessions/delete/{job_id}/": { - "get": { - "summary": "Get a bulk session deletion job", - "description": "Progress of a bulk session deletion that `POST /v3/sessions/delete/` answered with `202` (more than 20 matching sessions, selected by `session_numbers` or with `delete_all`). Poll every few seconds while `status` is `PENDING` or `PROCESSING`; stop when it is `COMPLETED` or `FAILED`.\n\nThe job reports how many sessions ended in each outcome and how many it has processed, never which sessions it deleted. A `FAILED` job carries the reason in `error`; the sessions it had not reached are still in place, so repeat the deletion request to delete them.\n\n**Authentication:** API key in the `x-api-key` header. A job is only visible to the application that created it; any other `job_id` returns `404`. Missing or invalid credentials return `403`; this API never returns `401`.", - "operationId": "get_session_deletion_job", - "tags": [ - "Sessions" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], + "/v3/id-verification/wallets/idin/": { + "post": { + "operationId": "start_idin_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_idin", + "auth": "api_key", + "server": true + }, + "description": "Start a iDIN sign-in (Dutch banks (Currence iDIN); NLD) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start iDIN verification", "parameters": [ { - "name": "job_id", - "in": "path", - "required": true, + "in": "header", + "name": "Idempotency-Key", "schema": { - "type": "string", - "format": "uuid" + "type": "string" }, - "description": "The `job_id` returned with the `202` from `POST /v3/sessions/delete/`." + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl 'https://verification.didit.me/v3/sessions/delete/JOB_ID/' \\\n -H 'x-api-key: YOUR_API_KEY'" - }, - { - "lang": "python", - "label": "Python", - "source": "import time\nimport requests\n\nurl = 'https://verification.didit.me/v3/sessions/delete/JOB_ID/'\nwhile True:\n job = requests.get(url, headers={'x-api-key': 'YOUR_API_KEY'}).json()\n if job['status'] in ('COMPLETED', 'FAILED'):\n break\n time.sleep(3)\nprint(job['status'], job['outcomes'], job['error'])" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const url = 'https://verification.didit.me/v3/sessions/delete/JOB_ID/';\nlet job;\ndo {\n await new Promise((r) => setTimeout(r, 3000));\n job = await (await fetch(url, { headers: { 'x-api-key': 'YOUR_API_KEY' } })).json();\n} while (job.status === 'PENDING' || job.status === 'PROCESSING');\nconsole.log(job.status, job.outcomes, job.error);" - } + "tags": [ + "Digital ID wallets" ], - "responses": { - "200": { - "description": "The job.", - "content": { - "application/json": { - "schema": { - "type": "object", - "description": "A bulk session deletion running in the background: progress and counts per outcome, never which sessions it deleted.", - "properties": { - "job_id": { - "type": "string", - "format": "uuid", - "description": "Poll `GET /v3/sessions/delete/{job_id}/` with this id." - }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED" - ], - "description": "`PENDING`: accepted, not started yet. `PROCESSING`: deleting; `processed` and `outcomes` are updating. `COMPLETED`: every selected session is accounted for. `FAILED`: the job stopped early; see `error`." - }, - "total": { - "type": "integer", - "description": "Sessions selected when the deletion was requested." - }, - "processed": { - "type": "integer", - "description": "Sessions the job has finished with so far." - }, - "outcomes": { - "type": "object", - "description": "How many sessions ended in each outcome. Every key is always present.", - "properties": { - "deleted": { - "type": "integer", - "description": "Sessions deleted together with their face embedding." - }, - "none": { - "type": "integer", - "description": "Sessions deleted that had no face embedding." - }, - "retained_with_user": { - "type": "integer", - "description": "Sessions deleted while one image-free biometric template was retained for their User, as the request or the application policy asked." - }, - "ineligible_no_vendor_user": { - "type": "integer", - "description": "Sessions deleted, with their face embedding, because retention was requested but they had no linked User." - }, - "failed_retryable": { - "type": "integer", - "description": "Sessions reported as not deleted. A job retries a group it could not delete by itself instead of counting it here." - }, - "already_deleted": { - "type": "integer", - "description": "Selected sessions that were already deleted, for example by another request, when the job reached them." - } - } - }, - "deleted_child_verifications": { - "type": "integer", - "description": "Verifications that the deleted business verifications created, deleted with them. Always `0` for a job started by `POST /v3/sessions/delete/`, which deletes KYC sessions only." - }, - "error": { - "type": "string", - "nullable": true, - "description": "`null` unless `status` is `FAILED`; then why the deletion stopped. The sessions the job had not reached are still in place: repeat the request to delete them." - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "When the deletion was requested." - }, - "completed_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the job finished; `null` while it runs." - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiIdinWalletVerificationStartRequest" + }, + "example": { + "country": "NLD", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" }, - "examples": { - "Processing": { - "value": { - "job_id": "5b8f2c1e-7d3a-4f6b-9e2c-1a0d8f7e6c5b", - "status": "PROCESSING", - "total": 250, - "processed": 100, - "outcomes": { - "deleted": 82, - "none": 18, - "retained_with_user": 0, - "ineligible_no_vendor_user": 0, - "failed_retryable": 0, - "already_deleted": 0 - }, - "deleted_child_verifications": 0, - "error": null, - "created_at": "2026-10-09T10:15:00Z", - "completed_at": null - } - }, - "Completed": { - "value": { - "job_id": "5b8f2c1e-7d3a-4f6b-9e2c-1a0d8f7e6c5b", - "status": "COMPLETED", - "total": 250, - "processed": 250, - "outcomes": { - "deleted": 204, - "none": 44, - "retained_with_user": 0, - "ineligible_no_vendor_user": 0, - "failed_retryable": 0, - "already_deleted": 2 - }, - "deleted_child_verifications": 0, - "error": null, - "created_at": "2026-10-09T10:15:00Z", - "completed_at": "2026-10-09T10:16:40Z" - } - }, - "Failed": { - "value": { - "job_id": "5b8f2c1e-7d3a-4f6b-9e2c-1a0d8f7e6c5b", - "status": "FAILED", - "total": 250, - "processed": 100, - "outcomes": { - "deleted": 82, - "none": 18, - "retained_with_user": 0, - "ineligible_no_vendor_user": 0, - "failed_retryable": 0, - "already_deleted": 0 - }, - "deleted_child_verifications": 0, - "error": "The deletion stopped before it finished. The sessions it had not reached are still in place; repeat the request to delete them.", - "created_at": "2026-10-09T10:15:00Z", - "completed_at": "2026-10-09T10:15:52Z" - } - } - } + "return_url": "https://app.example.com/identity/complete" } - } - }, - "403": { - "description": "Missing or invalid API key. This API returns `403` for authentication failures, never `401`.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } - }, - "examples": { - "Missing or invalid API key": { - "summary": "No x-api-key header, or the key is invalid", - "value": { - "detail": "You do not have permission to perform this action." - } - } - } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiIdinWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiIdinWalletVerificationStartRequest" } } }, - "404": { - "description": "No deletion job with this `job_id` exists for the calling application. Jobs are only visible to the application that created them." - } - } - } - }, - "/v3/sessions/{session_id}/reviews/": { - "get": { - "summary": "List session reviews and activity log", - "description": "Return the audit trail / activity feed for a User Verification (KYC) session, newest first, in a paginated envelope (`count` / `next` / `previous` / `results`, 50 entries per page by default).\n\nFeed entries are written by every actor that touches the session: manual decisions made through `PATCH /v3/session/{session_id}/update-status/` (which records `previous_status` → `new_status`), Console reviewer activity (comments with `@email` mentions, KYC/POA data edits, file uploads/removals, tag changes, detail-page views), AML ongoing-monitoring matches, and notes appended via `POST` on this same path. Reading this feed is the API-side equivalent of the **Activity** panel in the Didit Console session view.\n\nOnly User Verification (KYC) sessions are addressable here; Business Verification (KYB) session IDs return `404`.", - "operationId": "list_session_reviews", - "tags": [ - "Sessions" - ], - "parameters": [ - { - "name": "session_id", - "in": "path", - "required": true, - "description": "UUID of the User Verification (KYC) session. Must belong to the same application as the API key — cross-application UUIDs, deleted sessions, and Business Verification (KYB) session IDs all return `404`.", - "schema": { - "type": "string", - "format": "uuid", - "example": "8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "description": "Page size. Defaults to `50`.", - "schema": { - "type": "integer", - "default": 50, - "example": 50 - } - }, + "required": true + }, + "security": [ { - "name": "offset", - "in": "query", - "required": false, - "description": "Number of entries to skip from the start of the (ordered) feed. Defaults to `0`. The `next` / `previous` URLs in the response carry the right values for walking the feed.", - "schema": { - "type": "integer", - "default": 0, - "example": 0 - } + "ApiKeyAuth": [] }, { - "name": "ordering", - "in": "query", - "required": false, - "description": "Sort order. The feed defaults to newest first (`-created_at`); pass `created_at` to read it oldest first. Unknown values are silently ignored (default order is used); other serializer field names are also accepted.", - "schema": { - "type": "string", - "default": "-created_at", - "example": "created_at" - } + "BearerAuth": [] } ], "responses": { - "200": { - "description": "Paginated activity feed, newest first by default. `count` is the total number of entries; `next` / `previous` are ready-made page URLs (`null` at the ends). A `count` of `0` with empty `results` means no activity has been recorded yet.", + "201": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "count": { - "type": "integer", - "description": "Total number of feed entries for this session (across all pages).", - "example": 3 - }, - "next": { - "type": "string", - "nullable": true, - "description": "URL of the next page, or `null` on the last page.", - "example": null + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + }, + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "idin", + "name": "iDIN", + "issuing_authority": "Dutch banks (Currence iDIN)" }, - "previous": { - "type": "string", - "nullable": true, - "description": "URL of the previous page, or `null` on the first page.", - "example": null + "country": "NLD", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ReviewItem" - }, - "description": "Feed entries for this page." - } - } - }, - "examples": { - "DecisionTrail": { - "summary": "Manual approval plus API-added notes", - "value": { - "count": 3, - "next": null, - "previous": null, - "results": [ - { - "uuid": "de0561bc-5583-4409-9171-1a31a4c8a5f4", - "activity_type": "STATUS_UPDATED", - "actor_display": "API Client", - "actor_email": null, - "actor_type": "API_KEY", - "new_status": "Approved", - "previous_status": "In Review", - "comment": "Document re-checked manually; address matches.", - "mentioned_emails": [], - "previous_value": { - "status": "In Review" - }, - "new_value": { - "status": "Approved" - }, - "changed_fields": [ - "status" - ], - "metadata": null, - "created_at": "2026-06-12T00:19:31.918735Z" - }, - { - "uuid": "35370f58-5098-44ee-8a06-8c9af80b2455", - "activity_type": "STATUS_UPDATED", - "actor_display": "Unknown", - "actor_email": null, - "actor_type": "CONSOLE_USER", - "new_status": null, - "previous_status": null, - "comment": "Note without status.", - "mentioned_emails": [], - "previous_value": null, - "new_value": null, - "changed_fields": [], - "metadata": null, - "created_at": "2026-06-12T00:18:44.504157Z" - }, - { - "uuid": "00338f82-f1c5-4f57-8614-c49bb258af1a", - "activity_type": "STATUS_UPDATED", - "actor_display": "Unknown", - "actor_email": null, - "actor_type": "CONSOLE_USER", - "new_status": "In Review", - "previous_status": null, - "comment": "Flagging for manual document re-check.", - "mentioned_emails": [], - "previous_value": null, - "new_value": null, - "changed_fields": [], - "metadata": null, - "created_at": "2026-06-12T00:18:44.466537Z" - } - ] - } - }, - "PaginatedFirstPage": { - "summary": "First page with limit=2 — next holds the follow-up URL", - "value": { - "count": 4, - "next": "https://verification.didit.me/v3/sessions/8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d/reviews/?limit=2&offset=2", - "previous": null, - "results": [ - { - "uuid": "de0561bc-5583-4409-9171-1a31a4c8a5f4", - "activity_type": "STATUS_UPDATED", - "actor_display": "API Client", - "actor_email": null, - "actor_type": "API_KEY", - "new_status": "Approved", - "previous_status": "In Review", - "comment": "Document re-checked manually; address matches.", - "mentioned_emails": [], - "previous_value": { - "status": "In Review" - }, - "new_value": { - "status": "Approved" - }, - "changed_fields": [ - "status" - ], - "metadata": null, - "created_at": "2026-06-12T00:19:31.918735Z" - }, - { - "uuid": "bd4e6227-bdd2-42b2-a726-1d12f785a457", - "activity_type": "STATUS_UPDATED", - "actor_display": "Unknown", - "actor_email": null, - "actor_type": "CONSOLE_USER", - "new_status": null, - "previous_status": null, - "comment": null, - "mentioned_emails": [], - "previous_value": null, - "new_value": null, - "changed_fields": [], - "metadata": null, - "created_at": "2026-06-12T00:18:44.540238Z" - } - ] - } + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" }, - "Empty": { - "summary": "Session with no recorded activity yet", - "value": { - "count": 0, - "next": null, - "previous": null, - "results": [] - } - } + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" + } + } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, "403": { - "description": "Missing, malformed, expired, or revoked `x-api-key`. Authentication is checked before the session lookup, so an invalid key returns `403` even when the session UUID does not exist.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string", - "example": "You do not have permission to perform this action." - } - } - }, - "examples": { - "InvalidKey": { - "summary": "Missing or invalid x-api-key header", - "value": { - "detail": "You do not have permission to perform this action." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "404": { - "description": "No User Verification (KYC) session with this UUID is visible to your application: the UUID does not exist, the session was deleted, or it belongs to a different application. Business Verification (KYB) session IDs also return `404` here — the reviews feed is only addressable for KYC sessions.", + "409": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string", - "example": "Session not found." - } - } - }, - "examples": { - "NotFound": { - "summary": "Unknown, deleted, or cross-application session UUID", - "value": { - "detail": "Session not found." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "429": { - "description": "Rate limit exceeded. Inspect `Retry-After` and `X-RateLimit-*` response headers. See [Rate limiting](/integration/rate-limiting)." + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ - { - "ApiKeyAuth": [] - } - ], "x-codeSamples": [ { "lang": "curl", - "label": "curl", - "source": "curl -s 'https://verification.didit.me/v3/sessions/8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d/reviews/?limit=50' \\\n -H 'x-api-key: YOUR_API_KEY'" - }, - { - "lang": "Python", - "label": "Python (requests)", - "source": "import os, requests\n\nsession_id = \"8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d\"\nurl = f\"https://verification.didit.me/v3/sessions/{session_id}/reviews/\"\n\nwhile url: # walk every page of the feed\n resp = requests.get(url, headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]}, timeout=10)\n resp.raise_for_status()\n page = resp.json()\n for entry in page[\"results\"]:\n print(entry[\"created_at\"], entry[\"activity_type\"], entry[\"actor_display\"], entry.get(\"new_status\"))\n url = page[\"next\"]" + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/idin/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"NLD\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" }, { - "lang": "JavaScript", - "label": "Node.js (fetch)", - "source": "const sessionId = \"8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d\";\nconst res = await fetch(\n `https://verification.didit.me/v3/sessions/${sessionId}/reviews/`,\n { headers: { 'x-api-key': 'YOUR_API_KEY' } },\n);\nif (!res.ok) throw new Error(`Didit ${res.status}`);\nconst { count, results } = await res.json();\nconsole.log(`${count} feed entries`);\nfor (const entry of results) {\n console.log(entry.created_at, entry.activity_type, entry.actor_display, entry.new_status ?? \"\");\n}" + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'NLD',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/idin/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } - ] - }, - "post": { - "summary": "Add a review note to a session's audit trail", - "description": "Append a note to a User Verification (KYC) session's review / activity feed. **This endpoint only records an entry — it never changes the session.** The session's `status` is left untouched, no status-transition validation runs, and no webhook fires. To actually approve, decline, or re-open a session (which also writes a fully-attributed `STATUS_UPDATED` entry to this feed), use `PATCH /v3/session/{session_id}/update-status/` instead.\n\nBoth body fields are optional and an empty JSON object is accepted. `new_status` is stored purely as a label on the entry and accepts any of the ten session status values (case-sensitive). `@email` mentions inside `comment` are stored as plain text only — they trigger no notifications on this endpoint, and the entry's `mentioned_emails` stays empty.\n\nNote on attribution: entries created here carry no actor information. In the feed they show `actor_display: \"Unknown\"`, `actor_type: \"CONSOLE_USER\"`, and `activity_type: \"STATUS_UPDATED\"` (storage defaults), and the `201` body's `user` field is `\"Unknown\"`.", - "operationId": "create_session_review", - "tags": [ - "Sessions" ], + "x-didit-wallet-id": "idin" + } + }, + "/v3/id-verification/wallets/ftn/": { + "post": { + "operationId": "start_ftn_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_ftn", + "auth": "api_key", + "server": true + }, + "description": "Start a Finnish Trust Network sign-in (Finnish banks and mobile operators (FTN); FIN) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start Finnish Trust Network verification", "parameters": [ { - "name": "Content-Type", "in": "header", - "required": false, - "description": "Set to `application/json` when sending a JSON body.", - "schema": { - "type": "string", - "example": "application/json" - } - }, - { - "name": "session_id", - "in": "path", - "required": true, - "description": "UUID of the User Verification (KYC) session. Must belong to the same application as the API key — cross-application UUIDs, deleted sessions, and Business Verification (KYB) session IDs all return `404`.", + "name": "Idempotency-Key", "schema": { - "type": "string", - "format": "uuid", - "example": "8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d" - } + "type": "string" + }, + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], + "tags": [ + "Digital ID wallets" + ], "requestBody": { - "required": false, "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "new_status": { - "type": "string", - "nullable": true, - "enum": [ - "Not Started", - "In Progress", - "Approved", - "Declined", - "In Review", - "Expired", - "Abandoned", - "Kyc Expired", - "Resubmitted", - "Awaiting User" - ], - "description": "Optional status label to record on the entry. Accepts any session status value, case-sensitive (`\"approved\"` is rejected with `400`). Recording a status here does **not** change the session's actual status — use the update-status endpoint for that.", - "example": "In Review" - }, - "comment": { - "type": "string", - "nullable": true, - "description": "Optional free-text note (no enforced length limit). Stored verbatim; `@email` mentions are not processed on this endpoint.", - "example": "Flagging for manual document re-check." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiFtnWalletVerificationStartRequest" }, - "examples": { - "NoteWithStatusLabel": { - "summary": "Note carrying a status label (session status unchanged)", - "value": { - "new_status": "In Review", - "comment": "Flagging for manual document re-check." - } + "example": { + "country": "FIN", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" }, - "CommentOnly": { - "summary": "Plain note without a status label", - "value": { - "comment": "Customer emailed an updated utility bill; awaiting upload." - } - } + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiFtnWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiFtnWalletVerificationStartRequest" } } - } + }, + "required": true }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], "responses": { "201": { - "description": "Entry recorded on the session's audit trail. The session itself is unchanged — its `status` keeps its previous value and no webhook is sent. The new entry also appears at the top of `GET …/reviews/` (with `actor_display: \"Unknown\"`).", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "user": { - "type": "string", - "description": "Display name of the entry's author. Always `\"Unknown\"` for entries created through this endpoint, because it records no actor attribution.", - "example": "Unknown" - }, - "new_status": { - "type": "string", - "nullable": true, - "enum": [ - "Not Started", - "In Progress", - "Approved", - "Declined", - "In Review", - "Expired", - "Abandoned", - "Kyc Expired", - "Resubmitted", - "Awaiting User" - ], - "description": "The status label recorded on the entry, or `null` if none was sent. The session's actual status is not affected.", - "example": "In Review" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + }, + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "ftn", + "name": "Finnish Trust Network", + "issuing_authority": "Finnish banks and mobile operators (FTN)" }, - "comment": { - "type": "string", - "nullable": true, - "description": "The note recorded on the entry, or `null` if none was sent.", - "example": "Flagging for manual document re-check." + "country": "FIN", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "When the entry was written, ISO 8601 UTC.", - "example": "2026-06-12T00:18:44.466537Z" - } - } - }, - "examples": { - "NoteWithStatusLabel": { - "summary": "Note with a status label", - "value": { - "user": "Unknown", - "new_status": "In Review", - "comment": "Flagging for manual document re-check.", - "created_at": "2026-06-12T00:18:44.466537Z" - } + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" }, - "CommentOnly": { - "summary": "Plain note", - "value": { - "user": "Unknown", - "new_status": null, - "comment": "Note without status.", - "created_at": "2026-06-12T00:18:44.504157Z" - } - } + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" }, "400": { - "description": "Validation error, returned as a per-field map of messages. The usual cause is a `new_status` value that is not one of the ten allowed statuses — the check is case-sensitive.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "new_status": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "\"approved\" is not a valid choice." - ] - } - } - }, - "examples": { - "InvalidStatusChoice": { - "summary": "Lowercase / unknown status value", - "value": { - "new_status": [ - "\"approved\" is not a valid choice." - ] - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" } } - } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, "403": { - "description": "Missing, malformed, expired, or revoked `x-api-key`. Authentication is checked before the session lookup, so an invalid key returns `403` even when the session UUID does not exist.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string", - "example": "You do not have permission to perform this action." - } - } - }, - "examples": { - "InvalidKey": { - "summary": "Missing or invalid x-api-key header", - "value": { - "detail": "You do not have permission to perform this action." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "404": { - "description": "No User Verification (KYC) session with this UUID is visible to your application: the UUID does not exist, the session was deleted, or it belongs to a different application. Business Verification (KYB) session IDs also return `404` here — the reviews feed is only addressable for KYC sessions.", + "409": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string", - "example": "Session not found." - } - } - }, - "examples": { - "NotFound": { - "summary": "Unknown, deleted, or cross-application session UUID", - "value": { - "detail": "Session not found." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "429": { - "description": "Rate limit exceeded. Inspect `Retry-After` and `X-RateLimit-*` response headers. See [Rate limiting](/integration/rate-limiting)." + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ - { - "ApiKeyAuth": [] - } - ], "x-codeSamples": [ { "lang": "curl", - "label": "curl", - "source": "# Records an audit-trail note only -- the session's status does NOT change.\ncurl -s -X POST https://verification.didit.me/v3/sessions/8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d/reviews/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"new_status\": \"In Review\",\n \"comment\": \"Flagging for manual document re-check.\"\n }'" - }, - { - "lang": "Python", - "label": "Python (requests)", - "source": "import os, requests\n\n# Records an audit-trail note only -- the session's status does NOT change.\n# To approve/decline a session use PATCH /v3/session/{session_id}/update-status/.\nsession_id = \"8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d\"\nresp = requests.post(\n f\"https://verification.didit.me/v3/sessions/{session_id}/reviews/\",\n headers={\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Content-Type\": \"application/json\",\n },\n json={\n \"new_status\": \"In Review\",\n \"comment\": \"Flagging for manual document re-check.\",\n },\n timeout=10,\n)\nresp.raise_for_status()\nprint(resp.json()) # {'user': 'Unknown', 'new_status': 'In Review', ...}" + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/ftn/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"FIN\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" }, { - "lang": "JavaScript", - "label": "Node.js (fetch)", - "source": "// Records an audit-trail note only -- the session's status does NOT change.\n// To approve/decline a session use PATCH /v3/session/{session_id}/update-status/.\nconst sessionId = \"8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d\";\nconst res = await fetch(\n `https://verification.didit.me/v3/sessions/${sessionId}/reviews/`,\n {\n method: \"POST\",\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n new_status: \"In Review\",\n comment: \"Flagging for manual document re-check.\",\n }),\n },\n);\nif (!res.ok) throw new Error(`Didit ${res.status}: ${await res.text()}`);\nconsole.log(await res.json()); // { user: 'Unknown', new_status: 'In Review', ... }" + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'FIN',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/ftn/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } - ] + ], + "x-didit-wallet-id": "ftn" } }, - "/v3/billing/balance/": { - "get": { - "summary": "Get credit balance", - "description": "Get current pre-paid credit balance in USD (organization-wide) plus auto-refill config. Amounts are decimal strings — parse with a decimal library.", - "operationId": "get_billing_balance", + "/v3/id-verification/wallets/personalausweis/": { + "post": { + "operationId": "start_personalausweis_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_personalausweis", + "auth": "api_key", + "server": true + }, + "description": "Start a Personalausweis sign-in (Bundesministerium des Innern (eID); DEU) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start Personalausweis verification", + "parameters": [ + { + "in": "header", + "name": "Idempotency-Key", + "schema": { + "type": "string" + }, + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." + } + ], "tags": [ - "Billing" + "Digital ID wallets" ], - "x-codeSamples": [ + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiPersonalausweisWalletVerificationStartRequest" + }, + "example": { + "country": "DEU", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiPersonalausweisWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiPersonalausweisWalletVerificationStartRequest" + } + } + }, + "required": true + }, + "security": [ { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/billing/balance/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "ApiKeyAuth": [] }, { - "lang": "python", - "label": "Python", - "source": "import os\nfrom decimal import Decimal\n\nimport requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/billing/balance/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\ndata = resp.json()\nbalance = Decimal(data['balance'])\nif balance < Decimal('50'):\n print(f'Low balance: ${balance} - top up before launching new sessions.')\nelse:\n print(f'Balance OK: ${balance}')" + "BearerAuth": [] + } + ], + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + }, + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "personalausweis", + "name": "Personalausweis", + "issuing_authority": "Bundesministerium des Innern (eID)" + }, + "country": "DEU", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" + } + } + }, + "description": "" }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/billing/balance/', {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Billing balance failed: ${resp.status}`);\nconst { balance, auto_refill_enabled, auto_refill_threshold } = await resp.json();\nconsole.log({ balance, auto_refill_enabled, auto_refill_threshold });" - } - ], - "responses": { - "200": { - "description": "Current credit balance plus the organization's auto-refill configuration.", + "400": { "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "balance", - "auto_refill_enabled" - ], - "properties": { - "balance": { - "type": "string", - "description": "Current pre-paid credit balance in USD, as a fixed-point decimal string. Decreases as verifications are billed and increases when a top-up settles." - }, - "auto_refill_enabled": { - "type": "boolean", - "description": "When `true`, Didit will automatically charge the saved Stripe payment method to add credits once the balance crosses `auto_refill_threshold`. Configured from Console → Billing." - }, - "auto_refill_amount": { - "type": "string", - "nullable": true, - "description": "USD amount that will be charged on each auto-refill. `null` when auto-refill is disabled or unset." - }, - "auto_refill_threshold": { - "type": "string", - "nullable": true, - "description": "Trigger threshold in USD. Auto-refill fires when `balance` falls below this value. `null` when auto-refill is disabled or unset." - } - } - }, - "examples": { - "Healthy balance with auto-refill": { - "value": { - "balance": "1234.5600", - "auto_refill_enabled": true, - "auto_refill_amount": "500.00", - "auto_refill_threshold": "100.00" - } - }, - "Low balance, manual top-up only": { - "value": { - "balance": "12.0000", - "auto_refill_enabled": false, - "auto_refill_amount": null, - "auto_refill_threshold": null - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" } } - } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, "403": { - "description": "Missing, invalid, or revoked API key, or the key cannot read billing data for this organization. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "409": { "content": { "application/json": { - "examples": { - "Throttled": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ + "x-codeSamples": [ { - "ApiKeyAuth": [] + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/personalausweis/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"DEU\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'DEU',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/personalausweis/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } - ] + ], + "x-didit-wallet-id": "personalausweis" } }, - "/v3/billing/top-up/": { + "/v3/id-verification/wallets/frejaid/": { "post": { - "summary": "Top up credits", - "description": "Add credits via a hosted Stripe Checkout session (**$50 minimum**); not available on annual plans. Redirect the customer to the returned `checkout_session_url`.", - "operationId": "billing_top_up", + "operationId": "start_frejaid_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_frejaid", + "auth": "api_key", + "server": true + }, + "description": "Start a Freja eID sign-in (Freja eID Group; SWE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start Freja eID verification", + "parameters": [ + { + "in": "header", + "name": "Idempotency-Key", + "schema": { + "type": "string" + }, + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." + } + ], "tags": [ - "Billing" + "Digital ID wallets" ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "amount_in_dollars" - ], - "properties": { - "amount_in_dollars": { - "type": "number", - "minimum": 50, - "description": "Amount to charge, in USD. **Must be at least 50.** Tax is added automatically at checkout based on the customer's billing address." - }, - "success_url": { - "type": "string", - "format": "uri", - "description": "URL Stripe redirects the customer to after a successful payment. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. Append the literal placeholder `{CHECKOUT_SESSION_ID}` if you want Stripe to substitute the session ID." - }, - "cancel_url": { - "type": "string", - "format": "uri", - "description": "URL Stripe redirects the customer to when they abort or close the Checkout window. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiFrejaidWalletVerificationStartRequest" }, - "examples": { - "Minimum top-up": { - "value": { - "amount_in_dollars": 50 - } + "example": { + "country": "SWE", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" }, - "Custom amount with redirects": { - "value": { - "amount_in_dollars": 500, - "success_url": "https://yourapp.com/billing/top-up/success", - "cancel_url": "https://yourapp.com/billing/top-up/cancel" - } - } + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiFrejaidWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiFrejaidWalletVerificationStartRequest" } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/billing/top-up/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"amount_in_dollars\": 500,\n \"success_url\": \"https://yourapp.com/billing/success\",\n \"cancel_url\": \"https://yourapp.com/billing/cancel\"\n }'" }, + "required": true + }, + "security": [ { - "lang": "python", - "label": "Python", - "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/billing/top-up/',\n headers={\n 'x-api-key': os.environ['DIDIT_API_KEY'],\n 'Content-Type': 'application/json',\n },\n json={\n 'amount_in_dollars': 500,\n 'success_url': 'https://yourapp.com/billing/success',\n 'cancel_url': 'https://yourapp.com/billing/cancel',\n },\n timeout=10,\n)\nresp.raise_for_status()\nsession = resp.json()\nprint('Redirect customer to:', session['checkout_session_url'])" + "ApiKeyAuth": [] }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/billing/top-up/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n amount_in_dollars: 500,\n success_url: 'https://yourapp.com/billing/success',\n cancel_url: 'https://yourapp.com/billing/cancel',\n }),\n});\nif (!resp.ok) throw new Error(`Top-up failed: ${resp.status}`);\nconst { checkout_session_url } = await resp.json();\nwindow.location.assign(checkout_session_url);" + "BearerAuth": [] } ], "responses": { - "200": { - "description": "Stripe Checkout session created. Redirect the user to `checkout_session_url` to complete payment.", + "201": { "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "checkout_session_id", - "checkout_session_url" - ], - "properties": { - "checkout_session_id": { - "type": "string", - "description": "Stripe Checkout Session identifier (e.g. `cs_test_a1B2c3...`). Useful for server-side reconciliation." - }, - "checkout_session_url": { - "type": "string", - "format": "uri", - "description": "Single-use Stripe-hosted payment page URL. Redirect the customer here to complete payment." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" }, - "examples": { - "Created": { - "value": { - "checkout_session_id": "cs_test_a1B2c3D4e5F6g7H8i9J0", - "checkout_session_url": "https://checkout.stripe.com/c/pay/cs_test_a1B2c3D4e5F6g7H8i9J0" - } - } + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "frejaid", + "name": "Freja eID", + "issuing_authority": "Freja eID Group" + }, + "country": "SWE", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" }, "400": { - "description": "Invalid payload — the amount is missing, not a number, below the $50 minimum, or the organization is on an annual plan. The error body is a JSON array.", "content": { "application/json": { - "examples": { - "Missing amount": { - "value": [ - "amount_in_dollars is required." - ] - }, - "Not a number": { - "value": [ - "amount_in_dollars must be a number." - ] - }, - "Below minimum": { - "value": [ - "Amount must be at least $50." - ] - }, - "Annual plan": { - "value": [ - "You have an annual plan. Contact your account manager to top up." - ] - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" } } - } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, "403": { - "description": "Missing, invalid, or revoked API key, or the key cannot create top-up sessions for this organization. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "409": { "content": { "application/json": { - "examples": { - "Throttled": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - } - }, - "/v3/webhook/destinations/": { - "get": { - "summary": "List webhook destinations", - "description": "List all webhook destinations (unpaginated array, oldest first). Each row includes URL, subscribed events, and delivery health metrics. The signing secret is NOT included in the list — fetch a single destination (`GET /v3/webhook/destinations/{destination_uuid}/`) to read `secret_shared_key`.", - "operationId": "list_webhook_destinations", - "tags": [ - "Webhook" - ], "x-codeSamples": [ { "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/webhook/destinations/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/frejaid/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"SWE\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" }, { "lang": "python", "label": "Python", - "source": "import os\n\nimport requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/webhook/destinations/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nfor dest in resp.json():\n health = 'OK' if dest['error_rate_percentage'] < 5 else 'DEGRADED'\n print(f\"{dest['label']:30s} {dest['url']:50s} {health}\")" - }, + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'SWE',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/frejaid/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" + } + ], + "x-didit-wallet-id": "frejaid" + } + }, + "/v3/id-verification/wallets/uae-pass/": { + "post": { + "operationId": "start_uae_pass_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_uae_pass", + "auth": "api_key", + "server": true + }, + "description": "Start a UAE PASS sign-in (UAE Digital Government Authority; ARE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start UAE PASS verification", + "parameters": [ { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/webhook/destinations/', {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nconst destinations = await resp.json();\nfor (const d of destinations) {\n console.log(d.label, d.url, `${d.error_rate_percentage}% errors`);\n}" + "in": "header", + "name": "Idempotency-Key", + "schema": { + "type": "string" + }, + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], - "responses": { - "200": { - "description": "List of webhook destinations (unpaginated, oldest first).", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Stable destination identifier; use as `{destination_uuid}` for update/delete." - }, - "label": { - "type": "string", - "description": "Human-readable name shown in the Console." - }, - "url": { - "type": "string", - "format": "uri", - "description": "Destination URL that receives `POST` requests with the webhook payload." - }, - "enabled": { - "type": "boolean", - "description": "When `false`, Didit silently skips delivery to this destination." - }, - "webhook_version": { - "type": "string", - "enum": [ - "v1", - "v2", - "v3" - ], - "description": "Payload schema version sent to this destination. `v3` is the current/recommended format." - }, - "subscribed_events": { - "$ref": "#/components/schemas/WebhookSubscribedEvents" - }, - "subscribed_events_count": { - "type": "integer", - "description": "Cached length of `subscribed_events`, useful for UI rendering." - }, - "deliveries_count": { - "type": "integer", - "description": "Total delivery attempts (successful + failed) recorded for this destination." - }, - "failed_deliveries_count": { - "type": "integer", - "description": "Delivery attempts whose target responded with HTTP ≥ 400 (or did not respond)." - }, - "average_response_time_ms": { - "type": "integer", - "nullable": true, - "description": "Average target response time in milliseconds, rounded to an integer. `null` if no deliveries yet." - }, - "error_rate_percentage": { - "type": "integer", - "description": "`round(failed_deliveries_count / deliveries_count * 100)`; `0` when there are no deliveries." - }, - "last_delivery_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "Timestamp of the most recent delivery attempt (success or failure). `null` if never delivered." - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { - "type": "string", - "format": "date-time" - } - } - } - }, - "examples": { - "Two destinations": { - "value": [ - { - "uuid": "11111111-2222-3333-4444-555555555555", - "label": "Production session webhooks", - "url": "https://yourapp.com/webhooks/didit", - "enabled": true, - "webhook_version": "v3", - "subscribed_events": [ - "status.updated", - "data.updated" - ], - "subscribed_events_count": 2, - "deliveries_count": 12453, - "failed_deliveries_count": 14, - "average_response_time_ms": 187, - "error_rate_percentage": 0, - "last_delivery_at": "2026-05-17T09:21:18Z", - "created_at": "2026-01-04T08:00:00Z", - "updated_at": "2026-04-22T14:09:11Z" - }, - { - "uuid": "22222222-3333-4444-5555-666666666666", - "label": "Staging mirror", - "url": "https://staging.yourapp.com/webhooks/didit", - "enabled": false, - "webhook_version": "v3", - "subscribed_events": [ - "status.updated" - ], - "subscribed_events_count": 1, - "deliveries_count": 0, - "failed_deliveries_count": 0, - "average_response_time_ms": null, - "error_rate_percentage": 0, - "last_delivery_at": null, - "created_at": "2026-03-12T10:15:02Z", - "updated_at": "2026-03-12T10:15:02Z" - } - ] - } - } + "tags": [ + "Digital ID wallets" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiUaePassWalletVerificationStartRequest" + }, + "example": { + "country": "ARE", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiUaePassWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiUaePassWalletVerificationStartRequest" } } }, + "required": true + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + }, + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "uae_pass", + "name": "UAE PASS", + "issuing_authority": "UAE Digital Government Authority" + }, + "country": "ARE", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" + } + } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + }, "403": { - "description": "API key missing, malformed, expired, or scoped to a different application. Didit returns 403 (never 401) for all authentication failures on this endpoint.", "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "429": { - "description": "Rate limit exceeded; back off and retry.", + "409": { "content": { "application/json": { - "examples": { - "Throttled": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - }, - "post": { - "summary": "Create webhook destination", - "description": "Create a webhook destination. The response includes the auto-generated `secret_shared_key` HMAC signing secret (also readable later via `GET /v3/webhook/destinations/{destination_uuid}/`). Always send `subscribed_events` with at least one event (no wildcards): an explicit empty list is rejected with 400, and omitting the field creates a destination subscribed to nothing, which never receives a webhook. The `(application, url)` pair must be unique among non-deleted destinations.", - "operationId": "create_webhook_destination", - "tags": [ - "Webhook" - ], "x-codeSamples": [ { "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/webhook/destinations/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"label\": \"Production session webhooks\",\n \"url\": \"https://yourapp.com/webhooks/didit\",\n \"webhook_version\": \"v3\",\n \"subscribed_events\": [\"status.updated\", \"data.updated\"]\n }'" + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/uae-pass/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"ARE\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" }, { "lang": "python", "label": "Python", - "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/webhook/destinations/',\n headers={\n 'x-api-key': os.environ['DIDIT_API_KEY'],\n 'Content-Type': 'application/json',\n },\n json={\n 'label': 'Production session webhooks',\n 'url': 'https://yourapp.com/webhooks/didit',\n 'webhook_version': 'v3',\n 'subscribed_events': ['status.updated', 'data.updated'],\n },\n timeout=10,\n)\nresp.raise_for_status()\ndestination = resp.json()\n# Store secret_shared_key in your secret manager.\nprint('Destination UUID:', destination['uuid'])\nprint('Signing secret:', destination['secret_shared_key'])" - }, + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'ARE',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/uae-pass/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" + } + ], + "x-didit-wallet-id": "uae_pass" + } + }, + "/v3/id-verification/wallets/govbr/": { + "post": { + "operationId": "start_govbr_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_govbr", + "auth": "api_key", + "server": true + }, + "description": "Start a gov.br sign-in (Governo Federal do Brasil; BRA) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start gov.br verification", + "parameters": [ { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/webhook/destinations/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n label: 'Production session webhooks',\n url: 'https://yourapp.com/webhooks/didit',\n webhook_version: 'v3',\n subscribed_events: ['status.updated', 'data.updated'],\n }),\n});\nif (!resp.ok) throw new Error(await resp.text());\nconst destination = await resp.json();\nconsole.log('Store this secret securely:', destination.secret_shared_key);" + "in": "header", + "name": "Idempotency-Key", + "schema": { + "type": "string" + }, + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], + "tags": [ + "Digital ID wallets" + ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "label", - "url" - ], - "properties": { - "label": { - "type": "string", - "maxLength": 255, - "description": "Human-readable name displayed in Console → Webhooks. Helps distinguish destinations when you have several (e.g. \"Prod\", \"Staging mirror\")." - }, - "url": { - "type": "string", - "format": "uri", - "description": "HTTPS URL that will receive `POST` requests. Must be unique per application among non-deleted destinations. Use a stable, hardened endpoint — Didit retries failed deliveries on a backoff schedule." - }, - "enabled": { - "type": "boolean", - "default": true, - "description": "When `false`, the destination is saved but Didit will not push any webhooks to it. Useful for staging configs you want to keep parked." - }, - "webhook_version": { - "type": "string", - "enum": [ - "v1", - "v2", - "v3" - ], - "description": "Payload schema version Didit will send. `v3` is the current/recommended format; older versions are kept for backward compatibility with legacy integrations." - }, - "subscribed_events": { - "allOf": [ - { - "$ref": "#/components/schemas/WebhookSubscribedEvents" - } - ], - "description": "Events to deliver. Effectively required: an explicit `[]` is rejected with 400, and omitting the field creates a destination with no subscriptions that receives nothing." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiGovbrWalletVerificationStartRequest" }, - "examples": { - "session_updates": { - "summary": "Session status and data updates", - "value": { - "label": "Production session webhooks", - "url": "https://yourapp.com/webhooks/didit", - "webhook_version": "v3", - "subscribed_events": [ - "status.updated", - "data.updated" - ] - } - }, - "transaction_updates": { - "summary": "Transaction monitoring updates", - "value": { - "label": "Transaction monitoring webhooks", - "url": "https://yourapp.com/webhooks/didit-transactions", - "webhook_version": "v3", - "subscribed_events": [ - "transaction.created", - "transaction.status.updated" - ] - } + "example": { + "country": "BRA", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" }, - "entity_updates": { - "summary": "User and Business entity updates", - "value": { - "label": "Entity lifecycle webhooks", - "url": "https://yourapp.com/webhooks/didit-entities", - "webhook_version": "v3", - "subscribed_events": [ - "user.status.updated", - "user.data.updated", - "business.status.updated", - "business.data.updated", - "activity.created" - ] - } - } + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiGovbrWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiGovbrWalletVerificationStartRequest" } } - } + }, + "required": true }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], "responses": { "201": { - "description": "Destination created. `secret_shared_key` is the HMAC signing secret — store it now.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Stable destination identifier. Use as `{destination_uuid}` in subsequent calls." - }, - "label": { - "type": "string" - }, - "url": { - "type": "string", - "format": "uri" - }, - "enabled": { - "type": "boolean" - }, - "webhook_version": { - "type": "string", - "enum": [ - "v1", - "v2", - "v3" - ] - }, - "secret_shared_key": { - "type": "string", - "description": "Auto-generated HMAC signing secret bound to this destination (43-character URL-safe token). Use it to verify the signature headers on incoming webhooks. Always returned in full to API-key callers; Console users need the `read:webhooks` permission, otherwise they see an empty string." - }, - "subscribed_events": { - "$ref": "#/components/schemas/WebhookSubscribedEvents" - }, - "created_at": { - "type": "string", - "format": "date-time" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + }, + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "govbr", + "name": "gov.br", + "issuing_authority": "Governo Federal do Brasil" }, - "updated_at": { - "type": "string", - "format": "date-time" + "country": "BRA", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" }, - "summary": { - "type": "object", - "description": "Delivery health summary (all zero/null on a brand-new destination).", - "properties": { - "total_deliveries": { - "type": "integer" - }, - "failed_deliveries": { - "type": "integer" - }, - "error_rate_percentage": { - "type": "integer" - }, - "min_response_time_ms": { - "type": "integer", - "nullable": true - }, - "avg_response_time_ms": { - "type": "integer", - "nullable": true - }, - "max_response_time_ms": { - "type": "integer", - "nullable": true - }, - "last_delivery_at": { - "type": "string", - "format": "date-time", - "nullable": true - } - } - } - } - }, - "examples": { - "Created": { - "value": { - "uuid": "11111111-2222-3333-4444-555555555555", - "label": "Production session webhooks", - "url": "https://yourapp.com/webhooks/didit", - "enabled": true, - "webhook_version": "v3", - "secret_shared_key": "tno2NTeRC1ZK3sBOYlBJDyEzBVENl3pQ1AcZyAW0xnQ", - "subscribed_events": [ - "status.updated", - "data.updated" - ], - "created_at": "2026-05-17T10:00:00Z", - "updated_at": "2026-05-17T10:00:00Z", - "summary": { - "total_deliveries": 0, - "failed_deliveries": 0, - "error_rate_percentage": 0, - "min_response_time_ms": null, - "avg_response_time_ms": null, - "max_response_time_ms": null, - "last_delivery_at": null - } - } - } + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" }, "400": { - "description": "Validation failed — usually a duplicate URL, missing/invalid `subscribed_events`, or unknown event name.", "content": { "application/json": { - "examples": { - "Duplicate URL": { - "value": { - "url": [ - "This webhook destination already exists for the application." - ] - } - }, - "Empty events list": { - "value": { - "subscribed_events": [ - "At least one subscribed event is required." - ] - } - }, - "Unknown event": { - "value": { - "subscribed_events": [ - "Unsupported webhook events: foo.bar" - ] - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" } } - } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, "403": { - "description": "API key missing, malformed, expired, or scoped to a different application. Didit returns 403 (never 401) for all authentication failures on this endpoint.", "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "429": { - "description": "Rate limit exceeded; back off and retry.", + "409": { "content": { "application/json": { - "examples": { - "Throttled": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ + "x-codeSamples": [ { - "ApiKeyAuth": [] + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/govbr/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"BRA\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'BRA',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/govbr/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } - ] + ], + "x-didit-wallet-id": "govbr" } }, - "/v3/webhook/destinations/{destination_uuid}/": { - "get": { - "summary": "Get webhook destination", - "description": "Retrieve one webhook destination with its `secret_shared_key` signing secret and delivery health summary. API-key callers always receive the secret in full; Console users without `read:webhooks` see an empty string.", - "operationId": "get_webhook_destination", - "tags": [ - "Webhook" - ], + "/v3/id-verification/wallets/oneid/": { + "post": { + "operationId": "start_oneid_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_oneid", + "auth": "api_key", + "server": true + }, + "description": "Start a OneID sign-in (OneID (UK bank-verified identity); GBR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start OneID verification", "parameters": [ { - "name": "destination_uuid", - "in": "path", - "required": true, - "description": "UUID of the destination (the `uuid` returned from create / list).", + "in": "header", + "name": "Idempotency-Key", "schema": { - "type": "string", - "format": "uuid" + "type": "string" }, - "example": "11111111-2222-3333-4444-555555555555" + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "tags": [ + "Digital ID wallets" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiOneidWalletVerificationStartRequest" + }, + "example": { + "country": "GBR", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiOneidWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiOneidWalletVerificationStartRequest" + } + } }, + "required": true + }, + "security": [ { - "lang": "python", - "label": "Python", - "source": "import os\n\nimport requests\n\nuuid = '11111111-2222-3333-4444-555555555555'\nresp = requests.get(\n f'https://verification.didit.me/v3/webhook/destinations/{uuid}/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nprint(resp.json())" + "ApiKeyAuth": [] }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const uuid = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(`https://verification.didit.me/v3/webhook/destinations/${uuid}/`, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Lookup failed: ${resp.status}`);\nconst destination = await resp.json();\nconsole.log(destination);" + "BearerAuth": [] } ], "responses": { - "200": { - "description": "Webhook destination details.", + "201": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "label": { - "type": "string" - }, - "url": { - "type": "string", - "format": "uri" - }, - "enabled": { - "type": "boolean" - }, - "webhook_version": { - "type": "string", - "enum": [ - "v1", - "v2", - "v3" - ] - }, - "secret_shared_key": { - "type": "string", - "description": "HMAC signing secret (43-character URL-safe token). Always returned in full to API-key callers; Console users need `read:webhooks`, otherwise an empty string." - }, - "subscribed_events": { - "$ref": "#/components/schemas/WebhookSubscribedEvents" - }, - "created_at": { - "type": "string", - "format": "date-time" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + }, + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "oneid", + "name": "OneID", + "issuing_authority": "OneID (UK bank-verified identity)" }, - "updated_at": { - "type": "string", - "format": "date-time" + "country": "GBR", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" }, - "summary": { - "type": "object", - "description": "Delivery health metrics aggregated over the destination's lifetime.", - "properties": { - "total_deliveries": { - "type": "integer" - }, - "failed_deliveries": { - "type": "integer" - }, - "error_rate_percentage": { - "type": "integer" - }, - "min_response_time_ms": { - "type": "integer", - "nullable": true - }, - "avg_response_time_ms": { - "type": "integer", - "nullable": true - }, - "max_response_time_ms": { - "type": "integer", - "nullable": true - }, - "last_delivery_at": { - "type": "string", - "format": "date-time", - "nullable": true - } - } - } - } - }, - "examples": { - "Healthy destination": { - "value": { - "uuid": "11111111-2222-3333-4444-555555555555", - "label": "Production session webhooks", - "url": "https://yourapp.com/webhooks/didit", - "enabled": true, - "webhook_version": "v3", - "secret_shared_key": "tno2NTeRC1ZK3sBOYlBJDyEzBVENl3pQ1AcZyAW0xnQ", - "subscribed_events": [ - "status.updated", - "data.updated" - ], - "created_at": "2026-01-04T08:00:00Z", - "updated_at": "2026-04-22T14:09:11Z", - "summary": { - "total_deliveries": 12453, - "failed_deliveries": 14, - "error_rate_percentage": 0, - "min_response_time_ms": 41, - "avg_response_time_ms": 187, - "max_response_time_ms": 2103, - "last_delivery_at": "2026-05-17T09:21:18Z" - } - } - } + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" + } + } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, "403": { - "description": "API key missing, malformed, expired, or scoped to a different application. Didit returns 403 (never 401) for all authentication failures on this endpoint.", "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "404": { - "description": "No destination with this UUID exists, or it belongs to a different application, or it has been deleted.", + "409": { "content": { "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Webhook destination not found." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "429": { - "description": "Rate limit exceeded; back off and retry.", + "502": { "content": { "application/json": { - "examples": { - "Throttled": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ + "x-codeSamples": [ { - "ApiKeyAuth": [] + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/oneid/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"GBR\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'GBR',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/oneid/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } - ] - }, - "patch": { - "summary": "Update webhook destination", - "description": "Update a webhook destination. **`subscribed_events` is REPLACED wholesale, not merged** — fetch first, append, resend. Secret is not rotated.", - "operationId": "update_webhook_destination", - "tags": [ - "Webhook" ], + "x-didit-wallet-id": "oneid" + } + }, + "/v3/id-verification/wallets/govuk-wallet/": { + "post": { + "operationId": "start_govuk_wallet_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_govuk_wallet", + "auth": "api_key", + "server": true + }, + "description": "Start a GOV.UK Wallet sign-in (UK Government Digital Service; GBR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start GOV.UK Wallet verification", "parameters": [ { - "name": "destination_uuid", - "in": "path", - "required": true, - "description": "UUID of the destination to update.", + "in": "header", + "name": "Idempotency-Key", "schema": { - "type": "string", - "format": "uuid" + "type": "string" }, - "example": "11111111-2222-3333-4444-555555555555" + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], + "tags": [ + "Digital ID wallets" + ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "label": { - "type": "string", - "description": "New human-readable name." - }, - "url": { - "type": "string", - "format": "uri", - "description": "New target URL. Must be unique within the application." - }, - "enabled": { - "type": "boolean", - "description": "Set to `false` to pause deliveries to this destination without deleting it." - }, - "webhook_version": { - "type": "string", - "enum": [ - "v1", - "v2", - "v3" - ], - "description": "New payload schema version for future deliveries." - }, - "subscribed_events": { - "$ref": "#/components/schemas/WebhookSubscribedEvents" - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiGovukWalletWalletVerificationStartRequest" }, - "examples": { - "Pause destination": { - "value": { - "enabled": false - } - }, - "Add transaction events": { - "value": { - "subscribed_events": [ - "status.updated", - "data.updated", - "transaction.created", - "transaction.status.updated" - ] - } + "example": { + "country": "GBR", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" }, - "Move target URL": { - "value": { - "label": "Production session webhooks v2", - "url": "https://api.yourapp.com/webhooks/didit/v2" - } - } + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiGovukWalletWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiGovukWalletWalletVerificationStartRequest" } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH 'https://verification.didit.me/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"subscribed_events\": [\n \"status.updated\",\n \"data.updated\",\n \"transaction.created\"\n ]\n }'" }, + "required": true + }, + "security": [ { - "lang": "python", - "label": "Python", - "source": "import os\n\nimport requests\n\nuuid = '11111111-2222-3333-4444-555555555555'\nresp = requests.patch(\n f'https://verification.didit.me/v3/webhook/destinations/{uuid}/',\n headers={\n 'x-api-key': os.environ['DIDIT_API_KEY'],\n 'Content-Type': 'application/json',\n },\n json={'enabled': False},\n timeout=10,\n)\nresp.raise_for_status()\nprint('Paused destination:', resp.json()['label'])" + "ApiKeyAuth": [] }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const uuid = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(`https://verification.didit.me/v3/webhook/destinations/${uuid}/`, {\n method: 'PATCH',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ enabled: false }),\n});\nif (!resp.ok) throw new Error(await resp.text());\nconsole.log(await resp.json());" + "BearerAuth": [] } ], "responses": { - "200": { - "description": "Updated webhook destination. Body has the same shape as `GET /v3/webhook/destinations/{destination_uuid}/`.", + "201": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "label": { - "type": "string" - }, - "url": { - "type": "string", - "format": "uri" - }, - "enabled": { - "type": "boolean" - }, - "webhook_version": { - "type": "string", - "enum": [ - "v1", - "v2", - "v3" - ] - }, - "secret_shared_key": { - "type": "string" - }, - "subscribed_events": { - "$ref": "#/components/schemas/WebhookSubscribedEvents" - }, - "created_at": { - "type": "string", - "format": "date-time" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + }, + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "govuk_wallet", + "name": "GOV.UK Wallet", + "issuing_authority": "UK Government Digital Service" }, - "updated_at": { - "type": "string", - "format": "date-time" + "country": "GBR", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" }, - "summary": { - "type": "object", - "properties": { - "total_deliveries": { - "type": "integer" - }, - "failed_deliveries": { - "type": "integer" - }, - "error_rate_percentage": { - "type": "integer" - }, - "min_response_time_ms": { - "type": "integer", - "nullable": true - }, - "avg_response_time_ms": { - "type": "integer", - "nullable": true - }, - "max_response_time_ms": { - "type": "integer", - "nullable": true - }, - "last_delivery_at": { - "type": "string", - "format": "date-time", - "nullable": true - } - } - } - } - }, - "examples": { - "Paused": { - "value": { - "uuid": "11111111-2222-3333-4444-555555555555", - "label": "Production session webhooks", - "url": "https://yourapp.com/webhooks/didit", - "enabled": false, - "webhook_version": "v3", - "secret_shared_key": "tno2NTeRC1ZK3sBOYlBJDyEzBVENl3pQ1AcZyAW0xnQ", - "subscribed_events": [ - "status.updated", - "data.updated" - ], - "created_at": "2026-01-04T08:00:00Z", - "updated_at": "2026-05-17T11:02:55Z", - "summary": { - "total_deliveries": 12453, - "failed_deliveries": 14, - "error_rate_percentage": 0, - "min_response_time_ms": 41, - "avg_response_time_ms": 187, - "max_response_time_ms": 2103, - "last_delivery_at": "2026-05-17T09:21:18Z" - } - } - } - } - } - } - }, - "400": { - "description": "Validation failed (duplicate URL, empty/unknown events list, malformed body).", - "content": { - "application/json": { - "examples": { - "Duplicate URL": { - "value": { - "url": [ - "This webhook destination already exists for the application." - ] - } + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" }, - "Empty events": { - "value": { - "subscribed_events": [ - "At least one subscribed event is required." - ] - } - } - } - } - } - }, - "403": { - "description": "API key missing, malformed, expired, or scoped to a different application. Didit returns 403 (never 401) for all authentication failures on this endpoint.", - "content": { - "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" }, - "404": { - "description": "Destination with this UUID does not exist (or is deleted, or belongs to another application).", + "400": { "content": { "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Webhook destination not found." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" } } - } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, - "429": { - "description": "Rate limit exceeded; back off and retry.", + "403": { "content": { "application/json": { - "examples": { - "Throttled": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - }, - "delete": { - "summary": "Delete webhook destination", - "description": "Delete a webhook destination. Future events stop being pushed to its URL. Past delivery records are not affected and stay queryable from the Business Console, so deleting a destination is not a way to remove your delivery history. Calling again returns 404.", - "operationId": "delete_webhook_destination", - "tags": [ - "Webhook" - ], - "parameters": [ - { - "name": "destination_uuid", - "in": "path", - "required": true, - "description": "UUID of the destination to delete.", - "schema": { - "type": "string", - "format": "uuid" }, - "example": "11111111-2222-3333-4444-555555555555" - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X DELETE 'https://verification.didit.me/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/' \\\n -H 'x-api-key: YOUR_API_KEY'" - }, - { - "lang": "python", - "label": "Python", - "source": "import os\n\nimport requests\n\nuuid = '11111111-2222-3333-4444-555555555555'\nresp = requests.delete(\n f'https://verification.didit.me/v3/webhook/destinations/{uuid}/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nassert resp.status_code == 204" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const uuid = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(`https://verification.didit.me/v3/webhook/destinations/${uuid}/`, {\n method: 'DELETE',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (resp.status !== 204) throw new Error(`Delete failed: ${resp.status}`);" - } - ], - "responses": { - "204": { - "description": "Destination deleted. No response body." + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "403": { - "description": "API key missing, malformed, expired, or scoped to a different application. Didit returns 403 (never 401) for all authentication failures on this endpoint.", + "409": { "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "404": { - "description": "No destination with this UUID exists for the application (already deleted or never existed).", + "502": { "content": { "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Webhook destination not found." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." }, - "429": { - "description": "Rate limit exceeded; back off and retry.", + "503": { "content": { "application/json": { - "examples": { - "Throttled": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ + "x-codeSamples": [ { - "ApiKeyAuth": [] + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/govuk-wallet/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"GBR\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'GBR',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/govuk-wallet/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } - ] + ], + "x-didit-wallet-id": "govuk_wallet" } }, - "/v3/wallet-screening/": { + "/v3/id-verification/wallets/smart-id/": { "post": { - "summary": "Screen a wallet address", - "description": "Run on-demand AML screening for a single crypto wallet/address without creating a transaction. Resolves your application's configured blockchain analytics provider, screens the address synchronously, and returns the normalised risk result. LIGHTNING is available only with an Elliptic or Chainalysis BYOK connection added through Marketplace. Nothing is written to the transactions table. One AML monitoring usage is billed per successful screening (sandbox applications are not billed).", - "operationId": "screenWallet", - "tags": [ - "Transactions" - ], - "security": [ + "operationId": "start_smart_id_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_smart_id", + "auth": "api_key", + "server": true + }, + "description": "Start a Smart-ID sign-in (SK ID Solutions; EST, LVA, LTU, BEL) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=confirm_in_app with the comparison code to show the user; the user confirms it and enters their PIN in the app (never collect the PIN). Poll POST /v3/id-verification/wallets/verifications/{request_id}/poll/ every poll_interval_seconds, or rely on the webhook: Didit polls in the background too. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start Smart-ID verification", + "parameters": [ { - "ApiKeyAuth": [] + "in": "header", + "name": "Idempotency-Key", + "schema": { + "type": "string" + }, + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], + "tags": [ + "Digital ID wallets" + ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "wallet_address", - "blockchain" - ], - "properties": { - "wallet_address": { - "type": "string", - "description": "The crypto address or network identifier to screen. Must match the selected network (e.g. a `0x`-prefixed 40-hex-char address for EVM chains). Alias: `address`.", - "example": "0x94f1b9b64e2932f6a2db338f616844400cd58e8a" - }, - "blockchain": { - "type": "string", - "description": "Network identifier. Managed screening supports BTC, ETH, LTC, XRP, BCH, DOGE, TRX, SOL, MATIC, BNB, BASE (Base), OP (Optimism), and ARB (Arbitrum One). USDT and USDC are legacy Ethereum aliases, not multi-network searches. Connected-provider coverage varies. LIGHTNING requires a supported connected provider. Celo, Mode and Plume are not currently supported. Alias: `currency`.", - "enum": [ - "BTC", - "LIGHTNING", - "ETH", - "LTC", - "XRP", - "BCH", - "DOGE", - "TRX", - "SOL", - "MATIC", - "BNB", - "USDT", - "USDC", - "BASE", - "OP", - "ARB" - ], - "example": "ETH" - }, - "direction": { - "type": "string", - "description": "Optional. Whether the address is being screened as an inbound (deposit) or outbound (withdrawal) counterparty. Accepts `inbound`, `outbound`, `deposit`, or `withdrawal`. Defaults to a neutral pre-transfer screen when omitted. This is passed to the provider as context only; it does not change the returned `risk_score`, `severity`, or the `source_of_funds`/`destination_of_funds` breakdowns, which are always both returned regardless of `direction`. Changing or omitting `direction` therefore does not require re-screening an address.", - "enum": [ - "inbound", - "outbound", - "deposit", - "withdrawal" - ] - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiSmartIdWalletVerificationStartRequest" + }, + "example": { + "country": "EST", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "personal_code": "00000000000" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiSmartIdWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiSmartIdWalletVerificationStartRequest" } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/wallet-screening/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"wallet_address\": \"0x94f1b9b64e2932f6a2db338f616844400cd58e8a\",\n \"blockchain\": \"ETH\"\n }'" }, + "required": true + }, + "security": [ { - "lang": "python", - "label": "Python", - "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/wallet-screening/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={\n 'wallet_address': '0x94f1b9b64e2932f6a2db338f616844400cd58e8a',\n 'blockchain': 'ETH',\n },\n timeout=20,\n)\nresp.raise_for_status()\nresult = resp.json()\nprint(result['risk_score'], result['severity'], result['sanctions_hit'])" + "ApiKeyAuth": [] }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const result = await fetch('https://verification.didit.me/v3/wallet-screening/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n wallet_address: '0x94f1b9b64e2932f6a2db338f616844400cd58e8a',\n blockchain: 'ETH',\n }),\n}).then((r) => r.json());\nconsole.log(result.risk_score, result.severity, result.sanctions_hit);" + "BearerAuth": [] } ], "responses": { - "200": { - "description": "The normalised wallet screening result.", + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WalletScreeningResult" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" }, "example": { - "provider": "merklescience", - "screening_type": "WALLET_SCREENING", - "risk_score": 95, - "severity": "CRITICAL", - "status": "SCREENED", - "summary": "Merkle Science risk: Critical (5/5). SANCTIONS HIT. Dominant risk: sanctioned. Driven by: the address is attributed to Sanctions / OFAC (Example Sanctions)", - "wallet_address": "0x94f1b9b64e2932f6a2db338f616844400cd58e8a", - "transaction_hash": null, - "blockchain": "ETH", - "block_timestamp": null, - "total_value_usd": null, - "fee_usd": null, - "sender_rows": [], - "receiver_rows": [], - "transaction_flows": [], - "transaction_flows_status": null, - "transaction_flows_error": null, - "source_of_funds": [ - { - "category": "sanctioned", - "percentage": 100.0, - "amount_usd": 9813695.0, - "entity_name": "Example Sanctions", - "entity_type": "Sanctions", - "entity_subtype": "OFAC", - "hops": null, - "description": "Sanctions / OFAC", - "is_direct": true, - "exposure_direction": "incoming", - "exposure_type": "direct", - "provider_entity_id": "118885", - "provider_entity_type_id": "30", - "provider_entity_subtype_id": "125", - "country": "", - "raw_amount": null, - "raw_currency": null - } - ], - "destination_of_funds": [ - { - "category": "defi", - "percentage": 84.824453, - "amount_usd": 7783414.38, - "entity_name": "Example DeFi 7", - "entity_type": "DeFi", - "entity_subtype": "Bridges", - "hops": null, - "description": "DeFi / Bridges", - "is_direct": false, - "exposure_direction": "outgoing", - "exposure_type": "indirect", - "provider_entity_id": "118459", - "provider_entity_type_id": "33", - "provider_entity_subtype_id": "143", - "country": "", - "raw_amount": null, - "raw_currency": null - }, - { - "category": "defi", - "percentage": 15.175547, - "amount_usd": 1392494.37, - "entity_name": "Example DeFi 2", - "entity_type": "DeFi", - "entity_subtype": "Yield Aggregator", - "hops": null, - "description": "DeFi / Yield Aggregator", - "is_direct": false, - "exposure_direction": "outgoing", - "exposure_type": "indirect", - "provider_entity_id": "90192", - "provider_entity_type_id": "33", - "provider_entity_subtype_id": "142", - "country": "", - "raw_amount": null, - "raw_currency": null - } - ], - "counterparty_connections": [ - { - "entity_name": "Example Sanctions", - "entity_type": "Sanctions", - "entity_subtype": "OFAC", - "risk_level": "critical", - "categories": [ - "sanctioned" - ], - "received_usd": null, - "sent_usd": null, - "received_hops": null, - "sent_hops": null, - "percentage": 0.0, - "is_direct": true, - "country": null, - "provider_entity_id": null - }, - { - "entity_name": "Example Sanctions", - "entity_type": "Sanctions", - "entity_subtype": "OFAC", - "risk_level": "critical", - "categories": [ - "sanctioned" - ], - "received_usd": null, - "sent_usd": null, - "received_hops": null, - "sent_hops": null, - "percentage": 0.0, - "is_direct": true, - "country": null, - "provider_entity_id": null - } - ], - "network_nodes": [], - "network_edges": [], - "sanctions_hit": true, - "pep_counterparty": null, - "dominant_risk_category": "sanctioned", - "risk_factors": [ - { - "source": "attribution", - "category": "sanctioned", - "label": "Sanctions / OFAC", - "entity_name": "Example Sanctions", - "direction": "attribution", - "exposure_type": "attribution", - "amount_usd": null, - "percentage": 0.0, - "is_high_risk": true, - "description": "the address is attributed to Sanctions / OFAC (Example Sanctions)" - }, - { - "source": "exposure", - "category": "sanctioned", - "label": "Sanctions / OFAC", - "entity_name": "Example Sanctions", - "direction": "incoming", - "exposure_type": "direct", - "amount_usd": 9813695.0, - "percentage": 51.679304, - "is_high_risk": true, - "description": "51.6793% direct incoming exposure to Sanctions / OFAC (Example Sanctions)" + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "smart_id", + "name": "Smart-ID", + "issuing_authority": "SK ID Solutions" }, - { - "source": "exposure", - "category": "defi", - "label": "DeFi / Bridges", - "entity_name": "Example DeFi 7", - "direction": "outgoing", - "exposure_type": "indirect", - "amount_usd": 7783414.38, - "percentage": 40.987766, - "is_high_risk": false, - "description": "40.9878% indirect outgoing exposure to DeFi / Bridges (Example DeFi 7)" + "country": "EST", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "in_app", + "simulated": true, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "confirm_in_app", + "verification_code": "1234", + "poll_interval_seconds": 2 }, - { - "source": "exposure", - "category": "defi", - "label": "DeFi / Yield Aggregator", - "entity_name": "Example DeFi 2", - "direction": "outgoing", - "exposure_type": "indirect", - "amount_usd": 1392494.37, - "percentage": 7.33293, - "is_high_risk": false, - "description": "7.33293% indirect outgoing exposure to DeFi / Yield Aggregator (Example DeFi 2)" - } - ], - "raw_response": { - "...": "provider-specific details" + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" }, - "report_signature": "" + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" }, "400": { - "description": "Validation error — malformed wallet address, missing `wallet_address`, unsupported `blockchain`, or LIGHTNING without an Elliptic or Chainalysis BYOK Marketplace connection." + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" + } + } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, - "401": { - "description": "Missing or invalid `x-api-key`." + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" + } + } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, "409": { - "description": "No AML provider is configured for the application, or the configured provider does not support wallet screening yet. Configure transaction monitoring first." + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/smart-id/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"EST\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"personal_code\": \"00000000000\"\n}'" }, - "502": { - "description": "The AML provider could not complete the screening. Retry later." + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'EST',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'personal_code': '00000000000'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/smart-id/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } - } + ], + "x-didit-wallet-id": "smart_id" } }, - "/v3/wallet-screening/pdf/": { + "/v3/id-verification/wallets/mobile-id/": { "post": { - "summary": "Render a wallet-screening result as a PDF", - "description": "Stateless companion to `POST /v3/wallet-screening/`: repost the exact JSON body that endpoint returned — including the `report_signature` it added — and get back a Didit-branded PDF of the same result. No provider is called and nothing is billed or persisted; this only re-renders a result Didit already produced. The signature ties the PDF to an unmodified result: editing `risk_score`, `severity`, `sanctions_hit`, or any other signed field before reposting fails with `400`.", - "operationId": "screenWalletPdf", - "tags": [ - "Transactions" - ], - "security": [ + "operationId": "start_mobile_id_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_mobile_id", + "auth": "api_key", + "server": true + }, + "description": "Start a Mobile-ID sign-in (SK ID Solutions with the national mobile operators; EST, LTU) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=confirm_in_app with the comparison code to show the user; the user confirms it and enters their PIN in the app (never collect the PIN). Poll POST /v3/id-verification/wallets/verifications/{request_id}/poll/ every poll_interval_seconds, or rely on the webhook: Didit polls in the background too. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start Mobile-ID verification", + "parameters": [ { - "ApiKeyAuth": [] + "in": "header", + "name": "Idempotency-Key", + "schema": { + "type": "string" + }, + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], + "tags": [ + "Digital ID wallets" + ], "requestBody": { - "required": true, - "description": "The unmodified JSON object returned by `POST /v3/wallet-screening/`, including `report_signature`.", "content": { "application/json": { "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/WalletScreeningResult" - } - ], - "required": [ - "report_signature" - ], - "description": "Same shape as the `POST /v3/wallet-screening/` response — pass that response body back verbatim, including `report_signature`." + "$ref": "#/components/schemas/DigitalIdWalletApiMobileIdWalletVerificationStartRequest" + }, + "example": { + "country": "EST", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "personal_code": "00000000000", + "phone_number": "+37200000000" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiMobileIdWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiMobileIdWalletVerificationStartRequest" } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "# $RESULT is the saved JSON body from POST /v3/wallet-screening/\ncurl -X POST 'https://verification.didit.me/v3/wallet-screening/pdf/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d \"$RESULT\" \\\n -o wallet_screening_report.pdf" }, + "required": true + }, + "security": [ { - "lang": "python", - "label": "Python", - "source": "import requests\n\n# `result` is the dict returned by POST /v3/wallet-screening/, unmodified\nresponse = requests.post(\n 'https://verification.didit.me/v3/wallet-screening/pdf/',\n headers={'x-api-key': 'YOUR_API_KEY'},\n json=result,\n timeout=30,\n)\nresponse.raise_for_status()\nwith open('wallet_screening_report.pdf', 'wb') as fh:\n fh.write(response.content)" + "ApiKeyAuth": [] }, { - "lang": "javascript", - "label": "JavaScript", - "source": "// `result` is the object returned by POST /v3/wallet-screening/, unmodified\nconst response = await fetch('https://verification.didit.me/v3/wallet-screening/pdf/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(result),\n});\nif (!response.ok) throw new Error(`PDF generation failed: HTTP ${response.status}`);\nconst pdfBuffer = Buffer.from(await response.arrayBuffer());" + "BearerAuth": [] } ], "responses": { - "200": { - "description": "The rendered PDF document, returned directly as binary `application/pdf` — there is no JSON wrapper.", - "headers": { - "Content-Disposition": { - "description": "`attachment; filename=wallet_screening_{address}.pdf`, derived from the `wallet_address` (or `transaction_hash`) in the reposted result.", + "201": { + "content": { + "application/json": { "schema": { - "type": "string", - "example": "attachment; filename=wallet_screening_0x28c6c06298d514db0899.pdf" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + }, + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "mobile_id", + "name": "Mobile-ID", + "issuing_authority": "SK ID Solutions with the national mobile operators" + }, + "country": "EST", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "in_app", + "simulated": true, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "confirm_in_app", + "verification_code": "1234", + "poll_interval_seconds": 2 + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } }, + "description": "" + }, + "400": { "content": { - "application/pdf": { + "application/json": { "schema": { - "type": "string", - "format": "binary" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" } } - } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, - "400": { - "description": "The body is not a JSON object matching the wallet-screening result shape, or `report_signature` is missing or does not match the reposted fields (edited or truncated before repost).", + "403": { "content": { "application/json": { "schema": { - "type": "object" - }, - "examples": { - "Not an object": { - "value": { - "non_field_errors": [ - "Expected a JSON object matching a wallet-screening result." - ] - } - }, - "Missing or invalid signature": { - "value": { - "report_signature": [ - "This wallet-screening result is missing or fails its report signature - repost the exact JSON returned by the wallet-screening endpoint, unmodified." - ] - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } - }, - "401": { - "description": "Missing or invalid `x-api-key`." - } - } - } - }, - "/v3/transactions/": { - "get": { - "summary": "List transactions", - "description": "Paginated list of monitored transactions, newest first (`txn_date` desc). `count` is capped at 100; use `total_transactions` for the true row count. This endpoint does not support filtering or search parameters.", - "operationId": "listTransactions", - "tags": [ - "Transactions" - ], - "parameters": [ - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "default": 50, - "minimum": 1 }, - "description": "Page size. Defaults to 50 when omitted. There is no enforced maximum, but keep pages small for latency." - }, - { - "name": "offset", - "in": "query", - "schema": { - "type": "integer", - "default": 0, - "minimum": 0 - }, - "description": "Zero-based offset of the first record to return. Prefer following the `next` URL from the previous page." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/transactions/?limit=50' \\\n -H 'x-api-key: YOUR_API_KEY'" - }, - { - "lang": "python", - "label": "Python", - "source": "import os\n\nimport requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/transactions/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n params={'limit': 50},\n timeout=10,\n)\nresp.raise_for_status()\npage = resp.json()\nfor tx in page['results']:\n print(\n tx['transaction_number'],\n tx['status'],\n tx['severity'],\n f\"{tx['amount']} {tx['currency']}\",\n tx.get('subject_name') or tx.get('subject_vendor_data'),\n )" + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const url = new URL('https://verification.didit.me/v3/transactions/');\nurl.searchParams.set('limit', '50');\nconst page = await fetch(url, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n}).then((r) => r.json());\nfor (const tx of page.results) {\n console.log(tx.transaction_number, tx.status, tx.severity, tx.amount, tx.currency);\n}" - } - ], - "responses": { - "200": { - "description": "Paginated list of transaction summaries, newest first.", + "409": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "count": { - "type": "integer", - "description": "Capped count of matching transactions (exact up to 100, capped thereafter). Use `total_transactions` for the true count when paginating." - }, - "next": { - "type": "string", - "nullable": true, - "description": "Absolute URL of the next page, or `null` on the last page." - }, - "previous": { - "type": "string", - "nullable": true, - "description": "Absolute URL of the previous page, or `null` on the first page." - }, - "total_transactions": { - "type": "integer", - "description": "Exact total count of (non-deleted) transactions for the application. Present on paginated responses." - }, - "results": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Stable Didit transaction identifier. Use as `{transaction_id}` in detail/notes/related calls." - }, - "transaction_number": { - "type": "integer", - "description": "Application-scoped sequential transaction number shown in Console (e.g. `4123`)." - }, - "txn_id": { - "type": "string", - "description": "Customer-provided identifier from the create call (max 128 chars). Unique per application." - }, - "transaction_type": { - "type": "string", - "description": "Top-level category as stored: `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet`, `gamblingLimitChange`, `gamblingBonusChange`, `auditTrailEvent`. Note multi-word values are echoed back in camelCase even when submitted in snake_case." - }, - "action_type": { - "type": "string", - "description": "Sub-type within the category (e.g. `deposit`, `withdrawal`, `transfer`)." - }, - "status": { - "type": "string", - "enum": [ - "APPROVED", - "IN_REVIEW", - "DECLINED", - "AWAITING_USER" - ], - "description": "Current monitoring verdict." - }, - "direction": { - "type": "string", - "enum": [ - "INBOUND", - "OUTBOUND" - ], - "description": "Direction relative to the subject. Stored uppercase regardless of the casing submitted." - }, - "amount": { - "type": "string", - "description": "Transaction amount as a decimal string with trailing zeros stripped (e.g. `\"1500\"`, `\"0.5\"`, `\"0.123456789012345678\"`). Up to 18 decimal places." - }, - "currency": { - "type": "string", - "description": "Currency code of `amount`. Any ISO 4217 alphabetic code is accepted (e.g. `EUR`, `USD`, `NGN`), as is a crypto asset symbol (e.g. `BTC`). Prefer a currently-circulating code. Not every accepted code has an exchange rate: when none is available for the code (for example the ISO accounting unit `XSU`, or a crypto symbol Didit does not price), automatic conversion fails and `preferred_currency_amount` is returned null unless the request supplied a pre-converted `amount_in_default_currency`." - }, - "preferred_currency_amount": { - "type": "string", - "nullable": true, - "description": "`amount` converted to the application's preferred currency, when available." - }, - "preferred_currency_code": { - "type": "string", - "nullable": true, - "description": "Preferred currency code used for `preferred_currency_amount` - the application's base currency, any currently-circulating ISO 4217 code. A code with no available exchange rate leaves `preferred_currency_amount` null unless the request supplied a pre-converted `amount_in_default_currency`." - }, - "payment_details": { - "type": "string", - "nullable": true, - "description": "Free-text payment reference or memo from the submission." - }, - "score": { - "type": "integer", - "description": "Risk score (typically 0–100). Higher = riskier." - }, - "severity": { - "type": "string", - "nullable": true, - "enum": [ - "UNKNOWN", - "LOW", - "MEDIUM", - "HIGH", - "CRITICAL" - ], - "description": "Categorical risk severity set by rules/providers (`UNKNOWN`, `LOW`, `MEDIUM`, `HIGH`, `CRITICAL`). `null` until something sets it." - }, - "subject_name": { - "type": "string", - "nullable": true, - "description": "Full name of the `APPLICANT` party." - }, - "subject_vendor_data": { - "type": "string", - "nullable": true, - "description": "Your internal user/business identifier for the applicant." - }, - "subject_country": { - "type": "string", - "nullable": true, - "description": "Country code of the applicant, from the submitted `address.country` (typically ISO 3166-1 alpha-3)." - }, - "counterparty_name": { - "type": "string", - "nullable": true, - "description": "Full name of the `COUNTERPARTY` party, if any." - }, - "counterparty_vendor_data": { - "type": "string", - "nullable": true, - "description": "Your internal identifier for the counterparty." - }, - "counterparty_country": { - "type": "string", - "nullable": true, - "description": "Country code of the counterparty, from the submitted `address.country`." - }, - "tags": { - "type": "array", - "description": "Tags attached to the transaction (manually or by rules).", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string" - }, - "color": { - "type": "string" - }, - "description": { - "type": "string", - "nullable": true - }, - "source": { - "type": "string", - "enum": [ - "didit", - "custom" - ], - "description": "Whether the tag is a Didit preset or a custom tag." - } - } - } - }, - "txn_date": { - "type": "string", - "format": "date-time", - "description": "When the transaction occurred (from `transaction_at` at create time)." - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "When Didit recorded the submission." - }, - "decision_reason_code": { - "type": "string", - "nullable": true, - "description": "Machine-readable reason for the current `status` (e.g. `WALLET_HIGH_RISK`)." - }, - "decision_reason_label": { - "type": "string", - "nullable": true, - "description": "Human-readable label for `decision_reason_code`." - }, - "assigned_to_name": { - "type": "string", - "nullable": true, - "description": "Display name of the Console reviewer assigned to this transaction." - } - } - } - } - } - }, - "examples": { - "Single transaction": { - "value": { - "count": 1, - "next": null, - "previous": null, - "total_transactions": 1, - "results": [ - { - "uuid": "abcdef12-3456-4890-abcd-ef1234567890", - "transaction_number": 4123, - "txn_id": "your-txn-2026-05-17-001", - "transaction_type": "finance", - "action_type": "withdrawal", - "status": "IN_REVIEW", - "direction": "OUTBOUND", - "amount": "1500", - "currency": "EUR", - "preferred_currency_amount": "1620.45", - "preferred_currency_code": "USD", - "payment_details": "Withdrawal to crypto wallet", - "txn_date": "2026-05-17T08:42:00Z", - "created_at": "2026-05-17T08:42:11Z", - "score": 78, - "severity": "HIGH", - "decision_reason_code": "WALLET_HIGH_RISK", - "decision_reason_label": "Destination wallet flagged as high risk", - "tags": [ - { - "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", - "name": "Manual review", - "color": "#F59E0B", - "description": null, - "source": "custom" - } - ], - "subject_name": "Maria Garcia", - "subject_vendor_data": "user-12345", - "counterparty_name": null, - "counterparty_vendor_data": null, - "counterparty_country": null, - "subject_country": null, - "assigned_to_name": "Alex Reviewer" - } - ] - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "403": { - "description": "Missing, invalid, or revoked API key, or the key cannot list transactions for this application. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", + "502": { "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "503": { "content": { "application/json": { - "examples": { - "Throttled": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - }, - "post": { - "summary": "Create transaction", - "description": "Submit a transaction for synchronous AML/risk monitoring. The transaction is created `APPROVED`, then your rules (and crypto screening, when applicable) run inline and may flip the returned `status` to `IN_REVIEW` or `DECLINED`. **`transaction_id` is your idempotency key** (max 128 chars, unique per app); reusing it returns 400. A `transaction.created` webhook is dispatched on success (sandbox applications are not billed and do not emit webhooks).", - "operationId": "createTransaction", - "tags": [ - "Transactions" - ], + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." + } + }, "x-codeSamples": [ { "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/transactions/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"transaction_id\": \"your-txn-2026-05-17-001\",\n \"transaction_category\": \"finance\",\n \"transaction_details\": {\n \"direction\": \"outbound\",\n \"amount\": 1500,\n \"currency\": \"EUR\",\n \"action_type\": \"withdrawal\"\n },\n \"subject\": {\n \"entity_type\": \"individual\",\n \"vendor_data\": \"user-12345\",\n \"full_name\": \"Maria Garcia\"\n }\n }'" + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/mobile-id/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"EST\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"personal_code\": \"00000000000\",\n \"phone_number\": \"+37200000000\"\n}'" }, { "lang": "python", "label": "Python", - "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/transactions/',\n headers={\n 'x-api-key': os.environ['DIDIT_API_KEY'],\n 'Content-Type': 'application/json',\n },\n json={\n 'transaction_id': 'your-txn-2026-05-17-001',\n 'transaction_category': 'finance',\n 'transaction_details': {\n 'direction': 'outbound',\n 'amount': 1500,\n 'currency': 'EUR',\n 'action_type': 'withdrawal',\n },\n 'subject': {\n 'entity_type': 'individual',\n 'vendor_data': 'user-12345',\n 'full_name': 'Maria Garcia',\n },\n },\n timeout=15,\n)\nresp.raise_for_status()\ntx = resp.json()\nprint(tx['txn_id'], '->', tx['status'], tx['severity'])" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/transactions/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n transaction_id: 'your-txn-2026-05-17-001',\n transaction_category: 'finance',\n transaction_details: {\n direction: 'outbound',\n amount: 1500,\n currency: 'EUR',\n action_type: 'withdrawal',\n },\n subject: {\n entity_type: 'individual',\n vendor_data: 'user-12345',\n full_name: 'Maria Garcia',\n },\n }),\n});\nif (!resp.ok) throw new Error(await resp.text());\nconst tx = await resp.json();\nconsole.log(tx.txn_id, tx.status, tx.severity);" + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'EST',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'personal_code': '00000000000',\n 'phone_number': '+37200000000'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/mobile-id/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "transaction_id": { - "type": "string", - "description": "Your unique identifier for this transaction (max 128 characters).", - "example": "your-txn-2026-05-17-001" - }, - "transaction_at": { - "type": "string", - "format": "date-time", - "description": "When the transaction occurred. Defaults to now if omitted." - }, - "time_zone": { - "type": "string", - "description": "IANA time zone identifier, e.g. `Europe/Madrid`." - }, - "transaction_category": { - "type": "string", - "description": "Category of the transaction. CamelCase aliases (`travelRule`, `userPlatformEvent`, `gamblingBet`, …) are also accepted; the stored/echoed value is camelCase for multi-word categories.", - "example": "finance", - "enum": [ - "finance", - "kyc", - "travel_rule", - "user_event", - "audit_trail_event", - "gambling_bet", - "gambling_limit_change", - "gambling_bonus_change", - "travelRule" - ] - }, - "transaction_details": { - "type": "object", - "description": "Core financial details of the transaction.", - "required": [ - "direction", - "amount", - "currency" - ], - "properties": { - "direction": { - "type": "string", - "description": "Direction of the transaction relative to the subject. Case-insensitive; `in`/`out` are accepted as aliases. Echoed back uppercase (`INBOUND`/`OUTBOUND`).", - "example": "outbound", - "enum": [ - "inbound", - "outbound", - "in", - "out" - ] - }, - "amount": { - "type": "number", - "description": "Transaction amount (up to 18 decimal places). Send it as a decimal string (e.g. `\"0.123456789012345678\"`) when exact precision matters: a JSON number is parsed as a binary float first, so digits beyond about 15 significant digits are rounded before the amount is stored.", - "example": 1500 - }, - "currency": { - "type": "string", - "description": "Currency code. Any ISO 4217 alphabetic code is accepted (e.g. `EUR`, `USD`, `NGN`), as is a crypto asset symbol (e.g. `BTC`). Prefer a currently-circulating code. Not every accepted code has an exchange rate: when none is available for the code (for example the ISO accounting unit `XSU`, or a crypto symbol Didit does not price), automatic conversion fails and `preferred_currency_amount` is returned null unless the request supplied a pre-converted `amount_in_default_currency`.", - "example": "EUR" - }, - "currency_kind": { - "type": "string", - "description": "Whether the currency is `fiat` or `crypto`." - }, - "amount_in_default_currency": { - "type": "number", - "nullable": true, - "description": "Pre-converted amount in the default currency. If omitted, automatic FX conversion is attempted." - }, - "default_currency": { - "type": "string", - "description": "The default currency code for the converted amount. Any currently-circulating ISO 4217 code." - }, - "payment_details": { - "type": "string", - "nullable": true, - "description": "Free-text payment reference or memo." - }, - "payment_reference_id": { - "type": "string", - "nullable": true, - "description": "External payment system reference. For crypto AML transaction-hash screening, send the blockchain transaction hash and also provide `subject.payment_method.account_id` with the related address." - }, - "action_type": { - "type": "string", - "nullable": true, - "description": "Sub-type of the transaction, e.g. `deposit`, `withdrawal`, `transfer`." - }, - "crypto_params": { - "type": "object", - "description": "Crypto network selection, separate from currency (the asset symbol). Recording a custom token symbol does not guarantee transaction-hash coverage for its contract.", - "properties": { - "crypto_chain": { - "type": "string", - "description": "Actual network used for screening. Managed screening supports BTC, ETH, LTC, XRP, BCH, DOGE, TRX, SOL, MATIC, BNB, BASE (Base), OP (Optimism) and ARB (Arbitrum One). Always specify this for multi-network tokens. Connected-provider coverage varies; LIGHTNING requires a supported connected provider.", - "example": "BASE" - } - } - } - } - }, - "subject": { - "type": "object", - "description": "The subject (applicant) of the transaction.", - "required": [ - "vendor_data", - "full_name" - ], - "properties": { - "entity_type": { - "type": "string", - "description": "Entity type: `individual` or `company`." - }, - "vendor_data": { - "type": "string", - "description": "Your internal user/business identifier.", - "example": "user-12345" - }, - "full_name": { - "type": "string", - "description": "Full name of the subject.", - "example": "Maria Garcia" - }, - "first_name": { - "type": "string", - "description": "First name of the subject." - }, - "last_name": { - "type": "string", - "description": "Last name of the subject." - }, - "date_of_birth": { - "type": "string", - "format": "date", - "description": "Date of birth (YYYY-MM-DD)." - }, - "address": { - "type": "object", - "description": "Address object.", - "properties": { - "country": { - "type": "string", - "description": "Country code (ISO 3166-1 alpha-3 recommended). Copied to the party's `country_code`." - }, - "town": { - "type": "string" - }, - "state": { - "type": "string" - }, - "street": { - "type": "string" - }, - "post_code": { - "type": "string" - } - } - }, - "institution_details": { - "type": "object", - "description": "Institution details (for institutional subjects).", - "properties": { - "name": { - "type": "string" - }, - "code": { - "type": "string" - }, - "address": { - "type": "object" - } - } - }, - "device_context": { - "type": "object", - "description": "Device context captured at transaction time. browser_family and platform must be sent as explicit top-level keys — they are not derived from user_agent. The camelCase alias device is also accepted on the request.", - "properties": { - "fingerprint": { - "type": "string", - "description": "Unique device fingerprint." - }, - "persistent_device_id": { - "type": "string", - "description": "Stable cross-session device identifier (v2 fingerprint schema)." - }, - "user_agent": { - "type": "string", - "description": "Browser or app user agent string. CamelCase alias: userAgent." - }, - "browser_family": { - "type": "string", - "description": "Browser family (e.g. Chrome, Firefox, Safari). Must be sent explicitly — it is not derived from user_agent. CamelCase alias: browserFamily." - }, - "browser_version": { - "type": "string", - "description": "Browser version string. Must be sent explicitly. CamelCase alias: browserVersion." - }, - "platform": { - "type": "string", - "description": "Platform identifier (web, ios, android, macos, windows, linux). Must be sent explicitly — it is not derived from user_agent." - }, - "os_family": { - "type": "string", - "description": "Operating system family (e.g. iOS, Android, Windows). CamelCase alias: osFamily." - }, - "os_version": { - "type": "string", - "description": "Operating system version string. CamelCase alias: osVersion." - }, - "accept_language": { - "type": "string", - "description": "Accept-Language header value. CamelCase alias: acceptLang." - }, - "session_id": { - "type": "string", - "description": "Session identifier of the user current session. Stored in metadata. CamelCase alias: sessionId." - }, - "session_age_ms": { - "type": "number", - "description": "Age of the user session in milliseconds at transaction time. CamelCase alias: sessionAgeMs." - }, - "network_context": { - "type": "object", - "description": "Network context with IP address and geo-location. A top-level ipInfo block is also folded into this object.", - "properties": { - "ip_address": { - "type": "string", - "description": "Client IP address. CamelCase alias: ip." - }, - "ip_country_code": { - "type": "string", - "description": "Geo-resolved country of the IP (ISO 3166-1 alpha-3). CamelCase alias: countryCode3." - }, - "latitude": { - "type": "number", - "description": "Geographic latitude of the IP. CamelCase alias: lat." - }, - "longitude": { - "type": "number", - "description": "Geographic longitude of the IP. CamelCase alias: lon." - } - } - } - } - }, - "payment_method": { - "type": "object", - "description": "Payment method used by the subject.", - "properties": { - "method_type": { - "type": "string", - "description": "Type of payment method: `bank_card`, `bank_account`, `crypto_wallet`, `ewallet`, `unhosted_wallet`, `other`." - }, - "account_id": { - "type": "string", - "description": "Account identifier (e.g. IBAN, wallet address). For crypto screening, this should contain the wallet address for this participant. Inbound transaction-hash screening requires the service/customer deposit address on the subject. Outbound screening uses the destination wallet on the counterparty." - }, - "issuing_country": { - "type": "string", - "description": "ISO 3166-1 alpha-3 country code of the issuer." - } - } - } - } - }, - "counterparty": { - "type": "object", - "description": "The counterparty of the transaction (optional).", - "properties": { - "entity_type": { - "type": "string", - "description": "Entity type: `individual` or `company`." - }, - "vendor_data": { - "type": "string", - "description": "Your internal identifier for the counterparty." - }, - "full_name": { - "type": "string", - "description": "Full name of the counterparty." - }, - "first_name": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "date_of_birth": { - "type": "string", - "format": "date" - }, - "address": { - "type": "object", - "description": "Address object with `country` (ISO 3166-1 alpha-3)." - }, - "institution_details": { - "type": "object" - }, - "device_context": { - "type": "object", - "description": "Device context for the counterparty. browser_family and platform must be sent as explicit top-level keys — they are not derived from user_agent.", - "properties": { - "fingerprint": { - "type": "string", - "description": "Unique device fingerprint." - }, - "persistent_device_id": { - "type": "string", - "description": "Stable cross-session device identifier (v2 fingerprint schema)." - }, - "user_agent": { - "type": "string", - "description": "Browser or app user agent string. CamelCase alias: userAgent." - }, - "browser_family": { - "type": "string", - "description": "Browser family (e.g. Chrome, Firefox, Safari). Must be sent explicitly — it is not derived from user_agent. CamelCase alias: browserFamily." - }, - "browser_version": { - "type": "string", - "description": "Browser version string. Must be sent explicitly. CamelCase alias: browserVersion." - }, - "platform": { - "type": "string", - "description": "Platform identifier (web, ios, android, macos, windows, linux). Must be sent explicitly — it is not derived from user_agent." - }, - "os_family": { - "type": "string", - "description": "Operating system family (e.g. iOS, Android, Windows). CamelCase alias: osFamily." - }, - "os_version": { - "type": "string", - "description": "Operating system version string. CamelCase alias: osVersion." - }, - "accept_language": { - "type": "string", - "description": "Accept-Language header value. CamelCase alias: acceptLang." - }, - "session_id": { - "type": "string", - "description": "Session identifier for the counterparty session. Stored in metadata. CamelCase alias: sessionId." - }, - "session_age_ms": { - "type": "number", - "description": "Age of the user session in milliseconds at transaction time. CamelCase alias: sessionAgeMs." - }, - "network_context": { - "type": "object", - "description": "Network context with IP address and geo-location. A top-level ipInfo block is also folded into this object.", - "properties": { - "ip_address": { - "type": "string", - "description": "Client IP address. CamelCase alias: ip." - }, - "ip_country_code": { - "type": "string", - "description": "Geo-resolved country of the IP (ISO 3166-1 alpha-3). CamelCase alias: countryCode3." - }, - "latitude": { - "type": "number", - "description": "Geographic latitude of the IP. CamelCase alias: lat." - }, - "longitude": { - "type": "number", - "description": "Geographic longitude of the IP. CamelCase alias: lon." - } - } - } - } - }, - "payment_method": { - "type": "object", - "description": "Payment method used by the counterparty. On travelRule transactions, `method_type: \"unhosted_wallet\"` declares the counterparty wallet as self-hosted: the transfer skips counterparty-VASP routing, is created directly in `UNCONFIRMED_OWNERSHIP`, and (when `auto_wallet_verification` is enabled) a wallet-ownership widget session is auto-minted and returned in the transaction's `action_required` block.", - "properties": { - "method_type": { - "type": "string", - "description": "Type of payment method: `bank_card`, `bank_account`, `crypto_wallet`, `ewallet`, `unhosted_wallet`, `other`. Use `unhosted_wallet` to declare a self-hosted wallet for the Travel Rule." - }, - "account_id": { - "type": "string", - "description": "Account identifier (e.g. IBAN or wallet address)." - }, - "issuing_country": { - "type": "string" - } - } - } - } - }, - "custom_properties": { - "type": "object", - "description": "Arbitrary key-value pairs attached to the transaction. Each key can be referenced in rule conditions with the field path `custom_values.`.", - "additionalProperties": true, - "example": { - "merchant_id": "test", - "bank_name": "test_2" - } - }, - "travel_rule_details": { - "type": "object", - "description": "Travel Rule compliance metadata.", - "required": [ - "status" - ], - "properties": { - "status": { - "type": "string" - }, - "protocol": { - "type": "string" - }, - "required": { - "type": "boolean" - }, - "obligations_count": { - "type": "integer" - }, - "originator_data": { - "type": "object" - }, - "beneficiary_data": { - "type": "object" - }, - "metadata": { - "type": "object" - } - } - }, - "network_snapshot": { - "type": "object", - "description": "Pre-computed network graph snapshot.", - "properties": { - "nodes": { - "type": "array", - "items": { - "type": "object" - } - }, - "edges": { - "type": "array", - "items": { - "type": "object" - } - }, - "metrics": { - "type": "object" - } - } - }, - "include_crypto_screening": { - "type": "boolean", - "nullable": true, - "description": "Per-transaction override for crypto blockchain analytics screening. When `true`, crypto screening is performed regardless of the application default. When `false`, crypto screening is skipped even if enabled in the application settings. When omitted or `null`, the application-level default configured in the Console is used. Crypto screening requires `transaction_details.direction`, `currency_kind: \"crypto\"`, a chain/currency, and the wallet address selected by the direction: inbound pre-transfer screening uses `counterparty.payment_method.account_id`, inbound transaction-hash screening uses `subject.payment_method.account_id`, and outbound screening uses `counterparty.payment_method.account_id`. When `transaction_details.payment_reference_id` contains a blockchain transaction hash, Didit performs transaction screening and returns transaction summary enrichment when available." - } - }, - "required": [ - "transaction_id", - "transaction_category", - "transaction_details", - "subject" - ] - }, - "examples": { - "Minimal fiat withdrawal": { - "summary": "Smallest acceptable payload for a fiat outbound transfer", - "value": { - "transaction_id": "your-txn-2026-05-17-001", - "transaction_category": "finance", - "transaction_details": { - "direction": "outbound", - "amount": 1500, - "currency": "EUR", - "action_type": "withdrawal" - }, - "subject": { - "entity_type": "individual", - "vendor_data": "user-12345", - "full_name": "Maria Garcia" - } - } + "x-didit-wallet-id": "mobile_id" + } + }, + "/v3/id-verification/wallets/idcard-ee/": { + "post": { + "operationId": "start_idcard_ee_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_idcard_ee", + "auth": "api_key", + "server": true + }, + "description": "Start a Estonian ID-card sign-in (Police and Border Guard Board (Estonia); EST) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start Estonian ID-card verification", + "parameters": [ + { + "in": "header", + "name": "Idempotency-Key", + "schema": { + "type": "string" + }, + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." + } + ], + "tags": [ + "Digital ID wallets" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiIdcardEeWalletVerificationStartRequest" + }, + "example": { + "country": "EST", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" }, - "Crypto outbound with screening": { - "summary": "Outbound crypto withdrawal to an unhosted wallet, opt-in screening", - "value": { - "transaction_id": "your-txn-2026-05-17-002", - "transaction_category": "finance", - "include_crypto_screening": true, - "transaction_details": { - "direction": "outbound", - "amount": 0.5, - "currency": "BTC", - "currency_kind": "crypto", - "amount_in_default_currency": 32500, - "default_currency": "USD", - "action_type": "withdrawal" - }, - "subject": { - "entity_type": "individual", - "vendor_data": "user-12345", - "full_name": "Maria Garcia", - "payment_method": { - "method_type": "crypto_wallet", - "account_id": "bc1qexampleexchangewalletaddressxyz", - "issuing_country": "ESP" - } - }, - "counterparty": { - "entity_type": "individual", - "payment_method": { - "method_type": "unhosted_wallet", - "account_id": "bc1qexampleexternalwalletaddressabc" - } - } - } + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiIdcardEeWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiIdcardEeWalletVerificationStartRequest" + } + } + }, + "required": true + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" }, - "Travel Rule transfer": { - "summary": "Outbound crypto transfer with Travel Rule originator/beneficiary data", - "value": { - "transaction_id": "your-txn-2026-05-17-003", - "transaction_category": "travel_rule", - "transaction_details": { - "direction": "outbound", - "amount": 25000, - "currency": "USDT", - "currency_kind": "crypto", - "payment_reference_id": "0xabcdef1234567890", - "action_type": "transfer" - }, - "subject": { - "entity_type": "individual", - "vendor_data": "user-12345", - "full_name": "Maria Garcia", - "date_of_birth": "1992-03-14", - "address": { - "country": "ESP", - "town": "Madrid", - "street": "Calle Mayor 1", - "post_code": "28013" - } + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "idcard_ee", + "name": "Estonian ID-card", + "issuing_authority": "Police and Border Guard Board (Estonia)" }, - "counterparty": { - "entity_type": "individual", - "full_name": "John Beneficiary", - "institution_details": { - "name": "Beneficiary VASP, Inc.", - "code": "BVASP-US" - } + "country": "EST", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" }, - "travel_rule_details": { - "status": "PENDING", - "protocol": "IVMS101", - "required": true, - "originator_data": { - "name": "Maria Garcia" - }, - "beneficiary_data": { - "name": "John Beneficiary" - } - } - } + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" + } + } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" + } + } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + }, + "502": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/idcard-ee/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"EST\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'EST',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/idcard-ee/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" + } + ], + "x-didit-wallet-id": "idcard_ee" + } + }, + "/v3/id-verification/wallets/eparaksts-mobile/": { + "post": { + "operationId": "start_eparaksts_mobile_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_eparaksts_mobile", + "auth": "api_key", + "server": true + }, + "description": "Start a eParaksts mobile sign-in (LVRTC (Latvian State Radio and Television Centre); LVA) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start eParaksts mobile verification", + "parameters": [ + { + "in": "header", + "name": "Idempotency-Key", + "schema": { + "type": "string" + }, + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } + ], + "tags": [ + "Digital ID wallets" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiEparakstsMobileWalletVerificationStartRequest" + }, + "example": { + "country": "LVA", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiEparakstsMobileWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiEparakstsMobileWalletVerificationStartRequest" + } + } + }, + "required": true }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], "responses": { "201": { - "description": "Transaction submitted and monitored. The body is the full transaction detail (same shape as `GET /v3/transactions/{transaction_id}/`), reflecting any rule outcomes already applied.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TransactionDetail" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" }, - "examples": { - "Approved with no rule hits": { - "value": { - "uuid": "abcdef12-3456-4890-abcd-ef1234567890", - "environment": "live", - "transaction_number": 4123, - "txn_id": "your-txn-2026-05-17-001", - "txn_date": "2026-05-17T08:42:00Z", - "zone_id": "Europe/Madrid", - "transaction_type": "finance", - "action_type": "withdrawal", - "direction": "OUTBOUND", - "status": "APPROVED", - "source": "LIVE", - "amount": "1500", - "currency": "EUR", - "currency_type": "fiat", - "amount_in_default_currency": "1620.45", - "default_currency_code": "USD", - "preferred_currency_amount": "1620.45", - "preferred_currency_code": "USD", - "payment_details": "Withdrawal to crypto wallet", - "payment_txn_id": null, - "score": 0, - "severity": null, - "decision_reason_code": null, - "decision_reason_label": null, - "vendor_data": "user-12345", - "metadata": { - "subject": { - "entity_type": "individual", - "vendor_data": "user-12345", - "full_name": "Maria Garcia", - "address": {}, - "institution_details": {}, - "device_context": {} - }, - "counterparty": {} - }, - "props": {}, - "tags": [], - "parties": [ - { - "uuid": "b1111111-2222-4333-8444-555555555555", - "role": "APPLICANT", - "entity_type": "individual", - "kind": "USER", - "vendor_data": "user-12345", - "full_name": "Maria Garcia", - "first_name": null, - "last_name": null, - "country_code": null, - "dob": null, - "address": {}, - "institution_info": {}, - "device": {}, - "external_party_snapshot": null - } - ], - "payment_methods": [], - "documents": [], - "activities": [ - { - "uuid": "d4e5f6a7-8901-4bcd-9ef0-123456789abc", - "activity_type": "TRANSACTION_CREATED", - "source": "TRANSACTION", - "status": "APPROVED", - "title": "withdrawal", - "description": "Transaction recorded.", - "metadata": {}, - "occurred_at": "2026-05-17T08:42:11Z" - } - ], - "alerts": [], - "rule_runs": [], - "provider_results": [], - "travel_rule": null, - "network_snapshot": null, - "remediation": null, - "cost_breakdown": null - } + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "eparaksts_mobile", + "name": "eParaksts mobile", + "issuing_authority": "LVRTC (Latvian State Radio and Television Centre)" + }, + "country": "LVA", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" }, - "Flagged for review": { - "value": { - "uuid": "abcdef12-3456-4890-abcd-ef1234567891", - "environment": "live", - "transaction_number": 4124, - "txn_id": "your-txn-2026-05-17-002", - "txn_date": "2026-05-17T08:42:00Z", - "zone_id": "Europe/Madrid", - "transaction_type": "finance", - "action_type": "withdrawal", - "direction": "OUTBOUND", - "status": "IN_REVIEW", - "source": "LIVE", - "amount": "0.5", - "currency": "BTC", - "currency_type": "crypto", - "amount_in_default_currency": "32500", - "default_currency_code": "USD", - "preferred_currency_amount": "32500", - "preferred_currency_code": "USD", - "payment_details": "Withdrawal to crypto wallet", - "payment_txn_id": null, - "score": 78, - "severity": "HIGH", - "decision_reason_code": "WALLET_HIGH_RISK", - "decision_reason_label": "Destination wallet flagged as high risk", - "vendor_data": "user-12345", - "metadata": { - "subject": { - "entity_type": "individual", - "vendor_data": "user-12345", - "full_name": "Maria Garcia", - "address": {}, - "institution_details": {}, - "device_context": {} - }, - "counterparty": {} - }, - "props": { - "order_id": "ord-9988" - }, - "tags": [], - "parties": [ - { - "uuid": "b1111111-2222-4333-8444-555555555555", - "role": "APPLICANT", - "entity_type": "individual", - "kind": "USER", - "vendor_data": "user-12345", - "full_name": "Maria Garcia", - "first_name": null, - "last_name": null, - "country_code": null, - "dob": null, - "address": {}, - "institution_info": {}, - "device": {}, - "external_party_snapshot": null - } - ], - "payment_methods": [], - "documents": [], - "activities": [], - "alerts": [ - { - "uuid": "a7b8c9d0-1234-4abc-9def-567890abcdef", - "title": "Destination wallet flagged as high risk", - "description": "Provider risk score exceeded the configured threshold.", - "severity": "HIGH", - "status": "OPEN", - "source": "RULE", - "metadata": {}, - "created_at": "2026-05-17T08:42:12Z", - "due_at": null - } - ], - "rule_runs": [ - { - "uuid": "e5f6a7b8-9012-4cde-af01-23456789abcd", - "rule": "f6a7b8c9-0123-4def-b012-3456789abcde", - "rule_title": "High-risk wallet screening", - "rule_description": null, - "rule_category": "crypto", - "matched": true, - "is_test": false, - "mode": "ACTIVE", - "severity": "HIGH", - "score_delta": 78, - "status_target": "IN_REVIEW", - "action_summary": "score +78; change_status:IN_REVIEW", - "metadata": {}, - "created_at": "2026-05-17T08:42:12Z" - } - ], - "provider_results": [], - "travel_rule": null, - "network_snapshot": null, - "remediation": null, - "cost_breakdown": null - } - } + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" + } + } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" + } + } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "400": { - "description": "Validation failed (missing required fields, duplicate `transaction_id`, unknown enum value, invalid currency, etc.).", + "409": { "content": { "application/json": { - "examples": { - "Missing required field": { - "value": { - "transaction_details": { - "amount": [ - "This field is required." - ] - } - } - }, - "Duplicate transaction_id": { - "value": { - "transaction_id": [ - "A transaction with this transaction_id already exists." - ] - } - }, - "Invalid category": { - "value": { - "non_field_errors": [ - "Unsupported transaction category." - ] - } - }, - "Missing subject identity": { - "value": { - "subject": [ - "subject.vendor_data and subject.full_name are required." - ] - } - }, - "Blocklisted participant": { - "value": { - "vendor_data": [ - "The individual 'user-12345' is in the blocklist and cannot participate in transactions." - ] - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "403": { - "description": "Missing, invalid, or revoked API key, or the key cannot submit transactions for this application. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", + "502": { "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "503": { "content": { "application/json": { - "examples": { - "Throttled": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ + "x-codeSamples": [ { - "ApiKeyAuth": [] + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/eparaksts-mobile/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"LVA\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'LVA',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/eparaksts-mobile/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } - ] + ], + "x-didit-wallet-id": "eparaksts_mobile" } }, - "/v3/transactions/{transaction_id}/": { - "get": { - "summary": "Get transaction", - "description": "Retrieve the full monitoring record for one transaction: parties, score/severity, rule runs, alerts, Travel Rule outcome, remediation session.", - "operationId": "getTransaction", - "tags": [ - "Transactions" - ], + "/v3/id-verification/wallets/bankid-cz/": { + "post": { + "operationId": "start_bankid_cz_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_bankid_cz", + "auth": "api_key", + "server": true + }, + "description": "Start a Bank iD sign-in (Bankovní identita, a.s.; CZE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start Bank iD verification", "parameters": [ { - "name": "transaction_id", - "in": "path", - "required": true, - "description": "Didit-stable transaction UUID (the `uuid` returned from create / list, **not** your `txn_id`).", + "in": "header", + "name": "Idempotency-Key", "schema": { - "type": "string", - "format": "uuid" + "type": "string" }, - "example": "abcdef12-3456-7890-abcd-ef1234567890" + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/transactions/abcdef12-3456-7890-abcd-ef1234567890/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "tags": [ + "Digital ID wallets" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiBankidCzWalletVerificationStartRequest" + }, + "example": { + "country": "CZE", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiBankidCzWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiBankidCzWalletVerificationStartRequest" + } + } }, + "required": true + }, + "security": [ { - "lang": "python", - "label": "Python", - "source": "import os\n\nimport requests\n\nuuid = 'abcdef12-3456-7890-abcd-ef1234567890'\nresp = requests.get(\n f'https://verification.didit.me/v3/transactions/{uuid}/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\ntx = resp.json()\nfor alert in tx['alerts']:\n print(alert.get('code'), alert.get('severity'))" + "ApiKeyAuth": [] }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const uuid = 'abcdef12-3456-7890-abcd-ef1234567890';\nconst resp = await fetch(`https://verification.didit.me/v3/transactions/${uuid}/`, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Lookup failed: ${resp.status}`);\nconst tx = await resp.json();\nconsole.log(tx.status, tx.severity, tx.alerts.length, 'alerts');" + "BearerAuth": [] } ], "responses": { - "200": { - "description": "Full transaction detail.", + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TransactionDetail" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" }, - "examples": { - "In review with an alert": { - "value": { - "uuid": "abcdef12-3456-4890-abcd-ef1234567890", - "environment": "live", - "transaction_number": 4123, - "txn_id": "your-txn-2026-05-17-001", - "txn_date": "2026-05-17T08:42:00Z", - "zone_id": "Europe/Madrid", - "transaction_type": "finance", - "action_type": "withdrawal", - "direction": "OUTBOUND", - "status": "IN_REVIEW", - "source": "LIVE", - "amount": "1500", - "currency": "EUR", - "currency_type": "fiat", - "amount_in_default_currency": "1620.45", - "default_currency_code": "USD", - "preferred_currency_amount": "1620.45", - "preferred_currency_code": "USD", - "payment_details": "Withdrawal to crypto wallet", - "payment_txn_id": null, - "score": 78, - "severity": "HIGH", - "decision_reason_code": "WALLET_HIGH_RISK", - "decision_reason_label": "Destination wallet flagged as high risk", - "vendor_data": "user-12345", - "metadata": { - "subject": { - "entity_type": "individual", - "vendor_data": "user-12345", - "full_name": "Maria Garcia", - "address": {}, - "institution_details": {}, - "device_context": {} - }, - "counterparty": {} - }, - "props": { - "order_id": "ord-9988" - }, - "tags": [], - "parties": [ - { - "uuid": "b1111111-2222-4333-8444-555555555555", - "role": "APPLICANT", - "entity_type": "individual", - "kind": "USER", - "vendor_data": "user-12345", - "full_name": "Maria Garcia", - "first_name": null, - "last_name": null, - "country_code": null, - "dob": null, - "address": {}, - "institution_info": {}, - "device": {}, - "external_party_snapshot": null - } - ], - "payment_methods": [], - "documents": [], - "activities": [], - "alerts": [ - { - "uuid": "a7b8c9d0-1234-4abc-9def-567890abcdef", - "title": "Destination wallet flagged as high risk", - "description": "Provider risk score exceeded the configured threshold.", - "severity": "HIGH", - "status": "OPEN", - "source": "RULE", - "metadata": {}, - "created_at": "2026-05-17T08:42:12Z", - "due_at": null - } - ], - "rule_runs": [], - "provider_results": [], - "travel_rule": null, - "network_snapshot": null, - "remediation": null, - "cost_breakdown": null - } - } + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "bankid_cz", + "name": "Bank iD", + "issuing_authority": "Bankovní identita, a.s." + }, + "country": "CZE", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" + } + } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, "403": { - "description": "Missing, invalid, or revoked API key, or the key cannot read this application's transactions. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "404": { - "description": "No transaction with this UUID exists for the application (deleted, never created, belongs to another application, or the path segment is not a valid UUID).", + "409": { "content": { "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Not found." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "502": { "content": { "application/json": { - "examples": { - "Throttled": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ + "x-codeSamples": [ { - "ApiKeyAuth": [] + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/bankid-cz/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"CZE\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'CZE',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/bankid-cz/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } - ] + ], + "x-didit-wallet-id": "bankid_cz" } }, - "/v3/transactions/sdk-token/": { + "/v3/id-verification/wallets/mojeid/": { "post": { - "summary": "Mint an SDK transaction token", - "description": "Mint a short-lived scoped token your app passes to the Didit SDKs so they can submit transactions directly from the end-user's device via `POST /v1/transactions/`. The token is bound to one `vendor_data` (your user id): every transaction submitted with it has its subject identity enforced server-side from that binding, so a tampered client cannot submit on behalf of another user. Mint tokens from your backend - never ship the API key itself in an app. camelCase aliases (`vendorData`, `ttlSeconds`, `maxUses`) are also accepted.", - "operationId": "createTransactionSdkToken", - "tags": [ - "Transactions" - ], - "security": [ + "operationId": "start_mojeid_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_mojeid", + "auth": "api_key", + "server": true + }, + "description": "Start a MojeID sign-in (CZ.NIC; CZE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start MojeID verification", + "parameters": [ { - "ApiKeyAuth": [] + "in": "header", + "name": "Idempotency-Key", + "schema": { + "type": "string" + }, + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], + "tags": [ + "Digital ID wallets" + ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "vendor_data": { - "type": "string", - "description": "Your internal identifier for the end user this token is scoped to. Enforced as the subject identity of every transaction submitted with the token.", - "example": "user-042" - }, - "ttl_seconds": { - "type": "integer", - "default": 900, - "maximum": 86400, - "description": "Token lifetime in seconds. Defaults to 900 (15 minutes); maximum 86400 (24 hours)." - }, - "max_uses": { - "type": "integer", - "nullable": true, - "description": "Maximum number of successful submissions allowed with this token. Omit or null for unlimited uses within the TTL." - } - }, - "required": [ - "vendor_data" - ] + "$ref": "#/components/schemas/DigitalIdWalletApiMojeidWalletVerificationStartRequest" }, "example": { - "vendor_data": "user-042", - "ttl_seconds": 900, - "max_uses": 3 + "country": "CZE", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiMojeidWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiMojeidWalletVerificationStartRequest" } } - } + }, + "required": true }, - "x-codeSamples": [ + "security": [ { - "lang": "curl", - "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/transactions/sdk-token/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"vendor_data\": \"user-042\", \"ttl_seconds\": 900, \"max_uses\": 3}'" + "ApiKeyAuth": [] }, { - "lang": "python", - "label": "Python", - "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/transactions/sdk-token/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={'vendor_data': 'user-042', 'ttl_seconds': 900, 'max_uses': 3},\n timeout=10,\n)\nresp.raise_for_status()\ntoken = resp.json()\nprint(token['sdk_token'], token['expires_at'])" + "BearerAuth": [] } ], "responses": { "201": { - "description": "The minted token. Hand `sdk_token` to your app; it authenticates the device-facing `/v1/transactions/` endpoints via the `X-Transaction-Token` header until `expires_at`.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "sdk_token": { - "type": "string", - "description": "The scoped transaction token to pass to the SDK." - }, - "expires_at": { - "type": "string", - "format": "date-time", - "description": "When the token stops being accepted." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" }, "example": { - "sdk_token": "q7Zt...urlsafe", - "expires_at": "2026-07-07T12:15:00Z" + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "mojeid", + "name": "MojeID", + "issuing_authority": "CZ.NIC" + }, + "country": "CZE", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" + } + } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" + } + } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "400": { - "description": "Validation failed: missing `vendor_data`, or `ttl_seconds` outside the allowed range.", + "502": { "content": { "application/json": { - "examples": { - "Missing vendor_data": { - "value": { - "vendor_data": [ - "This field is required." - ] - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." }, - "403": { - "description": "Missing, invalid, or revoked API key.", + "503": { "content": { "application/json": { - "examples": { - "Invalid API Key": { - "value": { - "detail": "Invalid API Key." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } - } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/mojeid/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"CZE\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'CZE',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/mojeid/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" + } + ], + "x-didit-wallet-id": "mojeid" } }, - "/v1/transactions/": { + "/v3/id-verification/wallets/diia/": { "post": { - "summary": "Submit a transaction from an SDK", - "description": "Device-facing endpoint the Didit SDKs call to submit a transaction directly from the end-user's device. Authentication is a scoped token from `POST /v3/transactions/sdk-token/` sent in the `X-Transaction-Token` header instead of an API key, and the endpoint is CORS-enabled so browsers can call it cross-origin. The payload uses the camelCase wire aliases shown below (the SDK `submitTransaction` methods build it for you). Three server-side guarantees: the application is always taken from the token; the subject identity (`externalUserId`) is always enforced from the token's `vendor_data`, so spoofed payload values are ignored; and device intelligence (the SDK device fingerprint plus server-derived IP and user agent) is attached to the subject automatically.", - "operationId": "submitTransactionFromSdk", - "tags": [ - "Transactions" - ], - "security": [ + "operationId": "start_diia_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_diia", + "auth": "api_key", + "server": true + }, + "description": "Start a Diia sign-in (Ministry of Digital Transformation of Ukraine; UKR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start Diia verification", + "parameters": [ { - "TransactionTokenAuth": [] + "in": "header", + "name": "Idempotency-Key", + "schema": { + "type": "string" + }, + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], + "tags": [ + "Digital ID wallets" + ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "txnId": { - "type": "string", - "description": "Your unique identifier for this transaction (maps to `transaction_id`, max 128 characters).", - "example": "wd-2026-07-07-0042" - }, - "txnDate": { - "type": "string", - "format": "date-time", - "description": "When the transaction occurred (maps to `transaction_at`). Defaults to now if omitted." - }, - "zoneId": { - "type": "string", - "description": "IANA time zone identifier (maps to `time_zone`), e.g. `Europe/Madrid`." - }, - "type": { - "type": "string", - "description": "Transaction category (maps to `transaction_category`), e.g. `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet`.", - "example": "travelRule" - }, - "info": { - "type": "object", - "description": "Core financial details of the transaction (maps to `transaction_details`).", - "required": [ - "direction", - "amount", - "currency" - ], - "properties": { - "direction": { - "type": "string", - "description": "Direction relative to the subject. Case-insensitive; `in`/`out` are accepted as aliases. Echoed back uppercase (`INBOUND`/`OUTBOUND`).", - "example": "out" - }, - "amount": { - "type": "number", - "description": "Transaction amount (up to 18 decimal places). A decimal string is also accepted and is the only way to preserve more than about 15 significant digits: a JSON number is parsed as a binary float first, so `0.123456789012345678` is stored as `0.12345678901234568`.", - "example": 0.25 - }, - "currency": { - "type": "string", - "description": "Currency code. Any ISO 4217 alphabetic code is accepted (e.g. `EUR`, `USD`, `NGN`), as is a crypto asset symbol (e.g. `BTC`). Prefer a currently-circulating code. Not every accepted code has an exchange rate: when none is available for the code (for example the ISO accounting unit `XSU`, or a crypto symbol Didit does not price), automatic conversion fails and `preferred_currency_amount` is returned null unless the request supplied a pre-converted `amountInDefaultCurrency`.", - "example": "ETH" - }, - "currencyType": { - "type": "string", - "description": "Whether the currency is `fiat` or `crypto` (maps to `currency_kind`)." - }, - "amountInDefaultCurrency": { - "type": "number", - "nullable": true, - "description": "Pre-converted amount in the default currency (maps to `amount_in_default_currency`). If omitted, automatic FX conversion is attempted." - }, - "defaultCurrencyCode": { - "type": "string", - "description": "The default currency code for the converted amount (maps to `default_currency`). Any currently-circulating ISO 4217 code." - }, - "paymentDetails": { - "type": "string", - "nullable": true, - "description": "Free-text payment reference or memo (maps to `payment_details`)." - }, - "paymentTxnId": { - "type": "string", - "nullable": true, - "description": "External payment reference (maps to `payment_reference_id`); the on-chain transaction hash for crypto transactions." - }, - "type": { - "type": "string", - "nullable": true, - "description": "Sub-type of the transaction (maps to `action_type`), e.g. `deposit`, `withdrawal`, `transfer`." - }, - "cryptoParams": { - "type": "object", - "description": "Crypto-specific parameters, e.g. `{\"crypto_chain\": \"ETH\"}`." - } - } - }, - "subject": { - "type": "object", - "description": "The subject (applicant) of the transaction. `externalUserId` is ignored here: the subject identity is always enforced from the token's `vendor_data`.", - "required": [ - "fullName" - ], - "properties": { - "type": { - "type": "string", - "description": "Entity type (maps to `entity_type`): `individual` (default) or `company`." - }, - "externalUserId": { - "type": "string", - "description": "Ignored on the subject - enforced server-side from the token's `vendor_data`." - }, - "fullName": { - "type": "string", - "example": "Jane Doe" - }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "dob": { - "type": "string", - "format": "date", - "description": "Date of birth (YYYY-MM-DD; maps to `date_of_birth`)." - }, - "address": { - "type": "object", - "description": "Address object with `country` (ISO 3166-1 alpha-3), `town`, `state`, `street`, `post_code`." - }, - "institutionInfo": { - "type": "object", - "description": "Institution details for institutional participants (maps to `institution_details`)." - }, - "device": { - "type": "object", - "description": "Optional explicit device context. Merged with the automatically captured device intelligence; server-derived fields win on conflict." - }, - "paymentMethod": { - "type": "object", - "description": "Payment method used by the subject (maps to `payment_method`).", - "properties": { - "type": { - "type": "string", - "description": "Payment method kind (maps to `method_type`), e.g. `bank_card`, `bank_account`, `crypto_wallet`, `ewallet`, `unhosted_wallet`, `other`." - }, - "accountId": { - "type": "string", - "description": "Account identifier (maps to `account_id`): IBAN, card fingerprint, or wallet address." - }, - "issuingCountry": { - "type": "string", - "description": "ISO 3166-1 alpha-3 country code of the issuer (maps to `issuing_country`)." - } - } - } - } - }, - "counterparty": { - "type": "object", - "description": "The counterparty of the transaction (optional). Same shape as `subject`; `externalUserId` is meaningful here.", - "properties": { - "type": { - "type": "string", - "description": "Entity type (maps to `entity_type`): `individual` or `company`." - }, - "externalUserId": { - "type": "string", - "description": "Your internal identifier for the counterparty (maps to `vendor_data`)." - }, - "fullName": { - "type": "string" - }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "dob": { - "type": "string", - "format": "date" - }, - "address": { - "type": "object" - }, - "institutionInfo": { - "type": "object" - }, - "device": { - "type": "object" - }, - "paymentMethod": { - "type": "object", - "description": "Payment method used by the counterparty (maps to `payment_method`). On travelRule transactions, `type: \"unhosted_wallet\"` declares the counterparty wallet as self-hosted: the transfer skips counterparty-VASP routing, is created directly in `UNCONFIRMED_OWNERSHIP`, and (when `auto_wallet_verification` is enabled) a wallet-ownership widget session is auto-minted and returned in `action_required` so the SDK can launch it.", - "properties": { - "type": { - "type": "string", - "description": "Payment method kind (maps to `method_type`), e.g. `bank_card`, `bank_account`, `crypto_wallet`, `ewallet`, `unhosted_wallet`, `other`. Use `unhosted_wallet` to declare a self-hosted wallet for the Travel Rule." - }, - "accountId": { - "type": "string", - "description": "Account identifier (maps to `account_id`): IBAN, card fingerprint, or wallet address." - }, - "issuingCountry": { - "type": "string" - } - } - } - } - }, - "props": { - "type": "object", - "additionalProperties": true, - "description": "Custom key-value pairs (maps to `custom_properties`). Each key can be referenced in rule conditions as `custom_values.`." - }, - "travelRule": { - "type": "object", - "description": "Travel Rule details (maps to `travel_rule_details`). When Travel Rule is enabled for the application, Didit ignores any `status` you send and drives the exchange itself.", - "required": [ - "status" - ], - "properties": { - "status": { - "type": "string" - }, - "protocol": { - "type": "string" - }, - "required": { - "type": "boolean" - }, - "obligationsCount": { - "type": "integer" - }, - "originatorData": { - "type": "object" - }, - "beneficiaryData": { - "type": "object" - }, - "metadata": { - "type": "object" - } - } - }, - "includeCryptoScreening": { - "type": "boolean", - "nullable": true, - "description": "Per-transaction override for crypto blockchain analytics screening (maps to `include_crypto_screening`)." - }, - "fingerprint_v2": { - "type": "object", - "description": "The `didit-fp-v2` device fingerprint payload. Injected automatically by the SDKs; only relevant when calling the wire API directly." - } - }, - "required": [ - "txnId", - "type", - "info", - "subject" - ] + "$ref": "#/components/schemas/DigitalIdWalletApiDiiaWalletVerificationStartRequest" }, "example": { - "txnId": "wd-2026-07-07-0042", - "type": "travelRule", - "info": { - "direction": "out", - "amount": "0.25", - "currency": "ETH", - "currencyType": "crypto", - "cryptoParams": { - "crypto_chain": "ETH" - } - }, - "subject": { - "fullName": "Jane Doe" - }, - "counterparty": { - "fullName": "Ana Diaz", - "paymentMethod": { - "type": "unhosted_wallet", - "accountId": "0xBeneficiaryWallet01" - } - }, - "travelRule": { - "status": "AWAITING_COUNTERPARTY", - "beneficiaryData": { - "wallet_address": "0xBeneficiaryWallet01", - "name": "Ana Diaz" - } + "country": "UKR", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" }, - "includeCryptoScreening": true + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiDiiaWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiDiiaWalletVerificationStartRequest" } } - } + }, + "required": true }, - "x-codeSamples": [ + "security": [ { - "lang": "curl", - "label": "curl", - "source": "curl -X POST https://verification.didit.me/v1/transactions/ \\\n -H 'X-Transaction-Token: SDK_TOKEN' -H 'Content-Type: application/json' \\\n -d '{\"txnId\": \"wd-2026-07-07-0042\", \"type\": \"travelRule\", \"info\": {\"direction\": \"out\", \"amount\": \"0.25\", \"currency\": \"ETH\", \"currencyType\": \"crypto\"}, \"subject\": {\"fullName\": \"Jane Doe\"}, \"counterparty\": {\"fullName\": \"Ana Diaz\", \"paymentMethod\": {\"type\": \"unhosted_wallet\", \"accountId\": \"0xBeneficiaryWallet01\"}}, \"travelRule\": {\"status\": \"AWAITING_COUNTERPARTY\", \"beneficiaryData\": {\"wallet_address\": \"0xBeneficiaryWallet01\", \"name\": \"Ana Diaz\"}}, \"includeCryptoScreening\": true}'" + "ApiKeyAuth": [] }, { - "lang": "javascript", - "label": "Web SDK", - "source": "import { DiditSdk } from '@didit-protocol/sdk-web';\n\nconst result = await DiditSdk.shared.submitTransaction({\n transactionToken: sdkToken,\n transaction: {\n txnId: 'wd-2026-07-07-0042',\n category: 'travelRule',\n details: { direction: 'out', amount: '0.25', currency: 'ETH', currencyType: 'crypto' },\n subject: { fullName: 'Jane Doe' },\n counterparty: { fullName: 'Ana Diaz', paymentMethod: { type: 'unhosted_wallet', accountId: '0xBeneficiaryWallet01' } },\n travelRule: { status: 'AWAITING_COUNTERPARTY', beneficiaryData: { wallet_address: '0xBeneficiaryWallet01', name: 'Ana Diaz' } },\n includeCryptoScreening: true\n },\n onActionCompleted: (refreshed) => console.log(refreshed.status)\n});" + "BearerAuth": [] } ], "responses": { "201": { - "description": "Transaction submitted and monitored. Same detail shape as `GET /v3/transactions/{transaction_id}/`, including the `action_required` block when the end user must complete a follow-up action (the SDKs auto-launch it by default).", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TransactionDetail" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" }, - "examples": { - "Wallet ownership required": { - "value": { - "uuid": "22222222-3333-4444-5555-666666666666", - "txn_id": "wd-2026-07-07-0042", - "transaction_type": "travelRule", - "direction": "OUTBOUND", - "status": "AWAITING_USER", - "amount": "0.25", - "currency": "ETH", - "travel_rule": { - "uuid": "33333333-4444-5555-6666-777777777777", - "status": "ON_HOLD", - "required": true - }, - "action_required": { - "type": "wallet_ownership", - "url": "https://verification.didit.me/wallet-ownership/3s9x...urlsafe", - "widget_session_id": "77777777-8888-9999-aaaa-bbbbbbbbbbbb", - "expires_at": "2026-07-07T13:00:00Z" - } - } - } + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "diia", + "name": "Diia", + "issuing_authority": "Ministry of Digital Transformation of Ukraine" + }, + "country": "UKR", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" }, "400": { - "description": "Validation failed (missing required fields, duplicate `txnId`, unknown category).", "content": { "application/json": { - "examples": { - "Missing fields": { - "value": { - "info": { - "currency": [ - "This field is required." - ] - } - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" } } - } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, - "401": { - "description": "The transaction token was rejected: missing, unknown, revoked, over its `max_uses`, or expired. Expired-token responses always contain the word `expired` in the detail, which is how the SDKs distinguish `expired_token` from `invalid_token` errors.", + "403": { "content": { "application/json": { - "examples": { - "Invalid token": { - "value": { - "detail": "Invalid transaction token." - } - }, - "Expired token": { - "value": { - "detail": "Transaction token expired." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } - } - } - } - }, - "/v1/transactions/{transaction_id}/": { - "get": { - "summary": "Get a transaction from an SDK", - "description": "Device-facing read endpoint, authenticated with the same `X-Transaction-Token` used to submit. The SDKs poll it after an auto-launched action flow completes or its window closes, so the final state never depends on a single notification. Returns the same transaction detail as the submit response, including the current `action_required` block (`null` once the action is resolved).", - "operationId": "getTransactionFromSdk", - "tags": [ - "Transactions" - ], - "parameters": [ - { - "name": "transaction_id", - "in": "path", - "required": true, - "description": "Didit-stable transaction UUID (the `uuid` from the submit response, **not** your `txnId`).", - "schema": { - "type": "string", - "format": "uuid" }, - "example": "22222222-3333-4444-5555-666666666666" - } - ], - "security": [ - { - "TransactionTokenAuth": [] - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v1/transactions/22222222-3333-4444-5555-666666666666/' \\\n -H 'X-Transaction-Token: SDK_TOKEN'" - } - ], - "responses": { - "200": { - "description": "The full transaction detail, including `action_required` (`null` once the pending action is resolved).", + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + }, + "409": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TransactionDetail" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "401": { - "description": "The transaction token was rejected: missing, unknown, revoked, over its `max_uses`, or expired.", + "502": { "content": { "application/json": { - "examples": { - "Expired token": { - "value": { - "detail": "Transaction token expired." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." }, - "404": { - "description": "No transaction with this UUID exists for the token's application, or the path segment is not a valid UUID.", + "503": { "content": { "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Not found." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } - } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/diia/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"UKR\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'UKR',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/diia/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" + } + ], + "x-didit-wallet-id": "diia" } }, - "/v3/businesses/": { - "get": { - "summary": "List businesses", - "description": "List [businesses](/entities/businesses) Didit assembles from KYB sessions sharing the same `vendor_data` (free-form string, NOT a UUID), plus businesses created via the API. Paginated, ordered by `last_session_at` desc. This endpoint does not support filtering or search parameters — fetch pages and filter client-side.", - "operationId": "list_businesses", - "tags": [ - "Businesses" - ], + "/v3/id-verification/wallets/franceconnect/": { + "post": { + "operationId": "start_franceconnect_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_franceconnect", + "auth": "api_key", + "server": true + }, + "description": "Start a FranceConnect sign-in (DINUM (French state); FRA) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start FranceConnect verification", "parameters": [ { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "default": 50, - "minimum": 1 - }, - "description": "Page size. Defaults to 50 when omitted. There is no enforced maximum, but keep pages small for latency." - }, - { - "name": "offset", - "in": "query", + "in": "header", + "name": "Idempotency-Key", "schema": { - "type": "integer", - "default": 0, - "minimum": 0 + "type": "string" }, - "description": "Zero-based offset of the first record to return. Prefer following the `next` URL from the previous page over computing this by hand." + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/businesses/?limit=50' \\\n -H 'x-api-key: YOUR_API_KEY'" + "tags": [ + "Digital ID wallets" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiFranceconnectWalletVerificationStartRequest" + }, + "example": { + "country": "FRA", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiFranceconnectWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiFranceconnectWalletVerificationStartRequest" + } + } }, + "required": true + }, + "security": [ { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/businesses/',\n headers={'x-api-key': 'YOUR_API_KEY'},\n params={'limit': 50},\n)\nresp.raise_for_status()\npage = resp.json()\nfor biz in page['results']:\n print(biz['vendor_data'], biz['status'])" + "ApiKeyAuth": [] }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const url = new URL('https://verification.didit.me/v3/businesses/');\nurl.searchParams.set('limit', '50');\n\nconst page = await fetch(url, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n}).then((r) => r.json());\nconsole.log(page.count, page.results.length);" + "BearerAuth": [] } ], "responses": { - "200": { - "description": "Paginated list of businesses with status, session counts, and feature breakdown.", + "201": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "count": { - "type": "integer", - "description": "Number of matching rows — exact up to 100, capped at 100 beyond that for performance. Do not use it to detect the last page; rely on `next` being `null` instead." - }, - "next": { - "type": "string", - "nullable": true, - "description": "Absolute URL of the next page, or `null` on the last page." + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + }, + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "franceconnect", + "name": "FranceConnect", + "issuing_authority": "DINUM (French state)" }, - "previous": { - "type": "string", - "nullable": true, - "description": "Absolute URL of the previous page, or `null` on the first page." + "country": "FRA", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BusinessListItem" - } - } - } - }, - "examples": { - "Example": { - "value": { - "count": 1, - "next": null, - "previous": null, - "results": [ - { - "didit_internal_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", - "vendor_data": "company-123", - "display_name": null, - "legal_name": "Acme Trading Ltd", - "registration_number": "12345678", - "country_code": "GB", - "effective_name": "Acme Trading Ltd", - "status": "ACTIVE", - "session_count": 2, - "approved_count": 1, - "declined_count": 0, - "in_review_count": 1, - "features": { - "KYB": "Approved" - }, - "features_list": [ - { - "feature": "KYB", - "status": "Approved" - } - ], - "last_session_at": "2026-03-15T10:30:00Z", - "last_activity_at": "2026-03-15T10:30:00Z", - "first_session_at": "2026-01-10T08:00:00Z", - "tags": [ - { - "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", - "name": "VIP", - "color": "#2567FF" - } - ], - "created_at": "2026-01-10T08:00:00Z" - } - ] - } - } + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" }, - "403": { - "description": "Missing, invalid, or revoked API key, or the key cannot list businesses for this application. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", + "400": { "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" } } - } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "403": { "content": { "application/json": { - "examples": { - "Rate limited": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - } - }, - "/v3/customization/": { - "get": { - "summary": "Get branding customization", - "description": "Returns the white-label branding for the application resolved from the API key: colors, fonts, border radii, logo URLs and UI/UX options. Custom domain and white-label email settings are managed only in the Business Console and are not part of this response.", - "operationId": "get_customization", - "tags": [ - "Customization" - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/customization/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/customization/',\n headers={'x-api-key': 'YOUR_API_KEY'},\n)\nresp.raise_for_status()\nbranding = resp.json()\nprint(branding['font_family'], branding['color_primary'])" + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const branding = await fetch(\n 'https://verification.didit.me/v3/customization/',\n { headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n).then((r) => r.json());\nconsole.log(branding.font_family, branding.color_primary);" - } - ], - "responses": { - "200": { - "description": "Current branding customization for the application.", + "502": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BrandingCustomization" - }, - "examples": { - "Default branding": { - "value": { - "color_primary": "#000000", - "color_secondary": "#9DA1A1", - "color_background": "#FFFFFF", - "color_panel": "#2567FF", - "color_panel_10": "#1A1A1A", - "color_on_panel_1": "#9DA1A1", - "color_on_panel_2": "#F4F4F6", - "color_on_background": "#000000", - "color_button_1": "#2567FF", - "color_button_2": "#F8F8F8", - "color_button_text_1": "#FFFFFF", - "color_button_text_2": "#000000", - "color_pill_text": "#111827", - "border_radius_panel": 10, - "border_radius_buttons": 12, - "font_family": "Inter", - "font_weight": "regular", - "font_url": "https://fonts.googleapis.com/css2?family=Inter", - "logo_square": null, - "logo_rectangular": null, - "favicon": null, - "app_public_name": "Acme Verify", - "privacy_policy_url": "https://acme.com/privacy", - "skip_welcome_screen": false, - "disable_login_with_didit": false, - "hide_progress_bar": false, - "callback_seconds": 3 - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." }, - "403": { - "description": "API key missing, malformed, expired, or scoped to a different application. Didit returns 403 (never 401) for all authentication failures on this endpoint.", + "503": { "content": { "application/json": { - "examples": { - "Invalid token": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/franceconnect/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"FRA\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" }, - "429": { - "description": "Rate limit exceeded. Retry with exponential backoff." + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'FRA',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/franceconnect/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } + ], + "x-didit-wallet-id": "franceconnect" + } + }, + "/v3/id-verification/wallets/audkenni/": { + "post": { + "operationId": "start_audkenni_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_audkenni", + "auth": "api_key", + "server": true }, - "security": [ + "description": "Start a Auðkenni sign-in (Auðkenni (Icelandic electronic ID); ISL) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start Auðkenni verification", + "parameters": [ { - "ApiKeyAuth": [] + "in": "header", + "name": "Idempotency-Key", + "schema": { + "type": "string" + }, + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } - ] - }, - "patch": { - "summary": "Update branding customization", - "description": "Partial update of the application's white-label branding. Send `application/json` to change colors, fonts, radii and UI options, or `multipart/form-data` to upload logo images. Only the fields you include are changed. Custom domain and white-label email cannot be set through this endpoint — use the Business Console for those.", - "operationId": "update_customization", + ], "tags": [ - "Customization" + "Digital ID wallets" ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BrandingCustomizationUpdate" + "$ref": "#/components/schemas/DigitalIdWalletApiAudkenniWalletVerificationStartRequest" }, - "examples": { - "Update colors and font": { - "summary": "Change branding colors and font", - "value": { - "color_primary": "#123456", - "color_button_1": "#2567FF", - "font_family": "AR One Sans", - "border_radius_buttons": 16 - } - }, - "Update UI options": { - "summary": "Change public name and flow options", - "value": { - "app_public_name": "Acme Verify", - "privacy_policy_url": "https://acme.com/privacy", - "skip_welcome_screen": true, - "hide_progress_bar": true, - "callback_seconds": 5 - } + "example": { + "country": "ISL", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" }, - "Remove a logo": { - "summary": "Delete the current rectangular logo", - "value": { - "delete_logo_rectangular": true - } - } + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiAudkenniWalletVerificationStartRequest" } }, "multipart/form-data": { "schema": { - "type": "object", - "description": "Use multipart/form-data only to upload logo images. Branding fields above may also be sent as form fields in the same request.", - "properties": { - "image_square": { - "type": "string", - "format": "binary", - "description": "Square logo (1:1 aspect ratio). Max **1 MB**. Allowed: `png`, `jpg`, `jpeg`, `webp`." - }, - "image_rectangular": { - "type": "string", - "format": "binary", - "description": "Rectangular logo (2:1 aspect ratio). Max **1 MB**. Allowed: `png`, `jpg`, `jpeg`, `webp`." - }, - "image_favicon": { - "type": "string", - "format": "binary", - "description": "Favicon image. Max **500 KB**." - } - } - }, - "example": { - "image_rectangular": "(binary PNG logo, 2:1)", - "image_favicon": "(binary favicon)" + "$ref": "#/components/schemas/DigitalIdWalletApiAudkenniWalletVerificationStartRequest" } } - } + }, + "required": true }, - "x-codeSamples": [ + "security": [ { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH 'https://verification.didit.me/v3/customization/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"color_primary\": \"#123456\", \"font_family\": \"AR One Sans\"}'" + "ApiKeyAuth": [] }, { - "lang": "curl", - "label": "curl (logo upload)", - "source": "curl -X PATCH 'https://verification.didit.me/v3/customization/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'image_rectangular=@logo.png' \\\n -F 'image_favicon=@favicon.ico'" + "BearerAuth": [] + } + ], + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + }, + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "audkenni", + "name": "Auðkenni", + "issuing_authority": "Auðkenni (Icelandic electronic ID)" + }, + "country": "ISL", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" + } + } + }, + "description": "" }, - { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.patch(\n 'https://verification.didit.me/v3/customization/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'color_primary': '#123456', 'font_family': 'AR One Sans'},\n)\nresp.raise_for_status()\nprint(resp.json()['color_primary'])\n\n# Upload a logo (multipart):\nwith open('logo.png', 'rb') as logo:\n requests.patch(\n 'https://verification.didit.me/v3/customization/',\n headers={'x-api-key': 'YOUR_API_KEY'},\n files={'image_rectangular': logo},\n )" + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" + } + } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/customization/', {\n method: 'PATCH',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ color_primary: '#123456', font_family: 'AR One Sans' }),\n});\nconst branding = await resp.json();\nconsole.log(branding.color_primary);" - } - ], - "responses": { - "200": { - "description": "Branding updated. The FULL branding object is returned (all fields, not just the ones you changed).", + "403": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BrandingCustomization" - }, - "examples": { - "Updated": { - "value": { - "color_primary": "#123456", - "color_secondary": "#9DA1A1", - "color_background": "#FFFFFF", - "color_panel": "#2567FF", - "color_panel_10": "#1A1A1A", - "color_on_panel_1": "#9DA1A1", - "color_on_panel_2": "#F4F4F6", - "color_on_background": "#000000", - "color_button_1": "#2567FF", - "color_button_2": "#F8F8F8", - "color_button_text_1": "#FFFFFF", - "color_button_text_2": "#000000", - "color_pill_text": "#111827", - "border_radius_panel": 10, - "border_radius_buttons": 16, - "font_family": "AR One Sans", - "font_weight": "400", - "font_url": "https://fonts.googleapis.com/css2?family=AR+One+Sans", - "logo_square": null, - "logo_rectangular": "https://cdn.didit.me/white_label/logo_rectangular/...", - "favicon": null, - "app_public_name": "Acme Verify", - "privacy_policy_url": "https://acme.com/privacy", - "skip_welcome_screen": true, - "disable_login_with_didit": false, - "hide_progress_bar": false, - "callback_seconds": 3 - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "400": { - "description": "Invalid body — e.g. malformed hex color, unknown font family or weight, a logo failing the size/aspect-ratio check, or a Business-Console-only field (`domain`, `from_email`, `verify_domain`, `force_remove_domain`, `verify_email_domain`, `force_remove_email_domain`, and their read-only companions). Also returned when setting `disable_login_with_didit` to `false` while a custom domain is configured.", + "409": { "content": { "application/json": { - "examples": { - "Bad color": { - "value": { - "color_primary": [ - "Invalid color format. Use '#RRGGBB' or '#RRGGBBAA' format." - ] - } - }, - "Bad logo": { - "value": { - "image_rectangular": [ - "Image aspect ratio must be 2:1. Got 1024x1024 image dimensions" - ] - } - }, - "Console-only field": { - "value": { - "domain": [ - "Manage this field in the Business Console." - ] - } - }, - "Bad font weight": { - "value": { - "font_weight": [ - "Font weight 950 is not available for Inter. Available weights are: 100, 200, 300, 400, 500, 600, 700, 800, 900" - ] - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "403": { - "description": "API key missing, malformed, expired, or scoped to a different application. Didit returns 403 (never 401) for all authentication failures on this endpoint.", + "502": { "content": { "application/json": { - "examples": { - "Invalid token": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." }, - "429": { - "description": "Rate limit exceeded. Retry with exponential backoff." + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ + "x-codeSamples": [ { - "ApiKeyAuth": [] + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/audkenni/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"ISL\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'ISL',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/audkenni/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } - ] + ], + "x-didit-wallet-id": "audkenni" } }, - "/v3/businesses/{vendor_data}/": { - "get": { - "summary": "Get business", - "description": "Fetch one [business](/entities/businesses) by `vendor_data` (exact match). Adds `metadata` and `updated_at` to the list view. Deleted businesses return 404.", - "operationId": "get_business", - "tags": [ - "Businesses" - ], + "/v3/id-verification/wallets/connectid/": { + "post": { + "operationId": "start_connectid_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_connectid", + "auth": "api_key", + "server": true + }, + "description": "Start a ConnectID sign-in (Australian Payments Plus; AUS) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start ConnectID verification", "parameters": [ { - "name": "vendor_data", - "in": "path", - "required": true, + "in": "header", + "name": "Idempotency-Key", "schema": { "type": "string" }, - "description": "Your unique identifier for the business — a free-form string (NOT a UUID), matched exactly as sent.", - "example": "company-123" + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/businesses/company-123/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "tags": [ + "Digital ID wallets" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiConnectidWalletVerificationStartRequest" + }, + "example": { + "country": "AUS", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiConnectidWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiConnectidWalletVerificationStartRequest" + } + } }, + "required": true + }, + "security": [ { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/businesses/company-123/',\n headers={'x-api-key': 'YOUR_API_KEY'},\n)\nresp.raise_for_status()\nbiz = resp.json()\nprint(biz['didit_internal_id'], biz['status'])" + "ApiKeyAuth": [] }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const vendorData = encodeURIComponent('company-123');\nconst biz = await fetch(\n `https://verification.didit.me/v3/businesses/${vendorData}/`,\n { headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n).then((r) => r.json());\nconsole.log(biz.didit_internal_id, biz.status);" + "BearerAuth": [] } ], "responses": { - "200": { - "description": "Full business detail including metadata and aggregated session data.", + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BusinessDetailItem" + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" }, - "examples": { - "Active business": { - "value": { - "didit_internal_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", - "vendor_data": "company-123", - "display_name": null, - "legal_name": "Acme Trading Ltd", - "registration_number": "12345678", - "country_code": "GB", - "effective_name": "Acme Trading Ltd", - "status": "ACTIVE", - "session_count": 2, - "approved_count": 1, - "declined_count": 0, - "in_review_count": 1, - "features": { - "KYB": "Approved" - }, - "features_list": [ - { - "feature": "KYB", - "status": "Approved" - } - ], - "last_session_at": "2026-03-15T10:30:00Z", - "last_activity_at": "2026-03-15T10:30:00Z", - "first_session_at": "2026-01-10T08:00:00Z", - "tags": [ - { - "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", - "name": "VIP", - "color": "#2567FF" - } - ], - "created_at": "2026-01-10T08:00:00Z", - "metadata": { - "industry": "fintech" - }, - "updated_at": "2026-03-15T10:30:00Z" - } - } + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "connectid", + "name": "ConnectID", + "issuing_authority": "Australian Payments Plus" + }, + "country": "AUS", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" } } - } + }, + "description": "" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" + } + } + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, "403": { - "description": "Missing, invalid, or revoked API key, or the key cannot read this application's businesses. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "404": { - "description": "No (non-deleted) business with the supplied `vendor_data` exists for this application.", + "409": { "content": { "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Not found." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "502": { "content": { "application/json": { - "examples": { - "Rate limited": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." } }, - "security": [ + "x-codeSamples": [ { - "ApiKeyAuth": [] + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/connectid/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"AUS\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'AUS',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/connectid/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" } - ] - }, - "patch": { - "summary": "Update business", - "description": "Partial update of a business: `display_name`, `legal_name`, `registration_number`, `country_code`, `status`, `metadata`. `metadata` fully replaces. Setting `status` to `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry.", - "operationId": "update_business", - "tags": [ - "Businesses" ], + "x-didit-wallet-id": "connectid" + } + }, + "/v3/id-verification/wallets/eudi/": { + "post": { + "operationId": "start_eudi_wallet_verification", + "x-didit-sdk": { + "resource": "id_wallets", + "method": "start_eudi", + "auth": "api_key", + "server": true + }, + "description": "Start a EUDI Wallet sign-in (The user's own member state; the issuer differs per country; AUT, BEL, BGR, HRV, CYP, CZE, DNK, EST, FIN, FRA, DEU, GRC, HUN, IRL, ITA, LVA, LTU, LUX, MLT, NLD, POL, PRT, ROU, SVK, SVN, ESP, SWE, ISL, LIE, NOR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", + "summary": "Start EUDI Wallet verification", "parameters": [ { - "name": "vendor_data", - "in": "path", - "required": true, + "in": "header", + "name": "Idempotency-Key", "schema": { "type": "string" }, - "description": "Your unique identifier for the business (free-form string, NOT a UUID).", - "example": "company-123" + "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." } ], + "tags": [ + "Digital ID wallets" + ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "type": "object", - "description": "Partial-update body. Provide only the fields you want to change.", - "properties": { - "display_name": { - "type": "string", - "nullable": true, - "description": "Friendly display name shown in the console (takes precedence over `legal_name` for UI display)." - }, - "legal_name": { - "type": "string", - "nullable": true, - "description": "Official legal name from registry or manual entry." - }, - "registration_number": { - "type": "string", - "nullable": true, - "description": "Company registration or incorporation number." - }, - "country_code": { - "type": "string", - "nullable": true, - "description": "Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `US`)." - }, - "status": { - "type": "string", - "enum": [ - "ACTIVE", - "FLAGGED", - "BLOCKED" - ], - "description": "Business lifecycle status. Setting `BLOCKED` automatically adds the `vendor_data` to the system blocklist; flipping away from `BLOCKED` removes that blocklist entry." - }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Arbitrary JSON object you attach to the business. **Fully replaces** the existing metadata on update — merge client-side if you only want to add keys." - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiEudiWalletVerificationStartRequest" }, - "examples": { - "Update name": { - "summary": "Update display name", - "value": { - "display_name": "Acme Corp" - } - }, - "Flag for review": { - "summary": "Flip status", - "value": { - "status": "FLAGGED" - } + "example": { + "country": "AUT", + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" }, - "Add metadata": { - "summary": "Replace metadata JSON", - "value": { - "metadata": { - "industry": "fintech", - "risk_level": "low" - } - } + "return_url": "https://app.example.com/identity/complete" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiEudiWalletVerificationStartRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiEudiWalletVerificationStartRequest" + } + } + }, + "required": true + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" }, - "Fix registry data": { - "summary": "Correct extracted fields", - "value": { - "legal_name": "Acme Trading International Ltd", - "registration_number": "12345678", - "country_code": "GB" - } + "example": { + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "eudi", + "name": "EUDI Wallet", + "issuing_authority": "The user's own member state; the issuer differs per country" + }, + "country": "AUT", + "status": "Not Finished", + "outcome": "pending", + "error": null, + "interaction": "redirect", + "simulated": false, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize" + }, + "identity": null, + "attributes": null, + "level_of_assurance": null, + "verified_at": null, + "signature_valid": null, + "credential_type": "person_identification" + }, + "vendor_data": "your-customer-reference", + "metadata": { + "reference": "example" + }, + "created_at": "2026-01-01T10:00:00Z" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" } } - } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH 'https://verification.didit.me/v3/businesses/company-123/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"display_name\": \"Acme Corp\", \"status\": \"FLAGGED\"}'" - }, - { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.patch(\n 'https://verification.didit.me/v3/businesses/company-123/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'display_name': 'Acme Corp', 'status': 'FLAGGED'},\n)\nresp.raise_for_status()\nprint(resp.json()['status'])" + }, + "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/businesses/company-123/', {\n method: 'PATCH',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ display_name: 'Acme Corp', status: 'FLAGGED' }),\n});\nconst biz = await resp.json();\nconsole.log(biz.status);" - } - ], - "responses": { - "200": { - "description": "Business updated. The full updated business record is returned.", + "403": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BusinessDetailItem" - }, - "examples": { - "Updated": { - "value": { - "didit_internal_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", - "vendor_data": "company-123", - "display_name": "Acme Corp", - "legal_name": "Acme Trading Ltd", - "registration_number": "12345678", - "country_code": "GB", - "effective_name": "Acme Corp", - "status": "FLAGGED", - "session_count": 2, - "approved_count": 1, - "declined_count": 0, - "in_review_count": 1, - "features": { - "KYB": "Approved" - }, - "features_list": [ - { - "feature": "KYB", - "status": "Approved" - } - ], - "last_session_at": "2026-03-15T10:30:00Z", - "last_activity_at": "2026-03-15T10:30:00Z", - "first_session_at": "2026-01-10T08:00:00Z", - "tags": [ - { - "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", - "name": "VIP", - "color": "#2567FF" - } - ], - "created_at": "2026-01-10T08:00:00Z", - "metadata": { - "industry": "fintech", - "risk_level": "low" - }, - "updated_at": "2026-03-15T10:30:00Z" - } - } + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" } } - } + }, + "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." }, - "400": { - "description": "Invalid body — typically an unknown `status` or malformed `metadata`.", + "409": { "content": { "application/json": { - "examples": { - "Bad status": { - "value": { - "status": [ - "Invalid status. Valid options are: ACTIVE, FLAGGED, BLOCKED" - ] - } - }, - "Bad metadata": { - "value": { - "metadata": [ - "Metadata must be a JSON object." - ] - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." }, - "403": { - "description": "Missing, invalid, or revoked API key, or the key cannot update businesses for this application. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", + "502": { "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } - } + }, + "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." }, - "404": { - "description": "No (non-deleted) business with the supplied `vendor_data` exists for this application.", + "503": { "content": { "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Not found." - } - } + "schema": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" } } + }, + "description": "error wallet_not_configured: this environment cannot reach the wallet." + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "cURL", + "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/eudi/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"AUT\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'AUT',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/eudi/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" + } + ], + "x-didit-wallet-id": "eudi" + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/teams/": { + "get": { + "summary": "List review teams", + "description": "Requires `list:cases` for the requested application.", + "operationId": "list_case_teams", + "x-didit-sdk": { + "resource": "cases.teams", + "method": "list", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Case Assignment" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 } }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 1000000, + "default": 0 + } + } + ], + "responses": { + "200": { + "description": "Success", "content": { "application/json": { - "examples": { - "Rate limited": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseReviewTeam" + } + }, + "count": { + "type": "integer", + "description": "Matching count, capped at 100. Follow next to continue beyond the cap." + }, + "next": { + "type": "string", + "nullable": true + }, + "previous": { + "type": "string", + "nullable": true } } } } } + }, + "400": { + "description": "Invalid input." + }, + "403": { + "description": "Insufficient permission." } }, "security": [ @@ -31006,206 +50903,230 @@ "ApiKeyAuth": [] } ] - } - }, - "/v3/businesses/{vendor_data}/update-status/": { - "patch": { - "summary": "Update business status", - "description": "Flip only the lifecycle `status` of a business — VendorBusinessStatusChoices (`ACTIVE`/`FLAGGED`/`BLOCKED`, NOT session statuses). `BLOCKED` adds `vendor_data` to the system blocklist.", - "operationId": "update_business_status", + }, + "post": { + "summary": "Create review team", + "description": "Requires `write:cases` for the requested application.", + "operationId": "create_case_teams", + "x-didit-sdk": { + "resource": "cases.teams", + "method": "create", + "auth": "api_key", + "server": true, + "scope": "application" + }, "tags": [ - "Businesses" + "Case Assignment" ], "parameters": [ { - "name": "vendor_data", + "name": "organization_id", "in": "path", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" }, - "description": "Your unique identifier for the business (free-form string, NOT a UUID).", - "example": "company-123" + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." } ], + "responses": { + "201": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseReviewTeam" + } + } + } + }, + "400": { + "description": "Invalid input." + }, + "403": { + "description": "Insufficient permission." + } + }, "requestBody": { "required": true, "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "status" - ], - "properties": { - "status": { - "type": "string", - "enum": [ - "ACTIVE", - "FLAGGED", - "BLOCKED" - ], - "description": "New lifecycle status. `BLOCKED` also adds the `vendor_data` to the system blocklist." - } - } - }, - "examples": { - "Flag": { - "summary": "Flag the business for review", - "value": { - "status": "FLAGGED" - } - }, - "Block": { - "summary": "Block the business (adds to system blocklist)", - "value": { - "status": "BLOCKED" - } - }, - "Reactivate": { - "summary": "Reactivate the business", - "value": { - "status": "ACTIVE" - } - } + "$ref": "#/components/schemas/CaseReviewTeamCreate" } } } }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH 'https://verification.didit.me/v3/businesses/company-123/update-status/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"status\": \"BLOCKED\"}'" - }, + "security": [ { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.patch(\n 'https://verification.didit.me/v3/businesses/company-123/update-status/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'status': 'BLOCKED'},\n)\nresp.raise_for_status()\nprint(resp.json()['status'])" - }, + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/fraud/checks/{check_id}/": { + "get": { + "operationId": "root_retrieve", + "x-didit-sdk": { + "resource": "fraud_checks", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "description": "Fetch a previously created check by its `check_id`.", + "summary": "Retrieve a Fraud Check", + "parameters": [ { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/businesses/company-123/update-status/', {\n method: 'PATCH',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ status: 'BLOCKED' }),\n});\nconst biz = await resp.json();\nconsole.log(biz.status);" + "in": "path", + "name": "check_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true } ], "responses": { "200": { - "description": "Business status updated. Full business record returned.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BusinessDetailItem" - }, - "examples": { - "Blocked": { - "value": { - "didit_internal_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", - "vendor_data": "company-123", - "display_name": null, - "legal_name": "Acme Trading Ltd", - "registration_number": "12345678", - "country_code": "GB", - "effective_name": "Acme Trading Ltd", - "status": "BLOCKED", - "session_count": 2, - "approved_count": 1, - "declined_count": 0, - "in_review_count": 1, - "features": { - "KYB": "Approved" - }, - "features_list": [ - { - "feature": "KYB", - "status": "Approved" - } - ], - "last_session_at": "2026-03-15T10:30:00Z", - "last_activity_at": "2026-03-15T10:30:00Z", - "first_session_at": "2026-01-10T08:00:00Z", - "tags": [ - { - "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", - "name": "VIP", - "color": "#2567FF" - } - ], - "created_at": "2026-01-10T08:00:00Z", - "metadata": { - "industry": "fintech" - }, - "updated_at": "2026-03-15T10:30:00Z" - } - } + "$ref": "#/components/schemas/FraudCheckResponse" } } - } + }, + "description": "" }, - "400": { - "description": "Missing or invalid `status`.", + "401": { "content": { "application/json": { - "examples": { - "Missing status": { - "value": { - "status": [ - "This field is required." - ] - } - }, - "Bad status": { - "value": { - "status": [ - "Invalid status. Valid options are: ACTIVE, FLAGGED, BLOCKED" - ] - } - } + "schema": { + "$ref": "#/components/schemas/FraudClientCredentialsErrorMessage" } } - } + }, + "description": "" }, "403": { - "description": "Missing, invalid, or revoked API key, or the key cannot update businesses for this application. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/FraudClientCredentialsForbiddenErrorMessage" } } + }, + "description": "" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ], + "tags": [ + "Fraud Check" + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/": { + "get": { + "operationId": "organization_application_cases_requests_list", + "x-didit-sdk": { + "resource": "cases.requests", + "method": "list", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "case_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "name": "ordering", + "required": false, + "in": "query", + "description": "Which field to use when ordering the results.", + "schema": { + "type": "string" } }, - "404": { - "description": "No (non-deleted) business with the supplied `vendor_data` exists for this application.", - "content": { - "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Not found." - } - } - } - } + { + "name": "limit", + "required": false, + "in": "query", + "description": "Number of results to return per page.", + "schema": { + "type": "integer" } }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + { + "name": "offset", + "required": false, + "in": "query", + "description": "The initial index from which to return the results.", + "schema": { + "type": "integer" + } + } + ], + "tags": [ + "Case Management" + ], + "responses": { + "200": { "content": { "application/json": { - "examples": { - "Rate limited": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/CustomerMessagingPaginatedCaseRequestList" } } - } + }, + "description": "" + }, + "404": { + "description": "The request or case is unavailable for this caller." + }, + "403": { + "description": "The caller lacks the required cases permission." } }, "security": [ @@ -31213,1408 +51134,989 @@ "ApiKeyAuth": [] } ] - } - }, - "/v3/businesses/delete/": { + }, "post": { - "summary": "Batch delete businesses", - "description": "**Permanently delete** businesses — this cannot be undone. For blocking prefer [Update Business Status](#patch-/v3/businesses/-vendor_data-/update-status/) with `BLOCKED`.", - "operationId": "batch_delete_businesses", - "tags": [ - "Businesses" - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "description": "Provide `vendor_data_list`, `didit_internal_id_list`, or `delete_all: true`. Precedence when several are sent: `delete_all` > `vendor_data_list` > `didit_internal_id_list` (only the highest-precedence selector is used).", - "properties": { - "vendor_data_list": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Your own identifiers to delete, matched exactly as sent." - }, - "didit_internal_id_list": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - }, - "description": "Didit's internal UUIDs (`didit_internal_id`) to delete." - }, - "delete_all": { - "type": "boolean", - "default": false, - "description": "If `true`, deletes every business in the application. Cannot be combined with the list selectors." - } - } - }, - "examples": { - "Specific businesses": { - "summary": "Delete by vendor_data", - "value": { - "vendor_data_list": [ - "company-123", - "company-456" - ] - } - }, - "Specific internal IDs": { - "summary": "Delete by didit_internal_id", - "value": { - "didit_internal_id_list": [ - "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "b2c3d4e5-f6a7-8901-bcde-f12345678901" - ] - } - }, - "All businesses": { - "summary": "Wipe every business in this application", - "value": { - "delete_all": true - } - } - } - } - } + "operationId": "organization_application_cases_requests_create", + "x-didit-sdk": { + "resource": "cases.requests", + "method": "create", + "auth": "api_key", + "server": true, + "scope": "application" }, - "x-codeSamples": [ + "parameters": [ { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/businesses/delete/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"vendor_data_list\": [\"company-123\", \"company-456\"]}'" + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true }, { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/businesses/delete/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'vendor_data_list': ['company-123', 'company-456']},\n)\nresp.raise_for_status()\nprint('deleted', resp.json()['deleted'])" + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/businesses/delete/', {\n method: 'POST',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ vendor_data_list: ['company-123', 'company-456'] }),\n});\nconst body = await resp.json();\nconsole.log('deleted', body.deleted);" + "in": "path", + "name": "case_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true } ], - "responses": { - "200": { - "description": "Businesses deleted. Returns the number of rows actually removed.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "deleted": { - "type": "integer", - "description": "Number of businesses deleted (excludes vendor_data values that didn't match anything)." - } - } - }, - "examples": { - "Deleted some": { - "value": { - "deleted": 2 - } - }, - "Nothing matched": { - "value": { - "deleted": 0 - } - } - } + "tags": [ + "Case Management" + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CustomerMessagingCaseRequestConfirmRequest" } - } - }, - "400": { - "description": "No selector supplied (all of `vendor_data_list`, `didit_internal_id_list`, and `delete_all` missing or empty). The error body is a JSON array.", - "content": { - "application/json": { - "examples": { - "Missing selector": { - "value": [ - "Provide vendor_data_list, didit_internal_id_list, or set delete_all=true." - ] - } - } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerMessagingCaseRequestConfirmRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CustomerMessagingCaseRequestConfirmRequest" } } }, + "required": true + }, + "responses": { + "404": { + "description": "The request or case is unavailable for this caller." + }, "403": { - "description": "Missing, invalid, or revoked API key, or the key cannot delete businesses for this application. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", - "content": { - "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } - } - } - } + "description": "The caller lacks the required cases permission." }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + "400": { + "description": "Invalid inputs, unsupported purpose, unavailable verified contact or an existing immutable response." + }, + "409": { + "description": "The preview changed or expired. Preview again before confirming." + }, + "201": { "content": { "application/json": { - "examples": { - "Rate limited": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/CustomerMessagingCaseRequest" } } - } + }, + "description": "" } }, "security": [ { "ApiKeyAuth": [] } - ] + ], + "description": "Confirm the exact signed preview within 15 minutes. Only fixed templates and an OTP-verified contact belonging to the case subject are accepted. The request ID makes retries idempotent. Delivery is queued; SENT, SIMULATED and UNKNOWN are distinct outcomes. An actionable request sets the case to Awaiting user. A review update leaves its status unchanged." } }, - "/v3/users/aml-monitoring/": { + "/v3/fraud/checks/": { "post": { - "summary": "Enable or disable AML ongoing monitoring for users", - "description": "Turns AML ongoing monitoring on or off for the selected users, with the same semantics as the Business Console. Exactly one monitored AML screening is kept per user, so a profile that verified several times is monitored and billed once; disabling turns monitoring off across all of the profile's screenings. Users without any AML screening are screened first (billed as a regular AML screening) when they have the full name, date of birth and country needed; otherwise they are reported as `skipped_no_identity_data`.\n\nSet `estimate_only: true` to preview counts and costs without writing anything. Selections of 20 profiles or fewer resolve inline and return `200` with the completed job; larger selections return `202` and run in the background - poll [Get AML monitoring job](#get-/v3/aml-monitoring/jobs/-job_id-/) with the returned `job_id`. Enabling is refused with `403` when the organization balance does not cover the estimate. Identifiers that match nothing are listed in `unmatched` and never fail the request: a selection that matches nothing completes as an empty job (`total` 0), the same convention as the batch delete endpoints.", - "operationId": "toggle_users_aml_monitoring", - "tags": [ - "AML Ongoing Monitoring" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], + "operationId": "create_fraud_check", + "x-didit-sdk": { + "resource": "fraud_checks", + "method": "create", + "auth": "api_key", + "server": true + }, + "description": "Submit one or more typed identifiers and receive normalization, fresh enrichment, authoritative validation where the country and identifier type are covered, independent risk dimensions and a recommendation in one response.\n\nThese profiles support identifier checks. Selfie-based identity binding (`biometric_identity`) requires profile version `2026-09-14.2` and a Brazilian CPF; it runs only with the user's consent and when your organization is entitled to it. Earlier versions report it as `not_available_in_profile_version`. Modules that cannot run are reported as skipped with a deterministic reason; a source that could not answer is never reported as an identity mismatch.", + "summary": "Run a Fraud Check", "requestBody": { - "required": true, "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/FraudCheckRequestRequest" + } + }, "application/json": { "schema": { - "type": "object", - "required": [ - "is_enabled" - ], - "description": "Select users with `vendor_data_list`, `didit_internal_id_list`, or both (the two lists are OR'd). At most 1000 identifiers per request across both lists.", - "properties": { - "is_enabled": { - "type": "boolean", - "description": "true to enable monitoring, false to disable it." - }, - "estimate_only": { - "type": "boolean", - "default": false, - "description": "When true, returns the cost estimate and performs no writes - no screening, no billing, no job." - }, - "vendor_data_list": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Your own identifiers of the users to toggle, matched exactly as sent." - }, - "didit_internal_id_list": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - }, - "description": "Didit internal ids of the users to toggle - the `didit_internal_id` returned by the create, get and list endpoints." - } - } - }, - "examples": { - "Estimate first": { - "summary": "Preview the cost without writing", - "value": { - "is_enabled": true, - "estimate_only": true, - "vendor_data_list": [ - "user-123", - "user-456" - ] - } - }, - "Enable": { - "summary": "Enable monitoring for two users", - "value": { - "is_enabled": true, - "vendor_data_list": [ - "user-123", - "user-456" - ] - } - }, - "Disable": { - "summary": "Disable monitoring by Didit internal id", - "value": { - "is_enabled": false, - "didit_internal_id_list": [ - "3f1c0d3e-8f0a-4c2e-9a3d-9b1f5f4a2c11" - ] - } - } + "$ref": "#/components/schemas/FraudCheckRequestRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/FraudCheckRequestRequest" } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/users/aml-monitoring/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"is_enabled\": true, \"vendor_data_list\": [\"user-123\", \"user-456\"]}'" - }, - { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/users/aml-monitoring/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'is_enabled': True, 'vendor_data_list': ['user-123', 'user-456']},\n)\nresp.raise_for_status()\njob = resp.json()\nprint(job['status'], job['outcomes'])\nfor row in job['results']:\n print(row['vendor_data'], row['outcome'])" }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/users/aml-monitoring/', {\n method: 'POST',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ is_enabled: true, vendor_data_list: ['user-123', 'user-456'] }),\n});\nconst job = await resp.json();\nconsole.log(job.status, job.outcomes);\nfor (const row of job.results) console.log(row.vendor_data, row.outcome);" - } - ], + "required": true + }, "responses": { "200": { - "description": "The estimate (when `estimate_only` is true) or the completed job for a selection processed inline.", "content": { "application/json": { "schema": { - "oneOf": [ - { - "type": "object", - "required": [ - "users_total", - "already_enabled", - "with_existing_aml", - "needing_screening", - "skipped_no_identity_data", - "skipped_blocked", - "estimated_screening_cost_usd", - "estimated_annual_monitoring_cost_usd", - "balance_sufficient", - "unmatched" - ], - "description": "Cost preview. Returned directly when estimate_only is true (nothing is written).", - "properties": { - "users_total": { - "type": "integer", - "description": "Profiles matched by the selection." - }, - "already_enabled": { - "type": "integer", - "description": "Profiles already monitored (nothing to do when enabling; these are what disabling turns off)." - }, - "with_existing_aml": { - "type": "integer", - "description": "Profiles whose existing AML screening will be switched to monitored (no new screening)." - }, - "needing_screening": { - "type": "integer", - "description": "Profiles without any AML screening that will be screened first (billed as a regular AML screening)." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Profiles that cannot be screened because identity data is missing." - }, - "skipped_blocked": { - "type": "integer", - "description": "Blocked users that will be skipped (always 0 for businesses)." - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2, - "description": "One-time cost of the new screenings." - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07, - "description": "Yearly monitoring cost of the profiles that will become monitored." - }, - "balance_sufficient": { - "type": "boolean", - "description": "Whether the organization balance covers the screening plus the first year of monitoring." - }, - "unmatched": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Identifiers of the request that matched no profile in this application, exactly as sent." - } - } - }, - { - "type": "object", - "required": [ - "job_id", - "status", - "is_enabled", - "target_type", - "total", - "processed", - "outcomes", - "results", - "error", - "created_at", - "completed_at", - "estimate", - "unmatched" - ], - "properties": { - "job_id": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED" - ] - }, - "is_enabled": { - "type": "boolean", - "description": "true when the job enables monitoring, false when it disables it." - }, - "target_type": { - "type": "string", - "enum": [ - "user", - "business" - ], - "description": "Which profile family the job toggles." - }, - "total": { - "type": "integer", - "description": "Profiles the job processes." - }, - "processed": { - "type": "integer", - "description": "Profiles processed so far." - }, - "outcomes": { - "type": "object", - "description": "Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.", - "properties": { - "enabled": { - "type": "integer", - "description": "Monitoring enabled on an existing AML screening." - }, - "screened_and_enabled": { - "type": "integer", - "description": "A new AML screening was created (billed) and monitoring enabled on it." - }, - "already_enabled": { - "type": "integer", - "description": "Monitoring was already active; nothing changed and nothing was billed." - }, - "disabled": { - "type": "integer", - "description": "Monitoring turned off across every AML screening of the profile." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen." - }, - "skipped_blocked": { - "type": "integer", - "description": "Skipped: the user is blocked." - }, - "skipped_sandbox": { - "type": "integer", - "description": "Skipped: sandbox application, no screening is created." - }, - "failed_provider": { - "type": "integer", - "description": "The screening could not be completed; retry later." - }, - "failed_insufficient_balance": { - "type": "integer", - "description": "Balance ran out while the job was running." - } - } - }, - "results": { - "type": "array", - "description": "One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API.", - "items": { - "type": "object", - "properties": { - "didit_internal_id": { - "type": "string", - "format": "uuid", - "description": "Didit internal id of the profile." - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your own identifier for the profile." - }, - "outcome": { - "type": "string", - "enum": [ - "enabled", - "screened_and_enabled", - "already_enabled", - "disabled", - "skipped_no_identity_data", - "skipped_blocked", - "skipped_sandbox", - "failed_provider", - "failed_insufficient_balance" - ] - } - } - } - }, - "error": { - "type": "string", - "nullable": true, - "description": "Generic failure message when status is FAILED; the detailed cause is kept on the platform side." - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "completed_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "estimate": { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (nothing is written).", - "properties": { - "users_total": { - "type": "integer", - "description": "Profiles matched by the selection." - }, - "already_enabled": { - "type": "integer", - "description": "Profiles already monitored (nothing to do when enabling; these are what disabling turns off)." - }, - "with_existing_aml": { - "type": "integer", - "description": "Profiles whose existing AML screening will be switched to monitored (no new screening)." - }, - "needing_screening": { - "type": "integer", - "description": "Profiles without any AML screening that will be screened first (billed as a regular AML screening)." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Profiles that cannot be screened because identity data is missing." - }, - "skipped_blocked": { - "type": "integer", - "description": "Blocked users that will be skipped (always 0 for businesses)." - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2, - "description": "One-time cost of the new screenings." - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07, - "description": "Yearly monitoring cost of the profiles that will become monitored." - }, - "balance_sufficient": { - "type": "boolean", - "description": "Whether the organization balance covers the screening plus the first year of monitoring." - } - } - }, - "unmatched": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Identifiers of the request that matched no profile in this application, exactly as sent. They are not part of the job." - } - } - } - ] + "$ref": "#/components/schemas/FraudCheckResponse" + } + } + }, + "description": "" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FraudClientCredentialsErrorMessage" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FraudClientCredentialsForbiddenErrorMessage" } } - } + }, + "description": "" }, - "202": { - "description": "Larger selections run in the background. Poll the job endpoint with `job_id`; `results` fills in as profiles are processed.", + "409": { + "description": "Conflict, returned without a charge: the `client_reference` was already used with a different request body; a check with the same `client_reference` is still running (retry after `Retry-After` seconds with the same reference); or the verification this check records into was deleted before the check completed.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "job_id": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED" - ] - }, - "is_enabled": { - "type": "boolean", - "description": "true when the job enables monitoring, false when it disables it." - }, - "target_type": { - "type": "string", - "enum": [ - "user", - "business" - ], - "description": "Which profile family the job toggles." - }, - "total": { - "type": "integer", - "description": "Profiles the job processes." - }, - "processed": { - "type": "integer", - "description": "Profiles processed so far." - }, - "outcomes": { - "type": "object", - "description": "Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.", - "properties": { - "enabled": { - "type": "integer", - "description": "Monitoring enabled on an existing AML screening." - }, - "screened_and_enabled": { - "type": "integer", - "description": "A new AML screening was created (billed) and monitoring enabled on it." - }, - "already_enabled": { - "type": "integer", - "description": "Monitoring was already active; nothing changed and nothing was billed." - }, - "disabled": { - "type": "integer", - "description": "Monitoring turned off across every AML screening of the profile." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen." - }, - "skipped_blocked": { - "type": "integer", - "description": "Skipped: the user is blocked." - }, - "skipped_sandbox": { - "type": "integer", - "description": "Skipped: sandbox application, no screening is created." - }, - "failed_provider": { - "type": "integer", - "description": "The screening could not be completed; retry later." - }, - "failed_insufficient_balance": { - "type": "integer", - "description": "Balance ran out while the job was running." - } - } - }, - "results": { - "type": "array", - "description": "One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API.", - "items": { - "type": "object", - "properties": { - "didit_internal_id": { - "type": "string", - "format": "uuid", - "description": "Didit internal id of the profile." - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your own identifier for the profile." - }, - "outcome": { - "type": "string", - "enum": [ - "enabled", - "screened_and_enabled", - "already_enabled", - "disabled", - "skipped_no_identity_data", - "skipped_blocked", - "skipped_sandbox", - "failed_provider", - "failed_insufficient_balance" - ] - } - } - } - }, "error": { - "type": "string", - "nullable": true, - "description": "Generic failure message when status is FAILED; the detailed cause is kept on the platform side." - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "completed_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "estimate": { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (nothing is written).", - "properties": { - "users_total": { - "type": "integer", - "description": "Profiles matched by the selection." - }, - "already_enabled": { - "type": "integer", - "description": "Profiles already monitored (nothing to do when enabling; these are what disabling turns off)." - }, - "with_existing_aml": { - "type": "integer", - "description": "Profiles whose existing AML screening will be switched to monitored (no new screening)." - }, - "needing_screening": { - "type": "integer", - "description": "Profiles without any AML screening that will be screened first (billed as a regular AML screening)." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Profiles that cannot be screened because identity data is missing." - }, - "skipped_blocked": { - "type": "integer", - "description": "Blocked users that will be skipped (always 0 for businesses)." - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2, - "description": "One-time cost of the new screenings." - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07, - "description": "Yearly monitoring cost of the profiles that will become monitored." - }, - "balance_sufficient": { - "type": "boolean", - "description": "Whether the organization balance covers the screening plus the first year of monitoring." - } - } - }, - "unmatched": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Identifiers of the request that matched no profile in this application, exactly as sent. They are not part of the job." + "type": "string" + } + }, + "required": [ + "error" + ] + }, + "examples": { + "different_body": { + "summary": "Reference reused with a different body", + "value": { + "error": "client_reference has already been used with a different request body." + } + }, + "still_running": { + "summary": "A check with this reference is still running", + "value": { + "error": "A check with this client_reference is already running. Retry with the same reference later." + } + }, + "verification_deleted": { + "summary": "The verification was deleted before the check completed", + "value": { + "error": "This verification was deleted before the check completed." } } } } + }, + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying, sent when a check with the same `client_reference` is still running.", + "schema": { + "type": "integer" + } + } } + } + }, + "security": [ + { + "ApiKeyAuth": [] }, - "400": { - "description": "Validation error: `is_enabled` missing, no selector, or more than 1000 identifiers across both lists." + { + "BearerAuth": [] + } + ], + "tags": [ + "Fraud Check" + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/cancel/": { + "post": { + "operationId": "organization_application_cases_requests_cancel_create", + "x-didit-sdk": { + "resource": "cases.requests", + "method": "cancel", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true }, - "401": { - "description": "Missing or invalid API key." + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "case_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "request_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + } + ], + "tags": [ + "Case Management" + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerMessagingCaseRequest" + } + } + }, + "description": "" + }, + "404": { + "description": "The request or case is unavailable for this caller." }, "403": { - "description": "Insufficient balance to enable monitoring for this selection. The response carries the `estimate` so you can top up the right amount.", + "description": "The caller lacks the required cases permission." + }, + "400": { + "description": "Invalid inputs, unsupported purpose, unavailable verified contact or an existing immutable response." + }, + "409": { + "description": "The preview changed or expired. Preview again before confirming." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/options/": { + "get": { + "operationId": "organization_application_cases_requests_options_retrieve", + "x-didit-sdk": { + "resource": "cases.requests", + "method": "retrieve_options", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "case_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + } + ], + "tags": [ + "Case Management" + ], + "responses": { + "200": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "estimate": { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (nothing is written).", - "properties": { - "users_total": { - "type": "integer", - "description": "Profiles matched by the selection." - }, - "already_enabled": { - "type": "integer", - "description": "Profiles already monitored (nothing to do when enabling; these are what disabling turns off)." - }, - "with_existing_aml": { - "type": "integer", - "description": "Profiles whose existing AML screening will be switched to monitored (no new screening)." - }, - "needing_screening": { - "type": "integer", - "description": "Profiles without any AML screening that will be screened first (billed as a regular AML screening)." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Profiles that cannot be screened because identity data is missing." - }, - "skipped_blocked": { - "type": "integer", - "description": "Blocked users that will be skipped (always 0 for businesses)." - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2, - "description": "One-time cost of the new screenings." - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07, - "description": "Yearly monitoring cost of the profiles that will become monitored." - }, - "balance_sufficient": { - "type": "boolean", - "description": "Whether the organization balance covers the screening plus the first year of monitoring." - } - } - } - } + "$ref": "#/components/schemas/CustomerMessagingCaseRequestOptions" + } + } + }, + "description": "" + }, + "404": { + "description": "The request or case is unavailable for this caller." + }, + "403": { + "description": "The caller lacks the required cases permission." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/attachment/": { + "get": { + "operationId": "organization_application_cases_requests_attachment_retrieve", + "x-didit-sdk": { + "resource": "cases.requests", + "method": "retrieve_attachment", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "case_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "request_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + } + ], + "tags": [ + "Case Management" + ], + "responses": { + "200": { + "content": { + "image/jpeg": { + "schema": { + "type": "string", + "format": "binary" + } + } + }, + "description": "" + }, + "404": { + "description": "The request or case is unavailable for this caller." + }, + "403": { + "description": "The caller lacks the required cases permission." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/history/": { + "get": { + "operationId": "list_case_routing_rule_history", + "x-didit-sdk": { + "resource": "cases.routing_rules", + "method": "list_history", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "record_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "name": "ordering", + "required": false, + "in": "query", + "description": "Which field to use when ordering the results.", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Number of results to return per page.", + "schema": { + "type": "integer" + } + }, + { + "name": "offset", + "required": false, + "in": "query", + "description": "The initial index from which to return the results.", + "schema": { + "type": "integer" + } + } + ], + "tags": [ + "Case Assignment" + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingPublicationPaginatedPolicyRevisionList" } } - } + }, + "description": "" + }, + "403": { + "description": "Case access is required." } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/businesses/aml-monitoring/": { + "/v3/session/{sessionId}/resubmission-preview/": { "post": { - "summary": "Enable or disable AML ongoing monitoring for businesses", - "description": "Turns AML ongoing monitoring on or off for the selected businesses, with the same semantics as the Business Console. Exactly one monitored AML screening is kept per business, so a profile that verified several times is monitored and billed once; disabling turns monitoring off across all of the profile's screenings. Businesses without any AML screening are screened first (billed as a regular AML screening) when they have the legal or display name needed; otherwise they are reported as `skipped_no_identity_data`.\n\nSet `estimate_only: true` to preview counts and costs without writing anything. Selections of 20 profiles or fewer resolve inline and return `200` with the completed job; larger selections return `202` and run in the background - poll [Get AML monitoring job](#get-/v3/aml-monitoring/jobs/-job_id-/) with the returned `job_id`. Enabling is refused with `403` when the organization balance does not cover the estimate. Identifiers that match nothing are listed in `unmatched` and never fail the request: a selection that matches nothing completes as an empty job (`total` 0), the same convention as the batch delete endpoints.", - "operationId": "toggle_businesses_aml_monitoring", + "summary": "Preview a resubmission email", + "description": "Render the email that would accompany a KYC or KYB resubmission without changing the session, resetting checks, extending its expiry, running checks or sending mail. Requires the `write:sessions` privilege (a scoped API key needs write access to decisions) and a session in `Approved`, `Declined`, `In Review`, `Kyc Expired` or `Abandoned` status. The server validates the selected workflow steps and renders its fixed translated template with the applicable verified white-label sender and branding. The request cannot set a subject, message body or action URL.\n\n`email_address` is optional. Without it, the email goes to the person's own address: the one they verified in an email verification step of the flow, else the one given in `contact_details` when the session was created. An address you pass wins (`recipient_source: request`). When no address is known and you pass none, `recipient` and `preview_token` are empty strings and no email can be sent.\n\nThe response includes the recipient and where it comes from, the sender, subject, HTML, language, resolved steps, whether confirming sends the email (`will_be_sent`, `not_sent_reason`) and a signed `preview_token`. `requires_user_action` is `false` when every selected step runs without the person: the resubmission then finishes on its own and no email goes out (`not_sent_reason: no_user_action`).\n\nThis preview does not reserve session state or authorize delivery. After reviewing it, call [Update Session Status](/sessions-api/update-status) with `new_status: Resubmitted`, the same `nodes_to_resubmit` and `email_language` you previewed, `send_email: true`, `email_address` set to `recipient` and `email_preview_token` set to `preview_token`. The receipt expires after 15 minutes. When a receipt is supplied, changed inputs, message content, sender or session state return `409` before any mutation or send. Existing integrations may omit the receipt. Regenerate the preview after a conflict. Treat the response as sensitive verification content; responses use `Cache-Control: no-store`. Render HTML in a sandboxed frame, never directly in your application DOM.", + "operationId": "post_v3_session_resubmission_preview", + "x-didit-sdk": { + "resource": "sessions", + "method": "preview_resubmission", + "auth": "api_key", + "server": true + }, "tags": [ - "AML Ongoing Monitoring" + "Sessions" ], "security": [ { "ApiKeyAuth": [] } ], + "parameters": [ + { + "in": "path", + "name": "sessionId", + "required": true, + "description": "UUID of the verification session to update. Accepts both user (KYC) and business (KYB) session IDs — the service resolves the ID across both session types.", + "schema": { + "type": "string", + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } + } + ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", - "required": [ - "is_enabled" - ], - "description": "Select businesses with `vendor_data_list`, `didit_internal_id_list`, or both (the two lists are OR'd). At most 1000 identifiers per request across both lists.", "properties": { - "is_enabled": { - "type": "boolean", - "description": "true to enable monitoring, false to disable it." - }, - "estimate_only": { - "type": "boolean", - "default": false, - "description": "When true, returns the cost estimate and performs no writes - no screening, no billing, no job." - }, - "vendor_data_list": { + "nodes_to_resubmit": { "type": "array", + "description": "Steps to preview, using the same validation, selection and workflow ordering as a resubmission. Omit to auto-select recorded attempts needing resubmission.", "items": { - "type": "string" - }, - "description": "Your own identifiers of the businesses to toggle, matched exactly as sent." + "type": "object", + "required": [ + "node_id", + "feature" + ], + "properties": { + "node_id": { + "type": "string", + "description": "Node identifier as it appears in the session's workflow definition (for example `feature_ocr`, `feature_liveness`).", + "example": "feature_ocr" + }, + "feature": { + "type": "string", + "description": "Feature type of the node. The aliases `ID_VERIFICATION`, `POA`, `PHONE`, and `EMAIL` are normalized server-side to `OCR`, `PROOF_OF_ADDRESS`, `PHONE_VERIFICATION`, and `EMAIL_VERIFICATION`. `KYB_REGISTRY`, `KYB_KEY_PEOPLE`, and `KYB` pass schema validation but are always rejected with the business-rule `400` shown in the examples.", + "enum": [ + "OCR", + "OCR_BACK", + "NFC", + "AML", + "FACE", + "LIVENESS", + "FACE_MATCH", + "IP_ANALYSIS", + "AGE_ESTIMATION", + "PROOF_OF_ADDRESS", + "PHONE_VERIFICATION", + "EMAIL_VERIFICATION", + "FACE_SEARCH", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_DOCUMENTS", + "ID_VERIFICATION", + "POA", + "PHONE", + "EMAIL", + "KYB_REGISTRY", + "KYB_KEY_PEOPLE", + "KYB" + ], + "example": "OCR" + } + } + } }, - "didit_internal_id_list": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - }, - "description": "Didit internal ids of the businesses to toggle - the `didit_internal_id` returned by the create, get and list endpoints." + "email_address": { + "type": "string", + "format": "email", + "description": "Optional. Send the email to this address instead of the person's own. Without it, the recipient is the address the person verified in the flow, else the one given in `contact_details`. No email is sent by this call.", + "example": "user@example.com" + }, + "email_language": { + "type": "string", + "description": "Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`).", + "default": "en", + "example": "en" } } }, "examples": { - "Estimate first": { - "summary": "Preview the cost without writing", - "value": { - "is_enabled": true, - "estimate_only": true, - "vendor_data_list": [ - "company-123", - "company-456" - ] - } - }, - "Enable": { - "summary": "Enable monitoring for two businesses", + "Person's Own Address": { + "summary": "Preview a proof-of-address resubmission for the person's own address", "value": { - "is_enabled": true, - "vendor_data_list": [ - "company-123", - "company-456" - ] + "nodes_to_resubmit": [ + { + "node_id": "feature_poa_1", + "feature": "POA" + } + ], + "email_language": "en" } }, - "Disable": { - "summary": "Disable monitoring by Didit internal id", + "Named Recipient": { + "summary": "Preview the email for an address you name", "value": { - "is_enabled": false, - "didit_internal_id_list": [ - "3f1c0d3e-8f0a-4c2e-9a3d-9b1f5f4a2c11" - ] + "nodes_to_resubmit": [ + { + "node_id": "feature_ocr", + "feature": "OCR" + } + ], + "email_address": "person@example.com", + "email_language": "en" } } } } } }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/businesses/aml-monitoring/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"is_enabled\": true, \"vendor_data_list\": [\"company-123\", \"company-456\"]}'" - }, - { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/businesses/aml-monitoring/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'is_enabled': True, 'vendor_data_list': ['company-123', 'company-456']},\n)\nresp.raise_for_status()\njob = resp.json()\nprint(job['status'], job['outcomes'])\nfor row in job['results']:\n print(row['vendor_data'], row['outcome'])" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/businesses/aml-monitoring/', {\n method: 'POST',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ is_enabled: true, vendor_data_list: ['company-123', 'company-456'] }),\n});\nconst job = await resp.json();\nconsole.log(job.status, job.outcomes);\nfor (const row of job.results) console.log(row.vendor_data, row.outcome);" - } - ], "responses": { "200": { - "description": "The estimate (when `estimate_only` is true) or the completed job for a selection processed inline.", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "required": [ - "users_total", - "already_enabled", - "with_existing_aml", - "needing_screening", - "skipped_no_identity_data", - "skipped_blocked", - "estimated_screening_cost_usd", - "estimated_annual_monitoring_cost_usd", - "balance_sufficient", - "unmatched" - ], - "description": "Cost preview. Returned directly when estimate_only is true (nothing is written).", - "properties": { - "users_total": { - "type": "integer", - "description": "Profiles matched by the selection." - }, - "already_enabled": { - "type": "integer", - "description": "Profiles already monitored (nothing to do when enabling; these are what disabling turns off)." - }, - "with_existing_aml": { - "type": "integer", - "description": "Profiles whose existing AML screening will be switched to monitored (no new screening)." - }, - "needing_screening": { - "type": "integer", - "description": "Profiles without any AML screening that will be screened first (billed as a regular AML screening)." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Profiles that cannot be screened because identity data is missing." - }, - "skipped_blocked": { - "type": "integer", - "description": "Blocked users that will be skipped (always 0 for businesses)." - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2, - "description": "One-time cost of the new screenings." - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07, - "description": "Yearly monitoring cost of the profiles that will become monitored." - }, - "balance_sufficient": { - "type": "boolean", - "description": "Whether the organization balance covers the screening plus the first year of monitoring." - }, - "unmatched": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Identifiers of the request that matched no profile in this application, exactly as sent." - } - } - }, - { - "type": "object", - "required": [ - "job_id", - "status", - "is_enabled", - "target_type", - "total", - "processed", - "outcomes", - "results", - "error", - "created_at", - "completed_at", - "estimate", - "unmatched" - ], - "properties": { - "job_id": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED" - ] - }, - "is_enabled": { - "type": "boolean", - "description": "true when the job enables monitoring, false when it disables it." - }, - "target_type": { - "type": "string", - "enum": [ - "user", - "business" - ], - "description": "Which profile family the job toggles." - }, - "total": { - "type": "integer", - "description": "Profiles the job processes." - }, - "processed": { - "type": "integer", - "description": "Profiles processed so far." - }, - "outcomes": { - "type": "object", - "description": "Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.", - "properties": { - "enabled": { - "type": "integer", - "description": "Monitoring enabled on an existing AML screening." - }, - "screened_and_enabled": { - "type": "integer", - "description": "A new AML screening was created (billed) and monitoring enabled on it." - }, - "already_enabled": { - "type": "integer", - "description": "Monitoring was already active; nothing changed and nothing was billed." - }, - "disabled": { - "type": "integer", - "description": "Monitoring turned off across every AML screening of the profile." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen." - }, - "skipped_blocked": { - "type": "integer", - "description": "Skipped: the user is blocked." - }, - "skipped_sandbox": { - "type": "integer", - "description": "Skipped: sandbox application, no screening is created." - }, - "failed_provider": { - "type": "integer", - "description": "The screening could not be completed; retry later." - }, - "failed_insufficient_balance": { - "type": "integer", - "description": "Balance ran out while the job was running." - } - } - }, - "results": { - "type": "array", - "description": "One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API.", - "items": { - "type": "object", - "properties": { - "didit_internal_id": { - "type": "string", - "format": "uuid", - "description": "Didit internal id of the profile." - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your own identifier for the profile." - }, - "outcome": { - "type": "string", - "enum": [ - "enabled", - "screened_and_enabled", - "already_enabled", - "disabled", - "skipped_no_identity_data", - "skipped_blocked", - "skipped_sandbox", - "failed_provider", - "failed_insufficient_balance" - ] - } - } - } - }, - "error": { - "type": "string", - "nullable": true, - "description": "Generic failure message when status is FAILED; the detailed cause is kept on the platform side." - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "completed_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "estimate": { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (nothing is written).", - "properties": { - "users_total": { - "type": "integer", - "description": "Profiles matched by the selection." - }, - "already_enabled": { - "type": "integer", - "description": "Profiles already monitored (nothing to do when enabling; these are what disabling turns off)." - }, - "with_existing_aml": { - "type": "integer", - "description": "Profiles whose existing AML screening will be switched to monitored (no new screening)." - }, - "needing_screening": { - "type": "integer", - "description": "Profiles without any AML screening that will be screened first (billed as a regular AML screening)." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Profiles that cannot be screened because identity data is missing." - }, - "skipped_blocked": { - "type": "integer", - "description": "Blocked users that will be skipped (always 0 for businesses)." - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2, - "description": "One-time cost of the new screenings." - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07, - "description": "Yearly monitoring cost of the profiles that will become monitored." - }, - "balance_sufficient": { - "type": "boolean", - "description": "Whether the organization balance covers the screening plus the first year of monitoring." - } - } - }, - "unmatched": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Identifiers of the request that matched no profile in this application, exactly as sent. They are not part of the job." - } - } - } - ] - } - } - } - }, - "202": { - "description": "Larger selections run in the background. Poll the job endpoint with `job_id`; `results` fills in as profiles are processed.", + "description": "Preview only. The session is unchanged and no email is sent.", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "recipient", + "recipient_source", + "sender", + "sender_name", + "sender_email", + "sender_kind", + "subject", + "html", + "language", + "will_be_sent", + "not_sent_reason", + "preview_token", + "nodes_to_resubmit", + "requires_user_action" + ], "properties": { - "job_id": { + "recipient": { "type": "string", - "format": "uuid" + "description": "The address the email goes to. An empty string when no address is known for the person: then no email can be sent and `preview_token` is empty too. Pass it as `email_address` when you confirm.", + "example": "person@example.com" }, - "status": { + "recipient_source": { "type": "string", "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED" - ] + "email_verification", + "contact_details", + "request", + "" + ], + "description": "Where `recipient` comes from. `request`: the `email_address` you passed. `email_verification`: the address the person verified in an email verification step of the flow. `contact_details`: the address given in `contact_details` when the session was created. An empty string when there is no address.", + "example": "email_verification" }, - "is_enabled": { - "type": "boolean", - "description": "true when the job enables monitoring, false when it disables it." + "sender": { + "type": "string", + "description": "The `From` header the email goes out with.", + "example": "Example App " }, - "target_type": { + "sender_name": { + "type": "string", + "description": "The display name part of `sender`.", + "example": "Example App" + }, + "sender_email": { + "type": "string", + "format": "email", + "description": "The address part of `sender`.", + "example": "no-reply@mail.example.com" + }, + "sender_kind": { "type": "string", "enum": [ - "user", - "business" + "customer", + "didit" ], - "description": "Which profile family the job toggles." + "description": "`customer`: the email goes out from your application's verified sender domain (white label). `didit`: it goes out from Didit's own sender.", + "example": "customer" }, - "total": { - "type": "integer", - "description": "Profiles the job processes." + "subject": { + "type": "string", + "description": "The subject line exactly as it is sent.", + "example": "Action Required: Complete your verification in Example App" }, - "processed": { - "type": "integer", - "description": "Profiles processed so far." + "html": { + "type": "string", + "description": "The email's HTML exactly as it is sent, with its logo embedded as a data URL. Render it in a sandboxed frame, never directly in your application DOM.", + "example": "..." }, - "outcomes": { - "type": "object", - "description": "Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.", - "properties": { - "enabled": { - "type": "integer", - "description": "Monitoring enabled on an existing AML screening." - }, - "screened_and_enabled": { - "type": "integer", - "description": "A new AML screening was created (billed) and monitoring enabled on it." - }, - "already_enabled": { - "type": "integer", - "description": "Monitoring was already active; nothing changed and nothing was billed." - }, - "disabled": { - "type": "integer", - "description": "Monitoring turned off across every AML screening of the profile." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen." - }, - "skipped_blocked": { - "type": "integer", - "description": "Skipped: the user is blocked." - }, - "skipped_sandbox": { - "type": "integer", - "description": "Skipped: sandbox application, no screening is created." - }, - "failed_provider": { - "type": "integer", - "description": "The screening could not be completed; retry later." - }, - "failed_insufficient_balance": { - "type": "integer", - "description": "Balance ran out while the job was running." - } - } + "language": { + "type": "string", + "description": "The language the email is written in. English (`en`) when the `email_language` you asked for is not offered.", + "example": "en" }, - "results": { + "will_be_sent": { + "type": "boolean", + "description": "Whether confirming the change with `send_email: true` sends this email. `false` when `not_sent_reason` is set.", + "example": true + }, + "not_sent_reason": { + "type": "string", + "nullable": true, + "enum": [ + "no_user_action", + "no_recipient", + "sandbox", + null + ], + "description": "Why confirming would send no email, or `null` when it will be sent. `no_user_action`: every selected step runs without the person, so the resubmission finishes on its own and no email goes out. `no_recipient`: no address is known for the person and you passed none. `sandbox`: a sandbox session whose recipient has not opted into sandbox email delivery, so the send is only simulated.", + "example": null + }, + "nodes_to_resubmit": { "type": "array", - "description": "One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API.", "items": { "type": "object", + "required": [ + "node_id", + "feature" + ], "properties": { - "didit_internal_id": { - "type": "string", - "format": "uuid", - "description": "Didit internal id of the profile." - }, - "vendor_data": { + "node_id": { "type": "string", - "nullable": true, - "description": "Your own identifier for the profile." + "description": "Node identifier as it appears in the session's workflow definition (for example `feature_ocr`, `feature_liveness`).", + "example": "feature_ocr" }, - "outcome": { + "feature": { "type": "string", + "description": "Feature type of the node. The aliases `ID_VERIFICATION`, `POA`, `PHONE`, and `EMAIL` are normalized server-side to `OCR`, `PROOF_OF_ADDRESS`, `PHONE_VERIFICATION`, and `EMAIL_VERIFICATION`. `KYB_REGISTRY`, `KYB_KEY_PEOPLE`, and `KYB` pass schema validation but are always rejected with the business-rule `400` shown in the examples.", "enum": [ - "enabled", - "screened_and_enabled", - "already_enabled", - "disabled", - "skipped_no_identity_data", - "skipped_blocked", - "skipped_sandbox", - "failed_provider", - "failed_insufficient_balance" - ] + "OCR", + "OCR_BACK", + "NFC", + "AML", + "FACE", + "LIVENESS", + "FACE_MATCH", + "IP_ANALYSIS", + "AGE_ESTIMATION", + "PROOF_OF_ADDRESS", + "PHONE_VERIFICATION", + "EMAIL_VERIFICATION", + "FACE_SEARCH", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_DOCUMENTS", + "ID_VERIFICATION", + "POA", + "PHONE", + "EMAIL", + "KYB_REGISTRY", + "KYB_KEY_PEOPLE", + "KYB" + ], + "example": "OCR" } } } }, - "error": { - "type": "string", - "nullable": true, - "description": "Generic failure message when status is FAILED; the detailed cause is kept on the platform side." - }, - "created_at": { - "type": "string", - "format": "date-time" + "requires_user_action": { + "type": "boolean", + "description": "Whether at least one selected check requires user interaction. This does not predict the final workflow outcome." }, - "completed_at": { + "preview_token": { "type": "string", - "format": "date-time", - "nullable": true - }, - "estimate": { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (nothing is written).", - "properties": { - "users_total": { - "type": "integer", - "description": "Profiles matched by the selection." - }, - "already_enabled": { - "type": "integer", - "description": "Profiles already monitored (nothing to do when enabling; these are what disabling turns off)." - }, - "with_existing_aml": { - "type": "integer", - "description": "Profiles whose existing AML screening will be switched to monitored (no new screening)." - }, - "needing_screening": { - "type": "integer", - "description": "Profiles without any AML screening that will be screened first (billed as a regular AML screening)." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Profiles that cannot be screened because identity data is missing." - }, - "skipped_blocked": { - "type": "integer", - "description": "Blocked users that will be skipped (always 0 for businesses)." - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2, - "description": "One-time cost of the new screenings." - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07, - "description": "Yearly monitoring cost of the profiles that will become monitored." - }, - "balance_sufficient": { - "type": "boolean", - "description": "Whether the organization balance covers the screening plus the first year of monitoring." + "description": "Signed receipt for this exact email, valid for 15 minutes. Pass it as `email_preview_token` to [Update Session Status](/sessions-api/update-status). An empty string when there is no recipient.", + "example": "eyJkaWdlc3QiOiI0ZjJhIn0:1uExAm:Pl3x4mpleS1gnatureValue000000000000000" + } + } + }, + "examples": { + "Will Be Sent": { + "summary": "Default recipient: the address verified in the flow", + "value": { + "recipient": "person@example.com", + "recipient_source": "email_verification", + "sender": "Example App ", + "sender_name": "Example App", + "sender_email": "no-reply@mail.example.com", + "sender_kind": "customer", + "subject": "Action Required: Complete your verification in Example App", + "html": "...", + "language": "en", + "will_be_sent": true, + "not_sent_reason": null, + "nodes_to_resubmit": [ + { + "node_id": "feature_poa_1", + "feature": "PROOF_OF_ADDRESS" } - } - }, - "unmatched": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Identifiers of the request that matched no profile in this application, exactly as sent. They are not part of the job." + ], + "requires_user_action": true, + "preview_token": "eyJkaWdlc3QiOiI0ZjJhIn0:1uExAm:Pl3x4mpleS1gnatureValue000000000000000" + } + }, + "No User Action": { + "summary": "Only backend steps are selected, so no email goes out", + "value": { + "recipient": "person@example.com", + "recipient_source": "contact_details", + "sender": "Example App ", + "sender_name": "Example App", + "sender_email": "no-reply@mail.example.com", + "sender_kind": "customer", + "subject": "Action Required: Complete your verification in Example App", + "html": "...", + "language": "en", + "will_be_sent": false, + "not_sent_reason": "no_user_action", + "nodes_to_resubmit": [ + { + "node_id": "feature_aml", + "feature": "AML" + } + ], + "requires_user_action": false, + "preview_token": "eyJkaWdlc3QiOiI0ZjJhIn0:1uExAm:Pl3x4mpleS1gnatureValue000000000000000" } } } } + }, + "headers": { + "Cache-Control": { + "description": "Always `no-store`: the preview carries verification content and must not be cached.", + "schema": { + "type": "string", + "example": "no-store" + } + } } }, "400": { - "description": "Validation error: `is_enabled` missing, no selector, or more than 1000 identifiers across both lists." + "description": "The session is not in a status this endpoint accepts, it is already `Resubmitted`, the selected steps cannot be resubmitted, or a field is invalid.", + "content": { + "application/json": { + "examples": { + "Wrong Current Status": { + "summary": "Session is not in a status this endpoint accepts", + "value": { + "detail": "Only sessions with status Approved, Declined, In Review, Kyc Expired, Abandoned, Resubmitted can be updated. Current status: In Progress" + } + }, + "Already Resubmitted": { + "summary": "The session is already Resubmitted", + "value": [ + "The new status is the same as the current status." + ] + }, + "Nothing To Resubmit": { + "summary": "Auto-select found no step that needs resubmission", + "value": { + "nodes_to_resubmit": "No features found that need resubmission. All features are already approved." + } + }, + "Invalid Email": { + "summary": "email_address is not an email address", + "value": { + "email_address": [ + "Enter a valid email address." + ] + } + } + } + } + } }, "401": { - "description": "Missing or invalid API key." + "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": "Insufficient balance to enable monitoring for this selection. The response carries the `estimate` so you can top up the right amount.", + "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": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "estimate": { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (nothing is written).", - "properties": { - "users_total": { - "type": "integer", - "description": "Profiles matched by the selection." - }, - "already_enabled": { - "type": "integer", - "description": "Profiles already monitored (nothing to do when enabling; these are what disabling turns off)." - }, - "with_existing_aml": { - "type": "integer", - "description": "Profiles whose existing AML screening will be switched to monitored (no new screening)." - }, - "needing_screening": { - "type": "integer", - "description": "Profiles without any AML screening that will be screened first (billed as a regular AML screening)." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Profiles that cannot be screened because identity data is missing." - }, - "skipped_blocked": { - "type": "integer", - "description": "Blocked users that will be skipped (always 0 for businesses)." - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2, - "description": "One-time cost of the new screenings." - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07, - "description": "Yearly monitoring cost of the profiles that will become monitored." - }, - "balance_sufficient": { - "type": "boolean", - "description": "Whether the organization balance covers the screening plus the first year of monitoring." - } - } + "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 decisions." + } + } + } + } + } + }, + "404": { + "description": "No KYC or KYB session with this `session_id` exists.", + "content": { + "application/json": { + "examples": { + "Not Found": { + "summary": "Unknown session_id", + "value": { + "detail": "Not found." + } + } + } + } + } + }, + "429": { + "description": "The steps to resubmit are all backend-only (AML, DATABASE_VALIDATION, IP_ANALYSIS), and the session was already resubmitted for backend-only steps within the last 30 seconds, so Update Session Status would refuse the resubmission. Retry after the interval in `Retry-After`.", + "content": { + "application/json": { + "examples": { + "Backend-only resubmit throttled": { + "value": { + "detail": "This session was already resubmitted for backend-only features within the last 30 seconds; repeating it cannot change the outcome. Wait before resubmitting again." } } } @@ -32624,455 +52126,434 @@ } } }, - "/v3/aml-monitoring/jobs/{job_id}/": { + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/": { "get": { - "summary": "Get AML ongoing monitoring job", - "description": "Status, outcome counters and per-record results of a monitoring toggle job started from `POST /v3/users/aml-monitoring/` or `POST /v3/businesses/aml-monitoring/`. Poll every couple of seconds while `status` is `PENDING` or `PROCESSING`; `results` is complete once it is `COMPLETED`.", - "operationId": "get_aml_monitoring_job", + "summary": "Retrieve routing rule", + "description": "Requires `read:cases` for the requested application.", + "operationId": "retrieve_case_routing_rules", + "x-didit-sdk": { + "resource": "cases.routing_rules", + "method": "retrieve", + "auth": "api_key", + "server": true, + "scope": "application" + }, "tags": [ - "AML Ongoing Monitoring" + "Case Assignment" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "record_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseRoutingRule" + } + } + } + }, + "400": { + "description": "Invalid input." + }, + "403": { + "description": "Insufficient permission." + }, + "404": { + "description": "Record not found in this application." + } + }, "security": [ { "ApiKeyAuth": [] } + ] + }, + "patch": { + "summary": "Update routing rule", + "description": "Requires `write:cases` for the requested application.", + "operationId": "update_case_routing_rules", + "x-didit-sdk": { + "resource": "cases.routing_rules", + "method": "update", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Case Assignment" ], "parameters": [ { - "name": "job_id", + "name": "organization_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, - "description": "The `job_id` returned by the toggle endpoint." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl 'https://verification.didit.me/v3/aml-monitoring/jobs/JOB_ID/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "description": "Organization UUID." }, { - "lang": "python", - "label": "Python", - "source": "import time\nimport requests\n\nurl = 'https://verification.didit.me/v3/aml-monitoring/jobs/JOB_ID/'\nwhile True:\n job = requests.get(url, headers={'x-api-key': 'YOUR_API_KEY'}).json()\n if job['status'] in ('COMPLETED', 'FAILED'):\n break\n time.sleep(2)\nprint(job['outcomes'])" + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const url = 'https://verification.didit.me/v3/aml-monitoring/jobs/JOB_ID/';\nlet job;\ndo {\n await new Promise((r) => setTimeout(r, 2000));\n job = await (await fetch(url, { headers: { 'x-api-key': process.env.DIDIT_API_KEY } })).json();\n} while (job.status === 'PENDING' || job.status === 'PROCESSING');\nconsole.log(job.outcomes);" + "name": "record_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } } ], "responses": { "200": { - "description": "The job.", + "description": "Success", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "job_id": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED" - ] - }, - "is_enabled": { - "type": "boolean", - "description": "true when the job enables monitoring, false when it disables it." - }, - "target_type": { - "type": "string", - "enum": [ - "user", - "business" - ], - "description": "Which profile family the job toggles." - }, - "total": { - "type": "integer", - "description": "Profiles the job processes." - }, - "processed": { - "type": "integer", - "description": "Profiles processed so far." - }, - "outcomes": { - "type": "object", - "description": "Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.", - "properties": { - "enabled": { - "type": "integer", - "description": "Monitoring enabled on an existing AML screening." - }, - "screened_and_enabled": { - "type": "integer", - "description": "A new AML screening was created (billed) and monitoring enabled on it." - }, - "already_enabled": { - "type": "integer", - "description": "Monitoring was already active; nothing changed and nothing was billed." - }, - "disabled": { - "type": "integer", - "description": "Monitoring turned off across every AML screening of the profile." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen." - }, - "skipped_blocked": { - "type": "integer", - "description": "Skipped: the user is blocked." - }, - "skipped_sandbox": { - "type": "integer", - "description": "Skipped: sandbox application, no screening is created." - }, - "failed_provider": { - "type": "integer", - "description": "The screening could not be completed; retry later." - }, - "failed_insufficient_balance": { - "type": "integer", - "description": "Balance ran out while the job was running." - } - } - }, - "results": { - "type": "array", - "description": "One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API; a job started from the Business Console reports counters only.", - "items": { - "type": "object", - "properties": { - "didit_internal_id": { - "type": "string", - "format": "uuid", - "description": "Didit internal id of the profile." - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your own identifier for the profile." - }, - "outcome": { - "type": "string", - "enum": [ - "enabled", - "screened_and_enabled", - "already_enabled", - "disabled", - "skipped_no_identity_data", - "skipped_blocked", - "skipped_sandbox", - "failed_provider", - "failed_insufficient_balance" - ] - } - } - } - }, - "error": { - "type": "string", - "nullable": true, - "description": "Generic failure message when status is FAILED; the detailed cause is kept on the platform side." - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "completed_at": { - "type": "string", - "format": "date-time", - "nullable": true - } - } + "$ref": "#/components/schemas/CaseRoutingRule" } } } }, - "401": { - "description": "Missing or invalid API key." + "400": { + "description": "Invalid input." + }, + "403": { + "description": "Insufficient permission." }, "404": { - "description": "No such job for this application." + "description": "Record not found in this application." + }, + "409": { + "description": "The routing rule changed since expected_version. Review the latest rule before retrying." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseRoutingRuleUpdate" + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/case-requests/{request_uuid}/": { + "get": { + "operationId": "case_requests_retrieve", + "x-didit-sdk": { + "resource": "case_requests", + "method": "retrieve", + "auth": "device_token", + "server": false + }, + "parameters": [ + { + "in": "path", + "name": "request_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + } + ], + "tags": [ + "Case Management" + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerMessagingCaseRequestPublic" + } + } + }, + "description": "" + }, + "404": { + "description": "The request or case is unavailable for this caller." + } + }, + "security": [ + { + "CaseRequestToken": [] } - } - } - }, - "/v3/users/{vendor_data}/update-status/": { - "patch": { - "summary": "Update user status", - "description": "Flip only the lifecycle `status` of a user — `ACTIVE`/`FLAGGED`/`BLOCKED` (NOT session statuses). `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. Each change is recorded as a `STATUS_CHANGED` activity on the user.", - "operationId": "update_user_status", - "tags": [ - "Users" ], + "description": "Access one customer request using its request-specific token. No private case findings, original identity data or recipient address are returned. Actionable requests expire after seven days. Cancellation or case resolution invalidates the link. Responses are immutable and remain evidence for reviewer assessment; they never approve or update a source verification." + }, + "post": { + "operationId": "case_requests_create", + "x-didit-sdk": { + "resource": "case_requests", + "method": "respond", + "auth": "device_token", + "server": false + }, "parameters": [ { - "name": "vendor_data", "in": "path", - "required": true, + "name": "request_uuid", "schema": { - "type": "string" + "type": "string", + "format": "uuid" }, - "description": "Your unique identifier for the user." + "required": true } ], + "tags": [ + "Case Management" + ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "status" - ], - "properties": { - "status": { - "type": "string", - "enum": [ - "ACTIVE", - "FLAGGED", - "BLOCKED" - ], - "description": "New lifecycle status. `BLOCKED` also adds the `vendor_data` to the system blocklist." - } - } - }, - "examples": { - "Activate": { - "summary": "Reactivate the user", - "value": { - "status": "ACTIVE" - } - }, - "Flag": { - "summary": "Flag the user for review", - "value": { - "status": "FLAGGED" - } - }, - "Block": { - "summary": "Block the user (adds to system blocklist)", - "value": { - "status": "BLOCKED" - } - } + "$ref": "#/components/schemas/CustomerMessagingCaseCustomerResponseRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CustomerMessagingCaseCustomerResponseRequest" } } } }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH 'https://verification.didit.me/v3/users/user-abc-123/update-status/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"status\": \"BLOCKED\"}'" - }, - { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.patch(\n 'https://verification.didit.me/v3/users/user-abc-123/update-status/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'status': 'BLOCKED'},\n)\nresp.raise_for_status()\nprint(resp.json()['status'])" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/users/user-abc-123/update-status/', {\n method: 'PATCH',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ status: 'BLOCKED' }),\n});\nconst user = await resp.json();\nconsole.log(user.status);" - } - ], "responses": { "200": { - "description": "User status updated. Full user record returned.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserDetailItem" - }, - "examples": { - "Blocked": { - "value": { - "didit_internal_id": "f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418", - "vendor_data": "user-abc-123", - "display_name": null, - "full_name": "John Michael Doe", - "date_of_birth": "1990-05-15", - "effective_name": "John Michael Doe", - "status": "BLOCKED", - "metadata": { - "tier": "premium", - "source": "website" - }, - "portrait_image_url": "https:///...", - "session_count": 3, - "approved_count": 2, - "declined_count": 0, - "in_review_count": 1, - "issuing_states": [ - "USA" - ], - "approved_emails": [ - "john@example.com" - ], - "approved_phones": [ - "+14155551234" - ], - "features": { - "ID_VERIFICATION": "Approved", - "LIVENESS": "Approved", - "FACE_MATCH": "Approved", - "AML": "Approved" - }, - "features_list": [ - { - "feature": "ID_VERIFICATION", - "status": "Approved" - }, - { - "feature": "LIVENESS", - "status": "Approved" - }, - { - "feature": "FACE_MATCH", - "status": "Approved" - }, - { - "feature": "AML", - "status": "Approved" - } - ], - "last_session_at": "2025-06-15T10:30:00Z", - "last_activity_at": "2025-06-15T10:30:00Z", - "first_session_at": "2025-06-01T08:00:00Z", - "tags": [ - { - "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", - "tag": { - "uuid": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b", - "name": "VIP", - "color": "#2567FF", - "description": null, - "source": "custom", - "created_at": "2025-05-01T09:00:00Z", - "updated_at": "2025-05-01T09:00:00Z" - }, - "added_by_email": "analyst@acme.com", - "added_by_name": "Jane Analyst", - "created_at": "2025-06-02T11:00:00Z" - } - ], - "comments": [ - { - "uuid": "c1111111-2222-4333-8444-555555555555", - "comment_type": "STATUS_CHANGED", - "comment": null, - "actor_email": null, - "actor_name": null, - "previous_status": "ACTIVE", - "new_status": "BLOCKED", - "previous_value": null, - "new_value": null, - "changed_fields": [], - "metadata": null, - "mentioned_emails": [], - "created_at": "2025-06-15T10:30:00Z" - } - ], - "created_at": "2025-06-01T08:00:00Z", - "updated_at": "2025-06-15T10:30:00Z" - } - } + "$ref": "#/components/schemas/CustomerMessagingCaseCustomerResponseResult" } } - } + }, + "description": "" + }, + "404": { + "description": "The request or case is unavailable for this caller." }, "400": { - "description": "Missing or invalid `status`.", - "content": { - "application/json": { - "examples": { - "Missing status": { - "value": { - "status": [ - "This field is required." - ] - } - }, - "Bad status": { - "value": { - "status": [ - "Invalid status. Valid options are: ACTIVE, FLAGGED, BLOCKED" - ] - } - } - } - } - } + "description": "Invalid inputs, unsupported purpose, unavailable verified contact or an existing immutable response." }, - "401": { - "description": "No credentials supplied. Requests to `/v3/users/*` paths without an `x-api-key` header (or `Authorization: Bearer` token) are rejected by the authentication middleware before reaching the API.", - "content": { - "application/json": { - "examples": { - "Missing credentials": { - "value": { - "detail": "You must be authenticated with a valid access token to access this endpoint." - } - } - } + "409": { + "description": "The preview changed or expired. Preview again before confirming." + } + }, + "security": [ + { + "CaseRequestToken": [] + } + ], + "description": "Access one customer request using its request-specific token. No private case findings, original identity data or recipient address are returned. Actionable requests expire after seven days. Cancellation or case resolution invalidates the link. Responses are immutable and remain evidence for reviewer assessment; they never approve or update a source verification." + } + }, + "/v3/fraud/checks/plan/": { + "post": { + "operationId": "plan_create", + "x-didit-sdk": { + "resource": "fraud_checks", + "method": "plan", + "auth": "api_key", + "server": true + }, + "description": "Validate selected checks and estimate their price without executing, creating a session or billing.", + "summary": "Plan a Fraud Check", + "tags": [ + "Fraud Check" + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/FraudCheckRequestRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/FraudCheckRequestRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/FraudCheckRequestRequest" } } }, - "403": { - "description": "Invalid or revoked API key, or the key cannot update users for this application. Note: on this endpoint an *invalid* key returns `403` (not `401`).", + "required": true + }, + "responses": { + "200": { "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/FraudCheckPlan" } } - } + }, + "description": "" }, - "404": { - "description": "No (non-deleted) user with the supplied `vendor_data` exists for this application.", + "400": { "content": { "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Not found." - } - } + "schema": { + "$ref": "#/components/schemas/FraudBadRequestErrorMessage" } } + }, + "description": "" + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BearerAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/": { + "get": { + "summary": "List routing rules", + "description": "Requires `list:cases` for the requested application.", + "operationId": "list_case_routing_rules", + "x-didit-sdk": { + "resource": "cases.routing_rules", + "method": "list", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Case Assignment" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 } }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 1000000, + "default": 0 + } + } + ], + "responses": { + "200": { + "description": "Success", "content": { "application/json": { - "examples": { - "Rate limited": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseRoutingRule" + } + }, + "count": { + "type": "integer", + "description": "Matching count, capped at 100. Follow next to continue beyond the cap." + }, + "next": { + "type": "string", + "nullable": true + }, + "previous": { + "type": "string", + "nullable": true } } } } } + }, + "400": { + "description": "Invalid input." + }, + "403": { + "description": "Insufficient permission." } }, "security": [ @@ -33080,209 +52561,156 @@ "ApiKeyAuth": [] } ] - } - }, - "/v3/businesses/create/": { + }, "post": { - "summary": "Create business", - "description": "Pre-create a [business](/entities/businesses) *without* a KYB session. All fields are optional, but you should always send `vendor_data` — without it the record cannot be linked to sessions or transactions. When provided, `vendor_data` must be unique among non-deleted businesses (exact match; conflicts return 400). Not idempotent.", - "operationId": "create_business", + "summary": "Create routing rule", + "description": "Requires `write:cases` for the requested application. Set publish=false to create an unpublished draft; Publish creates its first live version. Discarding a never-published draft deletes it.", + "operationId": "create_case_routing_rules", + "x-didit-sdk": { + "resource": "cases.routing_rules", + "method": "create", + "auth": "api_key", + "server": true, + "scope": "application" + }, "tags": [ - "Businesses" + "Case Assignment" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + } ], + "responses": { + "201": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseRoutingRule" + } + } + } + }, + "400": { + "description": "Invalid input." + }, + "403": { + "description": "Insufficient permission." + } + }, "requestBody": { "required": true, "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your unique identifier for this business (free-form string, NOT a UUID). Optional but strongly recommended. When provided, must be unique among non-deleted businesses for the application; matched exactly as sent." - }, - "display_name": { - "type": "string", - "nullable": true, - "maxLength": 255, - "description": "Friendly display name shown in the console (takes precedence over `legal_name` for UI display)." - }, - "legal_name": { - "type": "string", - "nullable": true, - "maxLength": 255, - "description": "Official legal name of the company." - }, - "registration_number": { - "type": "string", - "nullable": true, - "maxLength": 100, - "description": "Company registration or incorporation number." - }, - "country_code": { - "type": "string", - "nullable": true, - "maxLength": 10, - "description": "Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `US`)." - }, - "status": { - "type": "string", - "enum": [ - "ACTIVE", - "FLAGGED", - "BLOCKED" - ], - "default": "ACTIVE", - "description": "Initial lifecycle status. Defaults to `ACTIVE`." - }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Arbitrary JSON object you attach to the business. Defaults to `{}`." - } - } - }, - "examples": { - "Basic": { - "summary": "Recommended minimum", - "value": { - "vendor_data": "company-456", - "legal_name": "New Corp Ltd", - "country_code": "US" - } - }, - "Full": { - "summary": "All fields", - "value": { - "vendor_data": "company-789", - "display_name": "New Corp", - "legal_name": "New Corp International Ltd", - "registration_number": "98765432", - "country_code": "DE", - "status": "ACTIVE", - "metadata": { - "industry": "fintech", - "tier": "enterprise" - } - } - } + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseRoutingRuleCreate" } } } }, - "x-codeSamples": [ + "security": [ { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/businesses/create/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"vendor_data\": \"company-456\", \"legal_name\": \"New Corp Ltd\", \"country_code\": \"US\"}'" + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/publish/": { + "post": { + "operationId": "publish_case_routing_rule", + "x-didit-sdk": { + "resource": "cases.routing_rules", + "method": "publish", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true }, { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/businesses/create/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={\n 'vendor_data': 'company-456',\n 'legal_name': 'New Corp Ltd',\n 'country_code': 'US',\n },\n)\nresp.raise_for_status()\nbiz = resp.json()\nprint(biz['didit_internal_id'])" + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/businesses/create/', {\n method: 'POST',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n vendor_data: 'company-456',\n legal_name: 'New Corp Ltd',\n country_code: 'US',\n }),\n});\nconst biz = await resp.json();\nconsole.log(biz.didit_internal_id);" + "in": "path", + "name": "record_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true } ], - "responses": { - "201": { - "description": "Business created. Full business record returned (same shape as Get Business).", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BusinessDetailItem" - }, - "examples": { - "Created": { - "value": { - "didit_internal_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", - "vendor_data": "company-456", - "display_name": null, - "legal_name": "New Corp Ltd", - "registration_number": null, - "country_code": "US", - "effective_name": "New Corp Ltd", - "status": "ACTIVE", - "session_count": 0, - "approved_count": 0, - "declined_count": 0, - "in_review_count": 0, - "features": {}, - "features_list": [], - "last_session_at": null, - "last_activity_at": "2026-03-15T10:30:00Z", - "first_session_at": null, - "tags": [], - "created_at": "2026-03-15T10:30:00Z", - "metadata": {}, - "updated_at": "2026-03-15T10:30:00Z" - } - } - } + "tags": [ + "Case Assignment" + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/RoutingPublicationPolicyDraftPreconditionRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingPublicationPolicyDraftPreconditionRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/RoutingPublicationPolicyDraftPreconditionRequest" } } }, - "400": { - "description": "Invalid body — typically a duplicate `vendor_data`, an unknown `status`, or malformed `metadata`.", + "required": true + }, + "responses": { + "200": { "content": { "application/json": { - "examples": { - "Duplicate": { - "value": { - "vendor_data": [ - "A business with this vendor_data already exists." - ] - } - }, - "Bad status": { - "value": { - "status": [ - "Invalid status. Valid options are: ACTIVE, FLAGGED, BLOCKED" - ] - } - }, - "Bad metadata": { - "value": { - "metadata": [ - "Metadata must be a JSON object." - ] - } - } + "schema": { + "$ref": "#/components/schemas/RoutingPublicationCaseRoutingRule" } } - } + }, + "description": "" }, "403": { - "description": "Missing, invalid, or revoked API key, or the key cannot create businesses for this application. This endpoint returns `403` (never `401`) for authentication failures, including requests with no credentials at all.", - "content": { - "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } - } - } - } + "description": "Case access is required." }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", - "content": { - "application/json": { - "examples": { - "Rate limited": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } - } - } - } + "409": { + "description": "The published version or draft revision has changed. Reload before retrying." } }, "security": [ @@ -33292,267 +52720,274 @@ ] } }, - "/v3/users/create/": { + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/publish/": { "post": { - "summary": "Create user", - "description": "Pre-create a [user](/entities/users) by `vendor_data` *without* a verification session. `vendor_data` must be unique among non-deleted users for the application (exact match; conflicts return 400). Not idempotent.", - "operationId": "create_user", + "operationId": "publish_case_blueprint", + "x-didit-sdk": { + "resource": "cases.blueprints", + "method": "publish", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "blueprint_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + } + ], "tags": [ - "Users" + "Case Management" ], "requestBody": { - "required": true, "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CasePublicationPolicyDraftPreconditionRequest" + } + }, "application/json": { "schema": { - "type": "object", - "required": [ - "vendor_data" - ], - "properties": { - "vendor_data": { - "type": "string", - "description": "Your unique identifier for this user (free-form string, NOT a UUID). Must be unique among non-deleted users for the application; matched exactly as sent." - }, - "full_name": { - "type": "string", - "nullable": true, - "maxLength": 512, - "description": "Full legal name of the user." - }, - "display_name": { - "type": "string", - "nullable": true, - "description": "Friendly display name shown in the console (takes precedence over `full_name` for UI display).", - "maxLength": 255 - }, - "date_of_birth": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Date of birth in `YYYY-MM-DD` format." - }, - "status": { - "type": "string", - "enum": [ - "ACTIVE", - "FLAGGED", - "BLOCKED" - ], - "default": "ACTIVE", - "description": "Initial lifecycle status. Defaults to `ACTIVE`." - }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Arbitrary JSON object you attach to the user. Defaults to `{}`." - }, - "approved_emails": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Pre-trusted email addresses for this user, e.g. `[\"john@example.com\"]`." - }, - "approved_phones": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Pre-trusted phone numbers for this user, e.g. `[\"+14155551234\"]`." - }, - "issuing_states": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Pre-recorded issuing countries (ISO 3166-1 alpha-3), e.g. `[\"USA\"]`." - } - } - }, - "examples": { - "Basic": { - "summary": "Create with basic info", - "value": { - "vendor_data": "user-abc-123", - "full_name": "John Doe" - } - }, - "Full": { - "summary": "All fields", - "value": { - "vendor_data": "user-abc-456", - "full_name": "Jane Smith", - "display_name": "Jane S.", - "date_of_birth": "1990-05-15", - "status": "ACTIVE", - "metadata": { - "tier": "premium", - "source": "api" - }, - "approved_emails": [ - "jane@example.com" - ] - } - } + "$ref": "#/components/schemas/CasePublicationPolicyDraftPreconditionRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CasePublicationPolicyDraftPreconditionRequest" } } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CasePublicationCaseBlueprint" + } + } + }, + "description": "" + }, + "409": { + "description": "Published version or saved draft revision changed. Reload before retrying." + }, + "403": { + "description": "Required case permission is missing." } }, - "x-codeSamples": [ + "security": [ { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/users/create/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"vendor_data\": \"user-abc-123\", \"full_name\": \"John Doe\"}'" + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/preview/": { + "post": { + "operationId": "organization_application_cases_requests_preview_create", + "x-didit-sdk": { + "resource": "cases.requests", + "method": "preview", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true }, { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/users/create/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={'vendor_data': 'user-abc-123', 'full_name': 'John Doe'},\n)\nresp.raise_for_status()\nuser = resp.json()\nprint(user['didit_internal_id'])" + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/users/create/', {\n method: 'POST',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ vendor_data: 'user-abc-123', full_name: 'John Doe' }),\n});\nconst user = await resp.json();\nconsole.log(user.didit_internal_id);" + "in": "path", + "name": "case_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true } ], + "tags": [ + "Case Management" + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CustomerMessagingCaseRequestPreviewRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerMessagingCaseRequestPreviewRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CustomerMessagingCaseRequestPreviewRequest" + } + } + }, + "required": true + }, "responses": { - "201": { - "description": "User created. Full user record returned (same shape as Get User).", + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserDetailItem" - }, - "examples": { - "Created": { - "value": { - "didit_internal_id": "f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418", - "vendor_data": "user-abc-123", - "display_name": null, - "full_name": "John Doe", - "date_of_birth": null, - "effective_name": "John Doe", - "status": "ACTIVE", - "metadata": {}, - "portrait_image_url": null, - "session_count": 0, - "approved_count": 0, - "declined_count": 0, - "in_review_count": 0, - "issuing_states": [], - "approved_emails": [], - "approved_phones": [], - "features": {}, - "features_list": [], - "last_session_at": null, - "last_activity_at": "2025-06-15T10:30:00Z", - "first_session_at": null, - "tags": [], - "comments": [], - "created_at": "2025-06-15T10:30:00Z", - "updated_at": "2025-06-15T10:30:00Z" - } - } + "$ref": "#/components/schemas/CustomerMessagingCaseRequestPreviewResponse" } } - } + }, + "description": "" + }, + "404": { + "description": "The request or case is unavailable for this caller." + }, + "403": { + "description": "The caller lacks the required cases permission." }, "400": { - "description": "Invalid body — typically a duplicate `vendor_data`, a missing required field, or a malformed value.", - "content": { - "application/json": { - "examples": { - "Duplicate": { - "value": { - "vendor_data": [ - "A user with this vendor_data already exists." - ] - } - }, - "Missing vendor_data": { - "value": { - "vendor_data": [ - "This field is required." - ] - } - }, - "Bad metadata": { - "value": { - "metadata": [ - "Metadata must be a JSON object." - ] - } - }, - "Bad status": { - "value": { - "status": [ - "Invalid status. Valid options are: ACTIVE, FLAGGED, BLOCKED" - ] - } - } - } - } - } + "description": "Invalid inputs, unsupported purpose, unavailable verified contact or an existing immutable response." }, - "401": { - "description": "No credentials supplied. Requests to `/v3/users/*` paths without an `x-api-key` header (or `Authorization: Bearer` token) are rejected by the authentication middleware before reaching the API.", - "content": { - "application/json": { - "examples": { - "Missing credentials": { - "value": { - "detail": "You must be authenticated with a valid access token to access this endpoint." - } - } - } - } - } + "409": { + "description": "The preview changed or expired. Preview again before confirming." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Render the production email without sending or changing the case. Returns the exact recipient, sender, subject, body and a signed preview_token. Send the same contact_id, purpose, language, request_id, expires_at and preview_token to confirm within 15 minutes." + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/action-receipts/": { + "get": { + "operationId": "action_receipts_retrieve", + "x-didit-sdk": { + "resource": "cases", + "method": "list_action_receipts", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "case_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true }, - "403": { - "description": "Invalid or revoked API key, or the key cannot create users for this application. Note: on this endpoint an *invalid* key returns `403` (not `401`).", - "content": { - "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } - } - } + { + "in": "query", + "name": "offset", + "schema": { + "type": "integer", + "maximum": 1000000, + "minimum": 0, + "default": 0 } - }, - "429": { - "description": "Rate limit exceeded; back off and retry after the interval indicated in `Retry-After`.", + } + ], + "tags": [ + "Case Management" + ], + "responses": { + "200": { "content": { "application/json": { - "examples": { - "Rate limited": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } + "schema": { + "$ref": "#/components/schemas/CaseActionCaseActionReceiptPage" } } - } + }, + "description": "" } }, "security": [ { "ApiKeyAuth": [] } - ] + ], + "description": "List subject-action receipts, newest first, with at most 20 results per page. Use next_offset for the next page. Input note text is not exposed in the history." } }, - "/v3/organization/{organization_id}/application/{application_id}/vendor-users/by-id/{didit_internal_id}/faces/upload/": { - "post": { - "summary": "Upload user face", - "description": "Attach a trusted imported face image to an existing User profile. Use this after creating or retrieving a User by `vendor_data` and reading its `didit_internal_id`. The uploaded face is stored on the User profile and indexed for duplicate detection and face search. It is not added to a face blocklist.", - "operationId": "upload_user_face", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/{note_uuid}/": { + "delete": { + "operationId": "delete_network_note", + "x-didit-sdk": { + "resource": "networks.notes", + "method": "delete", + "auth": "api_key", + "server": true, + "scope": "application" + }, "tags": [ - "Users" + "Networks" ], + "summary": "Delete your network note", + "description": "Requires write:networks and the authenticated author of the note. Deletion is irreversible. Connected account and verification data are unaffected.", "parameters": [ { "name": "organization_id", @@ -33562,7 +52997,7 @@ "type": "string", "format": "uuid" }, - "description": "Organization UUID that owns the application." + "description": "Organization UUID." }, { "name": "application_id", @@ -33572,206 +53007,49 @@ "type": "string", "format": "uuid" }, - "description": "Application UUID that owns the User profile." + "description": "Application UUID." }, { - "name": "didit_internal_id", + "name": "network_uuid", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, - "description": "Didit's internal User UUID, returned as `didit_internal_id` by `GET /v3/users/{vendor_data}/` and `POST /v3/users/create/`." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "image" - ], - "properties": { - "image": { - "type": "string", - "format": "byte", - "description": "Raw base64-encoded JPG or PNG face image. Do not include a `data:image/...;base64,` prefix. Max decoded size: 2 MB." - }, - "comment": { - "type": "string", - "description": "Optional operator-visible note for the uploaded face.", - "default": "" - } - } - }, - "examples": { - "Imported face": { - "summary": "Attach a face imported from a previous provider", - "value": { - "image": "/9j/4AAQSkZJRgABAQEA...", - "comment": "Imported from previous KYC provider" - } - } - } - } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/vendor-users/by-id/f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418/faces/upload/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"image\": \"/9j/4AAQSkZJRgABAQEA...\", \"comment\": \"Imported from previous KYC provider\"}'" + "description": "Network UUID." }, { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/vendor-users/by-id/f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418/faces/upload/',\n headers={'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json'},\n json={\n 'image': '/9j/4AAQSkZJRgABAQEA...',\n 'comment': 'Imported from previous KYC provider',\n },\n)\nresp.raise_for_status()\nface = resp.json()\nprint(face['uuid'], face['image_url'])" + "name": "note_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } }, { - "lang": "javascript", - "label": "JavaScript", - "source": "const resp = await fetch('https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/vendor-users/by-id/f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418/faces/upload/', {\n method: 'POST',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({\n image: '/9j/4AAQSkZJRgABAQEA...',\n comment: 'Imported from previous KYC provider',\n }),\n});\nconst face = await resp.json();\nconsole.log(face.uuid, face.image_url);" + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "description": "Console user access token in the form Bearer ." } ], "responses": { - "201": { - "description": "Face uploaded and attached to the User profile.", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "uuid", - "image_url", - "comment", - "uploaded_by", - "created_at" - ], - "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "UUID of the uploaded User face record." - }, - "image_url": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "Signed URL for the uploaded face image." - }, - "comment": { - "type": "string", - "nullable": true, - "description": "Comment supplied when the face was uploaded." - }, - "uploaded_by": { - "type": "string", - "nullable": true, - "description": "Email or actor identifier of the uploader when available." - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "Upload timestamp." - } - } - }, - "examples": { - "Uploaded": { - "value": { - "uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", - "image_url": "https://signed-url.example/face.jpg", - "comment": "Imported from previous KYC provider", - "uploaded_by": "api", - "created_at": "2026-05-18T12:00:00Z" - } - } - } - } - } - }, - "400": { - "description": "Invalid request body or image. Common causes: invalid base64, invalid image, no detectable face, image over 2 MB, missing embedding, or the User already has 5 uploaded faces.", - "content": { - "application/json": { - "examples": { - "No face": { - "value": { - "image": "No face detected in the image. Please upload a clear photo with a visible face." - } - }, - "Too many faces": { - "value": { - "detail": "Maximum of 5 faces per user reached." - } - }, - "Too large": { - "value": { - "image": "Image exceeds maximum size of 2 MB." - } - } - } - } - } + "204": { + "description": "Note deleted." }, "401": { - "description": "Missing or invalid API key.", - "content": { - "application/json": { - "examples": { - "Missing key": { - "value": { - "detail": "Authentication credentials were not provided." - } - } - } - } - } + "description": "Authentication is required." }, "403": { - "description": "The credential is valid but cannot write User profiles for this application.", - "content": { - "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } - } - } - } + "description": "Missing permission or Networks access." }, "404": { - "description": "No User exists for that `didit_internal_id` in the application.", - "content": { - "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Not found." - } - } - } - } - } - }, - "429": { - "description": "Rate limit exceeded; back off and retry.", - "content": { - "application/json": { - "examples": { - "Rate limited": { - "value": { - "detail": "Request was throttled. Expected available in 30 seconds." - } - } - } - } - } + "description": "Network or note is not visible to this application." } }, "security": [ @@ -33781,858 +53059,710 @@ ] } }, - "/v3/session/{sessionId}/update-data/": { - "patch": { - "summary": "Update KYC data extracted from the ID document on a verification session", - "description": "Correct the identity fields that were extracted (OCR / NFC) from the user's ID document — misread names, dates, document numbers, and so on — after the verification has finished. Typical callers are a compliance analyst working from your own back office, or an automated reconciliation job. Every field in the body is optional: only the keys you send are written, everything else is left untouched (`PATCH` semantics).\n\nThe session must belong to your application and be in `Approved`, `Declined`, `In Review`, or `Kyc Expired` status. Sessions in any other state (e.g. still `In Progress`) return `404` with `\"No Session matches the given query.\"` — distinct from the `\"Not found.\"` returned for an unknown or deleted `sessionId`. The session must also carry an ID-verification (KYC) record: sessions whose workflow never ran an ID-verification step return `404` (`\"The session does not have a KYC.\"`). For graph workflows with multiple ID-verification steps, target a specific step with `node_id` (query parameter or body field); when omitted, the most recent KYC record is patched.\n\nTwo derived fields are recomputed on every call: `full_name` (from `first_name` + `last_name`) and `issuing_state_name` (from `issuing_state`). If the free-text `address` changes, the server may also re-geocode it and refresh `parsed_address` (skipped for sandbox sessions). `extra_fields` and `parsed_address` are merged key-by-key rather than replaced — see the per-field descriptions for the `null`-to-delete semantics.\n\nSubmitting at least one valid field fires the `data.updated` [webhook](/integration/webhooks) (trigger `manual_review`) — even when the submitted values equal the stored ones. When values actually change, a `KYC_DATA_UPDATED` activity entry recording the changed fields with previous and new values is appended to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and the console activity timeline. The session's status and decision are **not** re-evaluated — use [Update Status](#patch-/v3/session/-sessionId-/update-status/) to change the outcome. An empty JSON body (`{}`) is accepted and simply returns the current KYC data without firing the webhook.\n\nAccepts an API key or a console user token; either way the credential needs the `write:sessions` permission. Authentication and permission failures both return `403` — this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. This endpoint has a dedicated rate limit of 10 requests per minute per API key (invalid requests count too); the shared write budget of 300 requests per minute also applies.", - "operationId": "patch_v3_session_update_data", - "tags": [ - "Sessions" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/action-receipts/{receipt_uuid}/retry/": { + "post": { + "operationId": "action_receipts_retry_create", + "x-didit-sdk": { + "resource": "cases", + "method": "retry_action_receipt", + "auth": "api_key", + "server": true, + "scope": "application" + }, "parameters": [ { - "name": "sessionId", "in": "path", - "required": true, - "description": "UUID of the User Verification (KYC) session whose extracted document data you are correcting. Business (KYB) session IDs are not valid here — for company data use [Update KYB Company Data](#patch-/v3/session/-sessionId-/kyb/-companyUuid-/update-data/).", - "schema": { - "type": "string", - "format": "uuid", - "example": "11111111-2222-3333-4444-555555555555" - } - }, - { - "name": "node_id", - "in": "query", - "required": false, - "description": "Workflow graph node whose KYC record to patch, for graph workflows with multiple ID-verification steps (also accepted as a `node_id` body field). When omitted, the most recent KYC record on the session is patched. If no KYC exists for the given node, the call returns `404` (`\"The session does not have a KYC for node ''.\"`).", - "schema": { - "type": "string", - "example": "feature_ocr" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "description": "All fields optional — send only what you want to change.", - "properties": { - "document_type": { - "type": "string", - "description": "Document type code. Accepts the short codes listed in the enum, plus six long-form aliases normalized server-side before validation: `Passport` → `P`, `Identity Card` → `ID`, `Driver's License` → `DL`, `Residence Permit` → `RP`, `Health Insurance Card` → `HIC`, `Tax Card` → `TC`. Any other value (including `PASSPORT` in caps) returns `400`.", - "enum": [ - "P", - "DL", - "ID", - "RP", - "SSC", - "HIC", - "WP", - "TC", - "VISA", - "PSC", - "BC", - "OTHER" - ], - "nullable": true, - "example": "P" - }, - "document_subtype": { - "type": "string", - "maxLength": 128, - "nullable": true, - "description": "Document subtype from the document registry, including the region prefix when the registry distinguishes regional variants." - }, - "document_number": { - "type": "string", - "maxLength": 255, - "nullable": true, - "description": "Document number as printed on the ID.", - "example": "PAB123456" - }, - "personal_number": { - "type": "string", - "maxLength": 255, - "nullable": true, - "description": "Personal / national identification number where the document carries one (e.g. Spanish DNI on a passport).", - "example": "99999999R" - }, - "date_of_birth": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Date of birth in `YYYY-MM-DD`. Any other format returns `400`.", - "example": "1980-01-01" - }, - "date_of_issue": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Issue date of the document in `YYYY-MM-DD`." - }, - "expiration_date": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Expiration date of the document in `YYYY-MM-DD`." - }, - "issuing_state": { - "type": "string", - "nullable": true, - "description": "Issuing country as an ISO 3166-1 alpha-3 code. Case-insensitive (`esp` is stored as `ESP`); the human-readable `issuing_state_name` shown in [Get Decision](#get-/v3/session/-sessionId-/decision/) is recomputed from this value. Invalid codes return `400`.", - "example": "ESP" - }, - "first_name": { - "type": "string", - "maxLength": 255, - "nullable": true, - "description": "Given name(s). The stored `full_name` is recomputed as `first_name + ' ' + last_name` on every call to this endpoint.", - "example": "Carmen" - }, - "last_name": { - "type": "string", - "maxLength": 255, - "nullable": true, - "description": "Family name(s). Also feeds the recomputed `full_name`.", - "example": "Espanola Espanola" - }, - "gender": { - "type": "string", - "enum": [ - "M", - "F", - "U" - ], - "nullable": true, - "description": "Sex as printed on the document: `M` (male), `F` (female), or `U` (unknown). Any other value — including `X` — returns `400`." - }, - "address": { - "type": "string", - "maxLength": 255, - "nullable": true, - "description": "Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it synchronously before responding — the 200 body already reflects the refreshed `parsed_address`. The re-geocode also requires `issuing_state` to be present on the KYC record." - }, - "place_of_birth": { - "type": "string", - "maxLength": 255, - "nullable": true, - "description": "Place of birth as printed on the document." - }, - "nationality": { - "type": "string", - "maxLength": 32, - "nullable": true, - "description": "Nationality as printed on the document. Free text up to 32 characters — typically an ISO 3166-1 alpha-3 code (e.g. `ESP`), but not validated as one." - }, - "marital_status": { - "type": "string", - "enum": [ - "SINGLE", - "MARRIED", - "DIVORCED", - "WIDOWED", - "UNKNOWN" - ], - "description": "Marital status where the document carries it. Any other value returns `400`." - }, - "extra_fields": { - "type": "object", - "additionalProperties": true, - "nullable": true, - "description": "Per-key overrides for document-specific extra fields (e.g. `blood_group`, `dl_categories`, `mrz_string`). Keys are merged into the existing override map: send a key with a value to set it, or with `null` / `\"\"` to remove that key from the merged view. Send the whole field as `null` to clear all previously applied overrides. Must be a JSON object, otherwise `400`. The response always returns the merged view (OCR-extracted values with your overrides applied)." - }, - "parsed_address": { - "type": "object", - "nullable": true, - "description": "Structured address. Only these nested keys are accepted: `address_type`, `city`, `region`, `street_1`, `street_2`, `postal_code`, `country` — any other key returns `400`. Per-key semantics: a value sets the key, `null` removes it. `formatted_address` is recomputed server-side by joining `street_1`, `street_2`, `city`, `region`, `postal_code`, and `country` and is returned in the response — you cannot set it directly.", - "properties": { - "address_type": { - "type": "string", - "nullable": true, - "description": "Free-form address classification (e.g. `home`)." - }, - "city": { - "type": "string", - "nullable": true, - "example": "Madrid" - }, - "region": { - "type": "string", - "nullable": true, - "description": "State / province / autonomous community." - }, - "street_1": { - "type": "string", - "nullable": true, - "example": "Avda de Madrid 34" - }, - "street_2": { - "type": "string", - "nullable": true - }, - "postal_code": { - "type": "string", - "nullable": true, - "example": "28013" - }, - "country": { - "type": "string", - "nullable": true, - "description": "ISO 3166-1 alpha-2 country code. Lowercase input is normalized to uppercase; alpha-3 codes are rejected with `400`.", - "example": "ES" - } - } - } - } - }, - "examples": { - "Correct name and DOB": { - "summary": "Fix OCR-misread name, date of birth, and structured address", - "value": { - "first_name": "Carmen", - "last_name": "Espanola Espanola", - "date_of_birth": "1980-01-01", - "issuing_state": "ESP", - "marital_status": "MARRIED", - "extra_fields": { - "blood_group": "O+" - }, - "parsed_address": { - "street_1": "Avda de Madrid 34", - "city": "Madrid", - "postal_code": "28013", - "country": "ES" - } - } - }, - "Remove an extra field": { - "summary": "Delete one extra-field key, keep the rest", - "value": { - "extra_fields": { - "blood_group": null - } - } - }, - "Long-form document type": { - "summary": "Long-form alias is normalized to the short code (here P)", - "value": { - "document_type": "Passport" - } + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "case_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "receipt_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + } + ], + "tags": [ + "Case Management" + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseActionCaseActionReceipt" } } - } + }, + "description": "" + } + }, + "security": [ + { + "ApiKeyAuth": [] } + ], + "description": "Retry only this failed subject action using its saved inputs. Current write permission is checked. Applied actions return their receipt without executing again. Case closure is independent." + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/": { + "get": { + "operationId": "get_case_blueprint_draft", + "x-didit-sdk": { + "resource": "cases.blueprints", + "method": "retrieve_draft", + "auth": "api_key", + "server": true, + "scope": "application" }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "blueprint_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + } + ], + "tags": [ + "Case Management" + ], "responses": { "200": { - "description": "KYC data updated. Returns the **full current KYC record** (every updatable field with its stored value, plus the merged `extra_fields` and the current `parsed_address`) — not just the fields you submitted. The `data.updated` webhook fires when at least one valid field was in the payload.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "document_type": { - "type": "string", - "description": "Document type code. Accepts the short codes listed in the enum, plus six long-form aliases normalized server-side before validation: `Passport` → `P`, `Identity Card` → `ID`, `Driver's License` → `DL`, `Residence Permit` → `RP`, `Health Insurance Card` → `HIC`, `Tax Card` → `TC`. Any other value (including `PASSPORT` in caps) returns `400`.", - "enum": [ - "P", - "DL", - "ID", - "RP", - "SSC", - "HIC", - "WP", - "TC", - "VISA", - "PSC", - "BC", - "OTHER" - ], - "nullable": true, - "example": "P" - }, - "document_subtype": { - "type": "string", - "maxLength": 128, - "nullable": true, - "description": "Document subtype from the document registry, including the region prefix when the registry distinguishes regional variants." - }, - "document_number": { - "type": "string", - "maxLength": 255, - "nullable": true, - "description": "Document number as printed on the ID.", - "example": "PAB123456" - }, - "personal_number": { - "type": "string", - "maxLength": 255, - "nullable": true, - "description": "Personal / national identification number where the document carries one (e.g. Spanish DNI on a passport).", - "example": "99999999R" - }, - "date_of_birth": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Date of birth in `YYYY-MM-DD`. Any other format returns `400`.", - "example": "1980-01-01" - }, - "date_of_issue": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Issue date of the document in `YYYY-MM-DD`." - }, - "expiration_date": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Expiration date of the document in `YYYY-MM-DD`." - }, - "issuing_state": { - "type": "string", - "nullable": true, - "description": "Issuing country as an ISO 3166-1 alpha-3 code. Case-insensitive (`esp` is stored as `ESP`); the human-readable `issuing_state_name` shown in [Get Decision](#get-/v3/session/-sessionId-/decision/) is recomputed from this value. Invalid codes return `400`.", - "example": "ESP" - }, - "first_name": { - "type": "string", - "maxLength": 255, - "nullable": true, - "description": "Given name(s). The stored `full_name` is recomputed as `first_name + ' ' + last_name` on every call to this endpoint.", - "example": "Carmen" - }, - "last_name": { - "type": "string", - "maxLength": 255, - "nullable": true, - "description": "Family name(s). Also feeds the recomputed `full_name`.", - "example": "Espanola Espanola" - }, - "gender": { - "type": "string", - "enum": [ - "M", - "F", - "U" - ], - "nullable": true, - "description": "Sex as printed on the document: `M` (male), `F` (female), or `U` (unknown). Any other value — including `X` — returns `400`." - }, - "address": { - "type": "string", - "maxLength": 255, - "nullable": true, - "description": "Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it in the background of the same request and may overwrite `parsed_address` with the geocoder result." - }, - "place_of_birth": { - "type": "string", - "maxLength": 255, - "nullable": true, - "description": "Place of birth as printed on the document." - }, - "nationality": { - "type": "string", - "maxLength": 32, - "nullable": true, - "description": "Nationality as printed on the document. Free text up to 32 characters — typically an ISO 3166-1 alpha-3 code (e.g. `ESP`), but not validated as one." - }, - "marital_status": { - "type": "string", - "enum": [ - "SINGLE", - "MARRIED", - "DIVORCED", - "WIDOWED", - "UNKNOWN" - ], - "description": "Marital status where the document carries it. Any other value returns `400`." - }, - "extra_fields": { - "type": "object", - "additionalProperties": true, - "nullable": true, - "description": "Merged view of the document's extra fields: the OCR-extracted values with all operator overrides applied. Keys removed via a `null` override are absent from this map." - }, - "parsed_address": { - "type": "object", - "nullable": true, - "description": "Current structured address (with internal bookkeeping keys stripped), or `null` when none exists. Includes the recomputed `formatted_address` alongside the seven editable keys.", - "properties": { - "address_type": { - "type": "string", - "nullable": true, - "description": "Free-form address classification (e.g. `home`)." - }, - "city": { - "type": "string", - "nullable": true, - "example": "Madrid" - }, - "region": { - "type": "string", - "nullable": true, - "description": "State / province / autonomous community." - }, - "street_1": { - "type": "string", - "nullable": true, - "example": "Avda de Madrid 34" - }, - "street_2": { - "type": "string", - "nullable": true - }, - "postal_code": { - "type": "string", - "nullable": true, - "example": "28013" - }, - "country": { - "type": "string", - "nullable": true, - "description": "ISO 3166-1 alpha-2 country code. Lowercase input is normalized to uppercase; alpha-3 codes are rejected with `400`.", - "example": "ES" - }, - "formatted_address": { - "type": "string", - "nullable": true, - "description": "Joined, human-readable form of the structured address. Recomputed server-side after every `parsed_address` edit.", - "example": "Avda de Madrid 34, Madrid, 28013, ES" - } - } - } - } - }, - "examples": { - "Updated KYC": { - "summary": "Full KYC record after the correction", - "value": { - "document_type": "P", - "document_subtype": null, - "document_number": "PAB123456", - "personal_number": "99999999R", - "date_of_birth": "1980-01-01", - "date_of_issue": "2021-02-09", - "expiration_date": "2031-02-08", - "issuing_state": "ESP", - "first_name": "Carmen", - "last_name": "Espanola Espanola", - "gender": "F", - "address": "AVDA DE MADRID 34, MADRID, MADRID 28013", - "place_of_birth": "MADRID", - "nationality": "ESP", - "marital_status": "MARRIED", - "extra_fields": { - "mrz_string": "P\": [\"\"]}`; errors on nested `parsed_address` / `extra_fields` keys use an object value: `{\"parsed_address\": {\"\": \"\"}}`.", + "required": true + }, + "responses": { + "200": { "content": { "application/json": { - "examples": { - "Invalid gender": { - "summary": "gender must be M, F, or U", - "value": { - "gender": [ - "\"X\" is not a valid choice." - ] - } - }, - "Invalid document type": { - "summary": "Not a short code or long-form alias", - "value": { - "document_type": [ - "\"PASSPORT\" is not a valid choice." - ] - } - }, - "Invalid issuing state": { - "summary": "issuing_state is not ISO 3166-1 alpha-3", - "value": { - "issuing_state": [ - "Invalid ISO 3166-1 alpha-3 country code for issuing_state." - ] - } - }, - "Invalid marital status": { - "summary": "marital_status not in the enum", - "value": { - "marital_status": [ - "\"ENGAGED\" is not a valid choice." - ] - } - }, - "Bad date format": { - "summary": "Dates must be YYYY-MM-DD", - "value": { - "date_of_birth": [ - "Date has wrong format. Use one of these formats instead: YYYY-MM-DD." - ] - } - }, - "Unsupported parsed_address key": { - "summary": "Only the seven documented keys are allowed", - "value": { - "parsed_address": { - "zipcode": "Unsupported key. Allowed keys: address_type, city, region, street_1, street_2, postal_code, country" - } - } - }, - "Invalid parsed_address country": { - "summary": "Nested country must be alpha-2, not alpha-3", - "value": { - "parsed_address": { - "country": "Invalid ISO 3166-1 alpha-2 country code." - } - } - }, - "extra_fields not an object": { - "summary": "extra_fields must be a JSON object", - "value": { - "extra_fields": [ - "This field must be a JSON object." - ] - } - } + "schema": { + "$ref": "#/components/schemas/CasePublicationPolicyDraft" } } + }, + "description": "" + }, + "409": { + "description": "Published version or saved draft revision changed. Reload before retrying." + }, + "403": { + "description": "Required case permission is missing." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "delete": { + "operationId": "discard_case_blueprint_draft", + "x-didit-sdk": { + "resource": "cases.blueprints", + "method": "discard_draft", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "blueprint_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + } + ], + "tags": [ + "Case Management" + ], + "responses": { + "204": { + "description": "No response body" + }, + "409": { + "description": "Published version or saved draft revision changed. Reload before retrying." + }, + "403": { + "description": "Required case permission is missing." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CasePublicationPolicyDraftPreconditionRequest" + } } + } + } + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/teams/{record_uuid}/": { + "get": { + "summary": "Retrieve review team", + "description": "Requires `read:cases` for the requested application.", + "operationId": "retrieve_case_teams", + "x-didit-sdk": { + "resource": "cases.teams", + "method": "retrieve", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Case Assignment" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, - "403": { - "description": "Missing/invalid credentials or insufficient permissions. This API returns `403` for authentication failures — never `401`.", + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "record_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", "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": "Credential lacks write:sessions", - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/CaseReviewTeam" } } } }, + "400": { + "description": "Invalid input." + }, + "403": { + "description": "Insufficient permission." + }, "404": { - "description": "Unknown session, session in a non-editable state, or no KYC record to patch. Each case has a distinct `detail` message (see examples). The unknown-session lookup runs before credential checks.", + "description": "Record not found in this application." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "patch": { + "summary": "Update review team", + "description": "Requires `write:cases` for the requested application.", + "operationId": "update_case_teams", + "x-didit-sdk": { + "resource": "cases.teams", + "method": "update", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Case Assignment" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "record_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", "content": { "application/json": { - "examples": { - "Unknown session": { - "summary": "No session with this ID in your application (or it was deleted)", - "value": { - "detail": "Not found." - } - }, - "Wrong session state": { - "summary": "Session exists but is not Approved / Declined / In Review / Kyc Expired", - "value": { - "detail": "No Session matches the given query." - } - }, - "No KYC record": { - "summary": "The workflow never ran an ID-verification step", - "value": { - "detail": "The session does not have a KYC." - } - }, - "No KYC for node": { - "summary": "node_id given but no KYC exists for it", - "value": { - "detail": "The session does not have a KYC for node 'feature_missing'." - } - } + "schema": { + "$ref": "#/components/schemas/CaseReviewTeam" } } } }, - "429": { - "description": "Dedicated rate limit exceeded — 10 KYC data updates per minute per API key (rejected requests count toward the budget). The shared 300/min write budget can also trigger with its own message. Inspect `Retry-After` and the `X-RateLimit-*` response headers before retrying.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } - }, - "examples": { - "Rate limited": { - "summary": "KYC update budget exhausted", - "value": { - "detail": "Session KYC data update rate limit exceeded. You can make up to 10 requests per minute." - } - } - } + "400": { + "description": "Invalid input." + }, + "403": { + "description": "Insufficient permission." + }, + "404": { + "description": "Record not found in this application." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseReviewTeamUpdate" } } } }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-data/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"first_name\": \"Carmen\",\n \"last_name\": \"Espanola Espanola\",\n \"date_of_birth\": \"1980-01-01\",\n \"issuing_state\": \"ESP\",\n \"parsed_address\": {\n \"street_1\": \"Avda de Madrid 34\",\n \"city\": \"Madrid\",\n \"postal_code\": \"28013\",\n \"country\": \"ES\"\n }\n }'" - }, - { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresponse = requests.patch(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-data/\",\n headers={\n \"x-api-key\": \"YOUR_API_KEY\",\n \"Content-Type\": \"application/json\",\n },\n json={\n \"first_name\": \"Carmen\",\n \"last_name\": \"Espanola Espanola\",\n \"date_of_birth\": \"1980-01-01\",\n \"issuing_state\": \"ESP\",\n \"extra_fields\": {\"blood_group\": \"O+\"},\n \"parsed_address\": {\"city\": \"Madrid\", \"postal_code\": \"28013\", \"country\": \"ES\"},\n },\n)\nresponse.raise_for_status()\nkyc = response.json() # full current KYC record\nprint(kyc[\"first_name\"], kyc[\"parsed_address\"][\"formatted_address\"])" - }, + "security": [ { - "lang": "javascript", - "label": "JavaScript", - "source": "const response = await fetch(\n 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-data/',\n {\n method: 'PATCH',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n first_name: 'Carmen',\n last_name: 'Espanola Espanola',\n date_of_birth: '1980-01-01',\n issuing_state: 'ESP',\n parsed_address: { city: 'Madrid', postal_code: '28013', country: 'ES' },\n }),\n },\n);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst kyc = await response.json(); // full current KYC record" + "ApiKeyAuth": [] } ] } }, - "/v3/session/{sessionId}/features/{nodeId}/update-status/": { - "patch": { - "summary": "Update an individual feature (step) status", - "description": "Set the status of a single verification feature (step) within a session **without changing the overall session decision**. Target the feature by its workflow graph `nodeId` (use `default` for non-graph workflows where the step has no node).\n\nWorks for both User Verification (KYC) and Business Verification (KYB) sessions and every step type — ID/OCR, Liveness, Face Match, NFC, AML, Proof of Address, Phone, Email, Database Validation, Questionnaire, and the KYB registry / documents / key-people checks.\n\n`new_status` must be one of `Approved`, `Declined`, or `In Review`, and must differ from the feature's current status. A successful call fires a `data.updated` webhook and appends an activity entry (with actor attribution) to the session's review trail. This endpoint is **not** available for standalone API sessions (`session_type = API`).\n\nAuthenticate with your API key (`x-api-key`) or a user authorization header — either must carry the `write:sessions` privilege.", - "operationId": "patch_v3_session_update_feature_status", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/report/": { + "get": { + "operationId": "download_network_report", + "x-didit-sdk": { + "resource": "networks", + "method": "download_report", + "auth": "api_key", + "server": true, + "scope": "application" + }, "tags": [ - "Sessions" + "Networks" + ], + "summary": "Download a network evidence report", + "description": "Download a PDF of the current organization network, its observed account connections, signal references and review status. Requires read:networks. Global intelligence and private analyst notes are not included. Each account and signal list is capped at 500 entries and the report states when it is incomplete. The request does not change network status. Errors are returned as JSON.", + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "network_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Network UUID." + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "description": "Console user access token in the form Bearer ." + } ], + "responses": { + "200": { + "description": "Network evidence report. Cache-Control: no-store.", + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "401": { + "description": "Authentication is required." + }, + "403": { + "description": "Missing permission or Networks access." + }, + "404": { + "description": "Network or note is not visible to this application." + } + }, "security": [ { "ApiKeyAuth": [] } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/": { + "get": { + "operationId": "list_network_notes", + "x-didit-sdk": { + "resource": "networks.notes", + "method": "list", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Networks" ], + "summary": "List private network notes", + "description": "Console users with read:networks can read notes on a network visible to the application. Notes stay within the organization and are not contributed to shared intelligence. Newest notes appear first.", "parameters": [ { + "name": "organization_id", "in": "path", - "name": "sessionId", "required": true, - "description": "UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs — the service resolves the ID across both session types.", "schema": { "type": "string", - "format": "uuid", - "example": "11111111-2222-3333-4444-555555555555" - } + "format": "uuid" + }, + "description": "Organization UUID." }, { + "name": "application_id", "in": "path", - "name": "nodeId", "required": true, - "description": "Workflow graph node ID of the feature to update (for example `feature_ocr_1`). Use the literal value `default` for non-graph (single-step) workflows where the feature has no node ID.", "schema": { "type": "string", - "example": "feature_ocr_1" + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "network_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Network UUID." + }, + { + "name": "Authorization", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "description": "Console user access token in the form Bearer ." + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 30 } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "new_status" - ], - "properties": { - "new_status": { - "type": "string", - "description": "Target status for this feature. Must differ from the current value.", - "enum": [ - "Approved", - "Declined", - "In Review" - ], - "example": "Approved" - }, - "comment": { - "type": "string", - "description": "Optional free-text note recorded on the session's review trail for this feature change.", - "example": "Document manually verified by compliance." - } - } - } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 1000000, + "default": 0 } } - }, + ], "responses": { "200": { - "description": "Feature status updated. Returns the targeted feature, its node ID, and the new status.", + "description": "A page of notes. Count is capped at 100; use next to continue.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "session_id": { - "type": "string", - "format": "uuid" + "count": { + "type": "integer" }, - "node_id": { + "next": { "type": "string", - "example": "feature_ocr_1" + "nullable": true }, - "feature_type": { + "previous": { "type": "string", - "description": "Model name of the updated feature (e.g. `KYC`, `Face`, `FaceMatch`, `AML`, `POA`).", - "example": "KYC" + "nullable": true }, - "new_status": { - "type": "string", - "example": "Approved" + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkNote" + } } } - }, - "example": { - "session_id": "11111111-2222-3333-4444-555555555555", - "node_id": "feature_ocr_1", - "feature_type": "KYC", - "new_status": "Approved" } } } }, - "400": { - "description": "Validation error. Returned when `new_status` is invalid or unchanged, or when the session is a standalone API session.", - "content": { - "application/json": { - "examples": { - "Same Status": { - "summary": "new_status equals the current status", - "value": { - "new_status": "The new status is the same as the current status." - } - }, - "API Session": { - "summary": "Feature status cannot be changed on standalone API sessions", - "value": { - "detail": "Cannot change feature status for API sessions." - } - } - } - } - } + "401": { + "description": "Authentication is required." }, "403": { - "description": "Missing/invalid credentials or insufficient permissions. This API returns `403` for authentication failures — never `401`.", - "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": "API key lacks write:sessions", - "value": { - "detail": "You do not have permission to perform this action." - } - } - } - } - } + "description": "Missing permission or Networks access." }, "404": { - "description": "The session was not found, or it has no feature with the given `nodeId`.", - "content": { - "application/json": { - "examples": { - "Session Not Found": { - "summary": "Unknown session_id", - "value": { - "detail": "Session not found." - } - }, - "Feature Not Found": { - "summary": "No feature for this node_id", - "value": { - "detail": "No feature found with node_id 'feature_ocr_1' in this session." - } - } - } - } - } + "description": "Network or note is not visible to this application." } }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/features/feature_ocr_1/update-status/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"new_status\": \"Approved\",\n \"comment\": \"Document manually verified by compliance.\"\n }'" - }, - { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresponse = requests.patch(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/features/feature_ocr_1/update-status/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\", \"Content-Type\": \"application/json\"},\n json={\"new_status\": \"Approved\", \"comment\": \"Document manually verified by compliance.\"},\n)\nprint(response.json())" - } - ] - } - }, - "/v3/session/{sessionId}/update-aml-hit-status/": { - "patch": { - "summary": "Update an AML hit review status", - "description": "Set the review status of a **single AML hit** (a potential sanction / PEP / watchlist / adverse-media match) returned by AML screening, without changing the overall session decision. Identify the hit by its `hit_id` (the `id` of an entry in the AML check's `hits` array — see the session decision).\n\n`review_status` is one of `Unreviewed`, `Confirmed Match`, `False Positive`, or `Inconclusive`. Reviewer decisions are preserved across ongoing-monitoring updates, so a hit you mark `False Positive` stays that way when the provider re-screens.\n\nA successful call fires a `data.updated` webhook and appends an activity entry (with actor attribution) to the session's review trail. For graph workflows with multiple AML checks, pass `node_id` to target a specific check.\n\nAuthenticate with your API key (`x-api-key`) or a user authorization header — either must carry the `write:sessions` privilege.", - "operationId": "patch_v3_session_update_aml_hit_status", - "tags": [ - "Sessions" - ], "security": [ { "ApiKeyAuth": [] } + ] + }, + "post": { + "operationId": "create_network_note", + "x-didit-sdk": { + "resource": "networks.notes", + "method": "create", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Networks" ], + "summary": "Add a private network note", + "description": "Requires a console user with write:networks. The author is assigned from the authenticated user. Supply a new random UUID client_reference for each note; retry the same body and reference after an uncertain response to avoid duplicates. Reusing a reference for another body, author or network returns 409. Notes follow the surviving investigation when networks merge.", "parameters": [ { + "name": "organization_id", "in": "path", - "name": "sessionId", "required": true, - "description": "UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs — the service resolves the ID across both session types.", "schema": { "type": "string", - "format": "uuid", - "example": "11111111-2222-3333-4444-555555555555" - } + "format": "uuid" + }, + "description": "Organization UUID." + }, + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + }, + { + "name": "network_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Network UUID." }, { - "in": "query", - "name": "node_id", - "required": false, - "description": "Workflow graph node ID. Required only when the session has multiple AML checks (one per node); omit it for single-AML sessions.", + "name": "Authorization", + "in": "header", + "required": true, "schema": { - "type": "string", - "example": "feature_aml_1" - } + "type": "string" + }, + "description": "Console user access token in the form Bearer ." } ], "requestBody": { @@ -34642,30 +53772,18 @@ "schema": { "type": "object", "required": [ - "hit_id", - "review_status" + "comment", + "client_reference" ], "properties": { - "hit_id": { - "type": "string", - "description": "The `id` of the hit in the AML check's `hits` array.", - "example": "abc123" - }, - "review_status": { + "comment": { "type": "string", - "description": "The new review status for the hit.", - "enum": [ - "Unreviewed", - "Confirmed Match", - "False Positive", - "Inconclusive" - ], - "example": "False Positive" + "minLength": 1, + "maxLength": 10000 }, - "node_id": { + "client_reference": { "type": "string", - "description": "Optional graph node ID. Use it (here or as a query parameter) when the session has multiple AML checks.", - "example": "feature_aml_1" + "format": "uuid" } } } @@ -34673,579 +53791,473 @@ } }, "responses": { - "200": { - "description": "Hit review status updated.", + "201": { + "description": "Saved private note.", "content": { "application/json": { - "example": { - "message": "Hit status updated successfully", - "hit_id": "abc123", - "review_status": "False Positive" + "schema": { + "$ref": "#/components/schemas/NetworkNote" } } } }, - "400": { - "description": "Validation error — `hit_id` empty or `review_status` not one of the allowed values.", + "200": { + "description": "Saved private note.", "content": { "application/json": { - "example": { - "review_status": [ - "\"Maybe\" is not a valid choice." - ] + "schema": { + "$ref": "#/components/schemas/NetworkNote" } } } }, + "400": { + "description": "Invalid or blank note." + }, + "409": { + "description": "The reference was already used for another note." + }, + "401": { + "description": "Authentication is required." + }, "403": { - "description": "Missing/invalid credentials or insufficient permissions. This API returns `403` for authentication failures — never `401`.", + "description": "Missing permission or Networks access." + }, + "404": { + "description": "Network or note is not visible to this application." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/": { + "get": { + "operationId": "get_case_routing_rule_draft", + "x-didit-sdk": { + "resource": "cases.routing_rules", + "method": "retrieve_draft", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "record_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + } + ], + "tags": [ + "Case Assignment" + ], + "responses": { + "200": { "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": "API key lacks write:sessions", - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/RoutingPublicationPolicyDraftResponse" } } + }, + "description": "" + }, + "403": { + "description": "Case access is required." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "put": { + "operationId": "save_case_routing_rule_draft", + "x-didit-sdk": { + "resource": "cases.routing_rules", + "method": "save_draft", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "record_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + } + ], + "tags": [ + "Case Assignment" + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/RoutingPublicationPolicyDraftWriteRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingPublicationPolicyDraftWriteRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/RoutingPublicationPolicyDraftWriteRequest" + } } }, - "404": { - "description": "The session was not found, the session has no AML check (optionally for the given `node_id`), or no hit matches `hit_id`.", + "required": true + }, + "responses": { + "200": { "content": { "application/json": { - "examples": { - "No AML Check": { - "summary": "Session has no AML check", - "value": { - "detail": "The session does not have an AML check." - } - }, - "Hit Not Found": { - "summary": "Unknown hit_id", - "value": { - "detail": "Hit with ID 'abc123' not found in the AML check." - } - } + "schema": { + "$ref": "#/components/schemas/RoutingPublicationPolicyDraft" } } - } + }, + "description": "" + }, + "403": { + "description": "Case access is required." + }, + "409": { + "description": "The published version or draft revision has changed. Reload before retrying." } }, - "x-codeSamples": [ + "security": [ { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-aml-hit-status/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"hit_id\": \"abc123\",\n \"review_status\": \"False Positive\"\n }'" + "ApiKeyAuth": [] + } + ] + }, + "delete": { + "operationId": "discard_case_routing_rule_draft", + "x-didit-sdk": { + "resource": "cases.routing_rules", + "method": "discard_draft", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true }, { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresponse = requests.patch(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-aml-hit-status/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\", \"Content-Type\": \"application/json\"},\n json={\"hit_id\": \"abc123\", \"review_status\": \"False Positive\"},\n)\nprint(response.json())" + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "record_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true } - ] - } - }, - "/v3/session/{sessionId}/bulk-update-aml-hit-status/": { - "patch": { - "summary": "Bulk update AML hit review statuses", - "description": "Update the review status of **multiple AML hits** in one call. Provide `hit_updates`, a list of `{ hit_id, review_status }` pairs. `review_status` is one of `Unreviewed`, `Confirmed Match`, `False Positive`, or `Inconclusive`.\n\nThe update is all-or-nothing: if any `hit_id` is not found in the AML check, the request returns `404` and no hit is changed. A successful call fires a single `data.updated` webhook and records one activity entry covering all changes. For graph workflows with multiple AML checks, pass `node_id` to target a specific check.\n\nAuthenticate with your API key (`x-api-key`) or a user authorization header — either must carry the `write:sessions` privilege.", - "operationId": "patch_v3_session_bulk_update_aml_hit_status", + ], "tags": [ - "Sessions" + "Case Assignment" ], + "responses": { + "204": { + "description": "No response body" + }, + "403": { + "description": "Case access is required." + }, + "409": { + "description": "The published version or draft revision has changed. Reload before retrying." + } + }, "security": [ { "ApiKeyAuth": [] } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingPublicationPolicyDraftPreconditionRequest" + } + } + } + } + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/duplicates/": { + "post": { + "operationId": "duplicates_create", + "x-didit-sdk": { + "resource": "cases", + "method": "find_duplicates", + "auth": "api_key", + "server": true, + "scope": "application" + }, "parameters": [ { "in": "path", - "name": "sessionId", - "required": true, - "description": "UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs — the service resolves the ID across both session types.", + "name": "organization_id", "schema": { "type": "string", - "format": "uuid", - "example": "11111111-2222-3333-4444-555555555555" - } + "format": "uuid" + }, + "required": true }, { - "in": "query", - "name": "node_id", - "required": false, - "description": "Workflow graph node ID. Required only when the session has multiple AML checks (one per node); omit it for single-AML sessions.", + "in": "path", + "name": "application_id", "schema": { "type": "string", - "example": "feature_aml_1" - } + "format": "uuid" + }, + "required": true } ], + "tags": [ + "Case Management" + ], "requestBody": { - "required": true, "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/CaseDuplicateCaseDuplicateQueryRequest" + } + }, "application/json": { "schema": { - "type": "object", - "required": [ - "hit_updates" - ], - "properties": { - "hit_updates": { - "type": "array", - "description": "Non-empty list of hit updates.", - "items": { - "type": "object", - "required": [ - "hit_id", - "review_status" - ], - "properties": { - "hit_id": { - "type": "string", - "example": "abc123" - }, - "review_status": { - "type": "string", - "enum": [ - "Unreviewed", - "Confirmed Match", - "False Positive", - "Inconclusive" - ], - "example": "Confirmed Match" - } - } - } - }, - "node_id": { - "type": "string", - "description": "Optional graph node ID. Use it (here or as a query parameter) when the session has multiple AML checks.", - "example": "feature_aml_1" - } - } + "$ref": "#/components/schemas/CaseDuplicateCaseDuplicateQueryRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CaseDuplicateCaseDuplicateQueryRequest" } } } }, "responses": { "200": { - "description": "All hits updated.", "content": { "application/json": { - "example": { - "message": "Updated 2 hit(s) successfully", - "updated_hits": [ - "abc123", - "def456" - ] + "schema": { + "$ref": "#/components/schemas/CaseDuplicateCaseDuplicatePage" } } - } + }, + "description": "" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Review existing cases for a subject or owned source record before manual case creation. Both open and resolved investigations are returned. Each page contains at most 20 cases; use next_offset to request another page." + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/preview/": { + "post": { + "summary": "Test case routing", + "description": "Requires `read:cases` for the requested application. Evaluates current rules or a supplied draft against current reviewer access, availability, skills and capacity. It does not save the draft or reserve reviewer capacity.", + "operationId": "preview_case_routing", + "x-didit-sdk": { + "resource": "cases.routing_rules", + "method": "preview", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Case Assignment" + ], + "parameters": [ + { + "name": "organization_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Organization UUID." }, - "400": { - "description": "Validation error — `hit_updates` empty, or an entry is missing `hit_id` / `review_status` or carries an invalid value.", + { + "name": "application_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Application UUID." + } + ], + "responses": { + "200": { + "description": "Success", "content": { "application/json": { - "example": { - "hit_updates": [ - "hit_updates cannot be empty" - ] + "schema": { + "$ref": "#/components/schemas/CaseRoutingPreview" } } } }, - "403": { - "description": "Missing/invalid credentials or insufficient permissions. This API returns `403` for authentication failures — never `401`.", - "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": "API key lacks write:sessions", - "value": { - "detail": "You do not have permission to perform this action." - } - } - } - } - } + "400": { + "description": "Invalid input." }, - "404": { - "description": "The session was not found, the session has no AML check, or one or more `hit_id`s do not exist (no hit is changed in that case).", - "content": { - "application/json": { - "example": { - "detail": "Hits not found: zzz999" - } + "403": { + "description": "Insufficient permission." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CaseRoutingPreviewRequest" } } } }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/bulk-update-aml-hit-status/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"hit_updates\": [\n { \"hit_id\": \"abc123\", \"review_status\": \"Confirmed Match\" },\n { \"hit_id\": \"def456\", \"review_status\": \"False Positive\" }\n ]\n }'" - }, + "security": [ { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresponse = requests.patch(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/bulk-update-aml-hit-status/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\", \"Content-Type\": \"application/json\"},\n json={\n \"hit_updates\": [\n {\"hit_id\": \"abc123\", \"review_status\": \"Confirmed Match\"},\n {\"hit_id\": \"def456\", \"review_status\": \"False Positive\"},\n ]\n },\n)\nprint(response.json())" + "ApiKeyAuth": [] } ] } }, - "/v3/session/{sessionId}/update-poa-data/": { - "patch": { - "summary": "Update Proof of Address data extracted from the POA document on a verification session", - "description": "Correct the fields extracted from the user's Proof of Address document — utility bill, bank statement, or other address evidence — after the verification has finished: issuer, dates, the printed name, the address itself, and banking details. Every field in the body is optional: only the keys you send are written, everything else is left untouched (`PATCH` semantics).\n\nThe session must belong to your application and be in `Approved`, `Declined`, `In Review`, or `Kyc Expired` status. Sessions in any other state (e.g. still `In Progress`) return `404` with `\"No Session matches the given query.\"` — distinct from the `\"Not found.\"` returned for an unknown or deleted `sessionId`. The session must also carry a POA record: sessions whose workflow never ran a Proof of Address step return `404` (`\"The session does not have a POA.\"`). For graph workflows with multiple POA steps, target a specific step with `node_id` (query parameter or body field); when omitted, the most recent POA record is patched.\n\n`extra_fields` is merged key-by-key: nine known keys (`bank_iban`, `additional_names`, …) write through to dedicated POA columns, any other key is kept as a custom extra, and `null` / `\"\"` deletes a key. `poa_parsed_address` accepts only the seven documented keys and recomputes `formatted_address` / `poa_formatted_address` after every edit. If the free-text `poa_address` changes, the server may also re-geocode it and refresh the structured address (skipped for sandbox sessions).\n\nUnlike the KYC variant of this endpoint, the `data.updated` [webhook](/integration/webhooks) (trigger `manual_review`) fires only when at least one stored value **actually changed** — re-sending identical values is a silent no-op. When values change, a `POA_DATA_UPDATED` activity entry recording the changed fields with previous and new values is appended to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and the console activity timeline. The session's status and decision are **not** re-evaluated — use [Update Status](#patch-/v3/session/-sessionId-/update-status/) to change the outcome.\n\nAccepts an API key or a console user token; either way the credential needs the `write:sessions` permission. Authentication and permission failures both return `403` — this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. This endpoint has a dedicated rate limit of 10 requests per minute per API key (invalid requests count too); the shared write budget of 300 requests per minute also applies.\n\nAn empty JSON body `{}` is a no-op: it returns `200` echoing the current POA record without firing the webhook (`has_changes` stays false).", - "operationId": "patch_v3_session_update_poa_data", - "tags": [ - "Sessions" - ], - "security": [ - { - "ApiKeyAuth": [] - } + "/v3/organization/{organization_id}/application/{application_id}/cases/reviewers/": { + "get": { + "summary": "List eligible case reviewers", + "description": "Requires `list:cases` for the requested application.", + "operationId": "list_case_reviewers", + "x-didit-sdk": { + "resource": "cases", + "method": "list_reviewers", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "tags": [ + "Case Assignment" ], "parameters": [ { - "name": "sessionId", + "name": "organization_id", "in": "path", "required": true, - "description": "UUID of the User Verification (KYC) session whose attached POA record you are correcting.", "schema": { "type": "string", - "format": "uuid", - "example": "11111111-2222-3333-4444-555555555555" - } + "format": "uuid" + }, + "description": "Organization UUID." }, { - "name": "node_id", - "in": "query", - "required": false, - "description": "Workflow graph node whose POA record to patch, for graph workflows with multiple Proof of Address steps (also accepted as a `node_id` body field). When omitted, the most recent POA record on the session is patched. If no POA exists for the given node, the call returns `404` (`\"The session does not have a POA for node ''.\"`).", + "name": "application_id", + "in": "path", + "required": true, "schema": { "type": "string", - "example": "feature_poa" - } + "format": "uuid" + }, + "description": "Application UUID." } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "description": "All fields optional — send only what you want to change.", - "properties": { - "issuing_state": { - "type": "string", - "nullable": true, - "description": "Country that issued the POA document, ISO 3166-1 alpha-3. Case-insensitive (normalized to uppercase); invalid codes return `400`.", - "example": "ESP" - }, - "document_type": { - "type": "string", - "enum": [ - "UTILITY_BILL", - "BANK_STATEMENT", - "GOVERNMENT_ISSUED_DOCUMENT", - "OTHER_POA_DOCUMENT", - "UNKNOWN" - ], - "description": "POA document category. Values are uppercase — lowercase variants like `utility_bill` return `400`.", - "example": "UTILITY_BILL" - }, - "document_language": { - "type": "string", - "maxLength": 50, - "nullable": true, - "description": "Language of the document, typically an ISO 639-1 code (e.g. `es`). Free text — not validated as a language code.", - "example": "es" - }, - "issuer": { - "type": "string", - "maxLength": 255, - "nullable": true, - "description": "Organization that issued the document (utility company, bank, government agency).", - "example": "Energy Provider S.A." - }, - "issue_date": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Issue date of the POA document in `YYYY-MM-DD`. Any other format returns `400`.", - "example": "2026-04-12" - }, - "poa_address": { - "type": "string", - "nullable": true, - "description": "Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it in the background of the same request and may overwrite `poa_parsed_address` / `poa_formatted_address` with the geocoder result. The re-geocode also requires `issuing_state` to be present on the POA record.", - "example": "Avda de Madrid 34, 28013 Madrid" - }, - "name_on_document": { - "type": "string", - "maxLength": 255, - "nullable": true, - "description": "Recipient name as printed on the document.", - "example": "Carmen Espanola" - }, - "extra_fields": { - "type": "object", - "additionalProperties": true, - "nullable": true, - "description": "Document-specific extras, merged key-by-key. Nine keys map to dedicated POA columns: `bank_account_number`, `bank_iban`, `bank_sort_code`, `bank_routing_number`, `bank_swift_bic`, `bank_branch_name`, `bank_branch_address`, `document_phone_number`, and `additional_names` (must be a list of strings or `null`, otherwise `400`). Any other key is stored as a custom extra field. Per-key semantics: a value sets the key; `null` or `\"\"` deletes it. Must be a JSON object, otherwise `400`." - }, - "poa_parsed_address": { - "type": "object", - "nullable": true, - "description": "Structured address. Only these nested keys are accepted: `address_type`, `city`, `region`, `street_1`, `street_2`, `postal_code`, `country` — any other key returns `400`. Per-key semantics: a value sets the key, `null` removes it. `formatted_address` (and the top-level `poa_formatted_address`) is recomputed server-side by joining `street_1`, `street_2`, `city`, `region`, `postal_code`, and `country` — you cannot set it directly.", - "properties": { - "address_type": { - "type": "string", - "nullable": true, - "description": "Free-form address classification (e.g. `home`)." - }, - "city": { - "type": "string", - "nullable": true, - "example": "Madrid" - }, - "region": { - "type": "string", - "nullable": true, - "description": "State / province / autonomous community." - }, - "street_1": { - "type": "string", - "nullable": true, - "example": "Avda de Madrid 34" - }, - "street_2": { - "type": "string", - "nullable": true - }, - "postal_code": { - "type": "string", - "nullable": true, - "example": "28013" - }, - "country": { - "type": "string", - "nullable": true, - "description": "ISO 3166-1 alpha-2 country code. Lowercase input is normalized to uppercase; alpha-3 codes are rejected with `400`.", - "example": "ES" - } - } - } - } - }, - "examples": { - "Correct issuer and address": { - "summary": "Fix document classification, issuer, and the structured address", - "value": { - "issuing_state": "ESP", - "document_type": "BANK_STATEMENT", - "issuer": "Banco Example S.A.", - "issue_date": "2026-05-02", - "name_on_document": "Carmen Espanola", - "poa_address": "Avda de Madrid 34, 28013 Madrid", - "extra_fields": { - "bank_iban": "ES9121000418450200051332", - "additional_names": [ - "Carmen E. Espanola" - ] - }, - "poa_parsed_address": { - "street_1": "Avda de Madrid 34", - "city": "Madrid", - "postal_code": "28013", - "country": "ES" - } - } - }, - "Remove a banking detail": { - "summary": "Delete one extra-field key, keep the rest", - "value": { - "extra_fields": { - "bank_iban": null - } - } - } - } - } - } - }, "responses": { "200": { - "description": "POA data updated. Returns the **full current POA record** (every updatable field with its stored value, plus the complete `extra_fields` map, `poa_parsed_address`, and `poa_formatted_address`) — not just the fields you submitted. The `data.updated` webhook fires only when at least one value actually changed.", + "description": "Success", "content": { "application/json": { "schema": { "type": "object", "properties": { - "issuing_state": { - "type": "string", - "nullable": true, - "description": "Country that issued the POA document, ISO 3166-1 alpha-3. Case-insensitive (normalized to uppercase); invalid codes return `400`.", - "example": "ESP" - }, - "document_type": { - "type": "string", - "enum": [ - "UTILITY_BILL", - "BANK_STATEMENT", - "GOVERNMENT_ISSUED_DOCUMENT", - "OTHER_POA_DOCUMENT", - "UNKNOWN" - ], - "description": "POA document category. Values are uppercase — lowercase variants like `utility_bill` return `400`.", - "example": "UTILITY_BILL" - }, - "document_language": { - "type": "string", - "maxLength": 50, - "nullable": true, - "description": "Language of the document, typically an ISO 639-1 code (e.g. `es`). Free text — not validated as a language code.", - "example": "es" - }, - "issuer": { - "type": "string", - "maxLength": 255, - "nullable": true, - "description": "Organization that issued the document (utility company, bank, government agency).", - "example": "Energy Provider S.A." - }, - "issue_date": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Issue date of the POA document in `YYYY-MM-DD`. Any other format returns `400`.", - "example": "2026-04-12" - }, - "poa_address": { - "type": "string", - "nullable": true, - "description": "Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it in the background of the same request and may overwrite `poa_parsed_address` / `poa_formatted_address` with the geocoder result.", - "example": "Avda de Madrid 34, 28013 Madrid" - }, - "name_on_document": { - "type": "string", - "maxLength": 255, - "nullable": true, - "description": "Recipient name as printed on the document.", - "example": "Carmen Espanola" - }, - "extra_fields": { - "type": "object", - "additionalProperties": true, - "nullable": true, - "description": "Always present in the response: the nine known keys (with `null` for unset ones, `additional_names` defaulting to `[]`) plus any custom keys previously stored." - }, - "poa_parsed_address": { - "type": "object", - "nullable": true, - "description": "Current structured address, or `null` when none exists. Includes the recomputed `formatted_address` alongside the seven editable keys. Returned as stored — after a server-side geocode it can contain bookkeeping keys (e.g. `raw_results`) alongside the editable keys.", - "properties": { - "address_type": { - "type": "string", - "nullable": true, - "description": "Free-form address classification (e.g. `home`)." - }, - "city": { - "type": "string", - "nullable": true, - "example": "Madrid" - }, - "region": { - "type": "string", - "nullable": true, - "description": "State / province / autonomous community." - }, - "street_1": { - "type": "string", - "nullable": true, - "example": "Avda de Madrid 34" - }, - "street_2": { - "type": "string", - "nullable": true - }, - "postal_code": { - "type": "string", - "nullable": true, - "example": "28013" - }, - "country": { - "type": "string", - "nullable": true, - "description": "ISO 3166-1 alpha-2 country code. Lowercase input is normalized to uppercase; alpha-3 codes are rejected with `400`.", - "example": "ES" - }, - "formatted_address": { - "type": "string", - "nullable": true, - "description": "Joined, human-readable form of the structured address. Recomputed server-side after every `poa_parsed_address` edit.", - "example": "Avda de Madrid 34, Madrid, 28013, ES" + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "format": "uuid" + }, + "email": { + "type": "string", + "format": "email" + }, + "name": { + "type": "string" + } } } - }, - "poa_formatted_address": { - "type": "string", - "nullable": true, - "description": "Top-level copy of the recomputed `formatted_address`, kept in sync when the structured address changes.", - "example": "Avda de Madrid 34, Madrid, 28013, ES" - } - } - }, - "examples": { - "Updated POA": { - "summary": "Full POA record after the correction", - "value": { - "issuing_state": "ESP", - "document_type": "BANK_STATEMENT", - "document_language": "es", - "issuer": "Banco Example S.A.", - "issue_date": "2026-05-02", - "poa_address": "Avda de Madrid 34, 28013 Madrid", - "name_on_document": "Carmen Espanola", - "extra_fields": { - "bank_account_number": null, - "bank_iban": "ES9121000418450200051332", - "bank_sort_code": null, - "bank_routing_number": null, - "bank_swift_bic": null, - "bank_branch_name": null, - "bank_branch_address": null, - "document_phone_number": null, - "additional_names": [ - "Carmen E. Espanola" - ] - }, - "poa_parsed_address": { - "city": "Madrid", - "street_1": "Avda de Madrid 34", - "postal_code": "28013", - "country": "ES", - "formatted_address": "Avda de Madrid 34, Madrid, 28013, ES" - }, - "poa_formatted_address": "Avda de Madrid 34, Madrid, 28013, ES" } } } @@ -35253,904 +54265,775 @@ } }, "400": { - "description": "Validation error. Field errors arrive as `{\"\": [\"\"]}`; errors on nested `poa_parsed_address` / `extra_fields` keys use an object value: `{\"extra_fields\": {\"\": \"\"}}`.", - "content": { - "application/json": { - "examples": { - "Invalid document type": { - "summary": "Enum is uppercase — lowercase variants are rejected", - "value": { - "document_type": [ - "\"utility_bill\" is not a valid choice." - ] - } - }, - "Invalid issuing state": { - "summary": "issuing_state is not ISO 3166-1 alpha-3", - "value": { - "issuing_state": [ - "Invalid ISO 3166-1 alpha-3 country code for issuing_state." - ] - } - }, - "additional_names not a list": { - "summary": "additional_names accepts a list of strings or null", - "value": { - "extra_fields": { - "additional_names": "Must be a list or null." - } - } - }, - "Unsupported poa_parsed_address key": { - "summary": "Only the seven documented keys are allowed", - "value": { - "poa_parsed_address": { - "zipcode": "Unsupported key. Allowed keys: address_type, city, region, street_1, street_2, postal_code, country" - } - } - }, - "Bad date format": { - "summary": "Dates must be YYYY-MM-DD", - "value": { - "issue_date": [ - "Date has wrong format. Use one of these formats instead: YYYY-MM-DD." - ] - } - } - } - } - } + "description": "Invalid input." }, "403": { - "description": "Missing/invalid credentials or insufficient permissions. This API returns `403` for authentication failures — never `401`.", - "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": "Credential lacks write:sessions", - "value": { - "detail": "You do not have permission to perform this action." - } - } - } - } + "description": "Insufficient permission." + }, + "503": { + "description": "Reviewer access could not be verified. Retry later." + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/history/": { + "get": { + "operationId": "list_case_blueprint_history", + "x-didit-sdk": { + "resource": "cases.blueprints", + "method": "list_history", + "auth": "api_key", + "server": true, + "scope": "application" + }, + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "in": "path", + "name": "blueprint_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true + }, + { + "name": "ordering", + "required": false, + "in": "query", + "description": "Which field to use when ordering the results.", + "schema": { + "type": "string" } }, - "404": { - "description": "Unknown session, session in a non-editable state, or no POA record to patch. Each case has a distinct `detail` message (see examples). The unknown-session lookup runs before credential checks.", - "content": { - "application/json": { - "examples": { - "Unknown session": { - "summary": "No session with this ID in your application (or it was deleted)", - "value": { - "detail": "Not found." - } - }, - "Wrong session state": { - "summary": "Session exists but is not Approved / Declined / In Review / Kyc Expired", - "value": { - "detail": "No Session matches the given query." - } - }, - "No POA record": { - "summary": "The workflow never ran a Proof of Address step", - "value": { - "detail": "The session does not have a POA." - } - }, - "No POA for node": { - "summary": "node_id given but no POA exists for it", - "value": { - "detail": "The session does not have a POA for node 'feature_missing'." - } - } - } - } + { + "name": "limit", + "required": false, + "in": "query", + "description": "Number of results to return per page.", + "schema": { + "type": "integer" } }, - "429": { - "description": "Dedicated rate limit exceeded — 10 POA data updates per minute per API key (rejected requests count toward the budget). The shared 300/min write budget can also trigger with its own message. Inspect `Retry-After` and the `X-RateLimit-*` response headers before retrying.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } - }, - "examples": { - "Rate limited": { - "summary": "POA update budget exhausted", - "value": { - "detail": "Session POA data update rate limit exceeded. You can make up to 10 requests per minute." - } - } + { + "name": "offset", + "required": false, + "in": "query", + "description": "The initial index from which to return the results.", + "schema": { + "type": "integer" + } + } + ], + "tags": [ + "Case Management" + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CasePublicationPaginatedPolicyRevisionList" } } - } + }, + "description": "" + }, + "403": { + "description": "Required case permission is missing." } }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH \\\n https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-poa-data/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"issuing_state\": \"ESP\",\n \"document_type\": \"BANK_STATEMENT\",\n \"issuer\": \"Banco Example S.A.\",\n \"issue_date\": \"2026-05-02\",\n \"name_on_document\": \"Carmen Espanola\",\n \"poa_address\": \"Avda de Madrid 34, 28013 Madrid\",\n \"extra_fields\": {\n \"bank_iban\": \"ES9121000418450200051332\"\n },\n \"poa_parsed_address\": {\n \"city\": \"Madrid\",\n \"postal_code\": \"28013\",\n \"country\": \"ES\"\n }\n }'" - }, - { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresponse = requests.patch(\n \"https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-poa-data/\",\n headers={\n \"x-api-key\": \"YOUR_API_KEY\",\n \"Content-Type\": \"application/json\",\n },\n json={\n \"issuing_state\": \"ESP\",\n \"document_type\": \"BANK_STATEMENT\",\n \"issuer\": \"Banco Example S.A.\",\n \"issue_date\": \"2026-05-02\",\n \"name_on_document\": \"Carmen Espanola\",\n \"poa_address\": \"Avda de Madrid 34, 28013 Madrid\",\n \"extra_fields\": {\"bank_iban\": \"ES9121000418450200051332\"},\n \"poa_parsed_address\": {\"city\": \"Madrid\", \"postal_code\": \"28013\", \"country\": \"ES\"},\n },\n)\nresponse.raise_for_status()\npoa = response.json() # full current POA record\nprint(poa[\"issuer\"], poa[\"poa_formatted_address\"])" - }, + "security": [ { - "lang": "javascript", - "label": "JavaScript", - "source": "const response = await fetch(\n 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-poa-data/',\n {\n method: 'PATCH',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n issuing_state: 'ESP',\n document_type: 'BANK_STATEMENT',\n issuer: 'Banco Example S.A.',\n issue_date: '2026-05-02',\n name_on_document: 'Carmen Espanola',\n poa_address: 'Avda de Madrid 34, 28013 Madrid',\n poa_parsed_address: { city: 'Madrid', postal_code: '28013', country: 'ES' },\n }),\n },\n);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst poa = await response.json(); // full current POA record" + "ApiKeyAuth": [] } ] } }, - "/v3/session/{sessionId}/kyb/{companyUuid}/update-data/": { - "patch": { - "summary": "Update registry-extracted company data on a Business Verification (KYB) session", - "description": "Correct the registry-extracted company data on a Business Verification (KYB) session — company name, registration number, addresses, status, contact details — identified by `companyUuid`. Typical callers are a compliance analyst fixing a registry misread, or your back office syncing corrected company data. Every field in the body is optional (`PATCH` semantics); an empty JSON body (`{}`) is a no-op that simply echoes the current company record.\n\nEdits are written in two places at once: fields with a canonical company column (name, registration number, country, incorporation date, tax number, type, statuses, capital fields (`registered_capital`, `registered_capital_amount`, `registered_capital_currency`), alternative names, nature of business, website, email, phone, LEI, location, and `legal_address` → `registered_address`) update that column, and **every** submitted key is also merged into the `registry_data` snapshot (dates as ISO strings) so the \"extracted from registry\" view reflects the correction. Fields without a column — `region`, `registration_date`, `dissolution_date`, `control_scheme` — land in `registry_data` only. `user_provided_data` (what the end user confirmed in the hosted flow) is never touched, and `last_console_edit_at` is stamped on every non-empty PATCH.\n\nUnlike the KYC/POA update endpoints, there is **no session-status gate**: the company can be edited at any point in the business session's lifecycle. The company must belong to the session in the path (`404` with `\"KYB company does not belong to the given session.\"` otherwise) and to your application. Every non-empty PATCH fires the `data.updated` [webhook](/integration/webhooks) on the business session (trigger `console_registry_edit`) — even when the submitted values equal the stored ones. The session's status and decision are **not** re-evaluated, and no review/audit entry is recorded.\n\nAccepts an API key or a console user token; either way the credential needs the `write:businesses` permission. Authentication and permission failures both return `403` — this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. The shared write budget of 300 requests per minute per API key applies.\n\nFor User Verification sessions, the KYC counterpart is `PATCH /v3/session/{sessionId}/update-data/`; to change the session decision itself use `PATCH /v3/session/{sessionId}/update-status/`.", - "operationId": "patch_v3_session_kyb_company_update_data", + "/v3/session/{sessionId}/signatures/{nodeId}/documents/{documentId}/file/": { + "get": { + "summary": "Download a signed document", + "description": "Download the signed PDF of one document of an Electronic Signature step. Use the `signed_file` path of `signatures[].documents[]` in the decision. The PDF holds the document the signer was shown with their field values stamped, the evidence page (unless `signature_evidence_page` is off) and the PAdES signature with its timestamp. Didit checks the file against `sha256_signed` before returning it.\n\nWorks for User Verification (KYC) and Business Verification (KYB) sessions. Needs `read:sessions`; your API key works. See the [Electronic Signature report](/core-technology/electronic-signature/report-electronic-signature#download-the-signed-documents).", + "operationId": "get_v3_session_signature_document_file", + "x-didit-sdk": { + "resource": "sessions.signatures", + "method": "download_document", + "auth": "api_key", + "server": true + }, "tags": [ "Sessions" ], - "security": [ - { - "ApiKeyAuth": [] - } - ], "parameters": [ { "name": "sessionId", "in": "path", "required": true, - "description": "UUID of the parent Business Verification (KYB) session.", + "description": "The User Verification (KYC) or Business Verification (KYB) session.", "schema": { "type": "string", "format": "uuid", - "example": "33333333-4444-5555-6666-777777777777" + "example": "11111111-2222-3333-4444-555555555555" } }, { - "name": "companyUuid", + "name": "nodeId", "in": "path", "required": true, - "description": "UUID of the company record to update (`registry_checks[].company.uuid` on the business session decision). Must belong to the session referenced by `sessionId`.", + "description": "The Electronic Signature step: `node_id` of the item in `signatures[]`.", "schema": { "type": "string", - "format": "uuid", - "example": "88888888-9999-aaaa-bbbb-cccccccccccc" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "description": "All fields optional — send only what you want to change. Provided fields update the canonical company columns (where one exists) and are merged into the `registry_data` snapshot.", - "properties": { - "company_name": { - "type": "string", - "maxLength": 255, - "description": "Legal name of the company. Written to the canonical column and the registry snapshot.", - "example": "Docs Probe Corp" - }, - "registration_number": { - "type": "string", - "maxLength": 100, - "description": "Official registration number with the company registry.", - "example": "DPC-001" - }, - "country_code": { - "type": "string", - "maxLength": 2, - "description": "Country of incorporation, ISO 3166-1 alpha-2. Case-insensitive (`us` is stored as `US`). Alpha-3 codes fail the 2-character limit with `400`.", - "example": "US" - }, - "region": { - "type": "string", - "maxLength": 10, - "nullable": true, - "description": "Subdivision / region tag where the registry has one (e.g. US state). Stored in the registry snapshot only — it has no canonical column." - }, - "incorporation_date": { - "type": "string", - "format": "date", - "description": "Incorporation date in `YYYY-MM-DD`. Optional like every other field — the `PATCH` is partial. Any other date format returns `400`.", - "example": "2015-04-12" - }, - "registration_date": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Registration date where the registry distinguishes it from incorporation. Stored in the registry snapshot only." - }, - "dissolution_date": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Dissolution date for companies that are no longer active. Stored in the registry snapshot only." - }, - "tax_number": { - "type": "string", - "maxLength": 100, - "description": "Tax identification number." - }, - "legal_address": { - "type": "string", - "description": "Legal / registered address. Aliased server-side to the canonical `registered_address` column — the response returns it under `registered_address`, while the registry snapshot keeps the `legal_address` key you sent.", - "example": "1 Example Plaza, Wilmington, DE 19801" - }, - "company_type": { - "type": "string", - "maxLength": 100, - "description": "Legal entity type (e.g. `Limited Liability Company`, `PLC`, `GmbH`).", - "example": "Limited Liability Company" - }, - "registry_status": { - "type": "string", - "enum": [ - "active", - "dissolved", - "deregistered", - "see full details", - "authorised", - "appointed representative", - "unauthorised", - "inactive", - "no longer authorised", - "closed", - "struck off" - ], - "nullable": true, - "description": "Registry-reported company status. Lowercase, some values contain spaces (e.g. `struck off`, `no longer authorised`). Any other value returns `400`.", - "example": "active" - }, - "verification_status": { - "type": "string", - "enum": [ - "verified", - "failed", - "unknown" - ], - "nullable": true, - "description": "Outcome of the registry verification. Any other value returns `400`.", - "example": "verified" - }, - "alternative_names": { - "type": "string", - "description": "Trade names, former legal names, DBA names (free text)." - }, - "nature_of_business": { - "type": "string", - "description": "Short description of the company's activity.", - "example": "Software development" - }, - "registered_capital": { - "type": "string", - "maxLength": 100, - "description": "Human-readable registered capital (e.g. `USD 50000`). Use `registered_capital_amount` + `registered_capital_currency` for structured values, or this field for free-form text.", - "example": "USD 50000" - }, - "registered_capital_amount": { - "type": "number", - "format": "double", - "nullable": true, - "description": "Numeric registered-capital amount (up to 20 digits, 2 decimal places). Sets the `registered_capital_amount` column and is mirrored into `registry_data` as a decimal string; the response echoes it as a decimal string (e.g. `\"1500000.50\"`)." - }, - "registered_capital_currency": { - "type": "string", - "maxLength": 3, - "nullable": true, - "description": "ISO 4217 currency code for `registered_capital_amount`. Case-insensitive (`usd` is stored as `USD`); values that are not exactly 3 letters return `400` with the ISO 4217 message. Sets the canonical `registered_capital_currency` column and is mirrored into `registry_data`.", - "example": "USD" - }, - "website": { - "type": "string", - "maxLength": 500, - "description": "Company website. Bare domains are accepted and normalized with an `https://` prefix (`docsprobe.example.com` is stored as `https://docsprobe.example.com`); values that still fail URL validation return `400`.", - "example": "https://docsprobe.example.com" - }, - "email": { - "type": "string", - "format": "email", - "description": "Contact email on file with the registry.", - "example": "legal@docsprobe.example.com" - }, - "phone": { - "type": "string", - "maxLength": 50, - "description": "Contact phone. Must match `+`, digits, spaces, dashes, dots, or parentheses, with 7-15 digits total — otherwise `400`.", - "example": "+1 302 555 0143" - }, - "legal_entity_identifier": { - "type": "string", - "maxLength": 50, - "description": "LEI, where the company has one." - }, - "location_of_registration": { - "type": "string", - "maxLength": 255, - "description": "City / subdivision of the registry of record." - }, - "vat_number": { - "type": "string", - "maxLength": 64, - "description": "EU VAT number. Merged into the `registry_data` snapshot only — editing it here does not re-run VAT and tax ID validation, so `vat_validation_status` and the other `vat_*` result fields are unchanged." - }, - "control_scheme": { - "type": "string", - "description": "Control / governance scheme when the registry exposes it. Stored in the registry snapshot only." - } - } - }, - "examples": { - "Correct registry data": { - "summary": "Fix the company's registry-extracted fields", - "value": { - "company_name": "Docs Probe Corp", - "registration_number": "DPC-001", - "country_code": "US", - "incorporation_date": "2015-04-12", - "company_type": "Limited Liability Company", - "legal_address": "1 Example Plaza, Wilmington, DE 19801", - "registry_status": "active", - "verification_status": "verified", - "website": "docsprobe.example.com", - "email": "legal@docsprobe.example.com", - "phone": "+1 302 555 0143", - "registered_capital": "USD 50000", - "nature_of_business": "Software development" - } - }, - "Single field": { - "summary": "Only the keys you send are written", - "value": { - "registry_status": "dissolved" - } - }, - "No-op": { - "summary": "Empty body echoes the current record without firing the webhook", - "value": {} - } - } + "example": "feature_signature" } - } - }, - "responses": { - "200": { - "description": "Company data updated (or echoed unchanged for an empty body). Returns the full KYB company record — the same shape as `registry_checks[].company` on the business session decision. Note how `legal_address` comes back as `registered_address`. `registered_capital_amount`/`registered_capital_currency` populate their canonical columns and are mirrored into `registry_data`.", - "content": { - "application/json": { - "schema": { - "type": "object", - "description": "Full KYB company record — the same shape as the `company` object inside `registry_checks[]` on the business session decision.", - "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Company record identifier (the `companyUuid` path parameter)." - }, - "node_id": { - "type": "string", - "nullable": true, - "description": "Workflow graph node this company record belongs to, when the workflow is graph-based." - }, - "status": { - "type": "string", - "description": "Feature lifecycle status of the registry check (e.g. `Not Finished`, `Approved`, `Declined`, `In Review`)." - }, - "registry_status": { - "type": "string", - "nullable": true, - "description": "Registry-reported company status (e.g. `active`, `dissolved`, `struck off`)." - }, - "data_resolved": { - "type": "boolean", - "description": "`true` when the company data is complete — always `true` for manual-entry companies; for registry-sourced companies, `true` once the registry fetch resolved." - }, - "company_name": { - "type": "string", - "nullable": true - }, - "registration_number": { - "type": "string", - "nullable": true - }, - "country_code": { - "type": "string", - "nullable": true, - "description": "ISO 3166-1 alpha-2 country of incorporation." - }, - "region": { - "type": "string", - "nullable": true - }, - "company_type": { - "type": "string", - "nullable": true - }, - "incorporation_date": { - "type": "string", - "format": "date", - "nullable": true - }, - "registered_address": { - "type": "string", - "nullable": true, - "description": "Canonical registered address — updated when you send `legal_address`." - }, - "tax_number": { - "type": "string", - "nullable": true - }, - "risk_level": { - "type": "string", - "nullable": true - }, - "verification_status": { - "type": "string", - "nullable": true, - "description": "`verified`, `failed`, or `unknown`." - }, - "is_from_registry": { - "type": "boolean", - "description": "`true` when the company was pre-filled from a registry search rather than entered manually." - }, - "fetch_status": { - "type": "string", - "nullable": true, - "description": "Registry fetch lifecycle for registry-sourced companies (`pending` / `resolved`); `null` for manual entries." - }, - "alternative_names": { - "type": "string", - "nullable": true, - "description": "Alternative, trading, former or original-language company names as text, when reported." - }, - "nature_of_business": { - "type": "string", - "nullable": true - }, - "registered_capital": { - "type": "string", - "nullable": true - }, - "registered_capital_amount": { - "type": "string", - "nullable": true, - "description": "Canonical column. Returned as a decimal string (e.g. `\"2750000.00\"`)." - }, - "registered_capital_currency": { - "type": "string", - "nullable": true, - "description": "Canonical column. Set by the registry flow or by console/API edits via this endpoint." - }, - "website": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string", - "nullable": true - }, - "phone": { - "type": "string", - "nullable": true - }, - "legal_entity_identifier": { - "type": "string", - "nullable": true - }, - "location_of_registration": { - "type": "string", - "nullable": true - }, - "vat_number": { - "type": "string", - "nullable": true, - "description": "EU VAT number, when collected during the hosted registry step or edited afterwards." - }, - "vat_validation_status": { - "type": "string", - "enum": [ - "valid", - "invalid", - "format_valid", - "invalid_format", - "could_not_validate", - "not_applicable" - ], - "description": "Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session." - }, - "vat_validated_number": { - "type": "string", - "nullable": true, - "description": "The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran." - }, - "vat_validation_source": { - "type": "string", - "nullable": true, - "enum": [ - "eu_vies", - "pl_vat_white_list", - "no_brreg", - "ch_uid_register", - "br_receita_federal", - "co_rues", - "cr_hacienda", - "ec_sri", - "py_dnit", - "format_rules", - null - ], - "description": "The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran." - }, - "vat_validated_name": { - "type": "string", - "nullable": true, - "description": "Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." - }, - "vat_validated_address": { - "type": "string", - "nullable": true, - "description": "Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." - }, - "vat_checked_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "Timestamp of the check. `null` when no check ran." - }, - "financial_summary": { - "type": "object", - "nullable": true, - "additionalProperties": true - }, - "officers": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true - }, - "description": "Directors / officers attached to the company, including per-person KYC progress." - }, - "beneficial_owners": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true - }, - "description": "Beneficial owners attached to the company, including per-person KYC progress." - }, - "addresses": { - "type": "array", - "nullable": true, - "items": { - "type": "object", - "additionalProperties": true - }, - "description": "Registry address detail, when the registry returned any." - }, - "industries": { - "type": "array", - "nullable": true, - "items": { - "type": "object", - "additionalProperties": true - }, - "description": "Registry industry classification, when available." - }, - "accounts": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts." - }, - "registry_data": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Raw registry payload snapshot. Every key you PATCH is merged into this map (dates serialized as ISO strings), so the \"extracted from registry\" view reflects operator corrections." - }, - "user_provided_data": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "What the end user typed during the hosted registry-confirmation step. Never modified by this endpoint." - }, - "confirmed_by_user_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the end user confirmed the company data in the hosted flow." - }, - "last_console_edit_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "Stamped on every successful non-empty PATCH, so UIs can surface \"edited by operator\" context." + }, + { + "name": "documentId", + "in": "path", + "required": true, + "description": "`id` of the document in `signatures[].documents[]`.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "The signed PDF.", + "headers": { + "Content-Disposition": { + "description": "Attachment with the file name.", + "schema": { + "type": "string", + "example": "attachment; filename=\"agreement-signed.pdf\"" + } + } + }, + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "403": { + "description": "Missing or invalid credentials, or a credential that can't read this session. Didit's API-key authentication answers `403`, never `401`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "example": { + "detail": "You do not have permission to perform this action." + } + } + } + }, + "404": { + "description": "No signed document matches: the session, step or document doesn't exist, was deleted, or isn't signed yet.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" }, - "is_editable": { - "type": "boolean", - "description": "`true` while the end user can still edit the registry-sourced data in the hosted flow (registry-sourced and not yet confirmed)." + "detail": { + "type": "string" } } }, "examples": { - "Updated company": { - "summary": "Full company record after the correction", + "Not signed yet": { + "summary": "The signer hasn't signed", "value": { - "uuid": "6b9479f5-6769-46c8-baf1-df63bbbc5f8c", - "node_id": null, - "status": "Not Finished", - "registry_status": "active", - "data_resolved": true, - "company_name": "Docs Probe Corp", - "registration_number": "DPC-001", - "country_code": "US", - "region": null, - "company_type": "Limited Liability Company", - "incorporation_date": "2015-04-12", - "registered_address": "1 Example Plaza, Wilmington, DE 19801", - "tax_number": null, - "risk_level": null, - "verification_status": "verified", - "is_from_registry": false, - "fetch_status": null, - "alternative_names": null, - "nature_of_business": "Software development", - "registered_capital": "USD 50000", - "registered_capital_amount": null, - "registered_capital_currency": "USD", - "website": "https://docsprobe.example.com", - "email": "legal@docsprobe.example.com", - "phone": "+1 302 555 0143", - "legal_entity_identifier": null, - "location_of_registration": null, - "financial_summary": null, - "officers": [], - "beneficial_owners": [], - "addresses": null, - "industries": null, - "accounts": null, - "registry_data": { - "company_name": "Docs Probe Corp", - "registration_number": "DPC-001", - "country_code": "US", - "incorporation_date": "2015-04-12", - "legal_address": "1 Example Plaza, Wilmington, DE 19801", - "company_type": "Limited Liability Company", - "registry_status": "active", - "verification_status": "verified", - "nature_of_business": "Software development", - "registered_capital": "USD 50000", - "registered_capital_currency": "USD", - "website": "https://docsprobe.example.com", - "email": "legal@docsprobe.example.com", - "phone": "+1 302 555 0143" - }, - "user_provided_data": null, - "confirmed_by_user_at": null, - "last_console_edit_at": "2026-06-12T00:02:34.276061Z", - "is_editable": false + "code": "signature_not_signed", + "detail": "signature_not_signed" + } + }, + "Unknown document": { + "summary": "No such document in this step", + "value": { + "code": "signature_not_found", + "detail": "signature_not_found" } } } } } }, - "400": { - "description": "Validation error. Field errors arrive as `{\"\": [\"\"]}`. Max-length checks run before format checks, so `country_code: \"USA\"` reports the 2-character limit rather than the ISO message.", + "409": { + "description": "The stored file no longer matches its recorded SHA-256. Nothing is returned.", "content": { "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "detail": { + "type": "string" + } + } + }, "examples": { - "Alpha-3 country code": { - "summary": "country_code must be alpha-2 — length check fires first", + "Changed file": { + "summary": "Stored file doesn't match its hash", "value": { - "country_code": [ - "Ensure this field has no more than 2 characters." - ] + "code": "signature_document_changed", + "detail": "signature_document_changed" } - }, - "Non-alphabetic country code": { - "summary": "2 characters but not letters", - "value": { - "country_code": [ - "country_code must be a 2-letter ISO 3166-1 alpha-2 code." - ] + } + } + } + } + } + } + } + }, + "/v3/session/{sessionId}/signatures/{nodeId}/evidence/": { + "get": { + "summary": "Download the signature evidence", + "description": "Download a signer's evidence PDF: the level and format, the signer and their verified identity session, the code confirmation, the IP address, each document with its original SHA-256 and field values, the certificate, the timestamp and the SHA-256 of every signed file. Use the `evidence_file` path of the signer's item in `signatures[]`.\n\nAlways pass `signature_id` when a Business Verification step has several signers: without it, the route picks one of the step's signers. Needs `read:sessions`; your API key works.", + "operationId": "get_v3_session_signature_evidence", + "x-didit-sdk": { + "resource": "sessions.signatures", + "method": "download_evidence", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "The User Verification (KYC) or Business Verification (KYB) session.", + "schema": { + "type": "string", + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } + }, + { + "name": "nodeId", + "in": "path", + "required": true, + "description": "The Electronic Signature step: `node_id` of the item in `signatures[]`.", + "schema": { + "type": "string", + "example": "feature_signature" + } + }, + { + "name": "signature_id", + "in": "query", + "required": false, + "description": "`id` of the signer's item in `signatures[]`.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "The evidence PDF.", + "headers": { + "Content-Disposition": { + "description": "Attachment with the file name.", + "schema": { + "type": "string", + "example": "attachment; filename=\"signature-evidence.pdf\"" + } + } + }, + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "403": { + "description": "Missing or invalid credentials, or a credential that can't read this session. Didit's API-key authentication answers `403`, never `401`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" } - }, - "Invalid currency": { - "summary": "3 characters but not an ISO 4217 code", - "value": { - "registered_capital_currency": [ - "Currency must be a 3-letter ISO 4217 code (e.g. USD, EUR, GBP)." - ] + } + }, + "example": { + "detail": "You do not have permission to perform this action." + } + } + } + }, + "404": { + "description": "No evidence matches: the session, step or signer doesn't exist, was deleted, or hasn't signed.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "detail": { + "type": "string" } - }, - "Invalid phone": { - "summary": "Phone fails the format / digit-count check", + } + }, + "examples": { + "Not signed yet": { + "summary": "The signer hasn't signed", "value": { - "phone": [ - "Invalid phone number format." - ] + "code": "signature_not_signed", + "detail": "signature_not_signed" } }, - "Invalid registry status": { - "summary": "registry_status not in the enum", + "Unknown signer": { + "summary": "No such signature in this step", "value": { - "registry_status": [ - "\"liquidated\" is not a valid choice." - ] + "code": "signature_not_found", + "detail": "signature_not_found" } - }, - "Invalid email": { - "summary": "Malformed email address", - "value": { - "email": [ - "Enter a valid email address." - ] + } + } + } + } + }, + "409": { + "description": "The stored file no longer matches its recorded SHA-256. Nothing is returned.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "detail": { + "type": "string" } - }, - "Invalid website": { - "summary": "Value fails URL validation even after https:// normalization", + } + }, + "examples": { + "Changed file": { + "summary": "Stored file doesn't match its hash", "value": { - "website": [ - "Enter a valid URL." - ] + "code": "signature_document_changed", + "detail": "signature_document_changed" } - }, - "Bad date format": { - "summary": "Dates must be YYYY-MM-DD", - "value": { - "incorporation_date": [ - "Date has wrong format. Use one of these formats instead: YYYY-MM-DD." - ] + } + } + } + } + } + } + } + }, + "/v3/session/{sessionId}/signatures/{nodeId}/fields/{fieldId}/image/": { + "get": { + "summary": "Download a signature image", + "description": "Download the signature or initials a signer applied to a field, as PNG. Use the `image_url` of the field in `signatures[].documents[].fields[]`. Needs `read:sessions`; your API key works.", + "operationId": "get_v3_session_signature_field_image", + "x-didit-sdk": { + "resource": "sessions.signatures", + "method": "download_field_image", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "The User Verification (KYC) or Business Verification (KYB) session.", + "schema": { + "type": "string", + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } + }, + { + "name": "nodeId", + "in": "path", + "required": true, + "description": "The Electronic Signature step: `node_id` of the item in `signatures[]`.", + "schema": { + "type": "string", + "example": "feature_signature" + } + }, + { + "name": "fieldId", + "in": "path", + "required": true, + "description": "The field on this signer's copy, as in the field's `image_url`.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "The image.", + "content": { + "image/png": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "The stored image no longer matches its recorded SHA-256.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "detail": { + "type": "string" } } + }, + "example": { + "code": "signature_document_changed", + "detail": "signature_document_changed" } } } }, "403": { - "description": "Missing/invalid credentials or insufficient permissions. This API returns `403` for authentication failures — never `401`.", + "description": "Missing or invalid credentials, or a credential that can't read this session. Didit's API-key authentication answers `403`, never `401`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "example": { + "detail": "You do not have permission to perform this action." + } + } + } + }, + "404": { + "description": "No image matches: the session, step or field doesn't exist, was deleted, or has no applied signature or initials.", "content": { "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "detail": { + "type": "string" + } + } + }, "examples": { - "Invalid Credentials": { - "summary": "Missing or invalid x-api-key", + "Unknown field": { + "summary": "No such field", "value": { - "detail": "Authentication credentials were not provided or are invalid." + "code": "signature_field_not_found", + "detail": "signature_field_not_found" } - }, - "Missing Privilege": { - "summary": "Credential lacks write:businesses", - "value": { - "detail": "You do not have permission to perform this action." + } + } + } + } + } + } + } + }, + "/v3/session/{sessionId}/signatures/{nodeId}/validate/": { + "post": { + "summary": "Validate signatures again", + "description": "Re-check the signed documents of an Electronic Signature step. Didit re-reads every signed PDF, checks its SHA-256 and validates the PAdES signature, the certificate, the revocation data and the timestamp, then saves the result in each signer's `validation` and returns the updated items. Nothing is signed again. Send `signature_id` to check one signer; without it, every signer of the step is checked, and the call fails if any of them hasn't signed.\n\nNeeds `write:sessions`; your API key works. In the console it's the **Validate again** button.", + "operationId": "post_v3_session_signature_validate", + "x-didit-sdk": { + "resource": "sessions.signatures", + "method": "validate", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "The User Verification (KYC) or Business Verification (KYB) session.", + "schema": { + "type": "string", + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } + }, + { + "name": "nodeId", + "in": "path", + "required": true, + "description": "The Electronic Signature step: `node_id` of the item in `signatures[]`.", + "schema": { + "type": "string", + "example": "feature_signature" + } + } + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature_id": { + "type": "string", + "format": "uuid", + "description": "`id` of one signer's item in `signatures[]`." + } + } + }, + "example": { + "signature_id": "0e6c2a8e-6c2f-4b8e-9a51-3f2f6a7d9b10" + } + } + } + }, + "responses": { + "200": { + "description": "The checked signers, with their updated `validation`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signatures": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SignatureResult" + } } } } } } }, - "404": { - "description": "Unknown session, unknown company, or a company that belongs to a different session. Each case has a distinct `detail` message (see examples). The unknown-session lookup runs before credential checks.", + "400": { + "description": "A stored signed PDF no longer matches the SHA-256 recorded when it was signed.", "content": { "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "detail": { + "type": "string" + } + } + }, "examples": { - "Unknown session": { - "summary": "No session with this ID in your application (or it was deleted)", + "Document changed": { + "summary": "A signed file no longer matches its hash", "value": { - "detail": "Not found." + "code": "signature_document_changed", + "detail": "signature_document_changed" } - }, - "Unknown company": { - "summary": "No company with this UUID in your application", - "value": { - "detail": "No KYBCompany matches the given query." + } + } + } + } + }, + "403": { + "description": "Missing or invalid credentials, or a credential that can't read this session. Didit's API-key authentication answers `403`, never `401`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "example": { + "detail": "You do not have permission to perform this action." + } + } + } + }, + "404": { + "description": "No signature matches: the session, step or signer doesn't exist or was deleted.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "detail": { + "type": "string" } - }, - "Company / session mismatch": { - "summary": "companyUuid exists but belongs to another session", + } + }, + "examples": { + "Unknown signer": { + "summary": "No such signature in this step", "value": { - "detail": "KYB company does not belong to the given session." + "code": "signature_not_found", + "detail": "signature_not_found" } } } } } }, - "429": { - "description": "Shared write rate limit exceeded (300 POST/PATCH/DELETE requests per minute per API key). Inspect `Retry-After` and the `X-RateLimit-*` response headers before retrying.", + "409": { + "description": "A signer of the step hasn't signed, so there's nothing to validate.", "content": { "application/json": { "schema": { "type": "object", "properties": { + "code": { + "type": "string" + }, "detail": { "type": "string" } } }, "examples": { - "Rate limited": { - "summary": "Write budget exhausted", + "Not signed": { + "summary": "A signer hasn't signed yet", "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + "code": "signature_not_signed", + "detail": "signature_not_signed" } } } } } } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH \\\n https://verification.didit.me/v3/session/33333333-4444-5555-6666-777777777777/kyb/88888888-9999-aaaa-bbbb-cccccccccccc/update-data/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"company_name\": \"Docs Probe Corp\",\n \"registration_number\": \"DPC-001\",\n \"country_code\": \"US\",\n \"incorporation_date\": \"2015-04-12\",\n \"company_type\": \"Limited Liability Company\",\n \"legal_address\": \"1 Example Plaza, Wilmington, DE 19801\",\n \"registry_status\": \"active\"\n }'" - }, - { - "lang": "python", - "label": "Python", - "source": "import requests\n\nresponse = requests.patch(\n \"https://verification.didit.me/v3/session/33333333-4444-5555-6666-777777777777/kyb/\"\n \"88888888-9999-aaaa-bbbb-cccccccccccc/update-data/\",\n headers={\n \"x-api-key\": \"YOUR_API_KEY\",\n \"Content-Type\": \"application/json\",\n },\n json={\n \"company_name\": \"Docs Probe Corp\",\n \"registration_number\": \"DPC-001\",\n \"country_code\": \"US\",\n \"incorporation_date\": \"2015-04-12\",\n \"legal_address\": \"1 Example Plaza, Wilmington, DE 19801\",\n \"registry_status\": \"active\",\n },\n)\nresponse.raise_for_status()\ncompany = response.json() # full company record\nprint(company[\"company_name\"], company[\"registered_address\"])" - }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const response = await fetch(\n 'https://verification.didit.me/v3/session/33333333-4444-5555-6666-777777777777/kyb/' +\n '88888888-9999-aaaa-bbbb-cccccccccccc/update-data/',\n {\n method: 'PATCH',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n company_name: 'Docs Probe Corp',\n registration_number: 'DPC-001',\n country_code: 'US',\n incorporation_date: '2015-04-12',\n legal_address: '1 Example Plaza, Wilmington, DE 19801',\n registry_status: 'active',\n }),\n },\n);\nif (!response.ok) throw new Error(`HTTP ${response.status}`);\nconst company = await response.json(); // full company record" - } - ] + } } }, - "/v3/session/{sessionId}/kyb/parties/{partyUuid}/": { + "/v3/document-ai/{sessionId}/documents/{documentUuid}/": { "patch": { - "summary": "Skip (or re-require) a Key People party on a Business Verification (KYB) session", - "description": "Exempt a Key People party from verification, or put it back in scope. This is the operator escape hatch for a party that can never finish its own check - most often because the individual KYC session handed to a UBO or director was deleted, which freezes that child's status and leaves the KYB parked in `Awaiting User` with nothing left that could move it.\n\nSetting `is_skipped: true` drops the party out of the Key People aggregation and immediately recomputes the `KYBKeyPeopleCheck` and the parent session status, so the business session can settle and then be approved or declined. Sending `is_skipped: false` puts the party back in scope and the check goes back to waiting on it.\n\nUnlike the applicant-facing skip in the hosted Key People screen, this endpoint is **not** gated by the role's `allow_skip` setting - that flag limits what the applicant may decide, not what your analyst may override. `requires_verification` is deliberately left untouched, so an overridden party reads as `requires_verification: true` **and** `is_skipped: true` and the record still shows what the rules asked for.\n\nThe party must belong to the session in the path (`404` with `\"Party does not belong to the given session.\"` otherwise) and to your application. A PATCH that actually changes the flag fires the `data.updated` [webhook](/integration/webhooks) on the business session with trigger `console_key_people_skip`; a PATCH that sets the flag to its current value is a no-op that echoes the party back.\n\nAccepts an API key or a console user token; either way the credential needs the `write:businesses` permission.", - "operationId": "patch_v3_session_kyb_party_update", + "summary": "Update a Document AI document", + "description": "Correct the fields Document AI extracted from one uploaded document, set its review status, or both, on a KYC or KYB session. `extracted_data` must be a JSON object and is merged key by key: a key you send replaces the stored value (a key sent as `null` is stored as `null`) and keys you leave out keep their values. The document status feeds the Document AI result of its workflow node in the session decision; the session status itself is not recomputed. Every successful call, including one that changes nothing, sends a `data.updated` [webhook](https://docs.didit.me/integration/webhooks) with trigger `manual_review`. A scoped API key needs `decisions: write`; see [Document AI](https://docs.didit.me/core-technology/document-ai/overview).", + "operationId": "patch_v3_document_ai_document", + "x-didit-sdk": { + "resource": "sessions", + "method": "update_document_ai_data", + "auth": "api_key", + "server": true + }, "tags": [ "Sessions" ], - "security": [ - { - "ApiKeyAuth": [] - } - ], "parameters": [ { "name": "sessionId", "in": "path", "required": true, - "description": "UUID of the parent Business Verification (KYB) session.", + "description": "UUID of the verification session (the `session_id` returned by `POST /v3/session/`). Accepts both User Verification (KYC) and Business Verification (KYB) session IDs: the server resolves the ID across both session types. Must be a well-formed UUID; a malformed value does not match the route and returns an HTML `404`.", "schema": { "type": "string", "format": "uuid", - "example": "33333333-4444-5555-6666-777777777777" + "example": "11111111-2222-3333-4444-555555555555" } }, { - "name": "partyUuid", + "name": "documentUuid", "in": "path", "required": true, - "description": "UUID of the Key People party to update (`key_people_checks[].submitted.parties[].uuid` on the business session decision). Must belong to the session referenced by `sessionId`.", + "description": "UUID of the Document AI document (`document_ai_documents[].items[].uuid` in `GET /v3/session/{sessionId}/decision/`). It must belong to the session in the path.", "schema": { "type": "string", "format": "uuid", - "example": "0a3f1e2c-7c2f-4a1b-9d55-2f8d2a5d6b41" + "example": "44444444-5555-6666-7777-888888888888" } } ], @@ -36160,104 +55043,166 @@ "application/json": { "schema": { "type": "object", - "required": [ - "is_skipped" - ], "properties": { - "is_skipped": { - "type": "boolean", - "description": "`true` exempts the party from verification so the Key People check can settle without it; `false` puts it back in scope.", - "example": true + "extracted_data": { + "type": "object", + "additionalProperties": true, + "description": "Field keys (the keys you configured for the document) and their corrected values. Must be a JSON object; merged key by key into the stored data. Leave it out rather than sending `null`, which returns `400`.", + "example": { + "invoice_number": "INV-2026-0042" + } + }, + "status": { + "type": "string", + "enum": [ + "Approved", + "Declined", + "In Review", + "Not Finished" + ], + "description": "New review status of the document.", + "example": "Approved" } } + }, + "example": { + "extracted_data": { + "invoice_number": "INV-2026-0042" + }, + "status": "Approved" } } } }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH 'https://verification.didit.me/v3/document-ai/11111111-2222-3333-4444-555555555555/documents/44444444-5555-6666-7777-888888888888/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"extracted_data\": {\"invoice_number\": \"INV-2026-0042\"}, \"status\": \"Approved\"}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nsession_id = '11111111-2222-3333-4444-555555555555'\ndocument_uuid = '44444444-5555-6666-7777-888888888888'\nresp = requests.patch(\n f'https://verification.didit.me/v3/document-ai/{session_id}/documents/{document_uuid}/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={'extracted_data': {'invoice_number': 'INV-2026-0042'}, 'status': 'Approved'},\n timeout=10,\n)\nresp.raise_for_status()\ndocument = resp.json()\nprint(document['status'], document['extracted_data'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const sessionId = '11111111-2222-3333-4444-555555555555';\nconst documentUuid = '44444444-5555-6666-7777-888888888888';\nconst resp = await fetch(`https://verification.didit.me/v3/document-ai/${sessionId}/documents/${documentUuid}/`, {\n method: 'PATCH',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ extracted_data: { invoice_number: 'INV-2026-0042' }, status: 'Approved' }),\n});\nif (!resp.ok) throw new Error(`Update failed: ${resp.status}`);\nconst document = await resp.json();\nconsole.log(document.status, document.extracted_data);" + } + ], "responses": { "200": { - "description": "Party updated (or echoed unchanged when the flag already had this value). Returns the party - the same shape as an entry of `key_people_checks[].submitted.parties[]` on the business session decision.", + "description": "Document updated. Returns the document after the merge.", "content": { "application/json": { "schema": { "type": "object", - "description": "Key People party record - the same shape as an entry of `key_people_checks[].submitted.parties[]` on the business session decision.", + "required": [ + "uuid", + "document_key", + "original_filename", + "file_url", + "status", + "extracted_data" + ], "properties": { "uuid": { "type": "string", "format": "uuid", - "description": "Party identifier." - }, - "entity_type": { - "type": "string", - "enum": [ - "person", - "company" - ], - "description": "Whether the party is an individual or a company." + "description": "Document identifier." }, - "name": { + "document_key": { "type": "string", - "description": "Display name of the party." - }, - "is_skipped": { - "type": "boolean", - "description": "Whether the party is exempt from verification." - }, - "requires_verification": { - "type": "boolean", - "description": "What the role and threshold rules asked for. Left untouched by this endpoint." + "description": "The configured document this upload corresponds to." }, - "kyc_session_status": { + "original_filename": { "type": "string", "nullable": true, - "description": "Status of the linked individual KYC session, frozen at deletion time when the session was deleted." + "description": "File name the document was uploaded with." }, - "kyc_session_url": { + "file_url": { "type": "string", "nullable": true, - "description": "Link to the linked individual KYC session. `null` when there is none or when it was deleted." + "description": "Storage path of the uploaded file. It is not a download link: read the presigned `file_url` from the session decision to download the file." }, - "kyc_session_deleted": { - "type": "boolean", - "description": "The linked individual KYC session was deleted, so it can never settle." + "status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ], + "description": "Review status of the document." }, - "kyb_sub_session_deleted": { - "type": "boolean", - "description": "Corporate-party counterpart of `kyc_session_deleted`." + "extracted_data": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "All extracted fields after the merge, keyed by field key." } } + }, + "example": { + "uuid": "44444444-5555-6666-7777-888888888888", + "document_key": "invoice", + "original_filename": "invoice-2026-09.pdf", + "file_url": "document_ai/11111111-2222-3333-4444-555555555555-66666666-7777-8888-9999-000000000000.pdf", + "status": "Approved", + "extracted_data": { + "invoice_number": "INV-2026-0042", + "total_amount": 1250.0, + "issue_date": "2026-09-01" + } } } } }, "400": { - "description": "Validation error on the request body. `is_skipped` is the only field this endpoint reads, and it is required. Field errors arrive as `{\"\": [\"\"]}`. Body validation runs *after* the party lookup, so a malformed `is_skipped` sent to a party that does not exist reports `404`, not `400`.", + "description": "Validation error, checked before the document is looked up: `status` not one of the allowed values, or `extracted_data` sent as `null`.", "content": { "application/json": { "examples": { - "Missing is_skipped": { - "summary": "Empty body, or a body that omits `is_skipped`", + "Invalid status": { + "summary": "status not in the enum", "value": { - "is_skipped": [ - "This field is required." + "status": [ + "\"Rejected\" is not a valid choice." ] } }, - "Non-boolean is_skipped": { - "summary": "Value is not a boolean — `\"maybe\"`, `2`, `[]`. Booleans plus `\"true\"`/`\"false\"`, `1`/`0` and `\"yes\"`/`\"no\"` are all accepted", + "Null extracted_data": { + "summary": "extracted_data is null", "value": { - "is_skipped": [ - "Must be a valid boolean." + "extracted_data": [ + "This field may not be null." ] } - }, - "Null is_skipped": { - "summary": "`is_skipped` is present but `null`", + } + } + } + } + }, + "401": { + "description": "The scoped API key has expired. This is the only case that returns `401`: a missing, invalid or revoked key returns `403`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Expired key": { + "summary": "The key's expiry date has passed", "value": { - "is_skipped": [ - "This field may not be null." - ] + "detail": "This API key has expired." } } } @@ -36265,41 +55210,77 @@ } }, "403": { - "description": "Missing/invalid credentials or insufficient permissions. This API returns `403` for authentication failures — never `401`.", + "description": "Authentication or authorization failure. Missing, invalid and revoked keys return `403` (not `401`); so do a key of another application, a scoped key without the permission this endpoint needs, and a request from outside the key's IP allowlist. Distinguish them by `detail`.", "content": { "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, "examples": { - "Invalid Credentials": { - "summary": "Missing or invalid x-api-key", + "Missing or invalid credentials": { + "summary": "No x-api-key header, or the key failed validation", "value": { "detail": "Authentication credentials were not provided or are invalid." } }, - "Missing Privilege": { - "summary": "Credential lacks write:businesses", + "Other application": { + "summary": "A valid key that belongs to a different application than the session", "value": { "detail": "You do not have permission to perform this action." } + }, + "Scoped key without decisions write": { + "summary": "Scoped key lacks decisions: write", + "value": { + "detail": "This API key has no write access to decisions." + } + }, + "IP not allowed": { + "summary": "Request from outside the key's IP allowlist", + "value": { + "detail": "This API key can't be used from this IP address." + } } } } } }, "404": { - "description": "Unknown party, or a party that belongs to a different session. Each case has a distinct `detail` message (see examples). Credential and permission checks run before the party lookup, so an unauthorised call reports `403` rather than `404`. There is no separate session lookup: an unknown `sessionId` paired with a party of yours reports the mismatch message.", + "description": "The session does not exist, was deleted or is outside a scoped key's limits, or no current document with this UUID belongs to it. The session lookup runs before the credential check, so an unknown `sessionId` returns `404` even without a valid key.", "content": { "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, "examples": { - "Unknown party": { - "summary": "No party with this UUID in your application", + "Unknown session": { + "summary": "No session with this ID exists, or it was deleted", "value": { "detail": "Not found." } }, - "Party / session mismatch": { - "summary": "partyUuid exists but hangs off another session", + "Outside key limits": { + "summary": "The session is outside a scoped key's limits (also returned for a session deleted moments ago)", "value": { - "detail": "Party does not belong to the given session." + "detail": "Session not found." + } + }, + "Unknown document": { + "summary": "documentUuid does not belong to the session", + "value": { + "detail": "Document AI document not found." } } } @@ -36307,444 +55288,350 @@ } }, "429": { - "description": "Shared write rate limit exceeded (300 POST/PATCH/DELETE requests per minute per API key). Inspect `Retry-After` and the `X-RateLimit-*` response headers before retrying.", + "description": "Rate limit exceeded: 300 write (POST, PATCH, DELETE) requests per minute per API key. Wait `Retry-After` seconds before retrying; the `X-RateLimit-*` headers show the budget.", "content": { "application/json": { "schema": { "type": "object", "properties": { "detail": { - "type": "string" + "type": "string", + "description": "Human-readable error message." } } }, "examples": { "Rate limited": { - "summary": "Write budget exhausted", + "summary": "Budget for the current minute exhausted", "value": { "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." } } } } + }, + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + } } } - } - } - }, - "/v3/travel-rule/settings/": { - "get": { - "summary": "Get Travel Rule settings", - "description": "Read the application's Travel Rule settings and VASP profile. Auto-creates a default (disabled) settings row and an empty profile on first read.", - "operationId": "getTravelRuleSettings", - "tags": [ - "Travel Rule" - ], + }, "security": [ { "ApiKeyAuth": [] } + ] + } + }, + "/v3/aml/dataset-freshness/": { + "get": { + "summary": "Get AML dataset freshness", + "description": "Report how current the datasets behind AML screening are, per source family (`sanctions`, `pep`, `warnings`, `adverse_media`), as audit evidence for [ongoing AML monitoring](https://docs.didit.me/core-technology/aml-screening/continuous-monitoring-aml-screening#proving-that-monitoring-ran). The answer is platform-wide: no session in the path, no customer data in the body, the same answer for every application.\n\nAlways `200` for an accepted key. Check `availability` first: when it is not `available`, `dataset_freshness` is `null`. Dates are never estimated: a family whose datasets reported no date is absent from `sources`, and a family where only some datasets reported one has `oldest_updated_at: null`. Readings are taken while ongoing monitoring runs, not on each call; `captured_at` says when.\n\nA scoped API key needs `sessions: read`; a key limited to some workflows or to approved sessions cannot call this endpoint. Nothing is billed.", + "operationId": "get_aml_dataset_freshness", + "x-didit-sdk": { + "resource": "aml", + "method": "retrieve_dataset_freshness", + "auth": "api_key", + "server": true + }, + "tags": [ + "Standalone APIs" ], + "parameters": [], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X GET https://verification.didit.me/v3/travel-rule/settings/ \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X GET 'https://verification.didit.me/v3/aml/dataset-freshness/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/aml/dataset-freshness/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nbody = resp.json()\nif body['availability'] == 'available':\n for family, info in body['dataset_freshness']['sources'].items():\n print(family, info['last_updated_at'], info['oldest_updated_at'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/aml/dataset-freshness/', {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Freshness lookup failed: ${resp.status}`);\nconst body = await resp.json();\nif (body.availability === 'available') {\n for (const [family, info] of Object.entries(body.dataset_freshness.sources)) {\n console.log(family, info.last_updated_at, info.oldest_updated_at);\n }\n}" } ], "responses": { "200": { - "description": "Current Travel Rule settings and VASP profile.", + "description": "The latest dataset freshness reading, or `null` with the reason.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TravelRuleSettingsDetail" + "type": "object", + "required": [ + "dataset_freshness", + "availability" + ], + "properties": { + "dataset_freshness": { + "allOf": [ + { + "$ref": "#/components/schemas/AMLDatasetFreshness" + } + ], + "nullable": true, + "description": "The latest reading, or `null` when `availability` is not `available`." + }, + "availability": { + "type": "string", + "enum": [ + "available", + "not_configured", + "not_yet_collected" + ], + "description": "`available` when a reading exists; `not_configured` when dataset dates are not collected in this environment; `not_yet_collected` when collection is set up but no reading exists yet." + } + } }, "examples": { - "Disabled (default)": { - "value": { - "is_enabled": false, - "jurisdiction": "EU", - "name_matching_strictness": "DEFAULT", - "confirmation_timeout_hours": 48, - "timeout_outcome": "HOLD", - "threshold_amount": "0.00", - "legal_name": "", - "lei": "", - "compliance_email": "", - "is_discoverable": true, - "disabled_networks": [], - "networks": { - "gtr": { - "rail": "GTR", - "mode": "unavailable", - "platform_available": false, - "connected": false - }, - "trust": { - "rail": "TRUST", - "mode": "unavailable", - "platform_available": false, - "connected": false - }, - "verifyvasp": { - "rail": "VERIFYVASP", - "mode": "unavailable", - "platform_available": false, - "connected": false - }, - "sygna": { - "rail": "SYGNA", - "mode": "unavailable", - "platform_available": false, - "connected": false + "Available": { + "value": { + "dataset_freshness": { + "captured_at": "2026-10-17T06:00:12.345678+00:00", + "provider": "aml_data_source", + "catalog_version": "20261017054500-abc", + "sources": { + "sanctions": { + "last_updated_at": "2026-10-17T04:10:00+00:00", + "oldest_updated_at": "2026-10-15T22:30:00+00:00", + "dataset_count": 48, + "dated_dataset_count": 48 + }, + "pep": { + "last_updated_at": "2026-10-16T23:05:00+00:00", + "oldest_updated_at": null, + "dataset_count": 12, + "dated_dataset_count": 11 + } } - } + }, + "availability": "available" + } + }, + "Not collected yet": { + "value": { + "dataset_freshness": null, + "availability": "not_yet_collected" } } } } } - } - } - }, - "put": { - "summary": "Update Travel Rule settings", - "description": "Upsert the application's Travel Rule settings and VASP profile. Partial update — only send the fields you want to change. Setting `is_enabled: true` requires a non-blank `legal_name` (already stored or included in the same request), otherwise returns 400.", - "operationId": "updateTravelRuleSettings", - "tags": [ - "Travel Rule" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TravelRuleSettingsUpdate" - }, - "example": { - "is_enabled": true, - "legal_name": "Origin CASP SL", - "jurisdiction": "EU", - "name_matching_strictness": "DEFAULT", - "confirmation_timeout_hours": 48, - "timeout_outcome": "HOLD", - "is_discoverable": true - } - } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PUT https://verification.didit.me/v3/travel-rule/settings/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"is_enabled\": true, \"legal_name\": \"Origin CASP SL\", \"jurisdiction\": \"EU\"}'" - } - ], - "responses": { - "200": { - "description": "Updated settings and profile, in the same shape as GET.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TravelRuleSettingsDetail" - } - } - } }, - "400": { - "description": "Validation error, e.g. enabling without a legal_name.", + "401": { + "description": "The API key has expired. Only a key created with an expiry date can expire; after that date every request with it answers `401`.", "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } }, - "example": { - "legal_name": [ - "legal_name is required to enable travel rule." - ] + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } } } } - } - } - } - }, - "/v3/travel-rule/wallet-addresses/": { - "get": { - "summary": "List wallet address book entries", - "description": "Paginated list of your application's Travel Rule wallet address book, newest first.", - "operationId": "listTravelRuleWalletAddresses", - "tags": [ - "Travel Rule" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], - "parameters": [ - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "default": 50, - "minimum": 1 - } }, - { - "name": "offset", - "in": "query", - "schema": { - "type": "integer", - "default": 0, - "minimum": 0 - } - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET https://verification.didit.me/v3/travel-rule/wallet-addresses/ \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "200": { - "description": "Paginated wallet address book entries.", + "403": { + "description": "The request is not allowed. A missing or invalid API key gets the generic message (never `401`).", "content": { "application/json": { "schema": { "type": "object", "properties": { - "count": { - "type": "integer" - }, - "next": { - "type": "string", - "nullable": true - }, - "previous": { - "type": "string", - "nullable": true - }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WalletAddressEntry" - } + "detail": { + "type": "string" + } + } + }, + "examples": { + "Missing or invalid key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without sessions read": { + "value": { + "detail": "This API key has no read access to sessions." + } + }, + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." + } + }, + "Outside the key's IP list": { + "value": { + "detail": "This API key can't be used from this IP address." } } - } - } - } - } - } - }, - "post": { - "summary": "Register a wallet address", - "description": "Add an entry to your Travel Rule wallet address book so inbound INTERNAL-rail transfers can resolve against it.", - "operationId": "createTravelRuleWalletAddress", - "tags": [ - "Travel Rule" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WalletAddressEntryCreate" - }, - "example": { - "address": "0xBeneficiaryWallet01", - "chain": "ethereum", - "holder_name": "Ana Diaz", - "holder_vendor_data": "user-042", - "self_declared": true - } - } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/travel-rule/wallet-addresses/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"address\": \"0xBeneficiaryWallet01\", \"chain\": \"ethereum\", \"holder_name\": \"Ana Diaz\", \"self_declared\": true}'" - } - ], - "responses": { - "201": { - "description": "The created entry.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WalletAddressEntry" } } } }, - "400": { - "description": "Duplicate address for this application and chain.", + "429": { + "description": "More than 600 GET requests per minute with this API key. Wait the number of seconds in `Retry-After`, then retry.", "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } }, - "example": { - "address": [ - "A wallet address entry with this address and chain already exists." - ] + "examples": { + "Throttled": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } } } - } - } - } - } - }, - "/v3/travel-rule/wallet-addresses/{entry_uuid}/": { - "patch": { - "summary": "Update wallet address holder metadata", - "description": "Partial update of holder_name, holder_vendor_data, entity_type, and travel_address. Setting travel_address attaches a TRP routing address to the entry; invalid values are rejected with 400. The response echoes only these four fields.", - "operationId": "updateTravelRuleWalletAddress", - "tags": [ - "Travel Rule" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], - "parameters": [ - { - "name": "entry_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WalletAddressEntryUpdate" + }, + "headers": { + "Retry-After": { + "description": "Seconds until the current one-minute window resets.", + "schema": { + "type": "integer", + "example": 23 + } }, - "example": { - "holder_name": "Ana Updated", - "travel_address": "ta..." - } - } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH https://verification.didit.me/v3/travel-rule/wallet-addresses/{entry_uuid}/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"holder_name\": \"Ana Updated\", \"travel_address\": \"ta...\"}'" - } - ], - "responses": { - "200": { - "description": "The updated fields.", - "content": { - "application/json": { + "X-RateLimit-Limit": { + "description": "Requests allowed per minute in this budget.", "schema": { - "$ref": "#/components/schemas/WalletAddressEntryUpdate" + "type": "integer" + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the current window.", + "schema": { + "type": "integer" + } + }, + "X-RateLimit-Reset": { + "description": "Unix time when the window resets.", + "schema": { + "type": "integer" } } } } - } - }, - "delete": { - "summary": "Delete a wallet address", - "description": "Deletes the entry. It stops matching new transfers immediately and the address is freed up for re-registration.", - "operationId": "deleteTravelRuleWalletAddress", - "tags": [ - "Travel Rule" - ], + }, "security": [ { "ApiKeyAuth": [] } + ] + } + }, + "/v3/lists/": { + "get": { + "summary": "List lists", + "description": "Returns the application's lists, newest first: the system lists Didit provisions (one blocklist per entry type, the **High Risk Countries** custom list, and any system allowlist) and the allowlists and custom lists you created. Filter with `list_type` and `entry_type`; deleted lists never appear. Results are paginated with `limit` and `offset`. A scoped API key needs `workflows: read`. See [Lists](https://docs.didit.me/management-api/lists/overview) for what each list type does.", + "operationId": "list_lists", + "x-didit-sdk": { + "resource": "lists", + "method": "list", + "auth": "api_key", + "server": true + }, + "tags": [ + "Lists" ], "parameters": [ { - "name": "entry_uuid", - "in": "path", - "required": true, + "name": "list_type", + "in": "query", + "required": false, + "description": "Only lists of this type.", "schema": { "type": "string", - "format": "uuid" + "enum": [ + "blocklist", + "allowlist", + "custom" + ] } - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X DELETE https://verification.didit.me/v3/travel-rule/wallet-addresses/{entry_uuid}/ \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "204": { - "description": "Deleted." - } - } - } - }, - "/v3/travel-rule/vasps/": { - "get": { - "summary": "Search the VASP directory", - "description": "Paginated search across discoverable Didit customers with Travel Rule enabled, plus VASPs catalogued in the internal counterparty registry. Excludes your own application's profile.", - "operationId": "listTravelRuleVasps", - "tags": [ - "Travel Rule" - ], - "security": [ + }, { - "ApiKeyAuth": [] - } - ], - "parameters": [ + "name": "entry_type", + "in": "query", + "required": false, + "description": "Only lists of this entry type.", + "schema": { + "type": "string", + "enum": [ + "face", + "document", + "phone", + "email", + "ip_address", + "device_fingerprint", + "wallet_address", + "bank_account", + "country", + "user", + "business", + "key" + ] + } + }, { - "name": "search", + "name": "ordering", "in": "query", + "required": false, + "description": "Sort order: `-created_at` (newest first, the default) or `created_at` (oldest first).", "schema": { - "type": "string" - }, - "description": "Case-insensitive substring match on VASP name." + "type": "string", + "enum": [ + "-created_at", + "created_at" + ], + "default": "-created_at" + } }, { "name": "limit", "in": "query", + "required": false, + "description": "Page size. Defaults to 50. A value above 1000000 returns `400`; zero, a negative or a non-numeric value falls back to 50.", "schema": { "type": "integer", "default": 50, - "minimum": 1 + "minimum": 1, + "maximum": 1000000 } }, { "name": "offset", "in": "query", + "required": false, + "description": "Zero-based index of the first item to return. Prefer following `next`. A value above 1000000 returns `400`; a negative or non-numeric value falls back to 0.", "schema": { "type": "integer", "default": 0, - "minimum": 0 + "minimum": 0, + "maximum": 1000000 } } ], @@ -36752,333 +55639,257 @@ { "lang": "curl", "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/travel-rule/vasps/?search=Bene' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X GET 'https://verification.didit.me/v3/lists/?list_type=blocklist' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/lists/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n params={'list_type': 'blocklist'},\n timeout=10,\n)\nresp.raise_for_status()\nfor item in resp.json()['results']:\n print(item['uuid'], item['entry_type'], item['name'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/lists/?list_type=blocklist', {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Request failed: ${resp.status}`);\nconst page = await resp.json();\nconsole.log(page.results.map((list) => [list.uuid, list.entry_type, list.name]));" } ], "responses": { "200": { - "description": "Paginated, name-sorted VASP directory results.", + "description": "A page of lists.", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "count", + "next", + "previous", + "results" + ], "properties": { "count": { - "type": "integer" + "type": "integer", + "minimum": 0, + "description": "Total number of lists matching the filters." }, "next": { "type": "string", - "nullable": true + "format": "uri", + "nullable": true, + "description": "Absolute URL of the next page, or `null` on the last page." }, "previous": { "type": "string", - "nullable": true + "format": "uri", + "nullable": true, + "description": "Absolute URL of the previous page, or `null` on the first page." }, "results": { "type": "array", "items": { - "$ref": "#/components/schemas/VaspDirectoryEntry" + "$ref": "#/components/schemas/ComplianceList" } } } - } - } - } - } - } - } - }, - "/v3/transactions/{transaction_id}/travel-rule/": { - "patch": { - "summary": "Finish, cancel, or resend a Travel Rule transfer", - "description": "Provide `payment_txn_id` to report the on-chain hash and move an outbound COMPLETED transfer to FINISHED (its INTERNAL-rail counterpart also moves to FINISHED), `{\"action\": \"cancel\"}` to cancel a non-terminal transfer, or `{\"action\": \"resend\"}` to re-run routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA (improve the counterparty data first, e.g. add a travel_address to the destination wallet entry).", - "operationId": "patchTravelRuleTransfer", - "tags": [ - "Travel Rule" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], - "parameters": [ - { - "name": "transaction_id", - "in": "path", - "required": true, - "description": "Didit-stable transaction UUID that owns the transfer.", - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TravelRuleTransferPatchRequest" - }, - "examples": { - "Finish": { - "value": { - "payment_txn_id": "0xchainhash" - } - }, - "Cancel": { - "value": { - "action": "cancel" - } }, - "Resend": { - "value": { - "action": "resend" + "examples": { + "Lists": { + "value": { + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "uuid": "22222222-3333-4444-5555-666666666666", + "name": "Trusted office IPs", + "description": "Office egress ranges", + "list_type": "allowlist", + "entry_type": "ip_address", + "is_system": false, + "entry_count": 3, + "last_entry_added_at": "2026-10-02T08:15:27.501334Z", + "created_at": "2026-09-30T13:20:45.889120Z", + "updated_at": "2026-09-30T13:20:45.889120Z" + }, + { + "uuid": "11111111-2222-3333-4444-555555555555", + "name": "Face Blocklist", + "description": "Blocked biometric faces. When a face matching an entry in this list is detected during liveness verification, the session will be automatically declined.", + "list_type": "blocklist", + "entry_type": "face", + "is_system": true, + "entry_count": 12, + "last_entry_added_at": "2026-10-09T16:42:03.118204Z", + "created_at": "2026-01-14T10:02:11.402917Z", + "updated_at": "2026-01-14T10:02:11.402917Z" + } + ] + } } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH https://verification.didit.me/v3/transactions/{transaction_id}/travel-rule/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"payment_txn_id\": \"0xchainhash\"}'" - } - ], - "responses": { - "200": { - "description": "The updated transfer.", + }, + "400": { + "description": "A query parameter is invalid.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/TravelRuleTransferDetail" + "examples": { + "Unknown list type": { + "value": { + "list_type": [ + "\"denylist\" is not a valid choice." + ] + } + }, + "Unsupported ordering": { + "value": { + "ordering": [ + "\"name\" is not a valid choice." + ] + } + }, + "Limit too large": { + "value": [ + "limit must not exceed 1000000." + ] + } } } } }, - "400": { - "description": "Neither payment_txn_id nor a supported action (cancel, resend) was provided." - }, - "409": { - "description": "The transfer is not in a state that allows this operation (e.g. not COMPLETED/outbound for finish, already terminal for cancel, or not an outbound transfer in a resendable status for resend)." - } - } - } - }, - "/v3/transactions/{transaction_id}/travel-rule/ownership/": { - "post": { - "summary": "Confirm or deny wallet ownership", - "description": "Called by the beneficiary's application when its INTERNAL-rail transfer is UNCONFIRMED_OWNERSHIP. Confirming marks the matching wallet address book entry as ownership-verified and runs the beneficiary name match on both sides of the transfer; denying declines both sides.", - "operationId": "confirmTravelRuleOwnership", - "tags": [ - "Travel Rule" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], - "parameters": [ - { - "name": "transaction_id", - "in": "path", - "required": true, - "description": "Didit-stable transaction UUID that owns the transfer (the beneficiary side's transaction).", - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TravelRuleOwnershipConfirmRequest" - }, - "example": { - "confirmed": true - } - } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/transactions/{transaction_id}/travel-rule/ownership/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"confirmed\": true}'" - } - ], - "responses": { - "200": { - "description": "The updated transfer. originator_data/beneficiary_data are omitted until ownership_confirmed is true on an inbound transfer.", + "401": { + "description": "The scoped API key has expired. Other authentication failures return `403`.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/TravelRuleTransferDetail" + "examples": { + "Expired API key": { + "value": { + "detail": "This API key has expired." + } + } } } } }, - "409": { - "description": "The transfer is not currently UNCONFIRMED_OWNERSHIP, or its matching wallet address book entry was deleted." - } - } - } - }, - "/v3/travel-rule/pickup/{token}/": { - "get": { - "summary": "Get email-rail pickup info", - "description": "Public, unauthenticated. Lets a non-Didit counterparty VASP preview the pending exchange from the emailed pickup link. The token itself is the authentication.", - "operationId": "getTravelRulePickup", - "tags": [ - "Travel Rule" - ], - "security": [], - "parameters": [ - { - "name": "token", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl https://verification.didit.me/v3/travel-rule/pickup/{token}/" - } - ], - "responses": { - "200": { - "description": "Pickup preview.", + "403": { + "description": "The `x-api-key` header is missing or invalid, or a scoped API key lacks `workflows: read` or is used from an IP address outside its allowlist.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/TravelRulePickupInfo" + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without workflows access": { + "value": { + "detail": "This API key has no read access to workflows." + } + } } } } }, - "404": { - "description": "Unknown token." - }, - "410": { - "description": "The pickup window has closed (expired, or the transfer already left AWAITING_COUNTERPARTY)." - } - } - } - }, - "/v3/travel-rule/pickup/{token}/respond/": { - "post": { - "summary": "Respond to an email-rail pickup", - "description": "Public, unauthenticated. Accepts or declines the exchange from the emailed pickup link. On accept, beneficiary_name is checked against the originator's name_matching_strictness policy.", - "operationId": "respondTravelRulePickup", - "tags": [ - "Travel Rule" - ], - "security": [], - "parameters": [ - { - "name": "token", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TravelRulePickupRespondRequest" - }, - "example": { - "decision": "accept", - "beneficiary_name": "Carlos Ruiz" - } - } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/travel-rule/pickup/{token}/respond/ \\\n -H 'Content-Type: application/json' \\\n -d '{\"decision\": \"accept\", \"beneficiary_name\": \"Carlos Ruiz\"}'" - } - ], - "responses": { - "200": { - "description": "The resulting exchange status.", + "429": { + "description": "The shared read limit of 600 requests per minute per API key was exceeded. Honor `Retry-After` before retrying.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "string" + "examples": { + "Rate limited": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." } } - }, - "example": { - "status": "COMPLETED" } } } - }, - "404": { - "description": "Unknown token." - }, - "410": { - "description": "The pickup window has closed." } - } - } - }, - "/v3/travel-rule/inbound/": { - "post": { - "summary": "Register an after-deposit (sunrise) inbound transfer", - "description": "Register Travel Rule data for a crypto deposit that already settled on-chain before any exchange took place. Creates an inbound travelRule transaction (or reuses an unclaimed deposit) and resolves the transfer against your wallet address book. A transaction.created webhook fires for newly-created transactions.", - "operationId": "registerTravelRuleInbound", - "tags": [ - "Travel Rule" - ], + }, "security": [ { "ApiKeyAuth": [] } + ] + }, + "post": { + "summary": "Create a list", + "description": "Creates an allowlist or a custom list in the application the API key belongs to. Blocklists cannot be created: Didit provisions one system blocklist per entry type, and you add to it with [Create a list entry](https://docs.didit.me/management-api/lists/create-entry). `entry_type` cannot change after creation and decides how entry values are validated and matched. Allowlist entries mark matching items as trusted (for example, a face allowlist skips the duplicated-face action) and never override a blocklist match; reference any list from a workflow or transaction rule condition with `value_type: \"list\"`. A scoped API key needs `workflows: write`.", + "operationId": "create_list", + "x-didit-sdk": { + "resource": "lists", + "method": "create", + "auth": "api_key", + "server": true + }, + "tags": [ + "Lists" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TravelRuleInboundRequest" + "type": "object", + "required": [ + "name", + "list_type", + "entry_type" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "List name. Must differ from the name of every other list of the application, system lists included." + }, + "description": { + "type": "string", + "default": "", + "description": "Optional free-text description." + }, + "list_type": { + "type": "string", + "enum": [ + "allowlist", + "custom" + ], + "description": "`allowlist` or `custom`. `blocklist` is refused with `400`." + }, + "entry_type": { + "type": "string", + "enum": [ + "face", + "document", + "phone", + "email", + "ip_address", + "device_fingerprint", + "wallet_address", + "bank_account", + "country", + "user", + "business", + "key" + ], + "description": "What the list holds: `face`, `document` (document number), `phone`, `email`, `ip_address` (IP address or CIDR range), `device_fingerprint`, `wallet_address`, `bank_account`, `country` (ISO 3166-1 alpha-3), `user` or `business` (your `vendor_data`), or `key` (any string). It decides how entry values are validated and matched. Cannot be changed later." + } + } }, - "example": { - "chain": "ethereum", - "txid": "0xdeposit-hash", - "wallet_address": "0xYourDepositWallet", - "amount": "500.00", - "currency": "USDC", - "originator_data": { - "name": "Origin CASP SL" - }, - "beneficiary_data": { - "name": "Ana Diaz" - }, - "originating_vasp": { - "name": "Origin CASP SL", - "lei": "", - "travel_address": "ta..." + "examples": { + "IP allowlist": { + "value": { + "name": "Trusted office IPs", + "description": "Office egress ranges", + "list_type": "allowlist", + "entry_type": "ip_address" + } + }, + "Custom country list": { + "value": { + "name": "Enhanced due diligence countries", + "list_type": "custom", + "entry_type": "country" + } } } } @@ -37088,160 +55899,303 @@ { "lang": "curl", "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/travel-rule/inbound/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"chain\":\"ethereum\",\"txid\":\"0xdeposit-hash\",\"wallet_address\":\"0xYourDepositWallet\",\"amount\":\"500.00\",\"currency\":\"USDC\"}'" + "source": "curl -X POST 'https://verification.didit.me/v3/lists/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"name\": \"Trusted office IPs\",\n \"description\": \"Office egress ranges\",\n \"list_type\": \"allowlist\",\n \"entry_type\": \"ip_address\"\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/lists/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={\n 'name': 'Trusted office IPs',\n 'description': 'Office egress ranges',\n 'list_type': 'allowlist',\n 'entry_type': 'ip_address',\n },\n timeout=10,\n)\nresp.raise_for_status()\nprint('Created list', resp.json()['uuid'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/lists/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n name: 'Trusted office IPs',\n description: 'Office egress ranges',\n list_type: 'allowlist',\n entry_type: 'ip_address',\n }),\n});\nif (!resp.ok) throw new Error(`Create failed: ${resp.status}`);\nconst list = await resp.json();\nconsole.log('Created list', list.uuid);" } ], "responses": { "201": { - "description": "A new inbound transaction and transfer were created.", + "description": "The list was created.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TravelRuleInboundResponse" + "$ref": "#/components/schemas/ComplianceList" + }, + "examples": { + "New allowlist": { + "value": { + "uuid": "22222222-3333-4444-5555-666666666666", + "name": "Trusted office IPs", + "description": "Office egress ranges", + "list_type": "allowlist", + "entry_type": "ip_address", + "is_system": false, + "entry_count": 0, + "last_entry_added_at": null, + "created_at": "2026-10-12T09:15:44.031876Z", + "updated_at": "2026-10-12T09:15:44.031876Z" + } + } } } } }, - "200": { - "description": "An existing unclaimed deposit was reused (created is false).", + "400": { + "description": "The body is invalid.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/TravelRuleInboundResponse" + "examples": { + "Blocklist refused": { + "value": { + "list_type": [ + "Cannot create additional blocklist lists. System blocklists are auto-created for each application." + ] + } + }, + "Duplicate name": { + "value": { + "name": [ + "A list with this name already exists for this application." + ] + } + }, + "Missing fields": { + "value": { + "name": [ + "This field is required." + ], + "list_type": [ + "This field is required." + ], + "entry_type": [ + "This field is required." + ] + } + }, + "Unknown entry type": { + "value": { + "entry_type": [ + "\"iban\" is not a valid choice." + ] + } + }, + "Name too long": { + "value": { + "name": [ + "Ensure this field has no more than 255 characters." + ] + } + } } } } - } - } - } - }, - "/v3/travel-rule/widget-session/": { - "post": { - "summary": "Mint a wallet-ownership widget session", - "description": "Mint a hosted wallet-ownership widget URL/token. Send the customer to the returned url to prove control of a wallet (message signing, Satoshi test, screenshot, or self-declaration). The widget is web-only in this release - open the url in the system browser or a custom tab, never inside a mobile SDK webview.", - "operationId": "createTravelRuleWidgetSession", - "tags": [ - "Travel Rule" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TravelRuleWidgetSessionCreate" - }, - "example": { - "wallet_address": "0xBeneficiaryWallet01", - "chain": "ethereum", - "holder_name": "Ana Diaz", - "vendor_data": "user-042", - "transaction_id": "22222222-3333-4444-5555-666666666666", - "expires_in_minutes": 60 + }, + "401": { + "description": "The scoped API key has expired. Other authentication failures return `403`.", + "content": { + "application/json": { + "examples": { + "Expired API key": { + "value": { + "detail": "This API key has expired." + } + } + } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/travel-rule/widget-session/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"wallet_address\":\"0xBeneficiaryWallet01\",\"chain\":\"ethereum\",\"holder_name\":\"Ana Diaz\"}'" - } - ], - "responses": { - "201": { - "description": "The minted widget session.", + }, + "403": { + "description": "The `x-api-key` header is missing or invalid, or a scoped API key lacks `workflows: write` or is used from an IP address outside its allowlist.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/TravelRuleWidgetSessionResponse" + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without workflows access": { + "value": { + "detail": "This API key has no write access to workflows." + } + } } } } + }, + "429": { + "$ref": "#/components/responses/RateLimitedWrite" } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/travel-rule/widget/{token}/": { + "/v3/lists/{list_uuid}/": { "get": { - "summary": "Get wallet-ownership widget context", - "description": "Public read endpoint keyed by the widget session token - no API key required. Returns the widget context plus the status of every proof, so you can poll it to detect completion; completed_at becomes non-null once a proof is verified. A SATOSHI_TEST proof can sit at status PENDING while its deposit is seen on-chain but unconfirmed - a background job automatically rechecks it and completes it once the deposit confirms, so no client action is needed while it waits. Each proof carries the fields its method needs: deposit_address, expected_amount, and its own expires_at for SATOSHI_TEST; challenge for MESSAGE_SIGNING. Prefer the travel_rule.status.updated webhook over polling when the session is linked to a transfer.", - "operationId": "getTravelRuleWidgetContext", + "summary": "Get a list", + "description": "Returns one list of the application with its entry count and when its newest entry was added. A list of another application, or one that was deleted, returns `404`. This returns the list only: read its values with [List list entries](https://docs.didit.me/management-api/lists/list-entries). A scoped API key needs `workflows: read`.", + "operationId": "get_list", + "x-didit-sdk": { + "resource": "lists", + "method": "retrieve", + "auth": "api_key", + "server": true + }, "tags": [ - "Travel Rule" + "Lists" ], - "security": [], "parameters": [ { - "name": "token", + "name": "list_uuid", "in": "path", "required": true, - "description": "Opaque session token returned when the widget session was minted.", + "description": "UUID of the list, as returned by List lists or Create a list.", "schema": { - "type": "string" - } + "type": "string", + "format": "uuid" + }, + "example": "11111111-2222-3333-4444-555555555555" } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl https://verification.didit.me/v3/travel-rule/widget/{token}/" + "source": "curl -X GET 'https://verification.didit.me/v3/lists/11111111-2222-3333-4444-555555555555/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nlist_uuid = '11111111-2222-3333-4444-555555555555'\nresp = requests.get(\n f'https://verification.didit.me/v3/lists/{list_uuid}/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nprint(resp.json())" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const listUuid = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(`https://verification.didit.me/v3/lists/${listUuid}/`, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Lookup failed: ${resp.status}`);\nconst list = await resp.json();\nconsole.log(list.name, list.entry_count);" } ], "responses": { "200": { - "description": "The widget context and proof statuses.", + "description": "The list.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TravelRuleWidgetContext" + "$ref": "#/components/schemas/ComplianceList" + }, + "examples": { + "System face blocklist": { + "value": { + "uuid": "11111111-2222-3333-4444-555555555555", + "name": "Face Blocklist", + "description": "Blocked biometric faces. When a face matching an entry in this list is detected during liveness verification, the session will be automatically declined.", + "list_type": "blocklist", + "entry_type": "face", + "is_system": true, + "entry_count": 12, + "last_entry_added_at": "2026-10-09T16:42:03.118204Z", + "created_at": "2026-01-14T10:02:11.402917Z", + "updated_at": "2026-01-14T10:02:11.402917Z" + } + } + } + } + } + }, + "401": { + "description": "The scoped API key has expired. Other authentication failures return `403`.", + "content": { + "application/json": { + "examples": { + "Expired API key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "The `x-api-key` header is missing or invalid, or a scoped API key lacks `workflows: read` or is used from an IP address outside its allowlist.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without workflows access": { + "value": { + "detail": "This API key has no read access to workflows." + } + } } } } }, "404": { - "description": "Unknown token." + "description": "No list with this UUID exists in the API key's application, or it was deleted.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "No List matches the given query." + } + } + } + } + } }, - "410": { - "description": "The session expired without completing." + "429": { + "description": "The shared read limit of 600 requests per minute per API key was exceeded. Honor `Retry-After` before retrying.", + "content": { + "application/json": { + "examples": { + "Rate limited": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } + } + } + } } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/": { + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, "patch": { - "summary": "Toggle AML ongoing monitoring for users", - "description": "Enables or disables AML ongoing monitoring at the user level. Exactly one monitored AML screening is kept per user; users without any AML screening (for example imported users) get one created and screened first, billed as a regular AML screening. Set estimate_only to preview counts and costs without writing. Batches of 20 or fewer resolve inline (200, COMPLETED); larger batches return 202 and run asynchronously.", - "operationId": "toggle_vendor_users_ongoing_monitoring", + "summary": "Update a list", + "description": "Renames a list or changes its description. Only `name` and `description` are read; any other field is ignored, so `list_type` and `entry_type` keep the values they were created with. System lists cannot be modified and return `400`. The response contains only `name` and `description`: call [Get a list](https://docs.didit.me/management-api/lists/get) for the full list. A scoped API key needs `workflows: write`.", + "operationId": "update_list", + "x-didit-sdk": { + "resource": "lists", + "method": "update", + "auth": "api_key", + "server": true + }, "tags": [ - "Users" + "Lists" ], "parameters": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", + "name": "list_uuid", "in": "path", "required": true, + "description": "UUID of the list, as returned by List lists or Create a list.", "schema": { "type": "string", "format": "uuid" }, - "description": "Application UUID." + "example": "11111111-2222-3333-4444-555555555555" } ], "requestBody": { @@ -37250,330 +56204,241 @@ "application/json": { "schema": { "type": "object", - "required": [ - "is_enabled" - ], "properties": { - "is_enabled": { - "type": "boolean", - "description": "true to enable monitoring, false to disable it." - }, - "estimate_only": { - "type": "boolean", - "default": false, - "description": "When true, returns the cost estimate and performs no writes." - }, - "update_all": { - "type": "boolean", - "default": false, - "description": "Apply to every entity of the application (scoped by the filter_* params when present)." - }, - "filter_search": { - "type": "string" - }, - "filter_status": { - "type": "array", - "items": { - "type": "string" - } - }, - "filter_country": { - "type": "string" - }, - "filter_date_from": { + "name": { "type": "string", - "format": "date" + "minLength": 1, + "maxLength": 255, + "description": "New name. Must differ from the name of every other list of the application." }, - "filter_date_to": { + "description": { "type": "string", - "format": "date" - }, - "didit_internal_id_list": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - }, - "description": "Entities to toggle. Required unless update_all is true." + "description": "New description. Send an empty string to clear it." + } + } + }, + "examples": { + "Rename": { + "value": { + "name": "Trusted office and VPN IPs" + } + }, + "Rename and describe": { + "value": { + "name": "Trusted office and VPN IPs", + "description": "Office egress ranges and the corporate VPN" } } } } } }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH 'https://verification.didit.me/v3/lists/11111111-2222-3333-4444-555555555555/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"name\": \"Trusted office and VPN IPs\"}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nlist_uuid = '11111111-2222-3333-4444-555555555555'\nresp = requests.patch(\n f'https://verification.didit.me/v3/lists/{list_uuid}/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={'name': 'Trusted office and VPN IPs'},\n timeout=10,\n)\nresp.raise_for_status()\nprint(resp.json())" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const listUuid = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(`https://verification.didit.me/v3/lists/${listUuid}/`, {\n method: 'PATCH',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ name: 'Trusted office and VPN IPs' }),\n});\nif (!resp.ok) throw new Error(`Update failed: ${resp.status}`);\nconsole.log(await resp.json());" + } + ], "responses": { "200": { - "description": "Estimate (when estimate_only) or the completed job for small batches processed inline.", + "description": "The list was updated. Only the editable fields are returned.", "content": { "application/json": { "schema": { - "oneOf": [ - { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (no writes are performed).", - "properties": { - "users_total": { - "type": "integer", - "description": "Entities in the selection." - }, - "already_enabled": { - "type": "integer" - }, - "with_existing_aml": { - "type": "integer", - "description": "Entities whose existing AML screening will be switched to monitored." - }, - "needing_screening": { - "type": "integer", - "description": "Entities without any AML screening that will be screened first (billed)." - }, - "skipped_no_identity_data": { - "type": "integer" - }, - "skipped_blocked": { - "type": "integer" - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2 - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07 - }, - "balance_sufficient": { - "type": "boolean" - } - } + "type": "object", + "required": [ + "name", + "description" + ], + "properties": { + "name": { + "type": "string", + "description": "The list's name after the update." }, - { - "type": "object", - "properties": { - "job_id": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED" - ] - }, - "is_enabled": { - "type": "boolean" - }, - "total": { - "type": "integer" - }, - "processed": { - "type": "integer" - }, - "outcomes": { - "type": "object", - "description": "Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.", - "properties": { - "enabled": { - "type": "integer", - "description": "Monitoring enabled on an existing AML screening." - }, - "screened_and_enabled": { - "type": "integer", - "description": "A new AML screening was created (billed) and monitoring enabled." - }, - "already_enabled": { - "type": "integer", - "description": "Monitoring was already active (legacy duplicate rows are consolidated)." - }, - "disabled": { - "type": "integer", - "description": "Monitoring disabled on every AML row of the entity." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Skipped: no full name + date of birth + country available to screen." - }, - "skipped_blocked": { - "type": "integer", - "description": "Skipped: the user is blocked." - }, - "skipped_sandbox": { - "type": "integer", - "description": "Skipped: sandbox application." - }, - "failed_provider": { - "type": "integer", - "description": "The screening provider call failed." - }, - "failed_insufficient_balance": { - "type": "integer", - "description": "Balance ran out mid-job." - } - } - }, - "error": { - "type": "string", - "nullable": true - }, - "estimate": { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (no writes are performed).", - "properties": { - "users_total": { - "type": "integer", - "description": "Entities in the selection." - }, - "already_enabled": { - "type": "integer" - }, - "with_existing_aml": { - "type": "integer", - "description": "Entities whose existing AML screening will be switched to monitored." - }, - "needing_screening": { - "type": "integer", - "description": "Entities without any AML screening that will be screened first (billed)." - }, - "skipped_no_identity_data": { - "type": "integer" - }, - "skipped_blocked": { - "type": "integer" - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2 - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07 - }, - "balance_sufficient": { - "type": "boolean" - } - } - } - } + "description": { + "type": "string", + "description": "The list's description after the update." + } + } + }, + "examples": { + "Renamed": { + "value": { + "name": "Trusted office and VPN IPs", + "description": "Office egress ranges" + } + } + } + } + } + }, + "400": { + "description": "The list is a system list, or the body is invalid.", + "content": { + "application/json": { + "examples": { + "System list": { + "value": { + "non_field_errors": [ + "System lists cannot be modified." + ] + } + }, + "Duplicate name": { + "value": { + "name": [ + "A list with this name already exists for this application." + ] + } + }, + "Blank name": { + "value": { + "name": [ + "This field may not be blank." + ] + } + } + } + } + } + }, + "401": { + "description": "The scoped API key has expired. Other authentication failures return `403`.", + "content": { + "application/json": { + "examples": { + "Expired API key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "The `x-api-key` header is missing or invalid, or a scoped API key lacks `workflows: write` or is used from an IP address outside its allowlist.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without workflows access": { + "value": { + "detail": "This API key has no write access to workflows." + } + } + } + } + } + }, + "404": { + "description": "No list with this UUID exists in the API key's application, or it was deleted.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "No List matches the given query." + } + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/RateLimitedWrite" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "delete": { + "summary": "Delete a list", + "description": "Deletes an allowlist or custom list together with its entries. Deletion is irreversible: the list and its entries no longer appear in any read, and workflow and transaction rule conditions that reference the list evaluate it as an empty list from then on. Sessions and transactions already evaluated keep their results, and the name can be reused for a new list. System lists cannot be deleted and return `400`; to empty one, [delete its entries](https://docs.didit.me/management-api/lists/delete-entry). A scoped API key needs `workflows: write`.", + "operationId": "delete_list", + "x-didit-sdk": { + "resource": "lists", + "method": "delete", + "auth": "api_key", + "server": true + }, + "tags": [ + "Lists" + ], + "parameters": [ + { + "name": "list_uuid", + "in": "path", + "required": true, + "description": "UUID of the list, as returned by List lists or Create a list.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "11111111-2222-3333-4444-555555555555" + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X DELETE 'https://verification.didit.me/v3/lists/11111111-2222-3333-4444-555555555555/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nlist_uuid = '11111111-2222-3333-4444-555555555555'\nresp = requests.delete(\n f'https://verification.didit.me/v3/lists/{list_uuid}/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nprint('Deleted:', resp.status_code == 204)" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const listUuid = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(`https://verification.didit.me/v3/lists/${listUuid}/`, {\n method: 'DELETE',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (resp.status !== 204) throw new Error(`Delete failed: ${resp.status}`);\nconsole.log('List deleted');" + } + ], + "responses": { + "204": { + "description": "The list and its entries were deleted. No body." + }, + "400": { + "description": "The list is a system list.", + "content": { + "application/json": { + "examples": { + "System list": { + "value": { + "detail": "System lists cannot be deleted." } - ] + } } } } }, - "202": { - "description": "Larger batches are processed asynchronously; poll the job endpoint with the returned job_id.", + "401": { + "description": "The scoped API key has expired. Other authentication failures return `403`.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "job_id": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED" - ] - }, - "is_enabled": { - "type": "boolean" - }, - "total": { - "type": "integer" - }, - "processed": { - "type": "integer" - }, - "outcomes": { - "type": "object", - "description": "Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.", - "properties": { - "enabled": { - "type": "integer", - "description": "Monitoring enabled on an existing AML screening." - }, - "screened_and_enabled": { - "type": "integer", - "description": "A new AML screening was created (billed) and monitoring enabled." - }, - "already_enabled": { - "type": "integer", - "description": "Monitoring was already active (legacy duplicate rows are consolidated)." - }, - "disabled": { - "type": "integer", - "description": "Monitoring disabled on every AML row of the entity." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Skipped: no full name + date of birth + country available to screen." - }, - "skipped_blocked": { - "type": "integer", - "description": "Skipped: the user is blocked." - }, - "skipped_sandbox": { - "type": "integer", - "description": "Skipped: sandbox application." - }, - "failed_provider": { - "type": "integer", - "description": "The screening provider call failed." - }, - "failed_insufficient_balance": { - "type": "integer", - "description": "Balance ran out mid-job." - } - } - }, - "error": { - "type": "string", - "nullable": true - }, - "estimate": { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (no writes are performed).", - "properties": { - "users_total": { - "type": "integer", - "description": "Entities in the selection." - }, - "already_enabled": { - "type": "integer" - }, - "with_existing_aml": { - "type": "integer", - "description": "Entities whose existing AML screening will be switched to monitored." - }, - "needing_screening": { - "type": "integer", - "description": "Entities without any AML screening that will be screened first (billed)." - }, - "skipped_no_identity_data": { - "type": "integer" - }, - "skipped_blocked": { - "type": "integer" - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2 - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07 - }, - "balance_sufficient": { - "type": "boolean" - } - } + "examples": { + "Expired API key": { + "value": { + "detail": "This API key has expired." } } } @@ -37581,164 +56446,269 @@ } }, "403": { - "description": "Missing write:sessions privilege, or insufficient balance to enable (the estimate is included in the error body)." + "description": "The `x-api-key` header is missing or invalid, or a scoped API key lacks `workflows: write` or is used from an IP address outside its allowlist.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without workflows access": { + "value": { + "detail": "This API key has no write access to workflows." + } + } + } + } + } + }, + "404": { + "description": "No list with this UUID exists in the API key's application, or it was already deleted.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "No List matches the given query." + } + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/RateLimitedWrite" } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/{job_id}/": { + "/v3/lists/{list_uuid}/entries/": { "get": { - "summary": "Get AML ongoing monitoring job", - "description": "Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING.", - "operationId": "get_vendor_users_ongoing_monitoring_job", + "summary": "List list entries", + "description": "Returns the current entries of one list, newest first. `search` matches part of an entry's `value` or `display_label` without regard to case, and `reference_session_id` narrows the results to the entries taken from one person verification session. Image URLs in `metadata` are short-lived signed URLs issued again on every call; a scoped API key without `media: read` receives `null` in their place. A scoped API key needs `workflows: read`.", + "operationId": "list_list_entries", + "x-didit-sdk": { + "resource": "lists.entries", + "method": "list", + "auth": "api_key", + "server": true + }, "tags": [ - "Users" + "Lists" ], "parameters": [ { - "name": "organization_id", + "name": "list_uuid", "in": "path", "required": true, + "description": "UUID of the list, as returned by List lists or Create a list.", "schema": { "type": "string", "format": "uuid" }, - "description": "Organization UUID." + "example": "11111111-2222-3333-4444-555555555555" }, { - "name": "application_id", - "in": "path", - "required": true, + "name": "search", + "in": "query", + "required": false, + "description": "Case-insensitive text that the entry's `value` or `display_label` must contain.", "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "type": "string" + } }, { - "name": "job_id", - "in": "path", - "required": true, + "name": "reference_session_id", + "in": "query", + "required": false, + "description": "Only the entries taken from this person verification session, for example to see which of its faces are already listed.", "schema": { "type": "string", "format": "uuid" - }, - "description": "Monitoring job UUID returned by the toggle endpoint." + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "description": "Page size. Defaults to 50. A value above 1000000 returns `400`; zero, a negative or a non-numeric value falls back to 50.", + "schema": { + "type": "integer", + "default": 50, + "minimum": 1, + "maximum": 1000000 + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "description": "Zero-based index of the first item to return. Prefer following `next`. A value above 1000000 returns `400`; a negative or non-numeric value falls back to 0.", + "schema": { + "type": "integer", + "default": 0, + "minimum": 0, + "maximum": 1000000 + } + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/lists/11111111-2222-3333-4444-555555555555/entries/?search=example.com' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nlist_uuid = '11111111-2222-3333-4444-555555555555'\nresp = requests.get(\n f'https://verification.didit.me/v3/lists/{list_uuid}/entries/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n params={'search': 'example.com', 'limit': 100},\n timeout=10,\n)\nresp.raise_for_status()\nfor entry in resp.json()['results']:\n print(entry['uuid'], entry['value'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const listUuid = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(\n `https://verification.didit.me/v3/lists/${listUuid}/entries/?search=example.com`,\n { headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n);\nif (!resp.ok) throw new Error(`Request failed: ${resp.status}`);\nconst page = await resp.json();\nconsole.log(page.count, page.results.map((entry) => entry.value));" } ], "responses": { "200": { - "description": "The job.", + "description": "A page of entries, newest first.", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "count", + "next", + "previous", + "results" + ], "properties": { - "job_id": { - "type": "string", - "format": "uuid" + "count": { + "type": "integer", + "minimum": 0, + "description": "Total number of entries matching the filters." }, - "status": { + "next": { "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED" - ] - }, - "is_enabled": { - "type": "boolean" - }, - "total": { - "type": "integer" - }, - "processed": { - "type": "integer" - }, - "outcomes": { - "type": "object", - "description": "Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.", - "properties": { - "enabled": { - "type": "integer", - "description": "Monitoring enabled on an existing AML screening." - }, - "screened_and_enabled": { - "type": "integer", - "description": "A new AML screening was created (billed) and monitoring enabled." - }, - "already_enabled": { - "type": "integer", - "description": "Monitoring was already active (legacy duplicate rows are consolidated)." - }, - "disabled": { - "type": "integer", - "description": "Monitoring disabled on every AML row of the entity." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Skipped: no full name + date of birth + country available to screen." - }, - "skipped_blocked": { - "type": "integer", - "description": "Skipped: the user is blocked." - }, - "skipped_sandbox": { - "type": "integer", - "description": "Skipped: sandbox application." - }, - "failed_provider": { - "type": "integer", - "description": "The screening provider call failed." - }, - "failed_insufficient_balance": { - "type": "integer", - "description": "Balance ran out mid-job." - } - } + "format": "uri", + "nullable": true, + "description": "Absolute URL of the next page, or `null` on the last page." }, - "error": { + "previous": { "type": "string", - "nullable": true + "format": "uri", + "nullable": true, + "description": "Absolute URL of the previous page, or `null` on the first page." }, - "estimate": { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (no writes are performed).", - "properties": { - "users_total": { - "type": "integer", - "description": "Entities in the selection." - }, - "already_enabled": { - "type": "integer" - }, - "with_existing_aml": { - "type": "integer", - "description": "Entities whose existing AML screening will be switched to monitored." - }, - "needing_screening": { - "type": "integer", - "description": "Entities without any AML screening that will be screened first (billed)." - }, - "skipped_no_identity_data": { - "type": "integer" - }, - "skipped_blocked": { - "type": "integer" - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2 - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07 + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ComplianceListEntry" + } + } + } + }, + "examples": { + "Entries": { + "value": { + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "uuid": "bbbbbbbb-cccc-dddd-eeee-ffffffffffff", + "value": "X1234567", + "display_label": "Jane Doe - X1234567", + "comment": "", + "added_by": "api", + "reference_session_id": "33333333-4444-5555-6666-777777777777", + "reference_session_number": 1042, + "reference_session_type": "kyc", + "reference_object_uuid": "66666666-7777-8888-9999-000000000000", + "metadata": { + "full_name": "Jane Doe", + "issuing_state": "ESP", + "document_type": "ID", + "region": "", + "document_number": "X1234567" + }, + "created_at": "2026-10-12T10:06:12.774915Z" }, - "balance_sufficient": { - "type": "boolean" + { + "uuid": "eeeeeeee-ffff-0000-1111-222222222222", + "value": "y7654321", + "display_label": "", + "comment": "Reported by a partner", + "added_by": "api", + "reference_session_id": null, + "reference_session_number": null, + "reference_session_type": null, + "reference_object_uuid": null, + "metadata": null, + "created_at": "2026-10-03T07:48:30.552810Z" } - } + ] + } + } + } + } + } + }, + "400": { + "description": "A query parameter is invalid.", + "content": { + "application/json": { + "examples": { + "Invalid session id": { + "value": { + "reference_session_id": [ + "Must be a valid UUID." + ] + } + }, + "Limit too large": { + "value": [ + "limit must not exceed 1000000." + ] + } + } + } + } + }, + "401": { + "description": "The scoped API key has expired. Other authentication failures return `403`.", + "content": { + "application/json": { + "examples": { + "Expired API key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "The `x-api-key` header is missing or invalid, or a scoped API key lacks `workflows: read` or is used from an IP address outside its allowlist.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without workflows access": { + "value": { + "detail": "This API key has no read access to workflows." } } } @@ -37746,576 +56716,558 @@ } }, "404": { - "description": "Unknown job for this application." + "description": "No list with this UUID exists in the API key's application, or it was deleted.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "List not found." + } + } + } + } + } + }, + "429": { + "description": "The shared read limit of 600 requests per minute per API key was exceeded. Honor `Retry-After` before retrying.", + "content": { + "application/json": { + "examples": { + "Rate limited": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } + } + } + } } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/": { - "patch": { - "summary": "Toggle AML ongoing monitoring for businesses", - "description": "Business-level equivalent of the user toggle: one monitored AML screening per business, with businesses lacking any AML screening screened first (company entity). Same estimate_only, inline fast-path and async job contract.", - "operationId": "toggle_vendor_businesses_ongoing_monitoring", + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + }, + "post": { + "summary": "Create a list entry", + "description": "Adds one entry to a list, in one of three ways: by `value`; from a verification session with `reference_session_id`, where Didit reads the value from the session's most recent record of the list's entry type and links the entry to it; or by `value` with a `reference_object_uuid` and `metadata.reference_type` pointing back to a transaction, user or business. On the system face, document, phone and email blocklists, the record read from the session is also flagged as blocklisted, so new matching sessions are declined; on the system user and business blocklists, matching users or businesses are set to `BLOCKED`. Sessions and transactions that already finished are not changed. A scoped API key needs `workflows: write`. See [Create a list entry](https://docs.didit.me/management-api/lists/create-entry) for each mode.", + "operationId": "create_list_entry", + "x-didit-sdk": { + "resource": "lists.entries", + "method": "create", + "auth": "api_key", + "server": true + }, "tags": [ - "Businesses" + "Lists" ], "parameters": [ { - "name": "organization_id", + "name": "list_uuid", "in": "path", "required": true, + "description": "UUID of the list to add to. For a face you only have as an image, use Upload a face to a list instead.", "schema": { "type": "string", "format": "uuid" }, - "description": "Organization UUID." + "example": "11111111-2222-3333-4444-555555555555" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Send `value`, `reference_session_id`, or both. Which fields apply depends on the list's `entry_type`.", + "properties": { + "value": { + "type": "string", + "description": "The value to list, validated against the list's `entry_type`: `email` a valid email address; `phone` a valid international number, stored in E.164; `ip_address` an IP address or CIDR range (for example `10.0.0.0/8`); `device_fingerprint` at least 8 characters among letters, digits, `-`, `_`, `:` and `.` (a blocklist refuses a fingerprint that several distinct devices have reported); `country` an ISO 3166-1 alpha-2 or alpha-3 code, stored as alpha-3; `user` and `business` the `vendor_data`; any other type a non-empty string. Stored in lower case except phone numbers and country codes, and a list holds each value once (case-insensitive). Required unless `reference_session_id` is sent; must be left empty with a document-capture `face_image_source`." + }, + "reference_session_id": { + "type": "string", + "format": "uuid", + "description": "A person or business verification session of your application to take the value from. Didit reads the session's most recent record of the list's entry type (`face`, `document`, `phone`, `email`, `ip_address` or `device_fingerprint`), links the entry to the session and to that record, and fills `display_label` and `metadata` for documents. For a `device_fingerprint` blocklist, a session that only has a device-class fingerprint, or was completed on more than one device, is refused: send the exact fingerprint as `value`. For other entry types, or when the session has no such record, send `value` too; your `value` is then stored and the entry is still linked to the session." + }, + "reference_object_uuid": { + "type": "string", + "format": "uuid", + "description": "UUID of the record the entry stands for when it does not come from a session: a transaction UUID, or the `didit_internal_id` of a user or business. Pair it with `metadata.reference_type`. Replaced by the session's record when `reference_session_id` provides one." + }, + "face_image_source": { + "type": "string", + "enum": [ + "liveness", + "front_image_camera_front", + "back_image_camera_front" + ], + "default": "liveness", + "description": "Face lists with `reference_session_id`: which of the session's images the face is taken from. `liveness` (default) is the liveness selfie. `front_image_camera_front` and `back_image_camera_front` are the selfies the front camera took while the user photographed the front or back of their document: accepted only on the system face blocklist, with `reference_session_id` and an empty `value`. Each selfie can be added once and must show one clear face." + }, + "reference_node_id": { + "type": "string", + "nullable": true, + "maxLength": 100, + "description": "With a document-capture `face_image_source`: the ID verification node whose selfie to use, for workflows with more than one. Defaults to the most recent ID verification that has that image. Refused with `liveness`." + }, + "display_label": { + "type": "string", + "maxLength": 512, + "description": "Human-readable label. When empty, Didit derives one from the session where it can (for example the holder's name and document number)." + }, + "comment": { + "type": "string", + "description": "Free-text note, such as the reason for listing." + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Free-form JSON stored with the entry and returned as stored. Use `reference_type` and `reference_id` so the console links back to the source. `reference_image_url` and `front_image_url` are set by Didit only and are dropped if you send them. For an entry read from a session, Didit's keys are merged with yours.", + "properties": { + "reference_type": { + "type": "string", + "enum": [ + "transaction", + "vendor_user", + "vendor_business" + ], + "description": "What `reference_object_uuid` points to." + }, + "reference_id": { + "type": "string", + "description": "Identifier of the source record." + } + } + } + } + }, + "examples": { + "By value": { + "value": { + "value": "+14155552671", + "comment": "Reported fraud" + } + }, + "From a session": { + "value": { + "reference_session_id": "33333333-4444-5555-6666-777777777777" + } + }, + "Face from a document-capture selfie": { + "value": { + "reference_session_id": "33333333-4444-5555-6666-777777777777", + "face_image_source": "front_image_camera_front" + } + }, + "From a transaction": { + "value": { + "value": "0x52908400098527886E0F7030069857D2E4169EE7", + "reference_object_uuid": "55555555-6666-7777-8888-999999999999", + "comment": "Linked to a chargeback", + "metadata": { + "reference_type": "transaction", + "reference_id": "55555555-6666-7777-8888-999999999999" + } + } + }, + "Country": { + "value": { + "value": "PRK", + "comment": "Sanctioned jurisdiction" + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/lists/11111111-2222-3333-4444-555555555555/entries/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"value\": \"+14155552671\", \"comment\": \"Reported fraud\"}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nlist_uuid = '11111111-2222-3333-4444-555555555555'\nresp = requests.post(\n f'https://verification.didit.me/v3/lists/{list_uuid}/entries/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={'reference_session_id': '33333333-4444-5555-6666-777777777777'},\n timeout=10,\n)\nresp.raise_for_status()\nentry = resp.json()\nprint('Listed', entry['value'], 'as', entry['uuid'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const listUuid = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(`https://verification.didit.me/v3/lists/${listUuid}/entries/`, {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ value: '+14155552671', comment: 'Reported fraud' }),\n});\nif (!resp.ok) throw new Error(`Create failed: ${resp.status} ${await resp.text()}`);\nconst entry = await resp.json();\nconsole.log('Listed', entry.value, 'as', entry.uuid);" + } + ], + "responses": { + "201": { + "description": "The entry was created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComplianceListEntry" + }, + "examples": { + "By value": { + "value": { + "uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "value": "+14155552671", + "display_label": "", + "comment": "Reported fraud", + "added_by": "api", + "reference_session_id": null, + "reference_session_number": null, + "reference_session_type": null, + "reference_object_uuid": null, + "metadata": null, + "created_at": "2026-10-12T10:04:51.220391Z" + } + }, + "Document from a session": { + "value": { + "uuid": "bbbbbbbb-cccc-dddd-eeee-ffffffffffff", + "value": "X1234567", + "display_label": "Jane Doe - X1234567", + "comment": "", + "added_by": "api", + "reference_session_id": "33333333-4444-5555-6666-777777777777", + "reference_session_number": 1042, + "reference_session_type": "kyc", + "reference_object_uuid": "66666666-7777-8888-9999-000000000000", + "metadata": { + "full_name": "Jane Doe", + "issuing_state": "ESP", + "document_type": "ID", + "region": "", + "document_number": "X1234567" + }, + "created_at": "2026-10-12T10:06:12.774915Z" + } + }, + "From a transaction": { + "value": { + "uuid": "dddddddd-eeee-ffff-0000-111111111111", + "value": "0x52908400098527886e0f7030069857d2e4169ee7", + "display_label": "", + "comment": "Linked to a chargeback", + "added_by": "api", + "reference_session_id": null, + "reference_session_number": null, + "reference_session_type": null, + "reference_object_uuid": "55555555-6666-7777-8888-999999999999", + "metadata": { + "reference_type": "transaction", + "reference_id": "55555555-6666-7777-8888-999999999999" + }, + "created_at": "2026-10-12T10:20:40.117062Z" + } + } + } + } + } }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "is_enabled" - ], - "properties": { - "is_enabled": { - "type": "boolean", - "description": "true to enable monitoring, false to disable it." + "400": { + "description": "The body is invalid, the value is already in the list, or the session cannot provide the value.", + "content": { + "application/json": { + "examples": { + "Nothing to list": { + "value": { + "value": [ + "Either 'value' or 'reference_session_id' must be provided." + ] + } }, - "estimate_only": { - "type": "boolean", - "default": false, - "description": "When true, returns the cost estimate and performs no writes." + "Duplicate value": { + "value": { + "value": [ + "An entry with this value already exists in this list." + ] + } }, - "update_all": { - "type": "boolean", - "default": false, - "description": "Apply to every entity of the application (scoped by the filter_* params when present)." + "Invalid phone": { + "value": { + "value": [ + "Invalid phone number. Provide a valid international number (e.g. +1234567890)." + ] + } }, - "filter_search": { - "type": "string" + "Invalid IP": { + "value": { + "value": [ + "Invalid IP address or CIDR range (e.g. 192.168.1.1 or 10.0.0.0/24)." + ] + } }, - "filter_status": { - "type": "array", - "items": { - "type": "string" + "Invalid country": { + "value": { + "value": [ + "Invalid country code: XX. Provide a valid ISO 3166-1 alpha-2 or alpha-3 country code." + ] } }, - "filter_country": { - "type": "string" + "Device-class fingerprint": { + "value": { + "value": [ + "This value is a device-class fingerprint: several distinct devices have reported it, so blocklisting it would decline unrelated people. Block the user by document, face, email or phone instead." + ] + } }, - "filter_date_from": { - "type": "string", - "format": "date" + "Unknown session": { + "value": { + "reference_session_id": [ + "Session not found or does not belong to this application." + ] + } }, - "filter_date_to": { - "type": "string", - "format": "date" + "No data in session": { + "value": { + "value": [ + "Either provide a 'value' or ensure the session contains data for this entry type." + ] + } }, - "didit_internal_id_list": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - }, - "description": "Entities to toggle. Required unless update_all is true." + "Selfie on another list": { + "value": { + "face_image_source": [ + "A document-capture selfie can only be added to the face blocklist." + ] + } + }, + "No clear face in selfie": { + "value": { + "face_image_source": [ + "No clear face was found in this selfie, so it cannot be blocklisted." + ] + } + }, + "Selfie already listed": { + "value": { + "face_image_source": [ + "This selfie is already in the blocklist." + ] + } } } } } - } - }, - "responses": { - "200": { - "description": "Estimate (when estimate_only) or the completed job for small batches processed inline.", + }, + "401": { + "description": "The scoped API key has expired. Other authentication failures return `403`.", "content": { "application/json": { - "schema": { - "oneOf": [ - { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (no writes are performed).", - "properties": { - "users_total": { - "type": "integer", - "description": "Entities in the selection." - }, - "already_enabled": { - "type": "integer" - }, - "with_existing_aml": { - "type": "integer", - "description": "Entities whose existing AML screening will be switched to monitored." - }, - "needing_screening": { - "type": "integer", - "description": "Entities without any AML screening that will be screened first (billed)." - }, - "skipped_no_identity_data": { - "type": "integer" - }, - "skipped_blocked": { - "type": "integer" - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2 - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07 - }, - "balance_sufficient": { - "type": "boolean" - } - } - }, - { - "type": "object", - "properties": { - "job_id": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED" - ] - }, - "is_enabled": { - "type": "boolean" - }, - "total": { - "type": "integer" - }, - "processed": { - "type": "integer" - }, - "outcomes": { - "type": "object", - "description": "Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.", - "properties": { - "enabled": { - "type": "integer", - "description": "Monitoring enabled on an existing AML screening." - }, - "screened_and_enabled": { - "type": "integer", - "description": "A new AML screening was created (billed) and monitoring enabled." - }, - "already_enabled": { - "type": "integer", - "description": "Monitoring was already active (legacy duplicate rows are consolidated)." - }, - "disabled": { - "type": "integer", - "description": "Monitoring disabled on every AML row of the entity." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Skipped: no full name + date of birth + country available to screen." - }, - "skipped_blocked": { - "type": "integer", - "description": "Skipped: the user is blocked." - }, - "skipped_sandbox": { - "type": "integer", - "description": "Skipped: sandbox application." - }, - "failed_provider": { - "type": "integer", - "description": "The screening provider call failed." - }, - "failed_insufficient_balance": { - "type": "integer", - "description": "Balance ran out mid-job." - } - } - }, - "error": { - "type": "string", - "nullable": true - }, - "estimate": { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (no writes are performed).", - "properties": { - "users_total": { - "type": "integer", - "description": "Entities in the selection." - }, - "already_enabled": { - "type": "integer" - }, - "with_existing_aml": { - "type": "integer", - "description": "Entities whose existing AML screening will be switched to monitored." - }, - "needing_screening": { - "type": "integer", - "description": "Entities without any AML screening that will be screened first (billed)." - }, - "skipped_no_identity_data": { - "type": "integer" - }, - "skipped_blocked": { - "type": "integer" - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2 - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07 - }, - "balance_sufficient": { - "type": "boolean" - } - } - } - } + "examples": { + "Expired API key": { + "value": { + "detail": "This API key has expired." } - ] + } } } } }, - "202": { - "description": "Larger batches are processed asynchronously; poll the job endpoint with the returned job_id.", + "403": { + "description": "The `x-api-key` header is missing or invalid, or a scoped API key lacks `workflows: write` or is used from an IP address outside its allowlist.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "job_id": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED" - ] - }, - "is_enabled": { - "type": "boolean" - }, - "total": { - "type": "integer" - }, - "processed": { - "type": "integer" - }, - "outcomes": { - "type": "object", - "description": "Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.", - "properties": { - "enabled": { - "type": "integer", - "description": "Monitoring enabled on an existing AML screening." - }, - "screened_and_enabled": { - "type": "integer", - "description": "A new AML screening was created (billed) and monitoring enabled." - }, - "already_enabled": { - "type": "integer", - "description": "Monitoring was already active (legacy duplicate rows are consolidated)." - }, - "disabled": { - "type": "integer", - "description": "Monitoring disabled on every AML row of the entity." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Skipped: no full name + date of birth + country available to screen." - }, - "skipped_blocked": { - "type": "integer", - "description": "Skipped: the user is blocked." - }, - "skipped_sandbox": { - "type": "integer", - "description": "Skipped: sandbox application." - }, - "failed_provider": { - "type": "integer", - "description": "The screening provider call failed." - }, - "failed_insufficient_balance": { - "type": "integer", - "description": "Balance ran out mid-job." - } - } - }, - "error": { - "type": "string", - "nullable": true - }, - "estimate": { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (no writes are performed).", - "properties": { - "users_total": { - "type": "integer", - "description": "Entities in the selection." - }, - "already_enabled": { - "type": "integer" - }, - "with_existing_aml": { - "type": "integer", - "description": "Entities whose existing AML screening will be switched to monitored." - }, - "needing_screening": { - "type": "integer", - "description": "Entities without any AML screening that will be screened first (billed)." - }, - "skipped_no_identity_data": { - "type": "integer" - }, - "skipped_blocked": { - "type": "integer" - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2 - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07 - }, - "balance_sufficient": { - "type": "boolean" - } - } + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without workflows access": { + "value": { + "detail": "This API key has no write access to workflows." } } } } } }, - "403": { - "description": "Missing write:sessions privilege, or insufficient balance to enable (the estimate is included in the error body)." + "404": { + "description": "No list with this UUID exists in the API key's application, or it was deleted.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "List not found." + } + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/RateLimitedWrite" + }, + "503": { + "description": "Face analysis of a document-capture selfie is temporarily unavailable. Nothing was added; retry later.", + "content": { + "application/json": { + "examples": { + "Unavailable": { + "value": { + "detail": "Service temporarily unavailable. Please try again later." + } + } + } + } + } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/{job_id}/": { - "get": { - "summary": "Get AML ongoing monitoring job", - "description": "Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING.", - "operationId": "get_vendor_businesses_ongoing_monitoring_job", + "/v3/lists/{list_uuid}/entries/{entry_uuid}/": { + "delete": { + "summary": "Delete a list entry", + "description": "Deletes one entry from a list, system lists included. The entry stops matching at once, and deletion is irreversible: to list the value again, create a new entry. On a system face, document, phone or email blocklist, the record the entry stands for is no longer flagged as blocklisted; on the system user or business blocklist, users or businesses of the application whose `vendor_data` equals the entry's value (or whose `didit_internal_id` equals its `reference_object_uuid`) and that are `BLOCKED` return to `ACTIVE`. Sessions and transactions that already finished keep their status. A scoped API key needs `workflows: write`.", + "operationId": "delete_list_entry", + "x-didit-sdk": { + "resource": "lists.entries", + "method": "delete", + "auth": "api_key", + "server": true + }, "tags": [ - "Businesses" + "Lists" ], "parameters": [ { - "name": "organization_id", + "name": "list_uuid", "in": "path", "required": true, + "description": "UUID of the list that holds the entry.", "schema": { "type": "string", "format": "uuid" }, - "description": "Organization UUID." + "example": "11111111-2222-3333-4444-555555555555" }, { - "name": "application_id", + "name": "entry_uuid", "in": "path", "required": true, + "description": "UUID of the entry, as returned by List list entries or Create a list entry.", "schema": { "type": "string", "format": "uuid" }, - "description": "Application UUID." + "example": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X DELETE 'https://verification.didit.me/v3/lists/11111111-2222-3333-4444-555555555555/entries/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/' \\\n -H 'x-api-key: YOUR_API_KEY'" }, { - "name": "job_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Monitoring job UUID returned by the toggle endpoint." + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nlist_uuid = '11111111-2222-3333-4444-555555555555'\nentry_uuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'\nresp = requests.delete(\n f'https://verification.didit.me/v3/lists/{list_uuid}/entries/{entry_uuid}/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nprint('Deleted:', resp.status_code == 204)" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const listUuid = '11111111-2222-3333-4444-555555555555';\nconst entryUuid = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';\nconst resp = await fetch(\n `https://verification.didit.me/v3/lists/${listUuid}/entries/${entryUuid}/`,\n { method: 'DELETE', headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n);\nif (resp.status !== 204) throw new Error(`Delete failed: ${resp.status}`);\nconsole.log('Entry deleted');" } ], "responses": { - "200": { - "description": "The job.", + "204": { + "description": "The entry was deleted. No body." + }, + "401": { + "description": "The scoped API key has expired. Other authentication failures return `403`.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "job_id": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED" - ] - }, - "is_enabled": { - "type": "boolean" - }, - "total": { - "type": "integer" - }, - "processed": { - "type": "integer" - }, - "outcomes": { - "type": "object", - "description": "Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.", - "properties": { - "enabled": { - "type": "integer", - "description": "Monitoring enabled on an existing AML screening." - }, - "screened_and_enabled": { - "type": "integer", - "description": "A new AML screening was created (billed) and monitoring enabled." - }, - "already_enabled": { - "type": "integer", - "description": "Monitoring was already active (legacy duplicate rows are consolidated)." - }, - "disabled": { - "type": "integer", - "description": "Monitoring disabled on every AML row of the entity." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Skipped: no full name + date of birth + country available to screen." - }, - "skipped_blocked": { - "type": "integer", - "description": "Skipped: the user is blocked." - }, - "skipped_sandbox": { - "type": "integer", - "description": "Skipped: sandbox application." - }, - "failed_provider": { - "type": "integer", - "description": "The screening provider call failed." - }, - "failed_insufficient_balance": { - "type": "integer", - "description": "Balance ran out mid-job." - } - } - }, - "error": { - "type": "string", - "nullable": true - }, - "estimate": { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (no writes are performed).", - "properties": { - "users_total": { - "type": "integer", - "description": "Entities in the selection." - }, - "already_enabled": { - "type": "integer" - }, - "with_existing_aml": { - "type": "integer", - "description": "Entities whose existing AML screening will be switched to monitored." - }, - "needing_screening": { - "type": "integer", - "description": "Entities without any AML screening that will be screened first (billed)." - }, - "skipped_no_identity_data": { - "type": "integer" - }, - "skipped_blocked": { - "type": "integer" - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2 - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07 - }, - "balance_sufficient": { - "type": "boolean" - } - } + "examples": { + "Expired API key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "The `x-api-key` header is missing or invalid, or a scoped API key lacks `workflows: write` or is used from an IP address outside its allowlist.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without workflows access": { + "value": { + "detail": "This API key has no write access to workflows." + } + } + } + } + } + }, + "404": { + "description": "No entry with this UUID exists in this list, the list is not in the API key's application or was deleted, or the entry was already deleted.", + "content": { + "application/json": { + "examples": { + "Not found": { + "value": { + "detail": "No ListEntry matches the given query." } } } } } }, - "404": { - "description": "Unknown job for this application." + "429": { + "$ref": "#/components/responses/RateLimitedWrite" } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/": { - "patch": { - "summary": "Toggle AML ongoing monitoring for business sessions", - "description": "Session-level toggle for KYB verifications, addressed by session number. Sessions without an AML screening get one created and attached to the session itself. Same estimate_only, inline fast-path and async job contract as the user-level endpoints. Replaces the legacy synchronous behavior that skipped sessions without an AML screening.", - "operationId": "toggle_business_sessions_ongoing_monitoring", + "/v3/lists/{list_uuid}/entries/face-upload/": { + "post": { + "summary": "Upload a face to a list", + "description": "Adds a face to a face blocklist or face allowlist from a base64-encoded JPEG or PNG image, for when you have no verification session to reference. Didit detects the faces in the image, uses the largest one, and creates an entry whose `value` and `reference_object_uuid` are the UUID of the new face. A blocklisted face declines new sessions whose face matches; an allowlisted face skips the duplicated-face action and never overrides a blocklist match. In a sandbox application the image is not analyzed and the entry matches nothing. A scoped API key needs `workflows: write`. To list a face from an existing session, use [Create a list entry](https://docs.didit.me/management-api/lists/create-entry) with `reference_session_id` instead.", + "operationId": "upload_list_entry_face", + "x-didit-sdk": { + "resource": "lists.entries", + "method": "upload_face", + "auth": "api_key", + "server": true + }, "tags": [ - "Businesses" + "Lists" ], "parameters": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", + "name": "list_uuid", "in": "path", "required": true, + "description": "UUID of a list with `entry_type: face` and `list_type: blocklist` or `allowlist`.", "schema": { "type": "string", "format": "uuid" }, - "description": "Application UUID." + "example": "11111111-2222-3333-4444-555555555555" } ], "requestBody": { @@ -38325,754 +57277,463 @@ "schema": { "type": "object", "required": [ - "is_enabled" + "image" ], "properties": { - "is_enabled": { - "type": "boolean", - "description": "true to enable monitoring, false to disable it." - }, - "estimate_only": { - "type": "boolean", - "default": false, - "description": "When true, returns the cost estimate and performs no writes." - }, - "update_all": { - "type": "boolean", - "default": false, - "description": "Apply to every entity of the application (scoped by the filter_* params when present)." - }, - "filter_search": { - "type": "string" - }, - "filter_status": { - "type": "array", - "items": { - "type": "string" - } - }, - "filter_country": { - "type": "string" - }, - "filter_date_from": { + "image": { "type": "string", - "format": "date" + "format": "byte", + "description": "Base64-encoded JPEG or PNG, at most 2 MB once decoded, showing a clearly visible face. Send the bare base64 string, without a `data:image/...;base64,` prefix." }, - "filter_date_to": { + "comment": { "type": "string", - "format": "date" - }, - "session_numbers": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Business session numbers to toggle. Required unless update_all (or the legacy toggle_all alias) is true." - }, - "toggle_all": { - "type": "boolean", - "default": false, - "description": "Legacy alias of update_all." - }, - "filter_workflow_id": { - "type": "string" + "default": "", + "description": "Optional free-text note, such as the reason for listing." + } + } + }, + "examples": { + "Face image": { + "value": { + "image": "/9j/4AAQSkZJRgABAQAAAQABAAD...", + "comment": "Known fraud attempt" } } } } } }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/lists/11111111-2222-3333-4444-555555555555/entries/face-upload/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d \"{\\\"image\\\": \\\"$(base64 < face.jpg | tr -d '\\n')\\\", \\\"comment\\\": \\\"Known fraud attempt\\\"}\"" + }, + { + "lang": "python", + "label": "Python", + "source": "import base64\nimport os\n\nimport requests\n\nlist_uuid = '11111111-2222-3333-4444-555555555555'\nwith open('face.jpg', 'rb') as image_file:\n image_b64 = base64.b64encode(image_file.read()).decode('ascii')\n\nresp = requests.post(\n f'https://verification.didit.me/v3/lists/{list_uuid}/entries/face-upload/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={'image': image_b64, 'comment': 'Known fraud attempt'},\n timeout=10,\n)\nresp.raise_for_status()\nprint('Face entry', resp.json()['uuid'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "import { readFile } from 'node:fs/promises';\n\nconst listUuid = '11111111-2222-3333-4444-555555555555';\nconst image = (await readFile('face.jpg')).toString('base64');\nconst resp = await fetch(`https://verification.didit.me/v3/lists/${listUuid}/entries/face-upload/`, {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ image, comment: 'Known fraud attempt' }),\n});\nif (!resp.ok) throw new Error(`Upload failed: ${resp.status} ${await resp.text()}`);\nconst entry = await resp.json();\nconsole.log('Face entry', entry.uuid);" + } + ], "responses": { - "200": { - "description": "Estimate (when estimate_only) or the completed job for small batches processed inline.", + "201": { + "description": "The face entry was created.", "content": { "application/json": { "schema": { - "oneOf": [ - { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (no writes are performed).", - "properties": { - "users_total": { - "type": "integer", - "description": "Entities in the selection." - }, - "already_enabled": { - "type": "integer" - }, - "with_existing_aml": { - "type": "integer", - "description": "Entities whose existing AML screening will be switched to monitored." - }, - "needing_screening": { - "type": "integer", - "description": "Entities without any AML screening that will be screened first (billed)." - }, - "skipped_no_identity_data": { - "type": "integer" - }, - "skipped_blocked": { - "type": "integer" - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2 - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07 - }, - "balance_sufficient": { - "type": "boolean" - } - } - }, - { - "type": "object", - "properties": { - "job_id": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED" - ] - }, - "is_enabled": { - "type": "boolean" - }, - "total": { - "type": "integer" - }, - "processed": { - "type": "integer" - }, - "outcomes": { - "type": "object", - "description": "Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.", - "properties": { - "enabled": { - "type": "integer", - "description": "Monitoring enabled on an existing AML screening." - }, - "screened_and_enabled": { - "type": "integer", - "description": "A new AML screening was created (billed) and monitoring enabled." - }, - "already_enabled": { - "type": "integer", - "description": "Monitoring was already active (legacy duplicate rows are consolidated)." - }, - "disabled": { - "type": "integer", - "description": "Monitoring disabled on every AML row of the entity." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Skipped: no full name + date of birth + country available to screen." - }, - "skipped_blocked": { - "type": "integer", - "description": "Skipped: the user is blocked." - }, - "skipped_sandbox": { - "type": "integer", - "description": "Skipped: sandbox application." - }, - "failed_provider": { - "type": "integer", - "description": "The screening provider call failed." - }, - "failed_insufficient_balance": { - "type": "integer", - "description": "Balance ran out mid-job." - } - } - }, - "error": { - "type": "string", - "nullable": true - }, - "estimate": { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (no writes are performed).", - "properties": { - "users_total": { - "type": "integer", - "description": "Entities in the selection." - }, - "already_enabled": { - "type": "integer" - }, - "with_existing_aml": { - "type": "integer", - "description": "Entities whose existing AML screening will be switched to monitored." - }, - "needing_screening": { - "type": "integer", - "description": "Entities without any AML screening that will be screened first (billed)." - }, - "skipped_no_identity_data": { - "type": "integer" - }, - "skipped_blocked": { - "type": "integer" - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2 - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07 - }, - "balance_sufficient": { - "type": "boolean" - } - } - } - } + "$ref": "#/components/schemas/ComplianceListEntry" + }, + "examples": { + "Uploaded face": { + "value": { + "uuid": "cccccccc-dddd-eeee-ffff-000000000000", + "value": "44444444-5555-6666-7777-888888888888", + "display_label": "Manually uploaded face", + "comment": "Known fraud attempt", + "added_by": "api", + "reference_session_id": null, + "reference_session_number": null, + "reference_session_type": null, + "reference_object_uuid": "44444444-5555-6666-7777-888888888888", + "metadata": { + "reference_image_url": "https://media.example.com/faces/reference.jpg?expires=1760263938&signature=3f9a1c" + }, + "created_at": "2026-10-12T10:12:18.904467Z" } - ] + }, + "Sandbox application": { + "value": { + "uuid": "cccccccc-dddd-eeee-ffff-000000000000", + "value": "44444444-5555-6666-7777-888888888888", + "display_label": "Sandbox face entry", + "comment": "Known fraud attempt", + "added_by": "api", + "reference_session_id": null, + "reference_session_number": null, + "reference_session_type": null, + "reference_object_uuid": "44444444-5555-6666-7777-888888888888", + "metadata": { + "sandbox": true + }, + "created_at": "2026-10-12T10:12:18.904467Z" + } + } } } } }, - "202": { - "description": "Larger batches are processed asynchronously; poll the job endpoint with the returned job_id.", + "400": { + "description": "The list does not take face uploads, or the image is missing, invalid, too large or shows no usable face. No entry was created.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "job_id": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED" + "examples": { + "Not a face list": { + "value": { + "detail": "Face upload is only available for face-type lists." + } + }, + "Custom face list": { + "value": { + "detail": "Face upload is only available for face blocklists and allowlists." + } + }, + "Missing image": { + "value": { + "image": [ + "This field is required." ] - }, - "is_enabled": { - "type": "boolean" - }, - "total": { - "type": "integer" - }, - "processed": { - "type": "integer" - }, - "outcomes": { - "type": "object", - "description": "Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.", - "properties": { - "enabled": { - "type": "integer", - "description": "Monitoring enabled on an existing AML screening." - }, - "screened_and_enabled": { - "type": "integer", - "description": "A new AML screening was created (billed) and monitoring enabled." - }, - "already_enabled": { - "type": "integer", - "description": "Monitoring was already active (legacy duplicate rows are consolidated)." - }, - "disabled": { - "type": "integer", - "description": "Monitoring disabled on every AML row of the entity." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Skipped: no full name + date of birth + country available to screen." - }, - "skipped_blocked": { - "type": "integer", - "description": "Skipped: the user is blocked." - }, - "skipped_sandbox": { - "type": "integer", - "description": "Skipped: sandbox application." - }, - "failed_provider": { - "type": "integer", - "description": "The screening provider call failed." - }, - "failed_insufficient_balance": { - "type": "integer", - "description": "Balance ran out mid-job." - } - } - }, - "error": { - "type": "string", - "nullable": true - }, - "estimate": { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (no writes are performed).", - "properties": { - "users_total": { - "type": "integer", - "description": "Entities in the selection." - }, - "already_enabled": { - "type": "integer" - }, - "with_existing_aml": { - "type": "integer", - "description": "Entities whose existing AML screening will be switched to monitored." - }, - "needing_screening": { - "type": "integer", - "description": "Entities without any AML screening that will be screened first (billed)." - }, - "skipped_no_identity_data": { - "type": "integer" - }, - "skipped_blocked": { - "type": "integer" - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2 - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07 - }, - "balance_sufficient": { - "type": "boolean" - } - } + } + }, + "Invalid base64": { + "value": { + "image": [ + "Invalid base64 encoding." + ] + } + }, + "Too large": { + "value": { + "image": [ + "Image exceeds maximum size of 2 MB." + ] + } + }, + "Unreadable image": { + "value": { + "image": "Invalid image. Provide a valid base64-encoded JPG or PNG." + } + }, + "No face": { + "value": { + "image": "No face detected in the image. Please upload a clear photo with a visible face." + } + }, + "No usable face": { + "value": { + "image": "Could not extract face embedding. Try a different image." } } } } } }, - "403": { - "description": "Missing write:sessions privilege, or insufficient balance to enable (the estimate is included in the error body)." - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/{job_id}/": { - "get": { - "summary": "Get AML ongoing monitoring job", - "description": "Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING.", - "operationId": "get_business_sessions_ongoing_monitoring_job", - "tags": [ - "Businesses" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." + "401": { + "description": "The scoped API key has expired. Other authentication failures return `403`.", + "content": { + "application/json": { + "examples": { + "Expired API key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "403": { + "description": "The `x-api-key` header is missing or invalid, or a scoped API key lacks `workflows: write` or is used from an IP address outside its allowlist.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without workflows access": { + "value": { + "detail": "This API key has no write access to workflows." + } + } + } + } + } }, - { - "name": "job_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Monitoring job UUID returned by the toggle endpoint." - } - ], - "responses": { - "200": { - "description": "The job.", + "404": { + "description": "No list with this UUID exists in the API key's application, or it was deleted.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "job_id": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED" - ] - }, - "is_enabled": { - "type": "boolean" - }, - "total": { - "type": "integer" - }, - "processed": { - "type": "integer" - }, - "outcomes": { - "type": "object", - "description": "Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.", - "properties": { - "enabled": { - "type": "integer", - "description": "Monitoring enabled on an existing AML screening." - }, - "screened_and_enabled": { - "type": "integer", - "description": "A new AML screening was created (billed) and monitoring enabled." - }, - "already_enabled": { - "type": "integer", - "description": "Monitoring was already active (legacy duplicate rows are consolidated)." - }, - "disabled": { - "type": "integer", - "description": "Monitoring disabled on every AML row of the entity." - }, - "skipped_no_identity_data": { - "type": "integer", - "description": "Skipped: no full name + date of birth + country available to screen." - }, - "skipped_blocked": { - "type": "integer", - "description": "Skipped: the user is blocked." - }, - "skipped_sandbox": { - "type": "integer", - "description": "Skipped: sandbox application." - }, - "failed_provider": { - "type": "integer", - "description": "The screening provider call failed." - }, - "failed_insufficient_balance": { - "type": "integer", - "description": "Balance ran out mid-job." - } - } - }, - "error": { - "type": "string", - "nullable": true - }, - "estimate": { - "type": "object", - "description": "Cost preview. Returned directly when estimate_only is true (no writes are performed).", - "properties": { - "users_total": { - "type": "integer", - "description": "Entities in the selection." - }, - "already_enabled": { - "type": "integer" - }, - "with_existing_aml": { - "type": "integer", - "description": "Entities whose existing AML screening will be switched to monitored." - }, - "needing_screening": { - "type": "integer", - "description": "Entities without any AML screening that will be screened first (billed)." - }, - "skipped_no_identity_data": { - "type": "integer" - }, - "skipped_blocked": { - "type": "integer" - }, - "estimated_screening_cost_usd": { - "type": "number", - "example": 0.2 - }, - "estimated_annual_monitoring_cost_usd": { - "type": "number", - "example": 0.07 - }, - "balance_sufficient": { - "type": "boolean" - } - } + "examples": { + "Not found": { + "value": { + "detail": "List not found." + } + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/RateLimitedWrite" + }, + "503": { + "description": "Face analysis is temporarily unavailable. No entry was created; retry later.", + "content": { + "application/json": { + "examples": { + "Unavailable": { + "value": { + "detail": "Service temporarily unavailable. Please try again later." } } } } } - }, - "404": { - "description": "Unknown job for this application." } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/cases/": { + "/v3/blocklist/": { "get": { - "summary": "List cases", - "description": "List cases for the application, newest first. Supports filtering, search, and sorting; see query parameters. Paginated with `limit`/`offset`.", - "operationId": "list_cases", + "summary": "List blocklisted items", + "description": "Returns one paginated feed of the faces, identity documents, phone numbers and email addresses of the application's verification sessions that are currently blocklisted, newest `blocklisted_at` first; filter with `item_type`. Items of deleted sessions are not included. The feed covers records blocklisted from a session (with [Add session items to the blocklist](https://docs.didit.me/management-api/blocklist/add), from the console, or by a list entry created from a session); values added to a list by hand, uploaded faces, faces from a document-capture selfie and the other entry types are read with [List list entries](https://docs.didit.me/management-api/lists/list-entries). `face_image` and `front_image` are short-lived signed URLs, and a scoped API key without `media: read` receives `null`. A scoped API key needs `workflows: read`.", + "operationId": "list_blocklist_items", + "x-didit-sdk": { + "resource": "blocklist", + "method": "list", + "auth": "api_key", + "server": true + }, "tags": [ - "Cases" + "Blocklist" ], "parameters": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, + "name": "item_type", + "in": "query", + "required": false, + "description": "Only items of this type. When omitted, every type is returned.", "schema": { "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "enum": [ + "face", + "document", + "phone", + "email" + ] + } }, { "name": "limit", "in": "query", + "required": false, + "description": "Page size. Defaults to 50. A value above 1000000 returns `400`; zero, a negative or a non-numeric value falls back to 50.", "schema": { "type": "integer", "default": 50, - "minimum": 1 - }, - "description": "Page size. Defaults to 50." + "minimum": 1, + "maximum": 1000000 + } }, { "name": "offset", "in": "query", + "required": false, + "description": "Zero-based index of the first item to return. Prefer following `next`. A value above 1000000 returns `400`; a negative or non-numeric value falls back to 0.", "schema": { "type": "integer", "default": 0, - "minimum": 0 - }, - "description": "Zero-based offset of the first record." - }, - { - "name": "status__in", - "in": "query", - "schema": { - "type": "string" - }, - "description": "Comma-separated statuses to include, e.g. OPEN,AWAITING_USER." - }, - { - "name": "priority", - "in": "query", - "schema": { - "type": "string" - }, - "description": "Comma-separated priorities to include." - }, - { - "name": "source__in", - "in": "query", - "schema": { - "type": "string" - }, - "description": "Comma-separated sources to include." - }, - { - "name": "blueprint", - "in": "query", - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Filter to cases on this blueprint." - }, - { - "name": "assigned_to", - "in": "query", - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Filter to cases assigned to this user." - }, - { - "name": "unassigned", - "in": "query", - "schema": { - "type": "boolean" - }, - "description": "true for unassigned cases, false for assigned cases." - }, - { - "name": "tag", - "in": "query", - "schema": { - "type": "string" - }, - "description": "Filter to cases carrying this exact tag." - }, - { - "name": "search", - "in": "query", - "schema": { - "type": "string" - }, - "description": "Matches title, case_number, tag, subject name/UUID/external ID, linked session UUID, or linked transaction UUID." - }, - { - "name": "date_from", - "in": "query", - "schema": { - "type": "string", - "format": "date" - }, - "description": "Created-at lower bound (inclusive)." - }, - { - "name": "date_to", - "in": "query", - "schema": { - "type": "string", - "format": "date" - }, - "description": "Created-at upper bound (inclusive)." - }, - { - "name": "due_before", - "in": "query", - "schema": { - "type": "string", - "format": "date-time" - }, - "description": "due_at upper bound." - }, - { - "name": "overdue", - "in": "query", - "schema": { - "type": "boolean" - }, - "description": "true to return only overdue open/awaiting-user cases." - }, - { - "name": "ordering", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "created_at", - "-created_at", - "due_at", - "-due_at" - ], - "default": "-created_at" - }, - "description": "Sort order. due_at sorts nulls last." + "minimum": 0, + "maximum": 1000000 + } } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/?status__in=OPEN,AWAITING_USER&ordering=-due_at' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X GET 'https://verification.didit.me/v3/blocklist/?item_type=document' \\\n -H 'x-api-key: YOUR_API_KEY'" }, { "lang": "python", "label": "Python", - "source": "import os\n\nimport requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n params={'status__in': 'OPEN,AWAITING_USER', 'ordering': '-due_at'},\n timeout=10,\n)\nresp.raise_for_status()\nfor case in resp.json()['results']:\n print(case['case_number'], case['title'], case['priority'], case['due_at'])" + "source": "import os\n\nimport requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/blocklist/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n params={'item_type': 'document'},\n timeout=10,\n)\nresp.raise_for_status()\nfor item in resp.json()['results']:\n print(item['session_id'], item['document_type'], item['document_number'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/blocklist/?item_type=document', {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Request failed: ${resp.status}`);\nconst page = await resp.json();\nconsole.log(page.count, page.results);" } ], "responses": { "200": { - "description": "Paginated case list.", + "description": "A page of blocklisted items, newest first.", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "count", + "next", + "previous", + "results" + ], "properties": { "count": { "type": "integer", - "description": "Number of matching rows. Exact up to 100, capped at 100 beyond that for performance. Rely on `next` being null to detect the last page." + "minimum": 0, + "description": "Total number of blocklisted items matching the filters." }, "next": { "type": "string", + "format": "uri", "nullable": true, - "description": "Absolute URL of the next page, or null." + "description": "Absolute URL of the next page, or `null` on the last page." }, "previous": { "type": "string", + "format": "uri", "nullable": true, - "description": "Absolute URL of the previous page, or null." + "description": "Absolute URL of the previous page, or `null` on the first page." }, "results": { "type": "array", "items": { - "$ref": "#/components/schemas/CaseListItem" + "$ref": "#/components/schemas/BlocklistedItem" } } } + }, + "examples": { + "Mixed feed": { + "value": { + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "type": "document", + "session_id": "33333333-4444-5555-6666-777777777777", + "session_number": 1042, + "blocklisted_at": "2026-10-12T10:06:12.701553Z", + "face_image": null, + "document_type": "ID", + "document_number": "X1234567", + "front_image": "https://media.example.com/documents/front.jpg?expires=1760263938&signature=8b2e47", + "phone_number": null, + "country_code": null, + "email": null + }, + { + "type": "phone", + "session_id": "77777777-8888-9999-0000-111111111111", + "session_number": 1017, + "blocklisted_at": "2026-10-08T15:31:09.400127Z", + "face_image": null, + "document_type": null, + "document_number": null, + "front_image": null, + "phone_number": "+14155552671", + "country_code": "US", + "email": null + } + ] + } + } + } + } + } + }, + "400": { + "description": "A query parameter is invalid.", + "content": { + "application/json": { + "examples": { + "Unknown item type": { + "value": { + "item_type": [ + "\"wallet\" is not a valid choice." + ] + } + }, + "Limit too large": { + "value": [ + "limit must not exceed 1000000." + ] + } + } + } + } + }, + "401": { + "description": "The scoped API key has expired. Other authentication failures return `403`.", + "content": { + "application/json": { + "examples": { + "Expired API key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "The `x-api-key` header is missing or invalid, or a scoped API key lacks `workflows: read` or is used from an IP address outside its allowlist.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without workflows access": { + "value": { + "detail": "This API key has no read access to workflows." + } + } } } } - } - } - }, - "post": { - "summary": "Create case", - "description": "Create a case anchored to exactly one subject (a user or a business), directly or by deriving the subject from an attached session/transaction link. Source is always `MANUAL` for API-created cases.", - "operationId": "create_case", - "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." }, + "429": { + "description": "The shared read limit of 600 requests per minute per API key was exceeded. Honor `Retry-After` before retrying.", + "content": { + "application/json": { + "examples": { + "Rate limited": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } + } + } + } + } + }, + "security": [ { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "ApiKeyAuth": [] } + ] + } + }, + "/v3/blocklist/add/": { + "post": { + "summary": "Add session items to the blocklist", + "description": "Blocklists the face, identity document, phone number or email address of one person verification session of the application. For each selected type, Didit takes the session's most recent record, flags it as blocklisted and adds an entry linked to the session to the matching system blocklist (`added_by: \"api\"`), so new sessions with a matching face, document number, phone number or email address are declined. Every selected item is checked first, so if one is missing nothing is blocklisted; repeating the call for an item already listed keeps its single entry and refreshes `blocklisted_at`. The session itself and sessions that already finished are not changed. A scoped API key needs `workflows: write`.", + "operationId": "add_to_blocklist", + "x-didit-sdk": { + "resource": "blocklist", + "method": "add", + "auth": "api_key", + "server": true + }, + "tags": [ + "Blocklist" ], "requestBody": { "required": true, @@ -39081,80 +57742,252 @@ "schema": { "type": "object", "required": [ - "title" + "session_id" ], + "description": "At least one `blocklist_*` flag must be `true`.", "properties": { - "title": { - "type": "string", - "maxLength": 255 - }, - "blueprint": { + "session_id": { "type": "string", "format": "uuid", - "nullable": true, - "description": "CaseBlueprint UUID. Omit for no blueprint." + "description": "A person verification session of your application. Deleted sessions are refused." }, - "priority": { - "type": "string", - "enum": [ - "LOW", - "MEDIUM", - "HIGH" - ], - "default": "MEDIUM" + "blocklist_face": { + "type": "boolean", + "default": false, + "description": "Blocklist the session's face." }, - "due_at": { - "type": "string", - "format": "date-time", - "nullable": true + "blocklist_document": { + "type": "boolean", + "default": false, + "description": "Blocklist the session's identity document. The document needs its issuing state, document type, document number and full name." }, - "tags": { - "type": "array", - "items": { - "type": "string" + "blocklist_phone": { + "type": "boolean", + "default": false, + "description": "Blocklist the session's phone number." + }, + "blocklist_email": { + "type": "boolean", + "default": false, + "description": "Blocklist the session's email address." + } + } + }, + "examples": { + "Face and document": { + "value": { + "session_id": "33333333-4444-5555-6666-777777777777", + "blocklist_face": true, + "blocklist_document": true + } + }, + "Phone only": { + "value": { + "session_id": "33333333-4444-5555-6666-777777777777", + "blocklist_phone": true + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/blocklist/add/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"session_id\": \"33333333-4444-5555-6666-777777777777\",\n \"blocklist_face\": true,\n \"blocklist_document\": true\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/blocklist/add/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={\n 'session_id': '33333333-4444-5555-6666-777777777777',\n 'blocklist_face': True,\n 'blocklist_document': True,\n },\n timeout=10,\n)\nresp.raise_for_status()\nprint(resp.json()['message'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/blocklist/add/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n session_id: '33333333-4444-5555-6666-777777777777',\n blocklist_face: true,\n blocklist_document: true,\n }),\n});\nif (!resp.ok) throw new Error(`Blocklist failed: ${resp.status} ${await resp.text()}`);\nconsole.log((await resp.json()).message);" + } + ], + "responses": { + "200": { + "description": "The selected items were blocklisted.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string", + "description": "Always `Successfully updated blocklist.`" + } + } + }, + "examples": { + "Blocklisted": { + "value": { + "message": "Successfully updated blocklist." + } + } + } + } + } + }, + "400": { + "description": "The body is invalid, the session is not a verification session of this application, or it has no record of a selected type. Nothing was blocklisted.", + "content": { + "application/json": { + "examples": { + "Unknown session": { + "value": { + "non_field_errors": [ + "Session not found" + ] } }, - "subject": { - "type": "object", - "description": "Required unless links is provided; the case's exactly-one subject.", - "properties": { - "type": { - "type": "string", - "enum": [ - "user", - "business" - ] - }, - "uuid": { - "type": "string", - "format": "uuid" - } + "No item selected": { + "value": { + "non_field_errors": [ + "Must specify at least one item to blocklist" + ] } }, - "links": { - "type": "array", - "description": "Sessions/business sessions/transactions to attach on creation, e.g. [{\"entity_type\": \"transaction\", \"entity_uuid\": \"...\"}]. When subject is omitted, the subject is derived from the first link's owner.", - "items": { - "type": "object" + "No face record": { + "value": { + "non_field_errors": [ + "Session does not have a face record" + ] } }, - "assigned_to": { + "Incomplete document": { + "value": { + "non_field_errors": [ + "To blocklist a document, the document must have an issuing state, document type, document number and full name" + ] + } + }, + "Invalid session id": { + "value": { + "session_id": [ + "Must be a valid UUID." + ] + } + }, + "Missing session id": { + "value": { + "session_id": [ + "This field is required." + ] + } + } + } + } + } + }, + "401": { + "description": "The scoped API key has expired. Other authentication failures return `403`.", + "content": { + "application/json": { + "examples": { + "Expired API key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "The `x-api-key` header is missing or invalid, or a scoped API key lacks `workflows: write` or is used from an IP address outside its allowlist.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without workflows access": { + "value": { + "detail": "This API key has no write access to workflows." + } + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/RateLimitedWrite" + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/blocklist/remove/": { + "post": { + "summary": "Remove session items from the blocklist", + "description": "Lifts the block on the face, identity document, phone number or email address of one person verification session of the application. For each selected type, Didit takes the session's most recent record, clears its blocklisted flag and deletes the system blocklist entries created from that record, so it no longer declines new sessions. Entries added to a list by value are not touched: find them with [List list entries](https://docs.didit.me/management-api/lists/list-entries) and delete them with [Delete a list entry](https://docs.didit.me/management-api/lists/delete-entry). Unblocking a record that is not blocklisted succeeds and changes nothing, and sessions that already finished are not changed. A scoped API key needs `workflows: write`.", + "operationId": "remove_from_blocklist", + "x-didit-sdk": { + "resource": "blocklist", + "method": "remove", + "auth": "api_key", + "server": true + }, + "tags": [ + "Blocklist" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "session_id" + ], + "description": "At least one `unblock_*` flag must be `true`.", + "properties": { + "session_id": { "type": "string", "format": "uuid", - "nullable": true, - "description": "User UUID to assign immediately." + "description": "A person verification session of your application. Deleted sessions are refused." }, - "review_duplicates": { + "unblock_face": { "type": "boolean", "default": false, - "description": "Recheck existing investigations before creating the case. When true, a subject with existing cases requires a nonblank separate_incident_reason. The console always enables this protection. Omitting it preserves existing API behavior." + "description": "Lift the block on the session's face." }, - "separate_incident_reason": { - "type": "string", - "maxLength": 1000 + "unblock_document": { + "type": "boolean", + "default": false, + "description": "Lift the block on the session's identity document." }, - "source_context": { - "$ref": "#/components/schemas/CaseDuplicateCaseFraudSourceContextRequest" + "unblock_phone": { + "type": "boolean", + "default": false, + "description": "Lift the block on the session's phone number." + }, + "unblock_email": { + "type": "boolean", + "default": false, + "description": "Lift the block on the session's email address." + } + } + }, + "examples": { + "Face and document": { + "value": { + "session_id": "33333333-4444-5555-6666-777777777777", + "unblock_face": true, + "unblock_document": true } } } @@ -39165,420 +57998,486 @@ { "lang": "curl", "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"title\": \"Manual - Jane Doe\", \"blueprint\": \"b1de0000-0000-4000-8000-000000000002\", \"priority\": \"HIGH\", \"subject\": {\"type\": \"user\", \"uuid\": \"5b9a0000-0000-4000-8000-00000000f00d\"}, \"tags\": [\"review\"]}'" + "source": "curl -X POST 'https://verification.didit.me/v3/blocklist/remove/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"session_id\": \"33333333-4444-5555-6666-777777777777\",\n \"unblock_face\": true,\n \"unblock_document\": true\n }'" }, { "lang": "python", "label": "Python", - "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={\n 'title': 'Manual - Jane Doe',\n 'blueprint': 'b1de0000-0000-4000-8000-000000000002',\n 'priority': 'HIGH',\n 'subject': {'type': 'user', 'uuid': '5b9a0000-0000-4000-8000-00000000f00d'},\n },\n timeout=10,\n)\nresp.raise_for_status()\ncase = resp.json()\nprint(case['case_number'], case['uuid'])" + "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/blocklist/remove/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={\n 'session_id': '33333333-4444-5555-6666-777777777777',\n 'unblock_face': True,\n 'unblock_document': True,\n },\n timeout=10,\n)\nresp.raise_for_status()\nprint(resp.json()['message'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/blocklist/remove/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n session_id: '33333333-4444-5555-6666-777777777777',\n unblock_face: true,\n unblock_document: true,\n }),\n});\nif (!resp.ok) throw new Error(`Unblock failed: ${resp.status} ${await resp.text()}`);\nconsole.log((await resp.json()).message);" } ], "responses": { - "201": { - "description": "Case created.", + "200": { + "description": "The block on the selected items was lifted.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CaseDetail" + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string", + "description": "Always `Successfully removed from blocklist.`" + } + } + }, + "examples": { + "Unblocked": { + "value": { + "message": "Successfully removed from blocklist." + } + } } } } }, "400": { - "description": "Neither subject nor links provided, or the subject/blueprint/assignee was not found.", + "description": "The body is invalid, the session is not a verification session of this application, or it has no record of a selected type. Nothing was changed.", "content": { "application/json": { - "schema": { - "type": "object" + "examples": { + "Unknown session": { + "value": { + "non_field_errors": [ + "Session not found" + ] + } + }, + "No item selected": { + "value": { + "non_field_errors": [ + "Must specify at least one item to unblock" + ] + } + }, + "No email record": { + "value": { + "non_field_errors": [ + "Session does not have an email record" + ] + } + }, + "Invalid session id": { + "value": { + "session_id": [ + "Must be a valid UUID." + ] + } + }, + "Missing session id": { + "value": { + "session_id": [ + "This field is required." + ] + } + } } } } }, - "409": { - "description": "Duplicate review is enabled and an existing case was found without a separate-incident reason. No case was created." + "401": { + "description": "The scoped API key has expired. Other authentication failures return `403`.", + "content": { + "application/json": { + "examples": { + "Expired API key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "The `x-api-key` header is missing or invalid, or a scoped API key lacks `workflows: write` or is used from an IP address outside its allowlist.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without workflows access": { + "value": { + "detail": "This API key has no write access to workflows." + } + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/RateLimitedWrite" } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/": { + "/v3/webhook/": { "get": { - "summary": "Get case", - "description": "Retrieve a case with its subject, blueprint, notes, events, links, and checklist.", - "operationId": "get_case", + "summary": "Get webhook configuration", + "description": "Return the application's webhook settings in one call: every [webhook destination](https://docs.didit.me/management-api/webhook/list-destinations) with its signing secret, the event types a destination can subscribe to, the application-level (legacy) webhook URL, version and secret, the data and biometric-template retention policy, and the Digital ID wallet return hosts. Didit delivers webhooks to destinations, each signed with its own `secret_shared_key`; the top-level `webhook_url`, `webhook_version` and `secret_shared_key` are used only while no destination is enabled, and then only for `status.updated` and `data.updated`. The response contains signing secrets, so keep it out of logs and client-side code. A scoped API key needs `webhooks: read`.", + "operationId": "get_webhook_config", + "x-didit-sdk": { + "resource": "webhooks", + "method": "retrieve_config", + "auth": "api_key", + "server": true + }, "tags": [ - "Cases" + "Webhook" ], - "parameters": [ + "parameters": [], + "x-codeSamples": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/webhook/' \\\n -H 'x-api-key: YOUR_API_KEY'" }, { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/webhook/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nconfig = resp.json()\nfor destination in config['endpoints']:\n print(destination['label'], destination['url'], destination['subscribed_events'])" }, { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/webhook/', {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Request failed: ${resp.status}`);\nconst config = await resp.json();\nfor (const destination of config.endpoints) {\n console.log(destination.label, destination.url, destination.subscribed_events);\n}" } ], "responses": { "200": { - "description": "Case detail.", + "description": "The webhook configuration.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CaseDetail" + "$ref": "#/components/schemas/WebhookConfig" + }, + "examples": { + "One destination": { + "value": { + "webhook_url": "https://yourapp.com/webhooks/didit", + "webhook_version": "v3", + "secret_shared_key": "dGhpcy1pcy10aGUtYXBwLWxldmVsLXNlY3JldC0wMDA", + "capture_method": "MOBILE", + "data_retention_months": 24, + "face_retention_policy": "delete_with_session", + "face_retention_days": null, + "available_events": [ + { + "value": "status.updated", + "label": "Session status updated" + }, + { + "value": "data.updated", + "label": "Session data updated" + }, + { + "value": "user.status.updated", + "label": "User status updated" + }, + { + "value": "user.data.updated", + "label": "User data updated" + }, + { + "value": "business.status.updated", + "label": "Business status updated" + }, + { + "value": "business.data.updated", + "label": "Business data updated" + }, + { + "value": "activity.created", + "label": "Activity created" + }, + { + "value": "transaction.created", + "label": "Transaction created" + }, + { + "value": "transaction.status.updated", + "label": "Transaction status updated" + }, + { + "value": "travel_rule.status.updated", + "label": "Travel Rule Status Updated" + }, + { + "value": "workflow.compliance_changed", + "label": "Workflow Compliance Changed" + } + ], + "allowed_return_hosts": [ + "shop.example.com" + ], + "endpoints": [ + { + "uuid": "11111111-2222-3333-4444-555555555555", + "label": "Production session webhooks", + "url": "https://yourapp.com/webhooks/didit", + "enabled": true, + "webhook_version": "v3", + "secret_shared_key": "dGhpcy1pcy1hbi1leGFtcGxlLXNlY3JldC0wMDAwMDA", + "subscribed_events": [ + "status.updated", + "data.updated" + ], + "created_at": "2026-01-04T08:00:00.000000Z", + "updated_at": "2026-04-22T14:09:11.523671Z" + } + ] + } + } } } } }, - "404": { - "description": "Case (or nested resource) not found in this application.", + "401": { + "description": "The API key has expired: it was created with an expiry date that has passed. A missing, invalid or revoked key gets `403`, not `401`.", "content": { "application/json": { "schema": { "type": "object", "properties": { "detail": { - "type": "string" + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." } } } } } - } - } - }, - "patch": { - "summary": "Update case", - "description": "Update the case's title, priority, due date, or tags. Logs PRIORITY_CHANGED/DUE_DATE_CHANGED/TAG_ADDED/TAG_REMOVED events as applicable.", - "operationId": "update_case", - "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { - "type": "string", - "maxLength": 255 - }, - "priority": { - "type": "string", - "enum": [ - "LOW", - "MEDIUM", - "HIGH" - ] + "403": { + "description": "The request is not allowed. A missing, invalid or revoked `x-api-key` gets `You do not have permission to perform this action.` A scoped key without the access this endpoint needs gets a message naming the missing access, and a key used from outside its IP allowlist gets `This API key can't be used from this IP address.`", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Missing or invalid key": { + "value": { + "detail": "You do not have permission to perform this action." + } }, - "due_at": { - "type": "string", - "format": "date-time", - "nullable": true + "Scoped key without webhooks read": { + "value": { + "detail": "This API key has no read access to webhooks." + } }, - "tags": { - "type": "array", - "items": { - "type": "string" + "Outside the IP allowlist": { + "value": { + "detail": "This API key can't be used from this IP address." } } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"priority\": \"HIGH\", \"due_at\": \"2026-08-01T00:00:00Z\"}'" - } - ], - "responses": { - "200": { - "description": "Updated case.", - "content": { - "application/json": { + }, + "429": { + "description": "Rate limit exceeded: more than 600 GET requests in the current one-minute window for this API key. Wait `Retry-After` seconds before retrying.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", "schema": { - "$ref": "#/components/schemas/CaseDetail" + "type": "integer", + "minimum": 1, + "example": 30 } - } - } - } - } - }, - "delete": { - "summary": "Delete case", - "description": "Delete a case. There is no undelete endpoint.", - "operationId": "delete_case", - "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X DELETE 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "204": { - "description": "Case deleted." - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/assign/": { - "post": { - "summary": "Assign or unassign case", - "description": "Assign the case to a user, or unassign it by sending assignee_user_id: null. assignee_email/assignee_name auto-provision a User row when the assignee does not exist yet.", - "operationId": "assign_case", - "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "assignee_user_id" - ], - "properties": { - "assignee_user_id": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "null to unassign." - }, - "assignee_email": { - "type": "string", - "format": "email", - "nullable": true - }, - "assignee_name": { - "type": "string", - "nullable": true - } + }, + "X-RateLimit-Limit": { + "description": "Requests allowed in the one-minute window (600).", + "schema": { + "type": "integer", + "example": 600 + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the current window.", + "schema": { + "type": "integer", + "example": 0 + } + }, + "X-RateLimit-Reset": { + "description": "Unix time (seconds) when the current window resets.", + "schema": { + "type": "integer", + "example": 1792056120 + } + }, + "X-RateLimit-Scope": { + "description": "Name of the budget that was exceeded.", + "schema": { + "type": "string", + "example": "generic-get" } } - } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/assign/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"assignee_user_id\": \"9a000000-0000-4000-8000-0000000000aa\"}'" - } - ], - "responses": { - "200": { - "description": "Case assigned/unassigned.", + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CaseDetail" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Throttled": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } } } - } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/resolve/": { - "post": { - "summary": "Resolve case", - "description": "Resolve the case as false_positive or valid_threat. Blocked with 400 if the blueprint's checklist is not optional and items remain incomplete, or if the blueprint requires a resolution note and none is given. When the case's blueprint has a four_eyes_checker_blueprint configured, the resolution is staged (case.pending_resolution) and the case moves to the checker blueprint instead of resolving immediately; a different user must call approve or reject-approval.", - "operationId": "resolve_case", - "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." + } + } + }, + "security": [ + { + "ApiKeyAuth": [] } + ] + }, + "patch": { + "summary": "Update webhook configuration", + "description": "Partially update application-level settings: the legacy webhook URL, version and secret, the data and biometric-template retention policy, and the Digital ID wallet return hosts. Send only the fields to change; the response is the full configuration, the same shape as `GET /v3/webhook/`. Destinations are not managed here: create, update and delete them with the `/v3/webhook/destinations/` endpoints, and do not send an `endpoints` field to this endpoint. A `data_retention_months` change also applies to existing sessions, and sessions it deletes cannot be restored; read [Data retention](https://docs.didit.me/console/data-retention) first. A scoped API key needs `webhooks: write`.", + "operationId": "update_webhook_config", + "x-didit-sdk": { + "resource": "webhooks", + "method": "update_config", + "auth": "api_key", + "server": true + }, + "tags": [ + "Webhook" ], + "parameters": [], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", - "required": [ - "resolution" - ], + "description": "Send only the fields to change. Destinations are not managed here.", "properties": { - "resolution": { + "webhook_url": { + "type": "string", + "format": "uri", + "nullable": true, + "maxLength": 2000, + "description": "Legacy application-level webhook URL. Didit uses it only while the application has no enabled destination, and then only for `status.updated` and `data.updated`. It does not create a destination, and the next create, update or delete of a destination replaces it. To receive webhooks, create a destination with `POST /v3/webhook/destinations/` instead." + }, + "webhook_version": { "type": "string", "enum": [ - "FALSE_POSITIVE", - "VALID_THREAT" - ] + "v1", + "v2", + "v3" + ], + "description": "Payload version of the legacy `webhook_url`." }, - "note": { + "rotate_secret_key": { + "type": "boolean", + "default": false, + "description": "`true` replaces the application-level `secret_shared_key` with a new random value at once; the new value is in the response. Destination secrets are not changed. Every call with `true` generates a new secret, so it is not idempotent." + }, + "capture_method": { "type": "string", + "enum": [ + "MOBILE", + "MOBILE_AND_DESKTOP" + ], + "description": "Legacy application setting. The verification flow does not read it." + }, + "data_retention_months": { + "type": "integer", "nullable": true, - "description": "Required when the blueprint sets require_resolution_note." + "minimum": 1, + "maximum": 120, + "description": "Automatic deletion window in months (a month counts as 30 days), or `null` for no automatic deletion. A change applies to existing sessions too: every user verification session older than the new window is deleted automatically, and that deletion is irreversible." + }, + "face_retention_policy": { + "type": "string", + "enum": [ + "delete_with_session", + "retain_until_user_deleted" + ], + "description": "What happens to a session's face embedding when the session is deleted. `retain_until_user_deleted` needs `face_retention_days` in the same request or already set. A change applies to session deletions from then on; it creates or purges no existing biometric template." + }, + "face_retention_days": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 3650, + "description": "Retention period, in days, of biometric templates under `retain_until_user_deleted`." + }, + "allowed_return_hosts": { + "type": "array", + "maxItems": 20, + "description": "Replaces the whole list of Digital ID wallet return hosts. Bare hostnames only, such as `shop.example.com`: no scheme, port, path or wildcard. Values are lowercased and duplicates are dropped. Send `[]` to clear the list.", + "items": { + "type": "string", + "maxLength": 253 + } + } + } + }, + "examples": { + "Enable biometric template retention": { + "value": { + "face_retention_policy": "retain_until_user_deleted", + "face_retention_days": 365 + } + }, + "Rotate the legacy secret": { + "value": { + "rotate_secret_key": true + } + }, + "Set data retention": { + "value": { + "data_retention_months": 24 + } + }, + "Register return hosts": { + "value": { + "allowed_return_hosts": [ + "shop.example.com", + "app.example.com" + ] } } } @@ -39589,977 +58488,2420 @@ { "lang": "curl", "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/resolve/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"resolution\": \"FALSE_POSITIVE\", \"note\": \"Confirmed legitimate activity after reviewing documents.\"}'" + "source": "curl -X PATCH 'https://verification.didit.me/v3/webhook/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"face_retention_policy\": \"retain_until_user_deleted\", \"face_retention_days\": 365}'" }, { "lang": "python", "label": "Python", - "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/resolve/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={'resolution': 'FALSE_POSITIVE', 'note': 'Confirmed legitimate activity.'},\n timeout=10,\n)\nresp.raise_for_status()\ncase = resp.json()\nprint(case['status'], case['resolution'], case.get('pending_resolution'))" + "source": "import os\n\nimport requests\n\nresp = requests.patch(\n 'https://verification.didit.me/v3/webhook/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={'face_retention_policy': 'retain_until_user_deleted', 'face_retention_days': 365},\n timeout=10,\n)\nresp.raise_for_status()\nconfig = resp.json()\nprint(config['face_retention_policy'], config['face_retention_days'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/webhook/', {\n method: 'PATCH',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ face_retention_policy: 'retain_until_user_deleted', face_retention_days: 365 }),\n});\nif (!resp.ok) throw new Error(await resp.text());\nconst config = await resp.json();\nconsole.log(config.face_retention_policy, config.face_retention_days);" } ], "responses": { "200": { - "description": "Resolved, or staged for 4-eyes approval.", + "description": "The full configuration after the update, the same shape as `GET /v3/webhook/`. When you rotated the secret, store the new `secret_shared_key` before you discard the response.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CaseDetail" + "$ref": "#/components/schemas/WebhookConfig" + }, + "examples": { + "Retention enabled": { + "value": { + "webhook_url": "https://yourapp.com/webhooks/didit", + "webhook_version": "v3", + "secret_shared_key": "dGhpcy1pcy10aGUtYXBwLWxldmVsLXNlY3JldC0wMDA", + "capture_method": "MOBILE", + "data_retention_months": 24, + "face_retention_policy": "retain_until_user_deleted", + "face_retention_days": 365, + "available_events": [ + { + "value": "status.updated", + "label": "Session status updated" + }, + { + "value": "data.updated", + "label": "Session data updated" + }, + { + "value": "user.status.updated", + "label": "User status updated" + }, + { + "value": "user.data.updated", + "label": "User data updated" + }, + { + "value": "business.status.updated", + "label": "Business status updated" + }, + { + "value": "business.data.updated", + "label": "Business data updated" + }, + { + "value": "activity.created", + "label": "Activity created" + }, + { + "value": "transaction.created", + "label": "Transaction created" + }, + { + "value": "transaction.status.updated", + "label": "Transaction status updated" + }, + { + "value": "travel_rule.status.updated", + "label": "Travel Rule Status Updated" + }, + { + "value": "workflow.compliance_changed", + "label": "Workflow Compliance Changed" + } + ], + "allowed_return_hosts": [ + "shop.example.com" + ], + "endpoints": [ + { + "uuid": "11111111-2222-3333-4444-555555555555", + "label": "Production session webhooks", + "url": "https://yourapp.com/webhooks/didit", + "enabled": true, + "webhook_version": "v3", + "secret_shared_key": "dGhpcy1pcy1hbi1leGFtcGxlLXNlY3JldC0wMDAwMDA", + "subscribed_events": [ + "status.updated", + "data.updated" + ], + "created_at": "2026-01-04T08:00:00.000000Z", + "updated_at": "2026-04-22T14:09:11.523671Z" + } + ] + } + } } } } }, "400": { - "description": "Incomplete checklist items, or a resolution note is required.", + "description": "Validation failed: each invalid field is mapped to a list of messages, and nothing is changed. A body that is not valid JSON returns `{\"detail\": \"JSON parse error - ...\"}`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Each invalid field mapped to a list of messages.", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "examples": { + "Retention days missing": { + "value": { + "face_retention_days": [ + "A finite retention duration is required when face retention is enabled." + ] + } + }, + "Retention window out of range": { + "value": { + "data_retention_months": [ + "Ensure this value is less than or equal to 120." + ] + } + }, + "Invalid return host": { + "value": { + "allowed_return_hosts": [ + "'https://shop.example.com' is not a hostname. Register the bare host (e.g. shop.example.com): no scheme, port, path or wildcard." + ] + } + }, + "Invalid choice": { + "value": { + "face_retention_policy": [ + "\"forever\" is not a valid choice." + ] + } + } + } + } + } + }, + "401": { + "description": "The API key has expired: it was created with an expiry date that has passed. A missing, invalid or revoked key gets `403`, not `401`.", "content": { "application/json": { "schema": { "type": "object", "properties": { "detail": { - "type": "string" + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "The request is not allowed. A missing, invalid or revoked `x-api-key` gets `You do not have permission to perform this action.` A scoped key without the access this endpoint needs gets a message naming the missing access, and a key used from outside its IP allowlist gets `This API key can't be used from this IP address.`", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Missing or invalid key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without webhooks write": { + "value": { + "detail": "This API key has no write access to webhooks." + } + }, + "Outside the IP allowlist": { + "value": { + "detail": "This API key can't be used from this IP address." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded: more than 300 write requests (POST, PATCH, PUT, DELETE) in the current one-minute window for this API key. Wait `Retry-After` seconds before retrying.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + }, + "X-RateLimit-Limit": { + "description": "Requests allowed in the one-minute window (300).", + "schema": { + "type": "integer", + "example": 300 + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the current window.", + "schema": { + "type": "integer", + "example": 0 + } + }, + "X-RateLimit-Reset": { + "description": "Unix time (seconds) when the current window resets.", + "schema": { + "type": "integer", + "example": 1792056120 + } + }, + "X-RateLimit-Scope": { + "description": "Name of the budget that was exceeded.", + "schema": { + "type": "string", + "example": "generic-write" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Throttled": { + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." } } } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reopen/": { - "post": { - "summary": "Reopen case", - "description": "Move an awaiting-user or resolved case back to open, clearing resolution, resolution_notes, resolved_at, and resolved_by_email.", - "operationId": "reopen_case", + "/v3/webhook/destinations/{destination_uuid}/analytics/": { + "get": { + "summary": "Get webhook destination analytics", + "description": "Delivery totals, failure rate and response times for one webhook destination, over the whole range (`summary`) and per UTC day (`points`). Every attempt counts as a delivery: the first send of an event, each automatic [retry](https://docs.didit.me/integration/webhooks#retry-policy) and each resend. A delivery is failed when its recorded HTTP status is 400 or above, so timeouts (recorded as `504`) and connection failures (`503`) count as failed; a delivery still queued counts in `total_deliveries` only. Delivery records are available for about 30 days. A scoped API key needs `webhooks: read`.", + "operationId": "get_webhook_destination_analytics", + "x-didit-sdk": { + "resource": "webhooks.destinations", + "method": "retrieve_analytics", + "auth": "api_key", + "server": true + }, "tags": [ - "Cases" + "Webhook" ], "parameters": [ { - "name": "organization_id", + "name": "destination_uuid", "in": "path", "required": true, + "description": "UUID of the destination (the `uuid` returned by `GET /v3/webhook/destinations/`).", "schema": { "type": "string", "format": "uuid" }, - "description": "Organization UUID." + "example": "11111111-2222-3333-4444-555555555555" }, { - "name": "application_id", - "in": "path", - "required": true, + "name": "date_from", + "in": "query", + "required": false, + "description": "First UTC day to include, as `YYYY-MM-DD` (inclusive). Omit to start from the oldest delivery available. `dateFrom` is accepted as an alias.", "schema": { "type": "string", - "format": "uuid" + "format": "date" }, - "description": "Application UUID." + "example": "2026-10-01" }, { - "name": "case_uuid", - "in": "path", - "required": true, + "name": "date_to", + "in": "query", + "required": false, + "description": "Last UTC day to include, as `YYYY-MM-DD` (inclusive). Must not be earlier than `date_from`. `dateTo` is accepted as an alias.", "schema": { "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" + "format": "date" }, - "description": "Case UUID." + "example": "2026-10-16" } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/reopen/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X GET 'https://verification.didit.me/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/analytics/?date_from=2026-10-01&date_to=2026-10-16' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\ndestination_uuid = '11111111-2222-3333-4444-555555555555'\nresp = requests.get(\n f'https://verification.didit.me/v3/webhook/destinations/{destination_uuid}/analytics/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n params={'date_from': '2026-10-01', 'date_to': '2026-10-16'},\n timeout=10,\n)\nresp.raise_for_status()\nsummary = resp.json()['summary']\nprint(f\"{summary['failed_deliveries']} of {summary['total_deliveries']} failed ({summary['error_rate_percentage']}%)\")" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const destinationUuid = '11111111-2222-3333-4444-555555555555';\nconst params = new URLSearchParams({ date_from: '2026-10-01', date_to: '2026-10-16' });\nconst resp = await fetch(`https://verification.didit.me/v3/webhook/destinations/${destinationUuid}/analytics/?${params}`, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Request failed: ${resp.status}`);\nconst { summary, points } = await resp.json();\nconsole.log(summary.error_rate_percentage, points.length);" } ], "responses": { "200": { - "description": "Reopen case.", + "description": "Delivery statistics for the destination.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CaseDetail" + "type": "object", + "required": [ + "summary", + "points" + ], + "properties": { + "summary": { + "type": "object", + "description": "Totals over the requested range.", + "required": [ + "total_deliveries", + "failed_deliveries", + "min_response_time_ms", + "avg_response_time_ms", + "max_response_time_ms", + "error_rate_percentage" + ], + "properties": { + "total_deliveries": { + "type": "integer", + "minimum": 0, + "description": "Delivery attempts, including queued ones." + }, + "failed_deliveries": { + "type": "integer", + "minimum": 0, + "description": "Attempts whose recorded HTTP status is 400 or above." + }, + "min_response_time_ms": { + "type": "integer", + "nullable": true, + "description": "Fastest attempt in milliseconds, or `null` when no attempt has a recorded time." + }, + "avg_response_time_ms": { + "type": "integer", + "nullable": true, + "description": "Mean attempt time in milliseconds, rounded, or `null` when no attempt has a recorded time." + }, + "max_response_time_ms": { + "type": "integer", + "nullable": true, + "description": "Slowest attempt in milliseconds, or `null` when no attempt has a recorded time." + }, + "error_rate_percentage": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "`failed_deliveries` / `total_deliveries` x 100, rounded to a whole number. `0` when there are no deliveries." + } + } + }, + "points": { + "type": "array", + "description": "One entry per UTC day that had deliveries, oldest first. Days without deliveries are omitted, so fill the gaps with zeros when you chart them.", + "items": { + "type": "object", + "required": [ + "date", + "total_deliveries", + "failed_deliveries", + "min_response_time_ms", + "avg_response_time_ms", + "max_response_time_ms" + ], + "properties": { + "date": { + "type": "string", + "format": "date", + "description": "UTC day, `YYYY-MM-DD`." + }, + "total_deliveries": { + "type": "integer", + "minimum": 0, + "description": "Delivery attempts, including queued ones." + }, + "failed_deliveries": { + "type": "integer", + "minimum": 0, + "description": "Attempts whose recorded HTTP status is 400 or above." + }, + "min_response_time_ms": { + "type": "integer", + "nullable": true, + "description": "Fastest attempt in milliseconds, or `null` when no attempt has a recorded time." + }, + "avg_response_time_ms": { + "type": "integer", + "nullable": true, + "description": "Mean attempt time in milliseconds, rounded, or `null` when no attempt has a recorded time." + }, + "max_response_time_ms": { + "type": "integer", + "nullable": true, + "description": "Slowest attempt in milliseconds, or `null` when no attempt has a recorded time." + } + } + } + } + } + }, + "examples": { + "Three days of deliveries": { + "value": { + "summary": { + "total_deliveries": 1250, + "failed_deliveries": 12, + "min_response_time_ms": 38, + "avg_response_time_ms": 164, + "max_response_time_ms": 5004, + "error_rate_percentage": 1 + }, + "points": [ + { + "date": "2026-10-14", + "total_deliveries": 410, + "failed_deliveries": 0, + "min_response_time_ms": 38, + "avg_response_time_ms": 152, + "max_response_time_ms": 911 + }, + { + "date": "2026-10-15", + "total_deliveries": 395, + "failed_deliveries": 12, + "min_response_time_ms": 41, + "avg_response_time_ms": 190, + "max_response_time_ms": 5004 + }, + { + "date": "2026-10-16", + "total_deliveries": 445, + "failed_deliveries": 0, + "min_response_time_ms": 40, + "avg_response_time_ms": 151, + "max_response_time_ms": 1220 + } + ] + } + }, + "No deliveries in range": { + "value": { + "summary": { + "total_deliveries": 0, + "failed_deliveries": 0, + "min_response_time_ms": null, + "avg_response_time_ms": null, + "max_response_time_ms": null, + "error_rate_percentage": 0 + }, + "points": [] + } + } + } + } + } + }, + "400": { + "description": "`date_from` or `date_to` is not a `YYYY-MM-DD` date, or `date_to` is earlier than `date_from`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Each invalid field mapped to a list of messages.", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "examples": { + "Malformed date": { + "value": { + "date_from": [ + "Date has wrong format. Use one of these formats instead: YYYY-MM-DD." + ] + } + }, + "Reversed range": { + "value": { + "date_to": [ + "date_to must be greater than or equal to date_from." + ] + } + } + } + } + } + }, + "401": { + "description": "The API key has expired: it was created with an expiry date that has passed. A missing, invalid or revoked key gets `403`, not `401`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "The request is not allowed. A missing, invalid or revoked `x-api-key` gets `You do not have permission to perform this action.` A scoped key without the access this endpoint needs gets a message naming the missing access, and a key used from outside its IP allowlist gets `This API key can't be used from this IP address.`", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Missing or invalid key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without webhooks read": { + "value": { + "detail": "This API key has no read access to webhooks." + } + }, + "Outside the IP allowlist": { + "value": { + "detail": "This API key can't be used from this IP address." + } + } + } + } + } + }, + "404": { + "description": "No destination with this UUID exists for the application, or it has been deleted.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Destination not found": { + "value": { + "detail": "Webhook destination not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded: more than 600 GET requests in the current one-minute window for this API key. Wait `Retry-After` seconds before retrying.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + }, + "X-RateLimit-Limit": { + "description": "Requests allowed in the one-minute window (600).", + "schema": { + "type": "integer", + "example": 600 + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the current window.", + "schema": { + "type": "integer", + "example": 0 + } + }, + "X-RateLimit-Reset": { + "description": "Unix time (seconds) when the current window resets.", + "schema": { + "type": "integer", + "example": 1792056120 + } + }, + "X-RateLimit-Scope": { + "description": "Name of the budget that was exceeded.", + "schema": { + "type": "string", + "example": "generic-get" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Throttled": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/request-info/": { - "post": { - "summary": "Request info (awaiting user)", - "description": "Move an open case to awaiting_user while more information is collected from the verified user. Fails with 400 if a resolution is currently pending 4-eyes approval.", - "operationId": "request_case_info", + "/v3/webhook/destinations/{destination_uuid}/deliveries/": { + "get": { + "summary": "List webhook destination deliveries", + "description": "Page through every delivery attempt to one webhook destination, newest first. The first send of each event, each automatic [retry](https://docs.didit.me/integration/webhooks#retry-policy) and each resend are separate entries. Entries carry the HTTP status and response time but not the payload: fetch one delivery to read its headers and body. `count` is exact up to 100 and reads 100 above that, so follow `next` until it is `null`. Delivery records are available for about 30 days. A scoped API key needs `webhooks: read` and `decisions: read`; a key limited to some workflows or to approved sessions cannot use this endpoint.", + "operationId": "list_webhook_destination_deliveries", + "x-didit-sdk": { + "resource": "webhooks.destinations", + "method": "list_deliveries", + "auth": "api_key", + "server": true + }, "tags": [ - "Cases" + "Webhook" ], "parameters": [ { - "name": "organization_id", + "name": "destination_uuid", "in": "path", "required": true, + "description": "UUID of the destination (the `uuid` returned by `GET /v3/webhook/destinations/`).", "schema": { "type": "string", "format": "uuid" }, - "description": "Organization UUID." + "example": "11111111-2222-3333-4444-555555555555" }, { - "name": "application_id", - "in": "path", - "required": true, + "name": "limit", + "in": "query", + "required": false, + "description": "Maximum number of deliveries per page. Defaults to 50. A value that is not a positive integer falls back to 50; a value above 1000000 returns `400`.", "schema": { - "type": "string", - "format": "uuid" + "type": "integer", + "default": 50, + "minimum": 1, + "maximum": 1000000 }, - "description": "Application UUID." + "example": 50 }, { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/request-info/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "200": { - "description": "Request info (awaiting user).", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseDetail" - } - } - } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/escalate/": { - "post": { - "summary": "Escalate case", - "description": "Move the case to its blueprint's configured escalation_blueprint. Requires the current blueprint to have escalation_enabled and an escalation_blueprint set, otherwise 400.", - "operationId": "escalate_case", - "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, + "name": "offset", + "in": "query", + "required": false, + "description": "Number of deliveries to skip. Defaults to 0. Prefer following the `next` and `previous` URLs from the response.", "schema": { - "type": "string", - "format": "uuid" + "type": "integer", + "default": 0, + "minimum": 0, + "maximum": 1000000 }, - "description": "Organization UUID." + "example": 0 }, { - "name": "application_id", - "in": "path", - "required": true, + "name": "date_from", + "in": "query", + "required": false, + "description": "First UTC day to include, as `YYYY-MM-DD` (inclusive). Omit to start from the oldest delivery available. `dateFrom` is accepted as an alias.", "schema": { "type": "string", - "format": "uuid" + "format": "date" }, - "description": "Application UUID." + "example": "2026-10-01" }, { - "name": "case_uuid", - "in": "path", - "required": true, + "name": "date_to", + "in": "query", + "required": false, + "description": "Last UTC day to include, as `YYYY-MM-DD` (inclusive). Must not be earlier than `date_from`. `dateTo` is accepted as an alias.", "schema": { "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" + "format": "date" }, - "description": "Case UUID." + "example": "2026-10-16" } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/escalate/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X GET 'https://verification.didit.me/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/deliveries/?limit=50&date_from=2026-10-01' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\ndestination_uuid = '11111111-2222-3333-4444-555555555555'\nurl = f'https://verification.didit.me/v3/webhook/destinations/{destination_uuid}/deliveries/'\nparams = {'limit': 50, 'date_from': '2026-10-01'}\n\nwhile url:\n resp = requests.get(\n url,\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n params=params,\n timeout=10,\n )\n resp.raise_for_status()\n page = resp.json()\n for delivery in page['results']:\n if delivery['response_status'] is None or delivery['response_status'] >= 400:\n print(delivery['uuid'], delivery['webhook_type'], delivery['response_status'])\n url, params = page['next'], None # next already carries the query string" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const destinationUuid = '11111111-2222-3333-4444-555555555555';\nlet url = `https://verification.didit.me/v3/webhook/destinations/${destinationUuid}/deliveries/?limit=50&date_from=2026-10-01`;\nwhile (url) {\n const resp = await fetch(url, { headers: { 'x-api-key': process.env.DIDIT_API_KEY } });\n if (!resp.ok) throw new Error(`Request failed: ${resp.status}`);\n const page = await resp.json();\n for (const delivery of page.results) {\n if (delivery.response_status === null || delivery.response_status >= 400) {\n console.log(delivery.uuid, delivery.webhook_type, delivery.response_status);\n }\n }\n url = page.next;\n}" } ], "responses": { "200": { - "description": "Escalate case.", + "description": "One page of deliveries, newest first.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CaseDetail" + "type": "object", + "required": [ + "count", + "next", + "previous", + "results" + ], + "properties": { + "count": { + "type": "integer", + "minimum": 0, + "description": "Matching deliveries, exact up to 100 and capped at 100 above that. Use `next` to know whether more pages exist." + }, + "next": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Absolute URL of the next page, or `null` on the last page." + }, + "previous": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Absolute URL of the previous page, or `null` on the first page." + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookDestinationDelivery" + } + } + } + }, + "examples": { + "Failed delivery, its retry and a queued event": { + "value": { + "count": 3, + "next": null, + "previous": null, + "results": [ + { + "uuid": "66666666-7777-8888-9999-000000000003", + "session_id": null, + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": null, + "response_time_ms": null, + "status": "APPROVED", + "webhook_type": "transaction.created", + "created_at": "2026-10-15T09:23:02.871190Z" + }, + { + "uuid": "66666666-7777-8888-9999-000000000002", + "session_id": "22222222-3333-4444-5555-666666666666", + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": 200, + "response_time_ms": 143, + "status": "In Progress", + "webhook_type": "status.updated", + "created_at": "2026-10-15T09:22:19.104233Z" + }, + { + "uuid": "66666666-7777-8888-9999-000000000001", + "session_id": "22222222-3333-4444-5555-666666666666", + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": 503, + "response_time_ms": 412, + "status": "In Progress", + "webhook_type": "status.updated", + "created_at": "2026-10-15T09:21:18.412345Z" + } + ] + } + } } } } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/transfer/": { - "post": { - "summary": "Transfer case", - "description": "Move the case to a different, active blueprint of the same application. Requires the current blueprint to have transfer_enabled, otherwise 400.", - "operationId": "transfer_case", - "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "400": { + "description": "`date_from` or `date_to` is not a `YYYY-MM-DD` date, `date_to` is earlier than `date_from`, or `limit` or `offset` is above 1000000 (that error is a JSON array of messages).", + "content": { + "application/json": { + "examples": { + "Malformed date": { + "value": { + "date_from": [ + "Date has wrong format. Use one of these formats instead: YYYY-MM-DD." + ] + } + }, + "Reversed range": { + "value": { + "date_to": [ + "date_to must be greater than or equal to date_from." + ] + } + }, + "Limit too large": { + "value": [ + "limit must not exceed 1000000." + ] + } + } + } + } }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "target_blueprint" - ], - "properties": { - "target_blueprint": { - "type": "string", - "format": "uuid" + "401": { + "description": "The API key has expired: it was created with an expiry date that has passed. A missing, invalid or revoked key gets `403`, not `401`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/transfer/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"target_blueprint\": \"b1de0000-0000-4000-8000-000000000002\"}'" - } - ], - "responses": { - "200": { - "description": "Case transferred.", + }, + "403": { + "description": "The request is not allowed. A missing, invalid or revoked `x-api-key` gets `You do not have permission to perform this action.` A scoped key without the access this endpoint needs gets a message naming the missing access, and a key used from outside its IP allowlist gets `This API key can't be used from this IP address.` A key limited to some workflows or to approved sessions gets `This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint.`", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CaseDetail" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Missing or invalid key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without webhooks read": { + "value": { + "detail": "This API key has no read access to webhooks." + } + }, + "Scoped key without decisions read": { + "value": { + "detail": "This API key has no read access to decisions." + } + }, + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." + } + }, + "Outside the IP allowlist": { + "value": { + "detail": "This API key can't be used from this IP address." + } + } + } + } + } + }, + "404": { + "description": "No destination with this UUID exists for the application, or it has been deleted.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Destination not found": { + "value": { + "detail": "Webhook destination not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded: more than 600 GET requests in the current one-minute window for this API key. Wait `Retry-After` seconds before retrying.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + }, + "X-RateLimit-Limit": { + "description": "Requests allowed in the one-minute window (600).", + "schema": { + "type": "integer", + "example": 600 + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the current window.", + "schema": { + "type": "integer", + "example": 0 + } + }, + "X-RateLimit-Reset": { + "description": "Unix time (seconds) when the current window resets.", + "schema": { + "type": "integer", + "example": 1792056120 + } + }, + "X-RateLimit-Scope": { + "description": "Name of the budget that was exceeded.", + "schema": { + "type": "string", + "example": "generic-get" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Throttled": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/approve/": { - "post": { - "summary": "Approve staged resolution (4-eyes)", - "description": "Apply a resolution staged by resolve on a maker blueprint. The caller must be a different user than the one who submitted it, otherwise 403.", - "operationId": "approve_case_resolution", + "/v3/webhook/destinations/{destination_uuid}/deliveries/{delivery_uuid}/": { + "get": { + "summary": "Get webhook destination delivery", + "description": "Retrieve one delivery attempt to a webhook destination with the exact request headers and JSON body Didit sent, the HTTP status your endpoint returned and the response time. `request_header` and `request_body` are JSON text: parse them to read the signature headers and the payload, for example to check your [signature verification](https://docs.didit.me/integration/webhooks#signature-verification) against the destination's `secret_shared_key`. A delivery never contains the signing secret, and Didit does not record your endpoint's response body. A scoped API key needs `webhooks: read` and `decisions: read`; a key limited to some workflows or to approved sessions cannot use this endpoint.", + "operationId": "get_webhook_destination_delivery", + "x-didit-sdk": { + "resource": "webhooks.destinations", + "method": "retrieve_delivery", + "auth": "api_key", + "server": true + }, "tags": [ - "Cases" + "Webhook" ], "parameters": [ { - "name": "organization_id", + "name": "destination_uuid", "in": "path", "required": true, + "description": "UUID of the destination the delivery was sent to.", "schema": { "type": "string", "format": "uuid" }, - "description": "Organization UUID." + "example": "11111111-2222-3333-4444-555555555555" }, { - "name": "application_id", + "name": "delivery_uuid", "in": "path", "required": true, + "description": "UUID of the delivery: the `uuid` of an entry returned by `GET /v3/webhook/destinations/{destination_uuid}/deliveries/`.", "schema": { "type": "string", "format": "uuid" }, - "description": "Application UUID." - }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." + "example": "66666666-7777-8888-9999-000000000001" } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/approve/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X GET 'https://verification.didit.me/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/deliveries/66666666-7777-8888-9999-000000000001/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import json\nimport os\n\nimport requests\n\ndestination_uuid = '11111111-2222-3333-4444-555555555555'\ndelivery_uuid = '66666666-7777-8888-9999-000000000001'\nresp = requests.get(\n f'https://verification.didit.me/v3/webhook/destinations/{destination_uuid}/deliveries/{delivery_uuid}/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\ndelivery = resp.json()\nheaders = json.loads(delivery['request_header'])\npayload = json.loads(delivery['request_body'])\nprint(delivery['response_status'], headers['X-Signature-V2'], payload['event_id'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const destinationUuid = '11111111-2222-3333-4444-555555555555';\nconst deliveryUuid = '66666666-7777-8888-9999-000000000001';\nconst resp = await fetch(\n `https://verification.didit.me/v3/webhook/destinations/${destinationUuid}/deliveries/${deliveryUuid}/`,\n { headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n);\nif (!resp.ok) throw new Error(`Request failed: ${resp.status}`);\nconst delivery = await resp.json();\nconst headers = JSON.parse(delivery.request_header);\nconst payload = JSON.parse(delivery.request_body);\nconsole.log(delivery.response_status, headers['X-Signature-V2'], payload.event_id);" } ], "responses": { "200": { - "description": "Resolution applied.", + "description": "The delivery, with its request headers and body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookDestinationDeliveryDetail" + }, + "examples": { + "Connection failure": { + "value": { + "uuid": "66666666-7777-8888-9999-000000000001", + "session_id": "22222222-3333-4444-5555-666666666666", + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": 503, + "response_time_ms": 412, + "status": "In Progress", + "webhook_type": "status.updated", + "created_at": "2026-10-15T09:21:18.412345Z", + "request_header": "{\"Content-Type\": \"application/json\", \"X-Signature\": \"a5e02a03404e1337a0198e6be15d34abbd053c658a0e51e5d0e78554213ef29b\", \"X-Signature-V2\": \"927e7cb770c94ce745939bff73f04aa200015f3bc64795ba46c2aaf78a6cedc5\", \"X-Signature-Simple\": \"e37b0d4c716c72bb5ae7127b9cc455d28ab4686e9e22312b4ec2327409a73302\", \"X-Timestamp\": \"1792056078\", \"User-Agent\": \"DiditWebhook/2.0 +https://didit.me\"}", + "request_body": "{\"application_id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"created_at\":1792056057,\"environment\":\"live\",\"event_id\":\"33333333-4444-5555-6666-777777777777\",\"sandbox_scenario\":null,\"session_id\":\"22222222-3333-4444-5555-666666666666\",\"status\":\"In Progress\",\"timestamp\":1792056078,\"vendor_data\":\"user-123\",\"webhook_type\":\"status.updated\",\"workflow_id\":\"44444444-5555-6666-7777-888888888888\",\"workflow_version\":3}" + } + } + } + } + } + }, + "401": { + "description": "The API key has expired: it was created with an expiry date that has passed. A missing, invalid or revoked key gets `403`, not `401`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "The request is not allowed. A missing, invalid or revoked `x-api-key` gets `You do not have permission to perform this action.` A scoped key without the access this endpoint needs gets a message naming the missing access, and a key used from outside its IP allowlist gets `This API key can't be used from this IP address.` A key limited to some workflows or to approved sessions gets `This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint.`", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Missing or invalid key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without webhooks read": { + "value": { + "detail": "This API key has no read access to webhooks." + } + }, + "Scoped key without decisions read": { + "value": { + "detail": "This API key has no read access to decisions." + } + }, + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." + } + }, + "Outside the IP allowlist": { + "value": { + "detail": "This API key can't be used from this IP address." + } + } + } + } + } + }, + "404": { + "description": "The destination does not exist for the application or has been deleted, or the delivery does not exist, belongs to another destination, or is no longer available.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CaseDetail" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Destination not found": { + "value": { + "detail": "Webhook destination not found." + } + }, + "Delivery not found": { + "value": { + "detail": "Webhook delivery not found." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded: more than 600 GET requests in the current one-minute window for this API key. Wait `Retry-After` seconds before retrying.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + }, + "X-RateLimit-Limit": { + "description": "Requests allowed in the one-minute window (600).", + "schema": { + "type": "integer", + "example": 600 + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the current window.", + "schema": { + "type": "integer", + "example": 0 + } + }, + "X-RateLimit-Reset": { + "description": "Unix time (seconds) when the current window resets.", + "schema": { + "type": "integer", + "example": 1792056120 + } + }, + "X-RateLimit-Scope": { + "description": "Name of the budget that was exceeded.", + "schema": { + "type": "string", + "example": "generic-get" } } - } - }, - "409": { - "description": "Case has no resolution pending approval.", + }, "content": { "application/json": { "schema": { "type": "object", "properties": { "detail": { - "type": "string" + "type": "string", + "description": "Human-readable error message." } } - } - } - } - }, - "403": { - "description": "The caller is the same user who submitted the resolution (or the case has no recorded maker); a different officer must approve it.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" + }, + "examples": { + "Throttled": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." } } } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reject-approval/": { + "/v3/webhook/destinations/{destination_uuid}/deliveries/{delivery_uuid}/resend/": { "post": { - "summary": "Reject staged resolution (4-eyes)", - "description": "Reject a resolution staged by resolve and return the case to its maker blueprint. The caller must be a different user than the one who submitted it, otherwise 403.", - "operationId": "reject_case_resolution", + "summary": "Resend webhook destination delivery", + "description": "Send a past delivery again, for example after your endpoint was down. Didit takes the stored payload, sets a fresh `timestamp`, signs it again with the destination's current `secret_shared_key` (`X-Signature`, `X-Signature-V2`, `X-Signature-Simple` and `X-Timestamp`) and posts it to the delivery's `request_url`, even if the destination's URL has changed since or the destination is disabled. `event_id` and the rest of the payload are unchanged, so deduplicate on `event_id`. The call waits up to 5 seconds for your endpoint and returns the new delivery with `200` whatever your endpoint answered: read `response_status`. A resend is not retried automatically, every call sends again and creates a new delivery, and the request has no body. A scoped API key needs `webhooks: write` and `decisions: read`; a key limited to some workflows or to approved sessions cannot use this endpoint.", + "operationId": "resend_webhook_destination_delivery", + "x-didit-sdk": { + "resource": "webhooks.destinations", + "method": "resend_delivery", + "auth": "api_key", + "server": true + }, "tags": [ - "Cases" + "Webhook" ], "parameters": [ { - "name": "organization_id", + "name": "destination_uuid", "in": "path", "required": true, + "description": "UUID of the destination the delivery was sent to.", "schema": { "type": "string", "format": "uuid" }, - "description": "Organization UUID." + "example": "11111111-2222-3333-4444-555555555555" }, { - "name": "application_id", + "name": "delivery_uuid", "in": "path", "required": true, + "description": "UUID of the delivery to resend: the `uuid` of an entry returned by `GET /v3/webhook/destinations/{destination_uuid}/deliveries/`.", "schema": { "type": "string", "format": "uuid" }, - "description": "Application UUID." - }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." + "example": "66666666-7777-8888-9999-000000000001" } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "notes": { - "type": "string", - "nullable": true - } - } - } - } - } - }, "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/reject-approval/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"notes\": \"Needs a clearer rationale before this can be approved.\"}'" + "source": "curl -X POST 'https://verification.didit.me/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/deliveries/66666666-7777-8888-9999-000000000001/resend/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\ndestination_uuid = '11111111-2222-3333-4444-555555555555'\ndelivery_uuid = '66666666-7777-8888-9999-000000000001'\nresp = requests.post(\n f'https://verification.didit.me/v3/webhook/destinations/{destination_uuid}/deliveries/{delivery_uuid}/resend/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nresent = resp.json()\nif resent['response_status'] is None or resent['response_status'] >= 400:\n print('Your endpoint still rejects it:', resent['response_status'])\nelse:\n print('Delivered as', resent['uuid'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const destinationUuid = '11111111-2222-3333-4444-555555555555';\nconst deliveryUuid = '66666666-7777-8888-9999-000000000001';\nconst resp = await fetch(\n `https://verification.didit.me/v3/webhook/destinations/${destinationUuid}/deliveries/${deliveryUuid}/resend/`,\n { method: 'POST', headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n);\nif (!resp.ok) throw new Error(await resp.text());\nconst resent = await resp.json();\nconsole.log(resent.uuid, resent.response_status);" } ], "responses": { "200": { - "description": "Resolution rejected, case returned to the maker blueprint.", + "description": "The new delivery, already attempted. `response_status` is what your endpoint answered this time (`504` timeout, `503` connection failure). The original delivery is unchanged.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CaseDetail" + "$ref": "#/components/schemas/WebhookDestinationDeliveryDetail" + }, + "examples": { + "Accepted on resend": { + "value": { + "uuid": "66666666-7777-8888-9999-000000000004", + "session_id": "22222222-3333-4444-5555-666666666666", + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": 200, + "response_time_ms": 151, + "status": "In Progress", + "webhook_type": "status.updated", + "created_at": "2026-10-16T14:05:42.330981Z", + "request_header": "{\"Content-Type\": \"application/json\", \"X-Signature\": \"b8eb1e376517ee0cad8ba98ff7f8fd35fc72eca231000963a96a335f82328835\", \"X-Signature-V2\": \"9efd2ce9ed1303e1d05273ef9608a59363ed9789a05253db4748b3e8e50cb109\", \"X-Signature-Simple\": \"bf3a9dc04b2c20ab539203b0cd5cca2a10f630bf62d8feb6d68d82af80f64774\", \"X-Timestamp\": \"1792159542\", \"User-Agent\": \"DiditWebhook/2.0 +https://didit.me\"}", + "request_body": "{\"application_id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"created_at\":1792056057,\"environment\":\"live\",\"event_id\":\"33333333-4444-5555-6666-777777777777\",\"sandbox_scenario\":null,\"session_id\":\"22222222-3333-4444-5555-666666666666\",\"status\":\"In Progress\",\"timestamp\":1792159542,\"vendor_data\":\"user-123\",\"webhook_type\":\"status.updated\",\"workflow_id\":\"44444444-5555-6666-7777-888888888888\",\"workflow_version\":3}" + } + } } } } }, - "409": { - "description": "Case has no resolution pending approval.", + "400": { + "description": "The stored delivery could not be signed again. Nothing is sent.", "content": { "application/json": { "schema": { "type": "object", "properties": { "detail": { - "type": "string" + "type": "string", + "description": "Human-readable error message." + }, + "code": { + "type": "string", + "description": "Stable machine-readable error code. Branch on this, not on `detail`." + } + } + }, + "examples": { + "Unsignable delivery": { + "value": { + "detail": "Invalid webhook data format", + "code": "invalid_webhook_payload" } } } } } }, - "403": { - "description": "The caller is the same user who submitted the resolution (or the case has no recorded maker); a different officer must reject it.", + "401": { + "description": "The API key has expired: it was created with an expiry date that has passed. A missing, invalid or revoked key gets `403`, not `401`.", "content": { "application/json": { "schema": { "type": "object", "properties": { "detail": { - "type": "string" + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." } } } } } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/entity-actions/": { - "post": { - "operationId": "apply_case_entity_action", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true }, - { - "in": "path", - "name": "case_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - } - ], - "tags": [ - "Case Management" - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CaseActionCaseEntityActionRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseActionCaseEntityActionRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/CaseActionCaseEntityActionRequest" + "403": { + "description": "The request is not allowed. A missing, invalid or revoked `x-api-key` gets `You do not have permission to perform this action.` A scoped key without the access this endpoint needs gets a message naming the missing access, and a key used from outside its IP allowlist gets `This API key can't be used from this IP address.` A key limited to some workflows or to approved sessions gets `This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint.`", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Missing or invalid key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without webhooks write": { + "value": { + "detail": "This API key has no write access to webhooks." + } + }, + "Scoped key without decisions read": { + "value": { + "detail": "This API key has no read access to decisions." + } + }, + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." + } + }, + "Outside the IP allowlist": { + "value": { + "detail": "This API key can't be used from this IP address." + } + } + } } } }, - "required": true - }, - "responses": { - "200": { + "404": { + "description": "The destination does not exist for the application or has been deleted, or the delivery does not exist, belongs to another destination, or is no longer available.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CaseActionCaseEntityActionResult" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Destination not found": { + "value": { + "detail": "Webhook destination not found." + } + }, + "Delivery not found": { + "value": { + "detail": "Webhook delivery not found." + } + } } } - }, - "description": "" + } }, - "409": { - "description": "The reference belongs to a different action payload." + "429": { + "description": "Rate limit exceeded: more than 300 write requests (POST, PATCH, PUT, DELETE) in the current one-minute window for this API key. Wait `Retry-After` seconds before retrying.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + }, + "X-RateLimit-Limit": { + "description": "Requests allowed in the one-minute window (300).", + "schema": { + "type": "integer", + "example": 300 + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the current window.", + "schema": { + "type": "integer", + "example": 0 + } + }, + "X-RateLimit-Reset": { + "description": "Unix time (seconds) when the current window resets.", + "schema": { + "type": "integer", + "example": 1792056120 + } + }, + "X-RateLimit-Scope": { + "description": "Name of the budget that was exceeded.", + "schema": { + "type": "string", + "example": "generic-write" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Throttled": { + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + } + } + } } }, "security": [ { "ApiKeyAuth": [] } - ], - "description": "Apply one explicit action to the case subject. Supply a unique client_reference to obtain an action receipt and make retries idempotent. Reusing that reference with a different payload returns 409. With a reference, inspect receipt.status even when HTTP status is 200: FAILED means no action was applied. Repeating a failed request does not execute it again unless retry is true. Omitting client_reference preserves the legacy response." + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/print/": { + "/v3/events/": { "get": { - "summary": "Print case (PDF)", - "description": "Render the full case (summary, subject, checklist state, notes, attached sessions/transactions, events, FIU report list) as a PDF and return a presigned download URL.", - "operationId": "print_case", + "summary": "List monitoring events", + "description": "List the monitoring events stored for your application, newest `occurred_at` first, with optional filters that combine with AND. Multi-value filters (`event_category`, `action_type`, `status`, `retention_class`) repeat the parameter and match any of the values; the other filters are exact matches, and unknown values simply return no events. Each result is a summary: call `GET /v3/events/{event_uuid}/` for the payload, device context, IP enrichment and rule evidence, and prefer `GET /v3/events/subjects/{vendor_data}/` for one subject's history. Reads work whether or not event ingestion is switched on. A scoped API key needs `transactions: read`. See [Event ingestion](https://docs.didit.me/transaction-monitoring/events).", + "operationId": "list_monitoring_events", + "x-didit-sdk": { + "resource": "monitoring_events", + "method": "list", + "auth": "api_key", + "server": true + }, "tags": [ - "Cases" + "Monitoring Events" ], "parameters": [ { - "name": "organization_id", - "in": "path", - "required": true, + "name": "limit", + "in": "query", + "required": false, + "description": "Maximum number of events per page. Defaults to 50. Values above 1000000 return `400`; a non-numeric, zero or negative value falls back to 50.", "schema": { - "type": "string", - "format": "uuid" + "type": "integer", + "default": 50, + "minimum": 1, + "maximum": 1000000 }, - "description": "Organization UUID." + "example": 50 }, { - "name": "application_id", - "in": "path", - "required": true, + "name": "offset", + "in": "query", + "required": false, + "description": "Number of events to skip. Defaults to 0. Prefer following the `next` URL from the response.", "schema": { - "type": "string", - "format": "uuid" + "type": "integer", + "default": 0, + "minimum": 0, + "maximum": 1000000 }, - "description": "Application UUID." + "example": 0 }, { - "name": "case_uuid", - "in": "path", - "required": true, + "name": "event_category", + "in": "query", + "required": false, + "description": "Only events in these categories. Repeat the parameter for several values (`?event_category=payment&event_category=dispute`); an event matching any of them is returned.", + "style": "form", + "explode": true, "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/print/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "200": { - "description": "Presigned PDF URL.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - } - } - } + "type": "array", + "items": { + "type": "string", + "enum": [ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom" + ] } - } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/": { - "get": { - "operationId": "list_case_notes", - "parameters": [ + }, + "example": [ + "payment", + "dispute" + ] + }, { - "in": "path", - "name": "organization_id", + "name": "action_type", + "in": "query", + "required": false, + "description": "Only events with these action types (exact, case-sensitive). Repeat for several values.", + "style": "form", + "explode": true, "schema": { - "type": "string", - "format": "uuid" + "type": "array", + "items": { + "type": "string" + } }, - "required": true + "example": [ + "deposit_completed", + "chargeback_opened" + ] }, { - "in": "path", - "name": "application_id", + "name": "status", + "in": "query", + "required": false, + "description": "Only events with these statuses. Repeat for several values.", + "style": "form", + "explode": true, "schema": { - "type": "string", - "format": "uuid" + "type": "array", + "items": { + "type": "string", + "enum": [ + "ACCEPTED", + "APPROVED", + "IN_REVIEW", + "DECLINED" + ] + } }, - "required": true + "example": [ + "IN_REVIEW", + "DECLINED" + ] }, { - "in": "path", - "name": "case_uuid", + "name": "retention_class", + "in": "query", + "required": false, + "description": "Only events in these retention classes. Repeat for several values.", + "style": "form", + "explode": true, "schema": { - "type": "string", - "format": "uuid" + "type": "array", + "items": { + "type": "string", + "enum": [ + "low_value", + "standard", + "risk_relevant", + "monetary_or_compliance" + ] + } }, - "required": true + "example": [ + "risk_relevant" + ] }, { - "name": "ordering", - "required": false, + "name": "subject_vendor_data", "in": "query", - "description": "Which field to use when ordering the results.", + "required": false, + "description": "Exact match on the subject's `vendor_data`. The same id can name an individual and a company; add `subject_entity_type` to pick one, or use `GET /v3/events/subjects/{vendor_data}/`.", "schema": { "type": "string" - } + }, + "example": "user-42" }, { - "name": "limit", - "required": false, + "name": "subject_entity_type", "in": "query", - "description": "Number of results to return per page.", + "required": false, + "description": "`company` returns only events of company subjects; any other non-empty value (for example `individual`) returns only events of individuals. Omit it to return both.", "schema": { - "type": "integer" - } + "type": "string", + "enum": [ + "individual", + "company" + ] + }, + "example": "individual" }, { - "name": "offset", - "required": false, + "name": "business_vendor_data", "in": "query", - "description": "The initial index from which to return the results.", + "required": false, + "description": "Exact match on the `business.vendor_data` sent with the event.", "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseCollaborationPaginatedCaseNoteList" - } - } + "type": "string" }, - "description": "" - }, - "403": { - "description": "The caller lacks the required cases permission." + "example": "merchant-7" }, - "404": { - "description": "The case or note is unavailable for this caller." - } - }, - "tags": [ - "Case Management" - ], - "security": [ { - "ApiKeyAuth": [] - } - ] - }, - "post": { - "operationId": "create_case_note", - "parameters": [ - { - "in": "path", - "name": "organization_id", + "name": "session_id", + "in": "query", + "required": false, + "description": "Exact match on `session.session_id`.", "schema": { - "type": "string", - "format": "uuid" + "type": "string" }, - "required": true + "example": "web-session-abc" }, { - "in": "path", - "name": "application_id", + "name": "device_fingerprint", + "in": "query", + "required": false, + "description": "Exact match on `device_context.fingerprint`.", "schema": { - "type": "string", - "format": "uuid" + "type": "string" }, - "required": true + "example": "fp-a8c1" }, { - "in": "path", - "name": "case_uuid", + "name": "ip_address", + "in": "query", + "required": false, + "description": "Exact match on the event's IP address.", "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CaseCollaborationCaseNoteCreateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseCollaborationCaseNoteCreateRequest" - } + "type": "string" }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/CaseCollaborationCaseNoteCreateRequest" - } - } + "example": "203.0.113.42" }, - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseCollaborationCaseNote" - } - } + { + "name": "game_id", + "in": "query", + "required": false, + "description": "Exact match on the promoted `game_id`.", + "schema": { + "type": "string" }, - "description": "" + "example": "poker-holdem-01" }, - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseCollaborationCaseNote" - } - } + { + "name": "round_id", + "in": "query", + "required": false, + "description": "Exact match on the promoted `round_id`.", + "schema": { + "type": "string" }, - "description": "" + "example": "round-88231" }, - "403": { - "description": "The caller lacks the required cases permission." + { + "name": "affiliate_code", + "in": "query", + "required": false, + "description": "Exact match on the promoted `affiliate_code`.", + "schema": { + "type": "string" + }, + "example": "aff-summer" }, - "404": { - "description": "The case or note is unavailable for this caller." + { + "name": "payment_reference_id", + "in": "query", + "required": false, + "description": "Exact match on the payment reference (from `transaction_context.payment_reference_id` or `payload.payment_reference_id`).", + "schema": { + "type": "string" + }, + "example": "dep_735" }, - "409": { - "description": "This client reference was already used for another note or for a note that has been deleted." - } - }, - "tags": [ - "Case Management" - ], - "security": [ { - "ApiKeyAuth": [] - } - ], - "description": "Add a case-scoped note. An optional client_reference makes an unchanged retry return the same note without duplicate activity or notifications. reply_to must identify a live note in this case. The response includes a bounded parent excerpt without copying that excerpt into the saved reply." - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/{note_uuid}/": { - "patch": { - "operationId": "root_partial_update", - "parameters": [ + "name": "counterparty_vendor_data", + "in": "query", + "required": false, + "description": "Exact match on the promoted `counterparty_vendor_data`.", + "schema": { + "type": "string" + }, + "example": "user-77" + }, { - "in": "path", - "name": "organization_id", + "name": "matched_only", + "in": "query", + "required": false, + "description": "`true` (also `1` or `yes`, case-insensitive) returns only events at least one rule matched (`rules_matched_count` above 0). Any other value is ignored.", "schema": { - "type": "string", - "format": "uuid" + "type": "boolean", + "default": false }, - "required": true + "example": true }, { - "in": "path", - "name": "application_id", + "name": "occurred_from", + "in": "query", + "required": false, + "description": "Only events with `occurred_at` at or after this instant (inclusive). Send an ISO 8601 value: `2026-10-01T00:00:00Z`, an explicit offset with the `+` percent-encoded (`2026-10-01T02:00:00%2B02:00`; an unencoded `+` is decoded as a space and the value no longer parses), or a date alone (`2026-10-01`), which means 00:00:00 UTC of that day. A value without an offset is read as UTC. Values in any other format are not accepted.", "schema": { "type": "string", - "format": "uuid" + "format": "date-time" }, - "required": true + "example": "2026-10-01T00:00:00Z" }, { - "in": "path", - "name": "case_uuid", + "name": "occurred_to", + "in": "query", + "required": false, + "description": "Only events with `occurred_at` at or before this instant (inclusive). A date alone means the start of that day, so `occurred_to=2026-10-15` excludes the rest of 15 October; send `2026-10-15T23:59:59Z` to include it. Send an ISO 8601 value: `2026-10-01T00:00:00Z`, an explicit offset with the `+` percent-encoded (`2026-10-01T02:00:00%2B02:00`; an unencoded `+` is decoded as a space and the value no longer parses), or a date alone (`2026-10-01`), which means 00:00:00 UTC of that day. A value without an offset is read as UTC. Values in any other format are not accepted.", "schema": { "type": "string", - "format": "uuid" + "format": "date-time" }, - "required": true + "example": "2026-10-17T23:59:59Z" }, { - "in": "path", - "name": "note_uuid", + "name": "search", + "in": "query", + "required": false, + "description": "Case-insensitive substring match on `event_id`, `action_type`, `subject_vendor_data` or `counterparty_vendor_data`.", "schema": { - "type": "string", - "format": "uuid" + "type": "string" }, - "required": true + "example": "chargeback" } ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CaseCollaborationPatchedCaseNoteUpdateRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseCollaborationPatchedCaseNoteUpdateRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/CaseCollaborationPatchedCaseNoteUpdateRequest" - } - } + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/events/?event_category=payment&event_category=dispute&matched_only=true&occurred_from=2026-10-01T00:00:00Z&limit=50' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nurl = 'https://verification.didit.me/v3/events/'\nparams = {\n 'event_category': ['payment', 'dispute'],\n 'matched_only': 'true',\n 'occurred_from': '2026-10-01T00:00:00Z',\n 'limit': 50,\n}\nwhile url:\n resp = requests.get(\n url,\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n params=params,\n timeout=10,\n )\n resp.raise_for_status()\n page = resp.json()\n for event in page['results']:\n print(event['event_uuid'], event['action_type'], event['status'])\n url, params = page['next'], None" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const params = new URLSearchParams([\n ['event_category', 'payment'],\n ['event_category', 'dispute'],\n ['matched_only', 'true'],\n ['occurred_from', '2026-10-01T00:00:00Z'],\n ['limit', '50'],\n]);\nlet url = `https://verification.didit.me/v3/events/?${params}`;\nwhile (url) {\n const resp = await fetch(url, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n });\n if (!resp.ok) throw new Error(`Request failed: ${resp.status}`);\n const page = await resp.json();\n for (const event of page.results) console.log(event.event_uuid, event.action_type, event.status);\n url = page.next;\n}" } - }, + ], "responses": { "200": { + "description": "One page of event summaries.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CaseCollaborationCaseNote" + "$ref": "#/components/schemas/MonitoringEventPage" + }, + "examples": { + "Payment and dispute events": { + "value": { + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "event_uuid": "11111111-2222-3333-4444-555555555555", + "event_id": "chargeback-2026-10-16-000318", + "event_category": "dispute", + "action_type": "chargeback_opened", + "occurred_at": "2026-10-16T14:02:11Z", + "received_at": "2026-10-16T14:02:12.418230Z", + "status": "IN_REVIEW", + "score": 60, + "subject_vendor_data": "user-42", + "subject_entity_type": "individual", + "subject_uuid": "66666666-7777-8888-9999-000000000000", + "counterparty_vendor_data": null, + "session_id": null, + "device_fingerprint": null, + "ip_address": null, + "ip_country": null, + "amount": "250.000000000000000000", + "currency": "EUR", + "game_id": null, + "round_id": null, + "bonus_id": null, + "affiliate_code": null, + "payment_reference_id": "dep_735", + "retention_class": "monetary_or_compliance", + "rules_matched_count": 1, + "correlation_pending": false, + "is_droppable": false + }, + { + "event_uuid": "22222222-3333-4444-5555-666666666666", + "event_id": "login-2026-10-15-000912", + "event_category": "auth", + "action_type": "login", + "occurred_at": "2026-10-15T08:30:00.120000Z", + "received_at": "2026-10-15T08:30:00.734512Z", + "status": "ACCEPTED", + "score": 0, + "subject_vendor_data": "user-42", + "subject_entity_type": "individual", + "subject_uuid": "66666666-7777-8888-9999-000000000000", + "counterparty_vendor_data": null, + "session_id": "web-session-abc", + "device_fingerprint": "fp-a8c1", + "ip_address": "203.0.113.42", + "ip_country": "ES", + "amount": null, + "currency": null, + "game_id": null, + "round_id": null, + "bonus_id": null, + "affiliate_code": null, + "payment_reference_id": null, + "retention_class": "standard", + "rules_matched_count": 0, + "correlation_pending": false, + "is_droppable": true + } + ] + } + } } } - }, - "description": "" + } + }, + "400": { + "description": "`limit` or `offset` is larger than 1000000. The body is a JSON array with one message. A non-numeric or non-positive `limit` falls back to the default of 50 instead of failing.", + "content": { + "application/json": { + "examples": { + "limit too large": { + "summary": "limit above 1000000", + "value": [ + "limit must not exceed 1000000." + ] + }, + "offset too large": { + "summary": "offset above 1000000", + "value": [ + "offset must not exceed 1000000." + ] + } + } + } + } + }, + "401": { + "description": "The scoped API key has expired. Only an expired key gets `401`; a missing or invalid key gets `403`.", + "content": { + "application/json": { + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } }, "403": { - "description": "The caller lacks the required cases permission." + "description": "Missing or invalid `x-api-key` (this endpoint answers `403`, never `401`, for a missing or invalid key), a scoped key without `transactions: read`, a key limited to some workflows or to approved sessions (such a key never reaches transaction monitoring data), or a request from outside the key's IP allowlist.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without access": { + "value": { + "detail": "This API key has no read access to transactions." + } + }, + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." + } + }, + "IP not allowed": { + "value": { + "detail": "This API key can't be used from this IP address." + } + } + } + } + } }, - "404": { - "description": "The case or note is unavailable for this caller." + "429": { + "description": "Rate limit exceeded: `GET` requests share a budget of 600 requests per minute per API key. Wait `Retry-After` seconds, then retry.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + } + }, + "content": { + "application/json": { + "examples": { + "Throttled": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } + } + } + } } }, - "tags": [ - "Case Management" - ], "security": [ { "ApiKeyAuth": [] } - ], - "description": "Pin or unpin an existing note. Pinned notes appear first. Pin changes are recorded in case activity and do not change evidence or source decisions." + ] }, - "delete": { - "operationId": "delete_case_note", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - }, + "post": { + "summary": "Create monitoring event", + "description": "Send one non-monetary event (login, session, gameplay, bonus, affiliate, payment, dispute, security, responsible gambling or custom) from your backend. The event is validated against its category contract, stored, IP-enriched once per unique public address, and evaluated by your real-time event rules before the response returns; cross-subject and long-window rules run asynchronously (`correlation_pending: true`), and in a sandbox application no rules run. `event_id` is the idempotency key: the same `event_id` with the same `event_category`, `action_type`, `occurred_at`, `subject.vendor_data` and `payload` returns `200` with `idempotent_replay: true` and the stored outcome (other envelope fields are not compared), while different values return `409`. Event ingestion is opt-in per application: until it is switched on this endpoint returns `403` with `code: event_monitoring_disabled` and stores nothing. A scoped API key needs `transactions: write`. See [Event ingestion](https://docs.didit.me/transaction-monitoring/events) and [Rules on events](https://docs.didit.me/transaction-monitoring/event-rules); to send events from a browser or app, use [SDK event submission](https://docs.didit.me/transaction-monitoring/sdk-event-submission).", + "operationId": "create_monitoring_event", + "x-didit-sdk": { + "resource": "monitoring_events", + "method": "create", + "auth": "api_key", + "server": true + }, + "tags": [ + "Monitoring Events" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MonitoringEventCreateRequest" + }, + "examples": { + "Login": { + "value": { + "event_id": "login-2026-10-17-000001", + "event_category": "auth", + "action_type": "login", + "occurred_at": "2026-10-17T09:15:02.481Z", + "time_zone": "Europe/Madrid", + "subject": { + "vendor_data": "user-42", + "entity_type": "individual" + }, + "session": { + "session_id": "web-session-abc", + "sequence": 142 + }, + "device_context": { + "fingerprint": "fp-a8c1", + "network_context": { + "ip_address": "203.0.113.42" + } + }, + "payload": { + "method": "password" + }, + "custom_values": { + "vip_tier": "gold" + } + } + }, + "Deposit completed": { + "value": { + "event_id": "deposit-2026-10-17-000002", + "event_category": "payment", + "action_type": "deposit_completed", + "occurred_at": "2026-10-17T09:16:40Z", + "subject": { + "vendor_data": "user-42" + }, + "transaction_context": { + "amount": "100.00", + "currency": "EUR", + "direction": "inbound", + "payment_reference_id": "dep_736" + }, + "payload": { + "payment_method": "card", + "status": "completed" + } + } + }, + "Chargeback opened": { + "value": { + "event_id": "chargeback-2026-10-16-000318", + "event_category": "dispute", + "action_type": "chargeback_opened", + "occurred_at": "2026-10-16T14:02:11Z", + "subject": { + "vendor_data": "user-42" + }, + "payload": { + "payment_reference_id": "dep_735", + "chargeback_id": "cb_5521", + "amount": "250.00", + "currency": "EUR", + "reason_code": "fraud" + } + } + } + } + } + } + }, + "x-codeSamples": [ { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/events/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"event_id\": \"login-2026-10-17-000001\",\n \"event_category\": \"auth\",\n \"action_type\": \"login\",\n \"occurred_at\": \"2026-10-17T09:15:02.481Z\",\n \"subject\": {\"vendor_data\": \"user-42\", \"entity_type\": \"individual\"},\n \"session\": {\"session_id\": \"web-session-abc\", \"sequence\": 142},\n \"device_context\": {\n \"fingerprint\": \"fp-a8c1\",\n \"network_context\": {\"ip_address\": \"203.0.113.42\"}\n },\n \"payload\": {\"method\": \"password\"}\n }'" }, { - "in": "path", - "name": "case_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/events/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={\n 'event_id': 'login-2026-10-17-000001',\n 'event_category': 'auth',\n 'action_type': 'login',\n 'occurred_at': '2026-10-17T09:15:02.481Z',\n 'subject': {'vendor_data': 'user-42', 'entity_type': 'individual'},\n 'session': {'session_id': 'web-session-abc', 'sequence': 142},\n 'device_context': {\n 'fingerprint': 'fp-a8c1',\n 'network_context': {'ip_address': '203.0.113.42'},\n },\n 'payload': {'method': 'password'},\n },\n timeout=10,\n)\nresp.raise_for_status()\nresult = resp.json()\nprint(result['event_uuid'], result['status'], result['idempotent_replay'])" }, { - "in": "path", - "name": "note_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/events/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n event_id: 'login-2026-10-17-000001',\n event_category: 'auth',\n action_type: 'login',\n occurred_at: '2026-10-17T09:15:02.481Z',\n subject: { vendor_data: 'user-42', entity_type: 'individual' },\n session: { session_id: 'web-session-abc', sequence: 142 },\n device_context: {\n fingerprint: 'fp-a8c1',\n network_context: { ip_address: '203.0.113.42' },\n },\n payload: { method: 'password' },\n }),\n});\nif (!resp.ok) throw new Error(await resp.text());\nconst result = await resp.json();\nconsole.log(result.event_uuid, result.status, result.idempotent_replay);" } ], "responses": { - "204": { - "description": "The note is permanently deleted. Replies remain, without the deleted parent excerpt. Attachments are deleted unless another note still uses them." + "201": { + "description": "The event was stored and evaluated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MonitoringEventIngestResult" + }, + "examples": { + "Stored, no rule matched": { + "value": { + "event_uuid": "33333333-4444-5555-6666-777777777777", + "event_id": "login-2026-10-17-000001", + "status": "ACCEPTED", + "score": 0, + "rules_evaluated_count": 6, + "rules_matched_count": 0, + "correlation_pending": false, + "retention_class": "standard", + "expires_at": "2026-11-16T09:15:02.481000Z", + "idempotent_replay": false, + "rule_matches": [] + } + }, + "Rule matched, async rule pending": { + "value": { + "event_uuid": "33333333-4444-5555-6666-777777777777", + "event_id": "login-2026-10-17-000001", + "status": "IN_REVIEW", + "score": 40, + "rules_evaluated_count": 6, + "rules_matched_count": 1, + "correlation_pending": true, + "retention_class": "risk_relevant", + "expires_at": "2027-01-15T09:15:02.481000Z", + "idempotent_replay": false, + "rule_matches": [ + { + "rule_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "library_key": null, + "mode": "ACTIVE", + "is_async": false + } + ] + } + } + } + } + } + }, + "200": { + "description": "Idempotent replay: an event with this `event_id` and the same content is already stored. Nothing was stored or evaluated again; the body describes the stored event.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MonitoringEventIngestResult" + }, + "examples": { + "Replay": { + "value": { + "event_uuid": "33333333-4444-5555-6666-777777777777", + "event_id": "login-2026-10-17-000001", + "status": "ACCEPTED", + "score": 0, + "rules_evaluated_count": 6, + "rules_matched_count": 0, + "correlation_pending": false, + "retention_class": "standard", + "expires_at": "2026-11-16T09:15:02.481000Z", + "idempotent_replay": true, + "rule_matches": [] + } + } + } + } + } + }, + "400": { + "description": "Validation failed. The body is keyed by field; nested fields are nested objects. Nothing is stored.", + "content": { + "application/json": { + "examples": { + "Missing fields": { + "value": { + "event_id": [ + "This field is required." + ], + "subject": [ + "This field is required." + ] + } + }, + "Unknown category": { + "value": { + "event_category": [ + "\"login\" is not a valid choice." + ] + } + }, + "occurred_at too old": { + "value": { + "occurred_at": [ + "occurred_at is more than 7 days in the past. Use the batch import lane for backfills." + ] + } + }, + "occurred_at in the future": { + "value": { + "occurred_at": [ + "occurred_at is more than 5 minutes in the future." + ] + } + }, + "Missing subject vendor_data": { + "value": { + "subject": { + "vendor_data": [ + "This field is required." + ] + } + } + }, + "Missing required payload keys": { + "value": { + "payload": [ + "payment.deposit_completed requires: payment_method, status." + ] + } + }, + "Missing one-of payload key": { + "value": { + "payload": [ + "dispute.chargeback_opened requires one of: dispute_id, chargeback_id." + ] + } + }, + "Security change without changed_field": { + "value": { + "payload": [ + "security.*_changed requires 'changed_field'." + ] + } + }, + "Payload too large": { + "value": { + "payload": [ + "payload exceeds 16384 bytes. Summarise telemetry client-side." + ] + } + }, + "Too many custom_values": { + "value": { + "custom_values": [ + "custom_values accepts at most 100 keys." + ] + } + }, + "Nested custom value": { + "value": { + "custom_values": [ + "custom_values['limits'] must be a scalar - rule conditions cannot address nested values." + ] + } + } + } + } + } + }, + "401": { + "description": "The scoped API key has expired. Only an expired key gets `401`; a missing or invalid key gets `403`.", + "content": { + "application/json": { + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } }, "403": { - "description": "The caller lacks the required cases permission." + "description": "Missing or invalid `x-api-key` (this endpoint answers `403`, never `401`, for a missing or invalid key), a scoped key without `transactions: write`, a key limited to some workflows or to approved sessions (such a key never reaches transaction monitoring data), or a request from outside the key's IP allowlist. Also returned with `code: event_monitoring_disabled` while event ingestion is switched off for the application; nothing is stored in that case, and the check runs before the body is validated.", + "content": { + "application/json": { + "examples": { + "Event monitoring disabled": { + "value": { + "detail": "Event monitoring is not enabled for this application. Turn on event ingestion in the transaction monitoring settings before submitting events.", + "code": "event_monitoring_disabled" + } + }, + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without access": { + "value": { + "detail": "This API key has no write access to transactions." + } + }, + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." + } + }, + "IP not allowed": { + "value": { + "detail": "This API key can't be used from this IP address." + } + } + } + } + } }, - "404": { - "description": "The case or note is unavailable for this caller." + "409": { + "description": "`event_id` was already used for an event with different content (category, action type, `occurred_at`, subject `vendor_data` or payload). Send the event under a new `event_id`.", + "content": { + "application/json": { + "examples": { + "event_id reused": { + "value": { + "event_id": "event_id 'login-2026-10-17-000001' already exists with a different payload." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded: write requests (`POST`, `PATCH`, `DELETE`) share a budget of 300 requests per minute per API key. Wait `Retry-After` seconds, then retry.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + } + }, + "content": { + "application/json": { + "examples": { + "Throttled": { + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + } + } + } } }, - "tags": [ - "Case Management" - ], "security": [ { "ApiKeyAuth": [] @@ -40567,62 +60909,71 @@ ] } }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/presign/": { + "/v3/events/batch/": { "post": { - "summary": "Upload note attachment", - "description": "Upload a file (JPEG, PNG, WebP, PDF, plain text, or CSV; max 5 MB) to attach to a note. Returns the s3_key to pass in the note's attachments array.", - "operationId": "upload_case_note_attachment", + "summary": "Create monitoring events in batch", + "description": "Submit 1 to 500 events in one request, each in the same envelope as `POST /v3/events/`. Every item is validated, stored and evaluated on its own and gets its own outcome (`accepted`, `replayed` or `rejected`), so one malformed event never rolls back the others: the response is `201` when no item was rejected and `207` when at least one was, and both mean the request was processed. Send `{\"events\": [...]}`; a bare JSON array of events is also accepted. Event ingestion must be switched on for the application, otherwise the whole request returns `403` with `code: event_monitoring_disabled` and nothing is stored. A scoped API key needs `transactions: write`. See [Batches](https://docs.didit.me/transaction-monitoring/events#batches).", + "operationId": "create_monitoring_events_batch", + "x-didit-sdk": { + "resource": "monitoring_events", + "method": "create_batch", + "auth": "api_key", + "server": true + }, "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." - } + "Monitoring Events" ], "requestBody": { "required": true, "content": { - "multipart/form-data": { + "application/json": { "schema": { "type": "object", "required": [ - "file" - ], - "properties": { - "file": { - "type": "string", - "format": "binary" + "events" + ], + "properties": { + "events": { + "type": "array", + "minItems": 1, + "maxItems": 500, + "description": "1 to 500 events. Each item follows `MonitoringEventCreateRequest`, including the 7-day past and 5-minute future window on `occurred_at`.", + "items": { + "$ref": "#/components/schemas/MonitoringEventCreateRequest" + } } } + }, + "example": { + "events": [ + { + "event_id": "login-2026-10-17-000001", + "event_category": "auth", + "action_type": "login", + "occurred_at": "2026-10-17T09:15:02.481Z", + "subject": { + "vendor_data": "user-42" + } + }, + { + "event_id": "deposit-2026-10-17-000002", + "event_category": "payment", + "action_type": "deposit_completed", + "occurred_at": "2026-10-17T09:16:40Z", + "subject": { + "vendor_data": "user-42" + }, + "transaction_context": { + "amount": "100.00", + "currency": "EUR", + "payment_reference_id": "dep_736" + }, + "payload": { + "payment_method": "card", + "status": "completed" + } + } + ] } } } @@ -40631,242 +60982,541 @@ { "lang": "curl", "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/notes/presign/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'file=@evidence.pdf'" + "source": "curl -X POST 'https://verification.didit.me/v3/events/batch/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"events\": [\n {\"event_id\": \"login-2026-10-17-000001\", \"event_category\": \"auth\", \"action_type\": \"login\",\n \"occurred_at\": \"2026-10-17T09:15:02.481Z\", \"subject\": {\"vendor_data\": \"user-42\"}},\n {\"event_id\": \"deposit-2026-10-17-000002\", \"event_category\": \"payment\", \"action_type\": \"deposit_completed\",\n \"occurred_at\": \"2026-10-17T09:16:40.000Z\", \"subject\": {\"vendor_data\": \"user-42\"},\n \"transaction_context\": {\"amount\": \"100.00\", \"currency\": \"EUR\", \"payment_reference_id\": \"dep_736\"},\n \"payload\": {\"payment_method\": \"card\", \"status\": \"completed\"}}\n ]\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nevents = [\n {\n 'event_id': 'login-2026-10-17-000001',\n 'event_category': 'auth',\n 'action_type': 'login',\n 'occurred_at': '2026-10-17T09:15:02.481Z',\n 'subject': {'vendor_data': 'user-42'},\n },\n {\n 'event_id': 'deposit-2026-10-17-000002',\n 'event_category': 'payment',\n 'action_type': 'deposit_completed',\n 'occurred_at': '2026-10-17T09:16:40.000Z',\n 'subject': {'vendor_data': 'user-42'},\n 'transaction_context': {'amount': '100.00', 'currency': 'EUR', 'payment_reference_id': 'dep_736'},\n 'payload': {'payment_method': 'card', 'status': 'completed'},\n },\n]\nresp = requests.post(\n 'https://verification.didit.me/v3/events/batch/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={'events': events},\n timeout=10,\n)\nresp.raise_for_status() # 201 and 207 are both success\nbatch = resp.json()\nfor item in batch['results']:\n if item['outcome'] == 'rejected':\n print('fix and resend', events[item['index']]['event_id'], item['errors'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const events = [\n {\n event_id: 'login-2026-10-17-000001',\n event_category: 'auth',\n action_type: 'login',\n occurred_at: '2026-10-17T09:15:02.481Z',\n subject: { vendor_data: 'user-42' },\n },\n {\n event_id: 'deposit-2026-10-17-000002',\n event_category: 'payment',\n action_type: 'deposit_completed',\n occurred_at: '2026-10-17T09:16:40.000Z',\n subject: { vendor_data: 'user-42' },\n transaction_context: { amount: '100.00', currency: 'EUR', payment_reference_id: 'dep_736' },\n payload: { payment_method: 'card', status: 'completed' },\n },\n];\nconst resp = await fetch('https://verification.didit.me/v3/events/batch/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ events }),\n});\nif (!resp.ok) throw new Error(await resp.text()); // 201 and 207 are both ok\nconst batch = await resp.json();\nfor (const item of batch.results) {\n if (item.outcome === 'rejected') console.log('fix and resend', events[item.index].event_id, item.errors);\n}" } ], "responses": { "201": { - "description": "File uploaded.", + "description": "No item was rejected: every item is `accepted` or `replayed`.", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "accepted_count", + "replayed_count", + "rejected_count", + "results" + ], "properties": { - "s3_key": { - "type": "string" + "accepted_count": { + "type": "integer", + "minimum": 0, + "description": "Items stored and evaluated." }, - "url": { - "type": "string", - "format": "uri" + "replayed_count": { + "type": "integer", + "minimum": 0, + "description": "Items already stored with the same content." }, - "filename": { - "type": "string" + "rejected_count": { + "type": "integer", + "minimum": 0, + "description": "Items not stored." }, - "file_size": { - "type": "integer" + "results": { + "type": "array", + "description": "One entry per item, in request order.", + "items": { + "type": "object", + "description": "Outcome of one item. Accepted and replayed items also carry every field of `MonitoringEventIngestResult`; rejected items carry `event_id` and `errors` instead.", + "required": [ + "index", + "outcome" + ], + "properties": { + "index": { + "type": "integer", + "minimum": 0, + "description": "Position of the item in your `events` array." + }, + "outcome": { + "type": "string", + "enum": [ + "accepted", + "replayed", + "rejected" + ], + "description": "`accepted`: stored and evaluated. `replayed`: already stored with the same content. `rejected`: not stored; see `errors`." + }, + "event_id": { + "type": "string", + "nullable": true, + "description": "Your `event_id`. On items rejected by validation it is read from the item's snake_case `event_id` key, so it is null when the item omitted it or sent the `eventId` alias." + }, + "errors": { + "type": "object", + "additionalProperties": true, + "description": "Rejected items only. Validation errors keyed by field, in the same shape as the `400` of `POST /v3/events/`; a reused `event_id` with different content is reported as a string under `errors.event_id`." + }, + "event_uuid": { + "type": "string", + "format": "uuid", + "description": "Accepted and replayed items only." + }, + "status": { + "type": "string", + "enum": [ + "ACCEPTED", + "APPROVED", + "IN_REVIEW", + "DECLINED" + ], + "description": "Accepted and replayed items only." + }, + "score": { + "type": "integer", + "description": "Accepted and replayed items only." + }, + "rules_evaluated_count": { + "type": "integer", + "description": "Accepted and replayed items only." + }, + "rules_matched_count": { + "type": "integer", + "description": "Accepted and replayed items only." + }, + "correlation_pending": { + "type": "boolean", + "description": "Accepted and replayed items only." + }, + "retention_class": { + "type": "string", + "enum": [ + "low_value", + "standard", + "risk_relevant", + "monetary_or_compliance" + ], + "description": "Accepted and replayed items only." + }, + "expires_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Accepted and replayed items only." + }, + "idempotent_replay": { + "type": "boolean", + "description": "Accepted and replayed items only." + }, + "rule_matches": { + "type": "array", + "description": "Accepted and replayed items only.", + "items": { + "type": "object", + "required": [ + "rule_id", + "library_key", + "mode", + "is_async" + ], + "properties": { + "rule_id": { + "type": "string", + "format": "uuid", + "description": "UUID of the rule." + }, + "library_key": { + "type": "string", + "nullable": true, + "description": "Library key when the rule was installed from the rules library, otherwise null." + }, + "mode": { + "type": "string", + "enum": [ + "ACTIVE", + "TEST" + ], + "description": "`TEST` rules record the match but apply no actions." + }, + "is_async": { + "type": "boolean", + "description": "`true` when the match came from the asynchronous pass." + } + } + } + } + } + } + } + } + }, + "examples": { + "All landed": { + "value": { + "accepted_count": 1, + "replayed_count": 1, + "rejected_count": 0, + "results": [ + { + "index": 0, + "outcome": "accepted", + "event_uuid": "33333333-4444-5555-6666-777777777777", + "event_id": "login-2026-10-17-000001", + "status": "ACCEPTED", + "score": 0, + "rules_evaluated_count": 6, + "rules_matched_count": 0, + "correlation_pending": false, + "retention_class": "standard", + "expires_at": "2026-11-16T09:15:02.481000Z", + "idempotent_replay": false, + "rule_matches": [] + }, + { + "index": 1, + "outcome": "replayed", + "event_uuid": "11111111-2222-3333-4444-555555555555", + "event_id": "deposit-2026-10-17-000002", + "status": "ACCEPTED", + "score": 0, + "rules_evaluated_count": 6, + "rules_matched_count": 0, + "correlation_pending": false, + "retention_class": "monetary_or_compliance", + "expires_at": null, + "idempotent_replay": true, + "rule_matches": [] + } + ] } } } } } }, - "400": { - "description": "No file, unsupported content type, or file exceeds 5 MB.", + "207": { + "description": "At least one item was rejected. The other items were stored; fix and resend only the rejected ones (resending an accepted item is safe, it comes back `replayed`).", "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "required": [ + "accepted_count", + "replayed_count", + "rejected_count", + "results" + ], + "properties": { + "accepted_count": { + "type": "integer", + "minimum": 0, + "description": "Items stored and evaluated." + }, + "replayed_count": { + "type": "integer", + "minimum": 0, + "description": "Items already stored with the same content." + }, + "rejected_count": { + "type": "integer", + "minimum": 0, + "description": "Items not stored." + }, + "results": { + "type": "array", + "description": "One entry per item, in request order.", + "items": { + "type": "object", + "description": "Outcome of one item. Accepted and replayed items also carry every field of `MonitoringEventIngestResult`; rejected items carry `event_id` and `errors` instead.", + "required": [ + "index", + "outcome" + ], + "properties": { + "index": { + "type": "integer", + "minimum": 0, + "description": "Position of the item in your `events` array." + }, + "outcome": { + "type": "string", + "enum": [ + "accepted", + "replayed", + "rejected" + ], + "description": "`accepted`: stored and evaluated. `replayed`: already stored with the same content. `rejected`: not stored; see `errors`." + }, + "event_id": { + "type": "string", + "nullable": true, + "description": "Your `event_id`. On items rejected by validation it is read from the item's snake_case `event_id` key, so it is null when the item omitted it or sent the `eventId` alias." + }, + "errors": { + "type": "object", + "additionalProperties": true, + "description": "Rejected items only. Validation errors keyed by field, in the same shape as the `400` of `POST /v3/events/`; a reused `event_id` with different content is reported as a string under `errors.event_id`." + }, + "event_uuid": { + "type": "string", + "format": "uuid", + "description": "Accepted and replayed items only." + }, + "status": { + "type": "string", + "enum": [ + "ACCEPTED", + "APPROVED", + "IN_REVIEW", + "DECLINED" + ], + "description": "Accepted and replayed items only." + }, + "score": { + "type": "integer", + "description": "Accepted and replayed items only." + }, + "rules_evaluated_count": { + "type": "integer", + "description": "Accepted and replayed items only." + }, + "rules_matched_count": { + "type": "integer", + "description": "Accepted and replayed items only." + }, + "correlation_pending": { + "type": "boolean", + "description": "Accepted and replayed items only." + }, + "retention_class": { + "type": "string", + "enum": [ + "low_value", + "standard", + "risk_relevant", + "monetary_or_compliance" + ], + "description": "Accepted and replayed items only." + }, + "expires_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Accepted and replayed items only." + }, + "idempotent_replay": { + "type": "boolean", + "description": "Accepted and replayed items only." + }, + "rule_matches": { + "type": "array", + "description": "Accepted and replayed items only.", + "items": { + "type": "object", + "required": [ + "rule_id", + "library_key", + "mode", + "is_async" + ], + "properties": { + "rule_id": { + "type": "string", + "format": "uuid", + "description": "UUID of the rule." + }, + "library_key": { + "type": "string", + "nullable": true, + "description": "Library key when the rule was installed from the rules library, otherwise null." + }, + "mode": { + "type": "string", + "enum": [ + "ACTIVE", + "TEST" + ], + "description": "`TEST` rules record the match but apply no actions." + }, + "is_async": { + "type": "boolean", + "description": "`true` when the match came from the asynchronous pass." + } + } + } + } + } + } + } + } + }, + "examples": { + "Partial success": { + "value": { + "accepted_count": 1, + "replayed_count": 0, + "rejected_count": 2, + "results": [ + { + "index": 0, + "outcome": "accepted", + "event_uuid": "33333333-4444-5555-6666-777777777777", + "event_id": "login-2026-10-17-000001", + "status": "ACCEPTED", + "score": 0, + "rules_evaluated_count": 6, + "rules_matched_count": 0, + "correlation_pending": false, + "retention_class": "standard", + "expires_at": "2026-11-16T09:15:02.481000Z", + "idempotent_replay": false, + "rule_matches": [] + }, + { + "index": 1, + "event_id": "deposit-2026-10-17-000002", + "outcome": "rejected", + "errors": { + "payload": [ + "payment.deposit_completed requires: payment_method, status." + ] + } + }, + { + "index": 2, + "event_id": "login-2026-10-16-000777", + "outcome": "rejected", + "errors": { + "event_id": "event_id 'login-2026-10-16-000777' already exists with a different payload." + } + } + ] + } + } } } } - } - } - }, - "get": { - "summary": "Get note attachment URL", - "description": "Mint a fresh presigned download URL for an attachment already stored on one of the case's notes.", - "operationId": "get_case_note_attachment_url", - "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." }, - { - "name": "s3_key", - "in": "query", - "schema": { - "type": "string" - }, - "description": "The attachment's s3_key, as returned by upload or listed on the note.", - "required": true - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/notes/presign/?s3_key=case-notes/c1a5e000-0000-4000-8000-000000000001/evidence.pdf' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "200": { - "description": "Presigned URL.", + "400": { + "description": "The batch itself is malformed: `events` is missing or not an array, is empty, or has more than 500 items. Nothing is stored; split large batches.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "s3_key": { - "type": "string" - }, - "url": { - "type": "string", - "format": "uri" - }, - "filename": { - "type": "string" + "examples": { + "Not a list": { + "value": { + "events": "Expected a list of events." + } + }, + "Empty": { + "value": { + "events": "At least one event is required." + } + }, + "Too many": { + "value": { + "events": "A batch accepts at most 500 events; received 612." } } } } } }, - "404": { - "description": "s3_key missing or does not belong to this case.", + "401": { + "description": "The scoped API key has expired. Only an expired key gets `401`; a missing or invalid key gets `403`.", "content": { "application/json": { - "schema": { - "type": "object" + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } } } } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/": { - "get": { - "summary": "List case links", - "description": "List sessions, business sessions, and transactions attached to the case.", - "operationId": "list_case_links", - "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "403": { + "description": "Missing or invalid `x-api-key` (this endpoint answers `403`, never `401`, for a missing or invalid key), a scoped key without `transactions: write`, a key limited to some workflows or to approved sessions (such a key never reaches transaction monitoring data), or a request from outside the key's IP allowlist. Also returned with `code: event_monitoring_disabled` while event ingestion is switched off for the application; nothing is stored in that case, and the check runs before the body is validated.", + "content": { + "application/json": { + "examples": { + "Event monitoring disabled": { + "value": { + "detail": "Event monitoring is not enabled for this application. Turn on event ingestion in the transaction monitoring settings before submitting events.", + "code": "event_monitoring_disabled" + } + }, + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without access": { + "value": { + "detail": "This API key has no write access to transactions." + } + }, + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." + } + }, + "IP not allowed": { + "value": { + "detail": "This API key can't be used from this IP address." + } + } + } + } + } }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" + "429": { + "description": "Rate limit exceeded: write requests (`POST`, `PATCH`, `DELETE`) share a budget of 300 requests per minute per API key. Wait `Retry-After` seconds, then retry.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + } }, - "description": "Case UUID." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/links/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "200": { - "description": "Case links.", "content": { "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaseLink" + "examples": { + "Throttled": { + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } } } } } } - } - }, - "post": { - "summary": "Add case link", - "description": "Attach a session, business session, or transaction to the case. Rejected with 400 if the entity belongs to a different subject than the case.", - "operationId": "create_case_link", - "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, + }, + "security": [ { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." + "ApiKeyAuth": [] } + ] + } + }, + "/v3/events/sdk-token/": { + "post": { + "summary": "Mint monitoring SDK token", + "description": "Mint a short-lived device credential bound to one subject, so your web or mobile app can send that user's events straight to Didit without an API key. The app sends the returned `monitoring_token` in the `X-Monitoring-Token` header to the device lane (`POST /v1/events/` and `POST /v1/events/batch/`), where the subject is always overwritten with this `vendor_data`, events outside `allowed_event_categories` are rejected, each submitted event spends one use, and a request whose `Origin` does not equal `bind_to_origin` is refused. Call it from your backend only; minting creates the credential and nothing else (no user is created). Event ingestion must be switched on for the application, otherwise this endpoint returns `403` with `code: event_monitoring_disabled` instead of minting a credential that could never write. A scoped API key needs `transactions: write`. See [SDK event submission](https://docs.didit.me/transaction-monitoring/sdk-event-submission).", + "operationId": "create_monitoring_event_sdk_token", + "x-didit-sdk": { + "resource": "monitoring_events", + "method": "create_sdk_token", + "auth": "api_key", + "server": true + }, + "tags": [ + "Monitoring Events" ], "requestBody": { "required": true, @@ -40874,24 +61524,74 @@ "application/json": { "schema": { "type": "object", + "description": "camelCase aliases are accepted: `vendorData`, `entityType`, `allowedEventCategories`, `ttlSeconds`, `maxUses`, `bindToOrigin`.", "required": [ - "entity_type", - "entity_uuid" + "vendor_data" ], "properties": { + "vendor_data": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Your id for the subject every event submitted with this credential belongs to." + }, "entity_type": { "type": "string", - "enum": [ - "session", - "business_session", - "transaction" - ] + "maxLength": 32, + "default": "individual", + "description": "`company` binds a company subject; any other value (default `individual`) binds an individual." }, - "entity_uuid": { + "allowed_event_categories": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom" + ] + }, + "description": "Categories the credential may submit. Omit it or send `[]` to allow every category." + }, + "ttl_seconds": { + "type": "integer", + "minimum": 60, + "maximum": 86400, + "default": 900, + "description": "Lifetime of the credential in seconds." + }, + "max_uses": { + "type": "integer", + "minimum": 1, + "nullable": true, + "default": 1000, + "description": "Number of events the credential may submit (a batch spends one use per event). Send `null` for no cap within the lifetime." + }, + "bind_to_origin": { "type": "string", - "format": "uuid" + "maxLength": 255, + "nullable": true, + "description": "When set, the browser `Origin` header of every device request must equal this value exactly; a request without `Origin` is refused." } } + }, + "example": { + "vendor_data": "user-42", + "allowed_event_categories": [ + "auth", + "session", + "gameplay" + ], + "ttl_seconds": 900, + "max_uses": 1000, + "bind_to_origin": "https://app.example.com" } } } @@ -40900,2445 +61600,2026 @@ { "lang": "curl", "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/links/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"entity_type\": \"transaction\", \"entity_uuid\": \"77000000-0000-4000-8000-000000000077\"}'" + "source": "curl -X POST 'https://verification.didit.me/v3/events/sdk-token/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"vendor_data\": \"user-42\",\n \"allowed_event_categories\": [\"auth\", \"session\", \"gameplay\"],\n \"ttl_seconds\": 900,\n \"max_uses\": 1000,\n \"bind_to_origin\": \"https://app.example.com\"\n }'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/events/sdk-token/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={\n 'vendor_data': 'user-42',\n 'allowed_event_categories': ['auth', 'session', 'gameplay'],\n 'ttl_seconds': 900,\n 'max_uses': 1000,\n 'bind_to_origin': 'https://app.example.com',\n },\n timeout=10,\n)\nresp.raise_for_status()\ncredential = resp.json()\n# Hand only monitoring_token to the app, never the API key.\nprint(credential['monitoring_token'], credential['expires_at'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/events/sdk-token/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n vendor_data: 'user-42',\n allowed_event_categories: ['auth', 'session', 'gameplay'],\n ttl_seconds: 900,\n max_uses: 1000,\n bind_to_origin: 'https://app.example.com',\n }),\n});\nif (!resp.ok) throw new Error(await resp.text());\nconst { monitoring_token, expires_at } = await resp.json();\n// Hand only monitoring_token to the app, never the API key.\nconsole.log(monitoring_token, expires_at);" } ], "responses": { "201": { - "description": "Link created.", + "description": "The credential was minted. Hand `monitoring_token` to the app and treat it as a secret until `expires_at`.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CaseLink" + "type": "object", + "required": [ + "monitoring_token", + "expires_at", + "allowed_event_categories", + "max_uses", + "bind_to_origin" + ], + "properties": { + "monitoring_token": { + "type": "string", + "description": "The device credential (a 43-character URL-safe token). Send it in the `X-Monitoring-Token` header." + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "When the credential stops being accepted." + }, + "allowed_event_categories": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom" + ] + }, + "description": "Categories the credential may submit; `[]` means every category." + }, + "max_uses": { + "type": "integer", + "nullable": true, + "description": "Event budget of the credential; null means no cap." + }, + "bind_to_origin": { + "type": "string", + "nullable": true, + "description": "Origin the credential is bound to, or null." + } + } + }, + "example": { + "monitoring_token": "q7ZtR2xKp9VbN4mWc8LdH1sYfJ3gT6uEaQ0oPiXzBkC", + "expires_at": "2026-10-17T09:30:02.481000Z", + "allowed_event_categories": [ + "auth", + "session", + "gameplay" + ], + "max_uses": 1000, + "bind_to_origin": "https://app.example.com" } } } }, "400": { - "description": "Entity belongs to a different profile than the case subject.", + "description": "Validation failed: missing `vendor_data`, `ttl_seconds` outside 60 to 86400, `max_uses` below 1, or an unknown category (keyed by its position in the array).", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" + "examples": { + "Missing vendor_data": { + "value": { + "vendor_data": [ + "This field is required." + ] + } + }, + "ttl_seconds too short": { + "value": { + "ttl_seconds": [ + "Ensure this value is greater than or equal to 60." + ] + } + }, + "Unknown category": { + "value": { + "allowed_event_categories": { + "1": [ + "\"login\" is not a valid choice." + ] + } } } } } } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/{link_uuid}/": { - "delete": { - "summary": "Remove case link", - "description": "Detach a linked session, business session, or transaction from the case.", - "operationId": "delete_case_link", - "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "401": { + "description": "The scoped API key has expired. Only an expired key gets `401`; a missing or invalid key gets `403`.", + "content": { + "application/json": { + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." + "403": { + "description": "Missing or invalid `x-api-key` (this endpoint answers `403`, never `401`, for a missing or invalid key), a scoped key without `transactions: write`, a key limited to some workflows or to approved sessions (such a key never reaches transaction monitoring data), or a request from outside the key's IP allowlist. Also returned with `code: event_monitoring_disabled` while event ingestion is switched off for the application; nothing is stored in that case, and the check runs before the body is validated.", + "content": { + "application/json": { + "examples": { + "Event monitoring disabled": { + "value": { + "detail": "Event monitoring is not enabled for this application. Turn on event ingestion in the transaction monitoring settings before submitting events.", + "code": "event_monitoring_disabled" + } + }, + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without access": { + "value": { + "detail": "This API key has no write access to transactions." + } + }, + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." + } + }, + "IP not allowed": { + "value": { + "detail": "This API key can't be used from this IP address." + } + } + } + } + } }, - { - "name": "link_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "11000000-0000-4000-8000-000000000004" + "429": { + "description": "Rate limit exceeded: write requests (`POST`, `PATCH`, `DELETE`) share a budget of 300 requests per minute per API key. Wait `Retry-After` seconds, then retry.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + } }, - "description": "Link UUID." + "content": { + "application/json": { + "examples": { + "Throttled": { + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + } + } + } } - ], - "x-codeSamples": [ + }, + "security": [ { - "lang": "curl", - "label": "curl", - "source": "curl -X DELETE 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/links/11000000-0000-4000-8000-000000000004/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "204": { - "description": "Link removed." + "ApiKeyAuth": [] } - } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/events/": { + "/v3/events/{event_uuid}/": { "get": { - "summary": "List case events", - "description": "Read-only audit timeline of the case (created, status changes, assignment, resolution, escalation, transfers, tag/priority/due-date changes, entity actions, and more).", - "operationId": "list_case_events", + "summary": "Get monitoring event", + "description": "Retrieve one monitoring event with everything the list returns plus the payload, custom values, device context, field provenance (`field_sources`), the promoted domain fields, `expires_at`, every matching rule run with its evidence, the IP enrichment and the tags rules attached. While `correlation_pending` is `true` an asynchronous rule still has to run; read the event again a few minutes later for the final `status`, `score` and `rule_runs`. Reads work whether or not event ingestion is switched on. A scoped API key needs `transactions: read`. See [Evidence](https://docs.didit.me/transaction-monitoring/event-rules#evidence) and [IP enrichment](https://docs.didit.me/transaction-monitoring/ip-enrichment).", + "operationId": "get_monitoring_event", + "x-didit-sdk": { + "resource": "monitoring_events", + "method": "retrieve", + "auth": "api_key", + "server": true + }, "tags": [ - "Cases" + "Monitoring Events" ], "parameters": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", + "name": "event_uuid", "in": "path", "required": true, + "description": "Didit identifier of the event (`event_uuid` from the create, batch, list or timeline response), not your own `event_id`.", "schema": { "type": "string", "format": "uuid" }, - "description": "Application UUID." - }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "default": 50, - "minimum": 1 - }, - "description": "Page size." - }, - { - "name": "offset", - "in": "query", - "schema": { - "type": "integer", - "default": 0, - "minimum": 0 - }, - "description": "Zero-based offset." + "example": "11111111-2222-3333-4444-555555555555" } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/events/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X GET 'https://verification.didit.me/v3/events/11111111-2222-3333-4444-555555555555/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nevent_uuid = '11111111-2222-3333-4444-555555555555'\nresp = requests.get(\n f'https://verification.didit.me/v3/events/{event_uuid}/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nevent = resp.json()\nprint(event['status'], event['score'])\nfor run in event['rule_runs']:\n print(run['rule_title'], run['score_delta'], run['action_summary'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const eventUuid = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(`https://verification.didit.me/v3/events/${eventUuid}/`, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Request failed: ${resp.status}`);\nconst event = await resp.json();\nconsole.log(event.status, event.score, event.rule_runs.length, 'rule runs');" } ], "responses": { "200": { - "description": "Paginated events, newest first.", + "description": "The event with its evidence.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "count": { - "type": "integer", - "description": "Number of matching rows. Exact up to 100, capped at 100 beyond that for performance. Rely on `next` being null to detect the last page." - }, - "next": { - "type": "string", - "nullable": true, - "description": "Absolute URL of the next page, or null." - }, - "previous": { - "type": "string", - "nullable": true, - "description": "Absolute URL of the previous page, or null." - }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaseEvent" - } + "$ref": "#/components/schemas/MonitoringEventDetail" + }, + "examples": { + "Login from a VPN, in review": { + "value": { + "event_uuid": "33333333-4444-5555-6666-777777777777", + "event_id": "login-2026-10-17-000001", + "event_category": "auth", + "action_type": "login", + "occurred_at": "2026-10-17T09:15:02.481000Z", + "received_at": "2026-10-17T09:15:02.903114Z", + "status": "IN_REVIEW", + "score": 40, + "subject_vendor_data": "user-42", + "subject_entity_type": "individual", + "subject_uuid": "66666666-7777-8888-9999-000000000000", + "counterparty_vendor_data": null, + "session_id": "web-session-abc", + "device_fingerprint": "fp-a8c1", + "ip_address": "203.0.113.42", + "ip_country": "ES", + "amount": null, + "currency": null, + "game_id": null, + "round_id": null, + "bonus_id": null, + "affiliate_code": null, + "payment_reference_id": null, + "retention_class": "risk_relevant", + "rules_matched_count": 1, + "correlation_pending": false, + "is_droppable": true, + "payload": { + "method": "password" + }, + "custom_values": { + "vip_tier": "gold", + "vpn_or_proxy_detected": true + }, + "device_context": { + "fingerprint": "fp-a8c1", + "network_context": { + "ip_address": "203.0.113.42", + "ip_country": "Spain", + "ip_country_code": "ES", + "ip_city": "Madrid", + "asn_number": 64500, + "asn_organization": "Example Networks", + "is_vpn_or_tor": true, + "is_data_center": false + } + }, + "field_sources": { + "subject.vendor_data": "customer", + "event_category": "customer", + "action_type": "customer", + "occurred_at": "customer", + "received_at": "server", + "payload.method": "customer", + "session.session_id": "customer", + "session.sequence": "customer", + "device_context.fingerprint": "customer", + "device_context.network_context.ip_address": "customer", + "device_context.network_context.ip_country": "server", + "device_context.network_context.ip_country_code": "server", + "device_context.network_context.ip_city": "server", + "device_context.network_context.asn_number": "server", + "device_context.network_context.asn_organization": "server", + "device_context.network_context.is_vpn_or_tor": "server", + "device_context.network_context.is_data_center": "server" + }, + "session_sequence": 142, + "table_id": null, + "hand_id": null, + "match_id": null, + "market_id": null, + "side": null, + "role": null, + "net_result_amount": null, + "payment_fingerprint": null, + "instrument_reference_hash": null, + "withdrawal_address_hash": null, + "dispute_id": null, + "chargeback_id": null, + "changed_field": null, + "identity_match_ref": null, + "schema_version": 1, + "expires_at": "2027-01-15T09:15:02.481000Z", + "rules_evaluated_count": 6, + "rule_runs": [ + { + "rule_uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "rule_title": "Login from VPN or Tor", + "library_key": null, + "matched": true, + "mode": "ACTIVE", + "is_async": false, + "severity": "MEDIUM", + "score_delta": 40, + "action_summary": "score +40; add_tag:VPN_LOGIN", + "matched_payload": [ + { + "field": "event.ip.is_vpn_or_tor", + "operator": "eq", + "expected_value": true, + "current_value": true, + "matched": true, + "group_index": null, + "group_logic": "AND", + "field_source": "server" + } + ], + "created_at": "2026-10-17T09:15:02.951207Z" + } + ], + "ip_enrichment": { + "ip_country_code": "ES", + "ip_city": "Madrid", + "asn_number": 64500, + "asn_organization": "Example Networks", + "is_vpn_or_tor": true, + "is_data_center": false, + "proxy_type": null, + "last_enriched_at": "2026-10-17T09:15:02.910442Z" + }, + "subject_profile": null, + "tags": [ + "VPN_LOGIN" + ] } } } } } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/checklist/": { - "get": { - "summary": "List case checklist", - "description": "List the case's checklist items, copied from the blueprint's content_config.checklist.items at creation, ordered.", - "operationId": "list_case_checklist", - "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/checklist/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "200": { - "description": "Checklist items.", + "401": { + "description": "The scoped API key has expired. Only an expired key gets `401`; a missing or invalid key gets `403`.", "content": { "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaseChecklistItem" + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } } } } } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/checklist/{item_uuid}/": { - "patch": { - "summary": "Toggle checklist item", - "description": "Mark a checklist item complete or incomplete. Resolution is blocked while any item is incomplete, unless the blueprint marks the checklist optional.", - "operationId": "toggle_case_checklist_item", - "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." }, - { - "name": "item_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c4ec0000-0000-4000-8000-000000000005" - }, - "description": "Checklist item UUID." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "is_completed" - ], - "properties": { - "is_completed": { - "type": "boolean" + "403": { + "description": "Missing or invalid `x-api-key` (this endpoint answers `403`, never `401`, for a missing or invalid key), a scoped key without `transactions: read`, a key limited to some workflows or to approved sessions (such a key never reaches transaction monitoring data), or a request from outside the key's IP allowlist.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without access": { + "value": { + "detail": "This API key has no read access to transactions." + } + }, + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." + } + }, + "IP not allowed": { + "value": { + "detail": "This API key can't be used from this IP address." + } } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/checklist/c4ec0000-0000-4000-8000-000000000005/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"is_completed\": true}'" - } - ], - "responses": { - "200": { - "description": "Updated checklist item.", + }, + "404": { + "description": "No event with this `event_uuid` exists in the application: it was never stored, it was deleted, or it belongs to another application. A path segment that is not a UUID does not match the route and also returns `404`.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseChecklistItem" + "examples": { + "Not found": { + "value": { + "detail": "No MonitoringEvent matches the given query." + } + } } } } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/aml/": { - "get": { - "summary": "List case AML hits", - "description": "List AML screening hits for the case's subject. Same shape as the hits returned by POST /v3/aml/.", - "operationId": "list_case_aml_hits", - "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/aml/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "200": { - "description": "AML hits.", + "429": { + "description": "Rate limit exceeded: `GET` requests share a budget of 600 requests per minute per API key. Wait `Retry-After` seconds, then retry.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + } + }, "content": { "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object" + "examples": { + "Throttled": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } } } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/cases/statistics/": { + "/v3/events/subjects/{vendor_data}/": { "get": { - "summary": "Case analytics", - "description": "Aggregate case analytics for the date range (defaults to the last 30 days; max 731 days). Charts, tables, totals, assignment events, and current queue counts use the same application-scoped filters. Filters use current case values. Dates apply to each metric event, so older cases assigned or resolved during the period remain included. Queue depth is independent of the date range. Invalid filters return 400; an unknown blueprint or assignee returns an empty result.", - "operationId": "get_case_analytics", + "summary": "Get subject event timeline", + "description": "List every monitoring event of one subject, newest `occurred_at` first. A `vendor_data` is unique only within an entity type, so the same id can name an individual and a company: `entity_type` picks which timeline you get, and it defaults to `individual`, so a company's events need `entity_type=company`. The filters of `GET /v3/events/` apply on top, and a `vendor_data` with no events returns an empty page rather than `404`. A scoped API key needs `transactions: read`. See [Reading events back](https://docs.didit.me/transaction-monitoring/events#reading-events-back).", + "operationId": "get_monitoring_event_subject_timeline", + "x-didit-sdk": { + "resource": "monitoring_events", + "method": "retrieve_subject_timeline", + "auth": "api_key", + "server": true + }, "tags": [ - "Cases" + "Monitoring Events" ], "parameters": [ { - "name": "organization_id", + "name": "vendor_data", "in": "path", "required": true, + "description": "Your id for the subject (`subject.vendor_data` of its events). URL-encode it; a value containing `/` cannot be addressed by this endpoint.", "schema": { "type": "string", - "format": "uuid" + "minLength": 1, + "maxLength": 255 }, - "description": "Organization UUID." + "example": "user-42" }, { - "name": "application_id", - "in": "path", - "required": true, + "name": "entity_type", + "in": "query", + "required": false, + "description": "`company` returns the company with this `vendor_data`; any other value, or none, returns the individual. `subject_entity_type` is accepted as an alias when `entity_type` is absent.", "schema": { "type": "string", - "format": "uuid" + "enum": [ + "individual", + "company" + ], + "default": "individual" }, - "description": "Application UUID." + "example": "individual" }, { - "name": "date_from", + "name": "limit", "in": "query", + "required": false, + "description": "Maximum number of events per page. Defaults to 50. Values above 1000000 return `400`; a non-numeric, zero or negative value falls back to 50.", "schema": { - "type": "string", - "format": "date" + "type": "integer", + "default": 50, + "minimum": 1, + "maximum": 1000000 }, - "description": "Range start (inclusive). Defaults to 30 days ago." + "example": 50 }, { - "name": "date_to", + "name": "offset", "in": "query", + "required": false, + "description": "Number of events to skip. Defaults to 0. Prefer following the `next` URL from the response.", "schema": { - "type": "string", - "format": "date" + "type": "integer", + "default": 0, + "minimum": 0, + "maximum": 1000000 }, - "description": "Range end (inclusive). Defaults to today." + "example": 0 }, { - "name": "blueprint", + "name": "event_category", "in": "query", "required": false, + "description": "Only events in these categories. Repeat the parameter for several values (`?event_category=payment&event_category=dispute`); an event matching any of them is returned.", + "style": "form", + "explode": true, "schema": { - "type": "string", - "format": "uuid" + "type": "array", + "items": { + "type": "string", + "enum": [ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom" + ] + } }, - "description": "Filter by the current blueprint UUID." + "example": [ + "payment", + "dispute" + ] }, { - "name": "assigned_to", + "name": "action_type", "in": "query", "required": false, + "description": "Only events with these action types (exact, case-sensitive). Repeat for several values.", + "style": "form", + "explode": true, "schema": { - "type": "string", - "format": "uuid" + "type": "array", + "items": { + "type": "string" + } }, - "description": "Filter by the current assigned reviewer user UUID." + "example": [ + "deposit_completed", + "chargeback_opened" + ] }, { - "name": "priority", + "name": "status", "in": "query", "required": false, + "description": "Only events with these statuses. Repeat for several values.", + "style": "form", + "explode": true, "schema": { - "type": "string", - "enum": [ - "LOW", - "MEDIUM", - "HIGH" - ] + "type": "array", + "items": { + "type": "string", + "enum": [ + "ACCEPTED", + "APPROVED", + "IN_REVIEW", + "DECLINED" + ] + } }, - "description": "Filter by current case priority." + "example": [ + "IN_REVIEW", + "DECLINED" + ] }, { - "name": "source", + "name": "retention_class", "in": "query", "required": false, + "description": "Only events in these retention classes. Repeat for several values.", + "style": "form", + "explode": true, "schema": { - "type": "string", - "enum": [ - "MANUAL", - "AML_SCREENING", - "TRANSACTION_RULE", - "WORKFLOW_BUILDER" - ] + "type": "array", + "items": { + "type": "string", + "enum": [ + "low_value", + "standard", + "risk_relevant", + "monetary_or_compliance" + ] + } }, - "description": "Filter by case creation source." + "example": [ + "risk_relevant" + ] }, { - "name": "status", + "name": "business_vendor_data", "in": "query", "required": false, + "description": "Exact match on the `business.vendor_data` sent with the event.", "schema": { - "type": "string", - "enum": [ - "OPEN", - "AWAITING_USER", - "RESOLVED" - ] + "type": "string" }, - "description": "Filter by current case status." - } - ], - "x-codeSamples": [ + "example": "merchant-7" + }, { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/statistics/?date_from=2026-06-01&date_to=2026-06-30' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "200": { - "description": "Analytics payload.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "series": { - "type": "array", - "items": { - "type": "object", - "properties": { - "date": { - "type": "string", - "format": "date" - }, - "created": { - "type": "integer" - }, - "reviewed": { - "type": "integer" - } - } - } - }, - "tables": { - "type": "object", - "properties": { - "officer": { - "type": "array", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string", - "description": "Dimension value: officer email, \"blueprint_name:uuid\", source, or status. \"unassigned\"/\"none\" for missing dimensions." - }, - "created": { - "type": "integer" - }, - "assigned": { - "type": "integer" - }, - "resolved": { - "type": "integer" - }, - "threat_rate": { - "type": "number", - "format": "float" - }, - "false_positive_rate": { - "type": "number", - "format": "float" - }, - "unresolved": { - "type": "integer" - }, - "reassignment_rate": { - "type": "number", - "format": "float" - }, - "escalation_rate": { - "type": "number", - "format": "float" - }, - "avg_assign_hours": { - "type": "number", - "format": "float" - }, - "avg_resolve_hours": { - "type": "number", - "format": "float" - }, - "avg_handling_hours": { - "type": "number", - "format": "float" - } - } - } - }, - "blueprint": { - "type": "array", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string", - "description": "Dimension value: officer email, \"blueprint_name:uuid\", source, or status. \"unassigned\"/\"none\" for missing dimensions." - }, - "created": { - "type": "integer" - }, - "assigned": { - "type": "integer" - }, - "resolved": { - "type": "integer" - }, - "threat_rate": { - "type": "number", - "format": "float" - }, - "false_positive_rate": { - "type": "number", - "format": "float" - }, - "unresolved": { - "type": "integer" - }, - "reassignment_rate": { - "type": "number", - "format": "float" - }, - "escalation_rate": { - "type": "number", - "format": "float" - }, - "avg_assign_hours": { - "type": "number", - "format": "float" - }, - "avg_resolve_hours": { - "type": "number", - "format": "float" - }, - "avg_handling_hours": { - "type": "number", - "format": "float" - } - } - } - }, - "source": { - "type": "array", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string", - "description": "Dimension value: officer email, \"blueprint_name:uuid\", source, or status. \"unassigned\"/\"none\" for missing dimensions." - }, - "created": { - "type": "integer" - }, - "assigned": { - "type": "integer" - }, - "resolved": { - "type": "integer" - }, - "threat_rate": { - "type": "number", - "format": "float" - }, - "false_positive_rate": { - "type": "number", - "format": "float" - }, - "unresolved": { - "type": "integer" - }, - "reassignment_rate": { - "type": "number", - "format": "float" - }, - "escalation_rate": { - "type": "number", - "format": "float" - }, - "avg_assign_hours": { - "type": "number", - "format": "float" - }, - "avg_resolve_hours": { - "type": "number", - "format": "float" - }, - "avg_handling_hours": { - "type": "number", - "format": "float" - } - } - } - }, - "status": { - "type": "array", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string", - "description": "Dimension value: officer email, \"blueprint_name:uuid\", source, or status. \"unassigned\"/\"none\" for missing dimensions." - }, - "created": { - "type": "integer" - }, - "assigned": { - "type": "integer" - }, - "resolved": { - "type": "integer" - }, - "threat_rate": { - "type": "number", - "format": "float" - }, - "false_positive_rate": { - "type": "number", - "format": "float" - }, - "unresolved": { - "type": "integer" - }, - "reassignment_rate": { - "type": "number", - "format": "float" - }, - "escalation_rate": { - "type": "number", - "format": "float" - }, - "avg_assign_hours": { - "type": "number", - "format": "float" - }, - "avg_resolve_hours": { - "type": "number", - "format": "float" - }, - "avg_handling_hours": { - "type": "number", - "format": "float" - } - } - } - } - } - }, - "totals": { - "type": "object", - "properties": { - "created": { - "type": "integer" - }, - "assigned": { - "type": "integer" - }, - "resolved": { - "type": "integer" - }, - "threat_rate": { - "type": "number", - "format": "float" - }, - "false_positive_rate": { - "type": "number", - "format": "float" - }, - "unresolved": { - "type": "integer" - }, - "reassignment_rate": { - "type": "number", - "format": "float" - }, - "escalation_rate": { - "type": "number", - "format": "float" - }, - "avg_assign_hours": { - "type": "number", - "format": "float" - }, - "avg_resolve_hours": { - "type": "number", - "format": "float" - }, - "avg_handling_hours": { - "type": "number", - "format": "float" - } - } - }, - "queue": { - "type": "object", - "description": "Live snapshot, not scoped to the date range.", - "properties": { - "open": { - "type": "integer" - }, - "overdue": { - "type": "integer" - } - } - } - } - } - } - } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/bulk/status/": { - "post": { - "summary": "Bulk update case status", - "description": "Apply the same status transition to several cases in one call: OPEN reopens, AWAITING_USER requests info, RESOLVED resolves (resolution required). Each case is evaluated independently; gate failures (incomplete checklist, missing required note) are reported per case without aborting the batch.", - "operationId": "bulk_update_case_status", - "tags": [ - "Cases" - ], - "parameters": [ + "name": "session_id", + "in": "query", + "required": false, + "description": "Exact match on `session.session_id`.", + "schema": { + "type": "string" + }, + "example": "web-session-abc" + }, + { + "name": "device_fingerprint", + "in": "query", + "required": false, + "description": "Exact match on `device_context.fingerprint`.", + "schema": { + "type": "string" + }, + "example": "fp-a8c1" + }, + { + "name": "ip_address", + "in": "query", + "required": false, + "description": "Exact match on the event's IP address.", + "schema": { + "type": "string" + }, + "example": "203.0.113.42" + }, + { + "name": "game_id", + "in": "query", + "required": false, + "description": "Exact match on the promoted `game_id`.", + "schema": { + "type": "string" + }, + "example": "poker-holdem-01" + }, + { + "name": "round_id", + "in": "query", + "required": false, + "description": "Exact match on the promoted `round_id`.", + "schema": { + "type": "string" + }, + "example": "round-88231" + }, + { + "name": "affiliate_code", + "in": "query", + "required": false, + "description": "Exact match on the promoted `affiliate_code`.", + "schema": { + "type": "string" + }, + "example": "aff-summer" + }, + { + "name": "payment_reference_id", + "in": "query", + "required": false, + "description": "Exact match on the payment reference (from `transaction_context.payment_reference_id` or `payload.payment_reference_id`).", + "schema": { + "type": "string" + }, + "example": "dep_735" + }, + { + "name": "counterparty_vendor_data", + "in": "query", + "required": false, + "description": "Exact match on the promoted `counterparty_vendor_data`.", + "schema": { + "type": "string" + }, + "example": "user-77" + }, + { + "name": "matched_only", + "in": "query", + "required": false, + "description": "`true` (also `1` or `yes`, case-insensitive) returns only events at least one rule matched (`rules_matched_count` above 0). Any other value is ignored.", + "schema": { + "type": "boolean", + "default": false + }, + "example": true + }, { - "name": "organization_id", - "in": "path", - "required": true, + "name": "occurred_from", + "in": "query", + "required": false, + "description": "Only events with `occurred_at` at or after this instant (inclusive). Send an ISO 8601 value: `2026-10-01T00:00:00Z`, an explicit offset with the `+` percent-encoded (`2026-10-01T02:00:00%2B02:00`; an unencoded `+` is decoded as a space and the value no longer parses), or a date alone (`2026-10-01`), which means 00:00:00 UTC of that day. A value without an offset is read as UTC. Values in any other format are not accepted.", "schema": { "type": "string", - "format": "uuid" + "format": "date-time" }, - "description": "Organization UUID." + "example": "2026-10-01T00:00:00Z" }, { - "name": "application_id", - "in": "path", - "required": true, + "name": "occurred_to", + "in": "query", + "required": false, + "description": "Only events with `occurred_at` at or before this instant (inclusive). A date alone means the start of that day, so `occurred_to=2026-10-15` excludes the rest of 15 October; send `2026-10-15T23:59:59Z` to include it. Send an ISO 8601 value: `2026-10-01T00:00:00Z`, an explicit offset with the `+` percent-encoded (`2026-10-01T02:00:00%2B02:00`; an unencoded `+` is decoded as a space and the value no longer parses), or a date alone (`2026-10-01`), which means 00:00:00 UTC of that day. A value without an offset is read as UTC. Values in any other format are not accepted.", "schema": { "type": "string", - "format": "uuid" + "format": "date-time" }, - "description": "Application UUID." + "example": "2026-10-17T23:59:59Z" + }, + { + "name": "search", + "in": "query", + "required": false, + "description": "Case-insensitive substring match on `event_id`, `action_type`, `subject_vendor_data` or `counterparty_vendor_data`.", + "schema": { + "type": "string" + }, + "example": "chargeback" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "case_uuids", - "status" - ], - "properties": { - "case_uuids": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - }, - "minItems": 1 - }, - "status": { - "type": "string", - "enum": [ - "OPEN", - "AWAITING_USER", - "RESOLVED" - ] - }, - "resolution": { - "type": "string", - "enum": [ - "FALSE_POSITIVE", - "VALID_THREAT" - ], - "description": "Required when status is RESOLVED." - }, - "note": { - "type": "string", - "nullable": true - } - } - } - } - } - }, "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/bulk/status/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"case_uuids\": [\"c1a5e000-0000-4000-8000-000000000001\"], \"status\": \"RESOLVED\", \"resolution\": \"FALSE_POSITIVE\"}'" + "source": "curl -X GET 'https://verification.didit.me/v3/events/subjects/user-42/?entity_type=individual&limit=50' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\nfrom urllib.parse import quote\n\nimport requests\n\nvendor_data = 'user-42'\nresp = requests.get(\n f'https://verification.didit.me/v3/events/subjects/{quote(vendor_data, safe=\"\")}/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n params={'entity_type': 'individual', 'limit': 50},\n timeout=10,\n)\nresp.raise_for_status()\nfor event in resp.json()['results']:\n print(event['occurred_at'], event['event_category'], event['action_type'], event['status'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const vendorData = 'user-42';\nconst url = `https://verification.didit.me/v3/events/subjects/${encodeURIComponent(vendorData)}/?entity_type=individual&limit=50`;\nconst resp = await fetch(url, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Request failed: ${resp.status}`);\nconst page = await resp.json();\nfor (const event of page.results) console.log(event.occurred_at, event.event_category, event.action_type, event.status);" } ], "responses": { "200": { - "description": "Per-case results.", + "description": "One page of the subject's events.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "updated": { - "type": "integer" - }, - "failures": { - "type": "array", - "items": { - "type": "object", - "properties": { - "case_uuid": { - "type": "string", - "format": "uuid" - }, - "detail": { - "type": "string" - } + "$ref": "#/components/schemas/MonitoringEventPage" + }, + "examples": { + "Individual timeline": { + "value": { + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "event_uuid": "11111111-2222-3333-4444-555555555555", + "event_id": "chargeback-2026-10-16-000318", + "event_category": "dispute", + "action_type": "chargeback_opened", + "occurred_at": "2026-10-16T14:02:11Z", + "received_at": "2026-10-16T14:02:12.418230Z", + "status": "IN_REVIEW", + "score": 60, + "subject_vendor_data": "user-42", + "subject_entity_type": "individual", + "subject_uuid": "66666666-7777-8888-9999-000000000000", + "counterparty_vendor_data": null, + "session_id": null, + "device_fingerprint": null, + "ip_address": null, + "ip_country": null, + "amount": "250.000000000000000000", + "currency": "EUR", + "game_id": null, + "round_id": null, + "bonus_id": null, + "affiliate_code": null, + "payment_reference_id": "dep_735", + "retention_class": "monetary_or_compliance", + "rules_matched_count": 1, + "correlation_pending": false, + "is_droppable": false + }, + { + "event_uuid": "22222222-3333-4444-5555-666666666666", + "event_id": "login-2026-10-15-000912", + "event_category": "auth", + "action_type": "login", + "occurred_at": "2026-10-15T08:30:00.120000Z", + "received_at": "2026-10-15T08:30:00.734512Z", + "status": "ACCEPTED", + "score": 0, + "subject_vendor_data": "user-42", + "subject_entity_type": "individual", + "subject_uuid": "66666666-7777-8888-9999-000000000000", + "counterparty_vendor_data": null, + "session_id": "web-session-abc", + "device_fingerprint": "fp-a8c1", + "ip_address": "203.0.113.42", + "ip_country": "ES", + "amount": null, + "currency": null, + "game_id": null, + "round_id": null, + "bonus_id": null, + "affiliate_code": null, + "payment_reference_id": null, + "retention_class": "standard", + "rules_matched_count": 0, + "correlation_pending": false, + "is_droppable": true } - } + ] + } + }, + "No events": { + "value": { + "count": 0, + "next": null, + "previous": null, + "results": [] } } } } } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/bulk/assign/": { - "post": { - "summary": "Bulk assign cases", - "description": "Assign (or unassign, with assignee_user_id: null) several cases at once. All cases must share the same blueprint; mismatches are reported per case without aborting the batch.", - "operationId": "bulk_assign_cases", - "tags": [ - "Cases" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "case_uuids", - "assignee_user_id" - ], - "properties": { - "case_uuids": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - }, - "minItems": 1 + "400": { + "description": "`limit` or `offset` is larger than 1000000. The body is a JSON array with one message. A non-numeric or non-positive `limit` falls back to the default of 50 instead of failing.", + "content": { + "application/json": { + "examples": { + "limit too large": { + "summary": "limit above 1000000", + "value": [ + "limit must not exceed 1000000." + ] }, - "assignee_user_id": { - "type": "string", - "format": "uuid", - "nullable": true + "offset too large": { + "summary": "offset above 1000000", + "value": [ + "offset must not exceed 1000000." + ] + } + } + } + } + }, + "401": { + "description": "The scoped API key has expired. Only an expired key gets `401`; a missing or invalid key gets `403`.", + "content": { + "application/json": { + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "Missing or invalid `x-api-key` (this endpoint answers `403`, never `401`, for a missing or invalid key), a scoped key without `transactions: read`, a key limited to some workflows or to approved sessions (such a key never reaches transaction monitoring data), or a request from outside the key's IP allowlist.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } }, - "assignee_email": { - "type": "string", - "format": "email", - "nullable": true + "Scoped key without access": { + "value": { + "detail": "This API key has no read access to transactions." + } }, - "assignee_name": { - "type": "string", - "nullable": true + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." + } + }, + "IP not allowed": { + "value": { + "detail": "This API key can't be used from this IP address." + } } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/bulk/assign/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"case_uuids\": [\"c1a5e000-0000-4000-8000-000000000001\"], \"assignee_user_id\": \"9a000000-0000-4000-8000-0000000000aa\"}'" - } - ], - "responses": { - "200": { - "description": "Per-case results.", + }, + "429": { + "description": "Rate limit exceeded: `GET` requests share a budget of 600 requests per minute per API key. Wait `Retry-After` seconds, then retry.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + } + }, "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "updated": { - "type": "integer" - }, - "failures": { - "type": "array", - "items": { - "type": "object", - "properties": { - "case_uuid": { - "type": "string", - "format": "uuid" - }, - "detail": { - "type": "string" - } - } - } + "examples": { + "Throttled": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." } } } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/": { + "/v3/events/enrichment-metrics/": { "get": { - "summary": "List blueprints", - "description": "List case blueprints for the application.", - "operationId": "list_case_blueprints", + "summary": "Get event enrichment metrics", + "description": "Return the event ingestion and IP-enrichment counters of your application for a period, summed over every lane (server, batch and device), so you can watch event volume and confirm that enrichment is resolved once per unique address rather than once per event. The period covers today plus the previous `days` days, counted by UTC calendar date. The three ratios are null when their denominator is zero. A scoped API key needs `transactions: read`. See [IP enrichment](https://docs.didit.me/transaction-monitoring/events#ip-enrichment).", + "operationId": "get_monitoring_event_enrichment_metrics", + "x-didit-sdk": { + "resource": "monitoring_events", + "method": "retrieve_enrichment_metrics", + "auth": "api_key", + "server": true + }, "tags": [ - "Case Blueprints" + "Monitoring Events" ], "parameters": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "default": 50, - "minimum": 1 - }, - "description": "Page size." - }, - { - "name": "offset", + "name": "days", "in": "query", + "required": false, + "description": "Length of the period in days. Defaults to 30; values are clamped to 1 to 365, and a non-integer value falls back to 30. The value applied is returned as `period_days`.", "schema": { "type": "integer", - "default": 0, - "minimum": 0 + "default": 30, + "minimum": 1, + "maximum": 365 }, - "description": "Zero-based offset." + "example": 30 } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/blueprints/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X GET 'https://verification.didit.me/v3/events/enrichment-metrics/?days=30' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nresp = requests.get(\n 'https://verification.didit.me/v3/events/enrichment-metrics/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n params={'days': 30},\n timeout=10,\n)\nresp.raise_for_status()\nmetrics = resp.json()\nprint(metrics['events_ingested'], metrics['cache_hit_rate'], metrics['provider_calls_per_1k_events'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const resp = await fetch('https://verification.didit.me/v3/events/enrichment-metrics/?days=30', {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Request failed: ${resp.status}`);\nconst metrics = await resp.json();\nconsole.log(metrics.events_ingested, metrics.cache_hit_rate, metrics.provider_calls_per_1k_events);" } ], "responses": { "200": { - "description": "Paginated blueprint list.", + "description": "Counters and ratios for the period.", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "events_ingested", + "events_rejected", + "enrichment_lookups", + "enrichment_cache_hits", + "enrichment_provider_calls", + "enrichment_skipped_low_value", + "unique_ips_seen", + "period_days", + "cache_hit_rate", + "unique_ip_ratio", + "provider_calls_per_1k_events" + ], "properties": { - "count": { + "events_ingested": { "type": "integer", - "description": "Number of matching rows, capped at 100 for performance." + "description": "New events stored. Idempotent replays are not counted." }, - "next": { - "type": "string", - "nullable": true + "events_rejected": { + "type": "integer", + "description": "Items rejected inside batch requests (server and device lanes). A rejected single-event request is not counted." }, - "previous": { - "type": "string", - "nullable": true + "enrichment_lookups": { + "type": "integer", + "description": "Events whose public IP address was resolved, from the cache or freshly." }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaseBlueprintListItem" - } + "enrichment_cache_hits": { + "type": "integer", + "description": "Lookups answered from the cache." + }, + "enrichment_provider_calls": { + "type": "integer", + "description": "Lookups that needed a fresh resolution of the address." + }, + "enrichment_skipped_low_value": { + "type": "integer", + "description": "Low-value telemetry events whose address was not already known. They never trigger a fresh resolution of their own." + }, + "unique_ips_seen": { + "type": "integer", + "description": "Addresses that needed a fresh resolution." + }, + "period_days": { + "type": "integer", + "description": "The `days` value applied after clamping." + }, + "cache_hit_rate": { + "type": "number", + "nullable": true, + "description": "`enrichment_cache_hits / enrichment_lookups`, rounded to 4 decimals. Null when there were no lookups." + }, + "unique_ip_ratio": { + "type": "number", + "nullable": true, + "description": "`unique_ips_seen / events_ingested`, rounded to 4 decimals. Null when no events were ingested." + }, + "provider_calls_per_1k_events": { + "type": "number", + "nullable": true, + "description": "Fresh resolutions per 1000 ingested events, rounded to 2 decimals. Null when no events were ingested." + } + } + }, + "examples": { + "Last 30 days": { + "value": { + "events_ingested": 128450, + "events_rejected": 214, + "enrichment_lookups": 41230, + "enrichment_cache_hits": 40118, + "enrichment_provider_calls": 1112, + "enrichment_skipped_low_value": 74320, + "unique_ips_seen": 1112, + "period_days": 30, + "cache_hit_rate": 0.973, + "unique_ip_ratio": 0.0087, + "provider_calls_per_1k_events": 8.66 + } + }, + "No events yet": { + "value": { + "events_ingested": 0, + "events_rejected": 0, + "enrichment_lookups": 0, + "enrichment_cache_hits": 0, + "enrichment_provider_calls": 0, + "enrichment_skipped_low_value": 0, + "unique_ips_seen": 0, + "period_days": 30, + "cache_hit_rate": null, + "unique_ip_ratio": null, + "provider_calls_per_1k_events": null } } } } } - } - } - }, - "post": { - "summary": "Create blueprint", - "description": "Create a case blueprint: general settings (assignment, escalation, transfer, 4-eyes pairing) plus content_config, the six sections that drive the case page. Set publish=false to create an unpublished draft; Publish creates its first live version. Discarding a never-published draft deletes it.", - "operationId": "create_case_blueprint", - "tags": [ - "Case Blueprints" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "name", - "assignee_pool" - ], - "properties": { - "name": { - "type": "string", - "maxLength": 24, - "description": "Max 24 characters, e.g. \"AML investigation\"." - }, - "description": { - "type": "string", - "nullable": true - }, - "assignee_pool": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - }, - "minItems": 1, - "description": "List of User UUIDs. Empty arrays are rejected with \"Pick at least one assignee\"." - }, - "assignment_method": { - "type": "string", - "enum": [ - "MANUAL", - "AUTOMATIC" - ], - "default": "MANUAL" - }, - "max_active_cases": { - "type": "integer", - "nullable": true, - "minimum": 1, - "description": "Automatic-mode threshold: \"fewer than N cases of this blueprint assigned\"." - }, - "escalation_enabled": { - "type": "boolean", - "default": false - }, - "escalation_blueprint": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Target blueprint UUID. Not enforced at save time, but the escalate action fails with 400 until both escalation_enabled and escalation_blueprint are set." - }, - "transfer_enabled": { - "type": "boolean", - "default": false - }, - "four_eyes_checker_blueprint": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Setting this enables 4-eyes review: resolutions on this blueprint stage on the checker blueprint's queue." - }, - "require_resolution_note": { - "type": "boolean", - "default": false - }, - "content_config": { - "$ref": "#/components/schemas/CaseContentConfig" - }, - "is_active": { - "type": "boolean", - "default": true - }, - "default_deadline_hours": { - "type": "integer", - "nullable": true, - "minimum": 1, - "maximum": 8760, - "description": "Default deadline for newly opened cases. Existing case deadlines are unchanged." - }, - "publish": { - "type": "boolean", - "writeOnly": true, - "default": true, - "description": "Set false to save a never-published draft. The record remains inactive at version 0 until the draft is explicitly published. Omitting this field preserves immediate creation for existing integrations." + "401": { + "description": "The scoped API key has expired. Only an expired key gets `401`; a missing or invalid key gets `403`.", + "content": { + "application/json": { + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/blueprints/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"name\": \"AML investigation\", \"assignee_pool\": [\"9a000000-0000-4000-8000-0000000000aa\"], \"assignment_method\": \"AUTOMATIC\", \"max_active_cases\": 15, \"require_resolution_note\": true, \"content_config\": {\"aml_control\": {\"enabled\": true, \"financial\": true, \"identity\": false}}}'" - } - ], - "responses": { - "201": { - "description": "Blueprint created.", + }, + "403": { + "description": "Missing or invalid `x-api-key` (this endpoint answers `403`, never `401`, for a missing or invalid key), a scoped key without `transactions: read`, a key limited to some workflows or to approved sessions (such a key never reaches transaction monitoring data), or a request from outside the key's IP allowlist.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseBlueprintDetail" + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without access": { + "value": { + "detail": "This API key has no read access to transactions." + } + }, + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." + } + }, + "IP not allowed": { + "value": { + "detail": "This API key can't be used from this IP address." + } + } } } } }, - "400": { - "description": "Name over 24 chars, empty assignee_pool, or a cross-referenced blueprint (escalation/checker) not found.", + "429": { + "description": "Rate limit exceeded: `GET` requests share a budget of 600 requests per minute per API key. Wait `Retry-After` seconds, then retry.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + } + }, "content": { "application/json": { - "schema": { - "type": "object" + "examples": { + "Throttled": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/": { - "get": { - "summary": "Get blueprint", - "description": "Retrieve a blueprint, including its connected creation sources (AML config, transaction rules, workflow nodes) and which blueprints escalate to it.", - "operationId": "get_case_blueprint", + "/v3/events/{event_uuid}/delete/": { + "delete": { + "summary": "Delete monitoring event", + "description": "Permanently delete one monitoring event together with its rule runs, notes and tag links, for example an event sent by mistake. After the `204` the event is gone from every read (retrieve returns `404`, and it no longer appears in the list or the subject timeline), the deletion is irreversible, and its `event_id` is free to be used for a new event. Not affected: the user or company the event resolved to, cases a rule opened, list entries a rule added, and the scores and statuses already applied to other events. To erase everything about a person or a company, delete the user or the company instead, which also deletes their events. A scoped API key needs `transactions: write`.", + "operationId": "delete_monitoring_event", + "x-didit-sdk": { + "resource": "monitoring_events", + "method": "delete", + "auth": "api_key", + "server": true + }, "tags": [ - "Case Blueprints" + "Monitoring Events" ], "parameters": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", + "name": "event_uuid", "in": "path", "required": true, + "description": "Didit identifier of the event (`event_uuid` from the create, batch, list or timeline response), not your own `event_id`.", "schema": { "type": "string", "format": "uuid" }, - "description": "Application UUID." - }, - { - "name": "blueprint_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "b1de0000-0000-4000-8000-000000000002" - }, - "description": "Blueprint UUID." + "example": "11111111-2222-3333-4444-555555555555" } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/blueprints/b1de0000-0000-4000-8000-000000000002/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X DELETE 'https://verification.didit.me/v3/events/11111111-2222-3333-4444-555555555555/delete/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nevent_uuid = '11111111-2222-3333-4444-555555555555'\nresp = requests.delete(\n f'https://verification.didit.me/v3/events/{event_uuid}/delete/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status() # 204 No Content\nprint('deleted', event_uuid)" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const eventUuid = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(`https://verification.didit.me/v3/events/${eventUuid}/delete/`, {\n method: 'DELETE',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (resp.status !== 204) throw new Error(`Delete failed: ${resp.status}`);\nconsole.log('deleted', eventUuid);" } ], "responses": { - "200": { - "description": "Blueprint detail.", + "204": { + "description": "The event was deleted. No body." + }, + "401": { + "description": "The scoped API key has expired. Only an expired key gets `401`; a missing or invalid key gets `403`.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseBlueprintDetail" + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } } } } - } - } - }, - "patch": { - "summary": "Update blueprint", - "description": "Partially update a blueprint's general settings or content_config.", - "operationId": "update_case_blueprint", - "tags": [ - "Case Blueprints" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." }, - { - "name": "blueprint_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "b1de0000-0000-4000-8000-000000000002" - }, - "description": "Blueprint UUID." - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string", - "maxLength": 24, - "description": "Max 24 characters, e.g. \"AML investigation\"." - }, - "description": { - "type": "string", - "nullable": true - }, - "assignee_pool": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - }, - "minItems": 1, - "description": "List of User UUIDs. Empty arrays are rejected with \"Pick at least one assignee\"." - }, - "assignment_method": { - "type": "string", - "enum": [ - "MANUAL", - "AUTOMATIC" - ] - }, - "max_active_cases": { - "type": "integer", - "nullable": true, - "minimum": 1, - "description": "Automatic-mode threshold: \"fewer than N cases of this blueprint assigned\"." - }, - "escalation_enabled": { - "type": "boolean" - }, - "escalation_blueprint": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Target blueprint UUID. Not enforced at save time, but the escalate action fails with 400 until both escalation_enabled and escalation_blueprint are set." - }, - "transfer_enabled": { - "type": "boolean" - }, - "four_eyes_checker_blueprint": { - "type": "string", - "format": "uuid", - "nullable": true, - "description": "Setting this enables 4-eyes review: resolutions on this blueprint stage on the checker blueprint's queue." - }, - "require_resolution_note": { - "type": "boolean" - }, - "content_config": { - "$ref": "#/components/schemas/CaseContentConfig" + "403": { + "description": "Missing or invalid `x-api-key` (this endpoint answers `403`, never `401`, for a missing or invalid key), a scoped key without `transactions: write`, a key limited to some workflows or to approved sessions (such a key never reaches transaction monitoring data), or a request from outside the key's IP allowlist.", + "content": { + "application/json": { + "examples": { + "Missing or invalid API key": { + "value": { + "detail": "You do not have permission to perform this action." + } }, - "is_active": { - "type": "boolean" + "Scoped key without access": { + "value": { + "detail": "This API key has no write access to transactions." + } }, - "default_deadline_hours": { - "type": "integer", - "nullable": true, - "minimum": 1, - "maximum": 8760, - "description": "Default deadline for newly opened cases. Existing case deadlines are unchanged." + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." + } }, - "expected_version": { - "type": "integer", - "minimum": 1, - "description": "The version opened by the editor. A stale version returns 409 without changing the review policy." + "IP not allowed": { + "value": { + "detail": "This API key can't be used from this IP address." + } } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/blueprints/b1de0000-0000-4000-8000-000000000002/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"transfer_enabled\": true}'" - } - ], - "responses": { - "200": { - "description": "Updated blueprint.", + }, + "404": { + "description": "No event with this `event_uuid` exists in the application: it was never stored, it was deleted, or it belongs to another application. A path segment that is not a UUID does not match the route and also returns `404`.", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseBlueprintDetail" + "examples": { + "Not found": { + "value": { + "detail": "No MonitoringEvent matches the given query." + } + } } } } }, - "409": { - "description": "The review policy changed since expected_version. Review the latest version before retrying." - } - } - }, - "delete": { - "summary": "Delete blueprint", - "description": "Delete a blueprint. Deletion always succeeds and does not touch the blueprint's cases: existing cases keep their blueprint reference and its settings continue to apply to them. A deleted blueprint no longer appears in blueprint lists and can no longer be chosen as a transfer target; if a maker blueprint is deleted while a resolution is pending 4-eyes approval, a rejected resolution leaves the case on the checker blueprint.", - "operationId": "delete_case_blueprint", - "tags": [ - "Case Blueprints" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "blueprint_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "b1de0000-0000-4000-8000-000000000002" + "429": { + "description": "Rate limit exceeded: write requests (`POST`, `PATCH`, `DELETE`) share a budget of 300 requests per minute per API key. Wait `Retry-After` seconds, then retry.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + } }, - "description": "Blueprint UUID." + "content": { + "application/json": { + "examples": { + "Throttled": { + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + } + } + } } - ], - "x-codeSamples": [ + }, + "security": [ { - "lang": "curl", - "label": "curl", - "source": "curl -X DELETE 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/blueprints/b1de0000-0000-4000-8000-000000000002/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "204": { - "description": "Blueprint deleted." + "ApiKeyAuth": [] } - } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/": { - "get": { - "summary": "List blueprint presets", - "description": "List the four built-in presets (Screening hit, AML investigation, Suspicious activity, KYC review) with their content_config and whether each is already installed on this application.", - "operationId": "list_case_blueprint_presets", + "/v3/session/{sessionId}/kyb/parties/{partyUuid}/regenerate/": { + "post": { + "summary": "Regenerate a Key People party's KYC session", + "description": "Give a Key People party of a Business Verification (KYB) session a new individual KYC session after its previous one was deleted, so the Key People check can finish again; send no request body. It works only when the party's KYC session was deleted (`kyc_session_deleted: true`): a party whose KYC session still exists (in any status), a party that never had one, or a company party returns `409`. Didit creates a new hosted KYC session with the workflow the deleted one ran (its latest published version) or, when that is no longer known, the workflow your Key People settings assign to the party's role, emails the link to the party when an email is on file, clears `is_skipped`, and recomputes the Key People check and the business session status. It then sends a `data.updated` [webhook](https://docs.didit.me/integration/webhooks) for the business session with trigger `console_key_people_regenerate`; to exempt the party instead, use `PATCH /v3/session/{sessionId}/kyb/parties/{partyUuid}/`. A scoped API key needs `decisions: write`, and without `sessions: write` it receives `kyc_session_url` as `null`.", + "operationId": "post_v3_session_kyb_party_regenerate", + "x-didit-sdk": { + "resource": "sessions", + "method": "regenerate_kyb_party", + "auth": "api_key", + "server": true + }, "tags": [ - "Case Blueprints" + "Sessions" ], "parameters": [ { - "name": "organization_id", + "name": "sessionId", "in": "path", "required": true, + "description": "UUID of the parent Business Verification (KYB) session.", "schema": { "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." + "format": "uuid", + "example": "33333333-4444-5555-6666-777777777777" + } }, { - "name": "application_id", + "name": "partyUuid", "in": "path", "required": true, + "description": "UUID of the Key People party (`key_people_checks[].submitted.parties[].uuid` on the business session decision). Must belong to the session referenced by `sessionId`.", "schema": { "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "format": "uuid", + "example": "0a3f1e2c-7c2f-4a1b-9d55-2f8d2a5d6b41" + } } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/blueprints/presets/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X POST 'https://verification.didit.me/v3/session/33333333-4444-5555-6666-777777777777/kyb/parties/0a3f1e2c-7c2f-4a1b-9d55-2f8d2a5d6b41/regenerate/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nsession_id = '33333333-4444-5555-6666-777777777777'\nparty_uuid = '0a3f1e2c-7c2f-4a1b-9d55-2f8d2a5d6b41'\nresp = requests.post(\n f'https://verification.didit.me/v3/session/{session_id}/kyb/parties/{party_uuid}/regenerate/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nparty = resp.json()\nprint(party['kyc_session_id'], party['kyc_session_status'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const sessionId = '33333333-4444-5555-6666-777777777777';\nconst partyUuid = '0a3f1e2c-7c2f-4a1b-9d55-2f8d2a5d6b41';\nconst resp = await fetch(`https://verification.didit.me/v3/session/${sessionId}/kyb/parties/${partyUuid}/regenerate/`, {\n method: 'POST',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Regenerate failed: ${resp.status}`);\nconst party = await resp.json();\nconsole.log(party.kyc_session_id, party.kyc_session_status);" } ], "responses": { "200": { - "description": "Preset catalogue.", + "description": "New KYC session created. Returns the updated party with its new child session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KYBKeyPeopleParty" + }, + "example": { + "uuid": "0a3f1e2c-7c2f-4a1b-9d55-2f8d2a5d6b41", + "entity_type": "person", + "source": "USER", + "name": "Jane Smith", + "first_name": "Jane", + "last_name": "Smith", + "company_name": null, + "registration_number": null, + "email": "user@example.com", + "nationality": "GB", + "phone_number": null, + "date_of_birth": "1980-05-04", + "position": "Director", + "custom_fields": {}, + "is_active": true, + "is_skipped": false, + "requires_verification": true, + "roles": [ + { + "role": "director", + "ownership_percent": null, + "voting_percent": null + } + ], + "kyc_session_id": "55555555-6666-7777-8888-999999999999", + "kyc_session_status": "Not Started", + "kyc_session_url": "https://verify.didit.me/session/abc123def456", + "kyc_session_deleted": false, + "kyb_sub_session_id": null, + "kyb_sub_session_status": null, + "kyb_sub_session_url": null, + "kyb_sub_session_deleted": false, + "verification_workflow_type": "kyc", + "verification_workflow_id": "12121212-3434-5656-7878-909090909090", + "verification_workflow_label": "Key People KYC", + "verification_workflow_features": [ + { + "feature": "ID_VERIFICATION", + "status": "Not Finished" + }, + { + "feature": "LIVENESS", + "status": "Not Finished" + }, + { + "feature": "AML", + "status": "Not Finished" + } + ], + "didit_internal_id": null + } + } + } + }, + "401": { + "description": "The scoped API key has expired. This is the only case that returns `401`: a missing, invalid or revoked key returns `403`.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "results": { - "type": "array", - "items": { - "type": "object", - "properties": { - "preset_key": { - "type": "string", - "enum": [ - "screening_hit", - "aml_investigation", - "suspicious_activity", - "kyc" - ] - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "content_config": { - "$ref": "#/components/schemas/CaseContentConfig" - }, - "installed": { - "type": "boolean" - } - } - } + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Expired key": { + "summary": "The key's expiry date has passed", + "value": { + "detail": "This API key has expired." } } } } } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/install/": { - "post": { - "summary": "Install blueprint presets", - "description": "Copy one or more presets into the application as editable blueprints. Idempotent per preset_key; omit preset_keys to install every preset not yet installed.", - "operationId": "install_case_blueprint_presets", - "tags": [ - "Case Blueprints" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "preset_keys": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true, - "description": "Subset of screening_hit, aml_investigation, suspicious_activity, kyc. Omit to install all." + "403": { + "description": "Authentication or authorization failure. Missing, invalid and revoked keys return `403` (not `401`); so do a key of another application, a scoped key without the permission this endpoint needs, and a request from outside the key's IP allowlist. Distinguish them by `detail`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Missing or invalid credentials": { + "summary": "No x-api-key header, or the key failed validation", + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + }, + "Other application": { + "summary": "A valid key that belongs to a different application than the session", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without decisions write": { + "summary": "Scoped key lacks decisions: write", + "value": { + "detail": "This API key has no write access to decisions." + } + }, + "IP not allowed": { + "summary": "Request from outside the key's IP allowlist", + "value": { + "detail": "This API key can't be used from this IP address." + } } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/blueprints/presets/install/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"preset_keys\": [\"aml_investigation\", \"kyc\"]}'" - } - ], - "responses": { - "201": { - "description": "One or more presets installed.", + }, + "404": { + "description": "The session does not exist, was deleted or is outside a scoped key's limits; no party with this UUID exists in your application; or the party belongs to another session. Each case has its own `detail`. The session lookup runs before the credential check, so an unknown `sessionId` returns `404` even without a valid key.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Unknown session": { + "summary": "No session with this ID exists, or it was deleted", + "value": { + "detail": "Not found." + } + }, + "Outside key limits": { + "summary": "The session is outside a scoped key's workflow or approved-only limits", + "value": { + "detail": "Session not found." + } + }, + "Unknown party": { + "summary": "No party with this UUID in your application", + "value": { + "detail": "No BusinessParty matches the given query." + } + }, + "Party of another session": { + "summary": "partyUuid exists but belongs to a different session", + "value": { + "detail": "Party does not belong to the given session." + } + } + } + } + } + }, + "409": { + "description": "The party's KYC session was not deleted (it still exists, the party never had one, or the party is a company), so there is nothing to replace. Branch on `code`.", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "detail", + "code" + ], "properties": { - "created": { - "type": "array", - "items": { - "type": "string" - } + "detail": { + "type": "string", + "description": "Human-readable error message." + }, + "code": { + "type": "string", + "enum": [ + "kyb_party_kyc_session_not_deleted" + ], + "description": "Stable machine-readable error code." } } + }, + "example": { + "detail": "This party's KYC session has not been deleted, so it cannot be regenerated.", + "code": "kyb_party_kyc_session_not_deleted" } } } }, - "200": { - "description": "No new presets to install (all requested presets already exist).", + "429": { + "description": "Rate limit exceeded: 300 write (POST, PATCH, DELETE) requests per minute per API key. Wait `Retry-After` seconds before retrying; the `X-RateLimit-*` headers show the budget.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "created": { - "type": "array", - "items": { - "type": "string" - } + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Rate limited": { + "summary": "Budget for the current minute exhausted", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." } } } } + }, + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/report-templates/": { + "/v3/transactions/{transaction_id}/notes/": { "get": { - "summary": "List report templates", - "description": "List FIU report templates for the application.", - "operationId": "list_report_templates", + "summary": "List transaction notes", + "description": "List the notes on one monitored transaction, newest first (`created_at` descending): notes you add through the API, notes and status changes reviewers make in the Console, and notes Didit adds automatically (a rule's `add_note` action, or a problem creating the user-action verification session), which carry `actor_name` `Rule Engine`. `metadata` tells the note types apart.\n\nPaginated with `limit` and `offset`. `count` is exact up to 100 and capped at 100 above that, so follow `next` until it is `null` instead of computing pages from `count`. This is a read with no side effects.\n\nA scoped API key needs `transactions: read`; a key limited to some workflows or to approved sessions cannot call this endpoint. Reviewers see the same list under **Notes** on the [transaction detail](https://docs.didit.me/transaction-monitoring/console#transaction-detail).", + "operationId": "list_transaction_notes", + "x-didit-sdk": { + "resource": "transactions.notes", + "method": "list", + "auth": "api_key", + "server": true + }, "tags": [ - "Report Templates" + "Transactions" ], "parameters": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", + "name": "transaction_id", "in": "path", "required": true, + "description": "Didit transaction UUID: the `uuid` returned by `POST /v3/transactions/` and `GET /v3/transactions/`. Your own `txn_id` is not accepted.", "schema": { "type": "string", "format": "uuid" }, - "description": "Application UUID." + "example": "abcdef12-3456-7890-abcd-ef1234567890" }, { "name": "limit", "in": "query", + "required": false, "schema": { "type": "integer", "default": 50, - "minimum": 1 + "minimum": 1, + "maximum": 1000000 }, - "description": "Page size." + "description": "Number of notes per page. Defaults to 50. A value that is not a positive integer falls back to 50; a value above 1000000 returns `400`." }, { "name": "offset", "in": "query", + "required": false, "schema": { "type": "integer", "default": 0, - "minimum": 0 + "minimum": 0, + "maximum": 1000000 }, - "description": "Zero-based offset." + "description": "Zero-based position of the first note to return. Prefer following `next`. A value that is not a whole number falls back to 0; a value above 1000000 returns `400`." } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/report-templates/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X GET 'https://verification.didit.me/v3/transactions/abcdef12-3456-7890-abcd-ef1234567890/notes/?limit=50' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\ntransaction_id = 'abcdef12-3456-7890-abcd-ef1234567890'\nurl = f'https://verification.didit.me/v3/transactions/{transaction_id}/notes/'\nparams = {'limit': 50}\nwhile url:\n resp = requests.get(\n url,\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n params=params,\n timeout=10,\n )\n resp.raise_for_status()\n page = resp.json()\n for note in page['results']:\n print(note['created_at'], note['actor_name'], note['note'])\n url, params = page['next'], None" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const transactionId = 'abcdef12-3456-7890-abcd-ef1234567890';\nlet url = `https://verification.didit.me/v3/transactions/${transactionId}/notes/?limit=50`;\nwhile (url) {\n const resp = await fetch(url, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n });\n if (!resp.ok) throw new Error(`List notes failed: ${resp.status}`);\n const page = await resp.json();\n for (const note of page.results) console.log(note.created_at, note.actor_name, note.note);\n url = page.next;\n}" } ], "responses": { "200": { - "description": "Paginated templates.", + "description": "A page of notes, newest first.", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "count", + "next", + "previous", + "results" + ], "properties": { "count": { "type": "integer", - "description": "Number of matching rows, capped at 100 for performance." + "description": "Number of notes on the transaction: exact up to 100, capped at 100 above that. Follow `next` instead of computing pages from it." }, "next": { "type": "string", - "nullable": true + "format": "uri", + "nullable": true, + "description": "Absolute URL of the next page, or `null` on the last page." }, "previous": { "type": "string", - "nullable": true + "format": "uri", + "nullable": true, + "description": "Absolute URL of the previous page, or `null` on the first page." }, "results": { "type": "array", "items": { - "$ref": "#/components/schemas/ReportTemplateDetail" + "$ref": "#/components/schemas/TransactionNote" } } } - } - } - } - } - } - }, - "post": { - "summary": "Create report template", - "description": "Create a reusable per-jurisdiction, per-report-type goAML header template. Rejected with 400 if any of the template-level obligatory fields (rentity_id, submission_code, currency_code_local) is missing from field_config; an invalid template cannot be saved or used to create a report. Per-filing header fields (action, submission_date, transactions) are enforced at report validate/finalize time instead.", - "operationId": "create_report_template", - "tags": [ - "Report Templates" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "name", - "jurisdiction", - "report_type" - ], - "properties": { - "name": { - "type": "string" - }, - "jurisdiction": { - "type": "string", - "minLength": 2, - "maxLength": 2, - "description": "ISO 3166-1 alpha-2 country code, upper-cased on save." - }, - "report_type": { - "type": "string", - "enum": [ - "SAR", - "STR", - "CTR", - "TTR", - "UTR", - "IFT", - "CBR", - "TFR" - ] + }, + "examples": { + "API, Console and rule notes": { + "value": { + "count": 3, + "next": null, + "previous": null, + "results": [ + { + "uuid": "c1111111-2222-4333-8444-555555555555", + "note": "Customer confirmed the withdrawal by phone.", + "actor_email": null, + "actor_name": null, + "metadata": { + "mentioned_emails": [ + "analyst@example.com" + ] + }, + "created_at": "2026-10-16T10:05:12.481203Z" + }, + { + "uuid": "c2222222-3333-4444-8555-666666666666", + "note": "Status changed from IN_REVIEW to APPROVED. Reason: Source of funds verified", + "actor_email": "reviewer@example.com", + "actor_name": "Alex Reviewer", + "metadata": { + "type": "status_change", + "previous_status": "IN_REVIEW", + "new_status": "APPROVED", + "reason_code": "SOF_VERIFIED", + "reason_label": "Source of funds verified" + }, + "created_at": "2026-10-15T17:22:40.103911Z" + }, + { + "uuid": "c3333333-4444-4555-8666-777777777777", + "note": "Large withdrawal to a new wallet: check the source of funds.", + "actor_email": null, + "actor_name": "Rule Engine", + "metadata": { + "rule_uuid": "d1111111-2222-4333-8444-555555555555", + "source": "rule_action" + }, + "created_at": "2026-10-15T08:42:12.004518Z" + } + ] + } }, - "field_config": { - "type": "object", - "description": "Reusable goAML header defaults. Template-level obligatory fields: rentity_id, submission_code, and currency_code_local (3-letter). Optional: enabled_fields (which optional report blocks to include) plus reporting_person, location, reason, and action defaults. Per-filing header fields (action, submission_date, transactions, jurisdiction) are enforced when the report is validated or finalized, not on the template." + "No notes yet": { + "value": { + "count": 0, + "next": null, + "previous": null, + "results": [] + } } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/report-templates/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"name\": \"US SAR default\", \"jurisdiction\": \"US\", \"report_type\": \"SAR\", \"field_config\": {\"rentity_id\": \"FIU-US-00123\", \"submission_code\": \"E\", \"currency_code_local\": \"USD\"}}'" }, - { - "lang": "python", - "label": "Python", - "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/report-templates/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={\n 'name': 'US SAR default',\n 'jurisdiction': 'US',\n 'report_type': 'SAR',\n 'field_config': {'rentity_id': 'FIU-US-00123', 'submission_code': 'E', 'currency_code_local': 'USD'},\n },\n timeout=10,\n)\nresp.raise_for_status()\nprint(resp.json()['uuid'])" - } - ], - "responses": { - "201": { - "description": "Template created.", + "400": { + "description": "`limit` or `offset` is above 1000000. The body is a JSON array of messages, not an object.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReportTemplateDetail" + "type": "array", + "items": { + "type": "string" + } + }, + "examples": { + "Limit too large": { + "value": [ + "limit must not exceed 1000000." + ] + }, + "Offset too large": { + "value": [ + "offset must not exceed 1000000." + ] + } } } } }, - "400": { - "description": "Unrecognized jurisdiction, or field_config fails obligatory-field validation.", + "401": { + "description": "The API key has expired. Only a key created with an expiry date can expire; after that date every request with it answers `401`.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "errors": { - "type": "array", - "items": { - "type": "string" - } + "detail": { + "type": "string" } } - } - } - } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/": { - "get": { - "summary": "Get report template", - "description": "Retrieve a report template.", - "operationId": "get_report_template", - "tags": [ - "Report Templates" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "template_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "7ec00000-0000-4000-8000-000000000006" - }, - "description": "Report template UUID." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/report-templates/7ec00000-0000-4000-8000-000000000006/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "200": { - "description": "Template detail.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReportTemplateDetail" - } - } - } - } - } - }, - "patch": { - "summary": "Update report template", - "description": "Partially update a report template. Re-validates obligatory goAML fields on save.", - "operationId": "update_report_template", - "tags": [ - "Report Templates" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "template_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "7ec00000-0000-4000-8000-000000000006" - }, - "description": "Report template UUID." - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "jurisdiction": { - "type": "string", - "minLength": 2, - "maxLength": 2, - "description": "ISO 3166-1 alpha-2 country code, upper-cased on save." - }, - "report_type": { - "type": "string", - "enum": [ - "SAR", - "STR", - "CTR", - "TTR", - "UTR", - "IFT", - "CBR", - "TFR" - ] - }, - "field_config": { - "type": "object", - "description": "Reusable goAML header defaults. Template-level obligatory fields: rentity_id, submission_code, and currency_code_local (3-letter). Optional: enabled_fields (which optional report blocks to include) plus reporting_person, location, reason, and action defaults. Per-filing header fields (action, submission_date, transactions, jurisdiction) are enforced when the report is validated or finalized, not on the template." + }, + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/report-templates/7ec00000-0000-4000-8000-000000000006/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"field_config\": {\"rentity_id\": \"FIU-US-00124\"}}'" - } - ], - "responses": { - "200": { - "description": "Updated template.", + }, + "403": { + "description": "The request is not allowed. A missing or invalid API key gets the generic message (never `401`).", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReportTemplateDetail" - } - } - } - } - } - }, - "delete": { - "summary": "Delete report template", - "description": "Delete a report template. Reports already created from it are unaffected.", - "operationId": "delete_report_template", - "tags": [ - "Report Templates" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "template_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "7ec00000-0000-4000-8000-000000000006" - }, - "description": "Report template UUID." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X DELETE 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/report-templates/7ec00000-0000-4000-8000-000000000006/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "204": { - "description": "Template deleted." - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/regulatory-reports/": { - "get": { - "summary": "List reports for case", - "description": "List FIU reports generated for the case.", - "operationId": "list_case_regulatory_reports", - "tags": [ - "Regulatory Reports" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Missing or invalid key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without transactions read": { + "value": { + "detail": "This API key has no read access to transactions." + } + }, + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." + } + }, + "Outside the key's IP list": { + "value": { + "detail": "This API key can't be used from this IP address." + } + } + } + } + } }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "default": 50, - "minimum": 1 - }, - "description": "Page size." + "404": { + "description": "No transaction with this UUID exists in the application (never created, deleted, or owned by another application). A `transaction_id` that is not a UUID does not reach the API: it returns a `404` HTML page instead of this JSON body.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Not found": { + "value": { + "detail": "No Transaction matches the given query." + } + } + } + } + } }, - { - "name": "offset", - "in": "query", - "schema": { - "type": "integer", - "default": 0, - "minimum": 0 - }, - "description": "Zero-based offset." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/regulatory-reports/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "200": { - "description": "Paginated reports for the case.", + "429": { + "description": "More than 600 GET requests per minute with this API key. Wait the number of seconds in `Retry-After`, then retry.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "count": { - "type": "integer", - "description": "Number of matching rows, capped at 100 for performance." - }, - "next": { - "type": "string", - "nullable": true - }, - "previous": { - "type": "string", - "nullable": true - }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RegulatoryReportDetail" - } + "detail": { + "type": "string" + } + } + }, + "examples": { + "Throttled": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." } } } } + }, + "headers": { + "Retry-After": { + "description": "Seconds until the current one-minute window resets.", + "schema": { + "type": "integer", + "example": 23 + } + }, + "X-RateLimit-Limit": { + "description": "Requests allowed per minute in this budget.", + "schema": { + "type": "integer" + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the current window.", + "schema": { + "type": "integer" + } + }, + "X-RateLimit-Reset": { + "description": "Unix time when the window resets.", + "schema": { + "type": "integer" + } + } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] }, "post": { - "summary": "Create report from case", - "description": "Create a draft FIU report for the case: DRAFT status, auto-populated from the optional template plus the case's subject, linked transactions, and narrative. Validate then finalize to produce the goAML XML and PDF.", - "operationId": "create_case_regulatory_report", + "summary": "Create transaction note", + "description": "Add a plain-text note to a monitored transaction's review timeline, where Console reviewers see it. A note is context, not a decision: the transaction's `status`, `score` and alerts do not change, no webhook is sent, and nothing is billed. The transaction's `activities` gain a `TRANSACTION_NOTE_ADDED` entry.\n\nNotes created with an API key have no author: `actor_email` and `actor_name` are `null`, so put the author in the text if you need it. Every address in `mentioned_emails` must belong to a member of your organization, otherwise the whole request fails with `400`; mentions are stored lower-cased and de-duplicated, and this endpoint does not notify the mentioned members.\n\nEach call creates a new note, so retrying a timed-out request can add it twice: list the notes before retrying. There is no API endpoint to edit or remove a note. A scoped API key needs `transactions: write`; a key limited to some workflows or to approved sessions cannot call this endpoint.", + "operationId": "create_transaction_note", + "x-didit-sdk": { + "resource": "transactions.notes", + "method": "create", + "auth": "api_key", + "server": true + }, "tags": [ - "Regulatory Reports" + "Transactions" ], "parameters": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", + "name": "transaction_id", "in": "path", "required": true, + "description": "Didit transaction UUID: the `uuid` returned by `POST /v3/transactions/` and `GET /v3/transactions/`. Your own `txn_id` is not accepted.", "schema": { "type": "string", "format": "uuid" }, - "description": "Application UUID." - }, - { - "name": "case_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "c1a5e000-0000-4000-8000-000000000001" - }, - "description": "Case UUID." + "example": "abcdef12-3456-7890-abcd-ef1234567890" } ], "requestBody": { - "required": false, + "required": true, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "note" + ], "properties": { - "report_type": { - "type": "string", - "enum": [ - "SAR", - "STR", - "CTR", - "TTR", - "UTR", - "IFT", - "CBR", - "TFR" - ], - "description": "Required unless a template is given; the template's report_type is used otherwise." - }, - "jurisdiction": { - "type": "string", - "minLength": 2, - "maxLength": 2, - "description": "Required unless a template is given." - }, - "template": { + "note": { "type": "string", - "format": "uuid", - "nullable": true, - "description": "Report template UUID; its field_config seeds the draft before case-derived data is merged in." + "minLength": 1, + "description": "Plain-text note. Leading and trailing whitespace is trimmed and a blank note is rejected. No length limit." }, - "transactions": { + "mentioned_emails": { "type": "array", "items": { "type": "string", - "format": "uuid" + "format": "email" }, - "nullable": true, - "description": "Transactions to attach; defaults to the case's linked transactions." - }, - "rentity_id": { - "type": "string", - "nullable": true - }, - "rentity_branch": { - "type": "string", - "nullable": true - }, - "submission_code": { - "type": "string", - "enum": [ - "E", - "M" - ], - "default": "E" - }, - "entity_reference": { - "type": "string", - "nullable": true - }, - "fiu_ref_number": { - "type": "string", - "nullable": true - }, - "currency_code_local": { - "type": "string", - "minLength": 3, - "maxLength": 3, - "nullable": true - }, - "submission_date": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "reporting_person": { - "type": "object" - }, - "location": { - "type": "object" - }, - "reason": { - "type": "string", - "nullable": true - }, - "action": { - "type": "string", - "nullable": true - }, - "activity": { - "type": "string", - "nullable": true - }, - "narrative": { - "type": "object", - "properties": { - "who": { - "type": "string" - }, - "what": { - "type": "string" - }, - "when": { - "type": "string" - }, - "where": { - "type": "string" - }, - "why": { - "type": "string" - }, - "how": { - "type": "string" - } - } - }, - "report_indicators": { - "type": "array", - "items": { - "type": "string" - } - }, - "included_fields": { - "type": "array", - "items": { - "type": "string" - } + "default": [], + "description": "Emails of members of your organization to mention. Each must be a valid email of an organization member." + } + } + }, + "examples": { + "Note with a mention": { + "value": { + "note": "Customer confirmed the withdrawal by phone.", + "mentioned_emails": [ + "analyst@example.com" + ] + } + }, + "Plain note": { + "value": { + "note": "Source of funds document requested by email." } } } @@ -43349,22 +63630,173 @@ { "lang": "curl", "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/regulatory-reports/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"template\": \"7ec00000-0000-4000-8000-000000000006\"}'" + "source": "curl -X POST 'https://verification.didit.me/v3/transactions/abcdef12-3456-7890-abcd-ef1234567890/notes/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"note\": \"Customer confirmed the withdrawal by phone.\", \"mentioned_emails\": [\"analyst@example.com\"]}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\ntransaction_id = 'abcdef12-3456-7890-abcd-ef1234567890'\nresp = requests.post(\n f'https://verification.didit.me/v3/transactions/{transaction_id}/notes/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={\n 'note': 'Customer confirmed the withdrawal by phone.',\n 'mentioned_emails': ['analyst@example.com'],\n },\n timeout=10,\n)\nresp.raise_for_status()\nnote = resp.json()\nprint(note['uuid'], note['created_at'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const transactionId = 'abcdef12-3456-7890-abcd-ef1234567890';\nconst resp = await fetch(`https://verification.didit.me/v3/transactions/${transactionId}/notes/`, {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n note: 'Customer confirmed the withdrawal by phone.',\n mentioned_emails: ['analyst@example.com'],\n }),\n});\nif (!resp.ok) throw new Error(`Create note failed: ${resp.status}`);\nconst note = await resp.json();\nconsole.log(note.uuid, note.created_at);" } ], "responses": { "201": { - "description": "Draft report created.", + "description": "The note was created.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RegulatoryReportDetail" + "$ref": "#/components/schemas/TransactionNote" + }, + "examples": { + "Created": { + "value": { + "uuid": "c1111111-2222-4333-8444-555555555555", + "note": "Customer confirmed the withdrawal by phone.", + "actor_email": null, + "actor_name": null, + "metadata": { + "mentioned_emails": [ + "analyst@example.com" + ] + }, + "created_at": "2026-10-16T10:05:12.481203Z" + } + } } } } }, "400": { - "description": "The referenced template was not found.", + "description": "The body is invalid: `note` missing, null or blank, an invalid address in `mentioned_emails` (errors keyed by list position), or a mentioned address that is not a member of your organization. Nothing is created.", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Field errors keyed by field name.", + "additionalProperties": {} + }, + "examples": { + "Missing note": { + "value": { + "note": [ + "This field is required." + ] + } + }, + "Blank note": { + "value": { + "note": [ + "This field may not be blank." + ] + } + }, + "Invalid email": { + "value": { + "mentioned_emails": { + "0": [ + "Enter a valid email address." + ] + } + } + }, + "Not an organization member": { + "value": { + "mentioned_emails": [ + "Mentioned users must belong to this organization and have access to this application." + ] + } + } + } + } + } + }, + "401": { + "description": "The API key has expired. Only a key created with an expiry date can expire; after that date every request with it answers `401`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "The request is not allowed. A missing or invalid API key gets the generic message (never `401`).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Missing or invalid key": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without transactions write": { + "value": { + "detail": "This API key has no write access to transactions." + } + }, + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." + } + }, + "Outside the key's IP list": { + "value": { + "detail": "This API key can't be used from this IP address." + } + } + } + } + } + }, + "404": { + "description": "No transaction with this UUID exists in the application (never created, deleted, or owned by another application). A `transaction_id` that is not a UUID does not reach the API: it returns a `404` HTML page instead of this JSON body.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Not found": { + "value": { + "detail": "No Transaction matches the given query." + } + } + } + } + } + }, + "429": { + "description": "More than 300 write requests per minute with this API key. Wait the number of seconds in `Retry-After`, then retry.", "content": { "application/json": { "schema": { @@ -43374,333 +63806,325 @@ "type": "string" } } + }, + "examples": { + "Throttled": { + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } + } + } + }, + "headers": { + "Retry-After": { + "description": "Seconds until the current one-minute window resets.", + "schema": { + "type": "integer", + "example": 23 + } + }, + "X-RateLimit-Limit": { + "description": "Requests allowed per minute in this budget.", + "schema": { + "type": "integer" + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the current window.", + "schema": { + "type": "integer" + } + }, + "X-RateLimit-Reset": { + "description": "Unix time when the window resets.", + "schema": { + "type": "integer" } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/": { + "/v3/transactions/{transaction_id}/related/": { "get": { - "summary": "List reports", - "description": "Application-wide FIU report list, optionally filtered by status, report_type, or case.", - "operationId": "list_regulatory_reports", + "summary": "List related transactions", + "description": "List the other transactions in your application that involve the same people or companies as this one. A transaction is related when any of its parties, in any role, has the same `vendor_data` as one of this transaction's parties. The transaction itself is never returned, and parties without `vendor_data` are ignored: when none of this transaction's parties has one, the result is always empty.\n\nResults use the summary shape of `GET /v3/transactions/`, newest first (`txn_date`, then `created_at`). Paginated with `limit` and `offset`; `count` is exact up to 100 and capped at 100 above that, so follow `next` until it is `null`.\n\nA scoped API key needs `transactions: read`; a key limited to some workflows or to approved sessions cannot call this endpoint. Send the same `vendor_data` for the same person or company on every [transaction](https://docs.didit.me/transaction-monitoring/transactions#subject-and-counterparty) to make this list complete.", + "operationId": "list_related_transactions", + "x-didit-sdk": { + "resource": "transactions", + "method": "list_related", + "auth": "api_key", + "server": true + }, "tags": [ - "Regulatory Reports" + "Transactions" ], "parameters": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", + "name": "transaction_id", "in": "path", "required": true, + "description": "Didit transaction UUID: the `uuid` returned by `POST /v3/transactions/` and `GET /v3/transactions/`. Your own `txn_id` is not accepted.", "schema": { "type": "string", "format": "uuid" }, - "description": "Application UUID." + "example": "abcdef12-3456-7890-abcd-ef1234567890" }, { "name": "limit", "in": "query", + "required": false, "schema": { "type": "integer", "default": 50, - "minimum": 1 + "minimum": 1, + "maximum": 1000000 }, - "description": "Page size." + "description": "Number of transactions per page. Defaults to 50. A value that is not a positive integer falls back to 50; a value above 1000000 returns `400`." }, { "name": "offset", "in": "query", + "required": false, "schema": { "type": "integer", - "default": 0, - "minimum": 0 - }, - "description": "Zero-based offset." - }, - { - "name": "status", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "DRAFT", - "VALIDATED", - "FINALIZED" - ] - }, - "description": "Filter by status." - }, - { - "name": "report_type", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "SAR", - "STR", - "CTR", - "TTR", - "UTR", - "IFT", - "CBR", - "TFR" - ] - }, - "description": "Filter by report type." - }, - { - "name": "case", - "in": "query", - "schema": { - "type": "string", - "format": "uuid" + "default": 0, + "minimum": 0, + "maximum": 1000000 }, - "description": "Filter to reports on this case." + "description": "Zero-based position of the first transaction to return. Prefer following `next`. A value that is not a whole number falls back to 0; a value above 1000000 returns `400`." } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X GET 'https://verification.didit.me/v3/transactions/abcdef12-3456-7890-abcd-ef1234567890/related/?limit=50' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\ntransaction_id = 'abcdef12-3456-7890-abcd-ef1234567890'\nresp = requests.get(\n f'https://verification.didit.me/v3/transactions/{transaction_id}/related/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n params={'limit': 50},\n timeout=10,\n)\nresp.raise_for_status()\nfor tx in resp.json()['results']:\n print(tx['uuid'], tx['txn_date'], tx['status'], tx['amount'], tx['currency'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const transactionId = 'abcdef12-3456-7890-abcd-ef1234567890';\nconst resp = await fetch(\n `https://verification.didit.me/v3/transactions/${transactionId}/related/?limit=50`,\n { headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n);\nif (!resp.ok) throw new Error(`Related lookup failed: ${resp.status}`);\nconst page = await resp.json();\nfor (const tx of page.results) console.log(tx.uuid, tx.status, tx.amount, tx.currency);" } ], "responses": { "200": { - "description": "Paginated reports across the application.", + "description": "A page of related transactions, newest first.", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "count", + "next", + "previous", + "results" + ], "properties": { "count": { "type": "integer", - "description": "Number of matching rows, capped at 100 for performance." + "description": "Number of related transactions: exact up to 100, capped at 100 above that. Follow `next` instead of computing pages from it." }, "next": { "type": "string", - "nullable": true + "format": "uri", + "nullable": true, + "description": "Absolute URL of the next page, or `null` on the last page." }, "previous": { "type": "string", - "nullable": true + "format": "uri", + "nullable": true, + "description": "Absolute URL of the previous page, or `null` on the first page." }, "results": { "type": "array", "items": { - "$ref": "#/components/schemas/RegulatoryReportDetail" + "$ref": "#/components/schemas/TransactionSummary" } } } + }, + "examples": { + "One related transaction": { + "value": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "uuid": "abcdef99-3456-4890-abcd-ef1234567890", + "transaction_number": 4087, + "txn_id": "your-txn-2026-10-02-017", + "transaction_type": "finance", + "action_type": "deposit", + "status": "APPROVED", + "source": "LIVE", + "direction": "INBOUND", + "amount": "250", + "currency": "EUR", + "currency_type": "fiat", + "preferred_currency_amount": "270.08", + "preferred_currency_code": "USD", + "payment_details": "Card top-up", + "txn_date": "2026-10-02T14:10:00Z", + "created_at": "2026-10-02T14:10:03.512244Z", + "score": 12, + "severity": "LOW", + "decision_reason_code": null, + "decision_reason_label": null, + "tags": [], + "subject_name": "Maria Garcia", + "subject_vendor_data": "user-12345", + "subject_type": "user", + "subject_uuid": "e1111111-2222-4333-8444-555555555555", + "counterparty_name": null, + "counterparty_vendor_data": null, + "counterparty_country": null, + "subject_country": "ESP", + "assigned_to_name": null, + "travel_rule_status": null, + "application_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "application_name": "Production app", + "organization_id": "11111111-2222-3333-4444-555555555555", + "organization_name": "Example Org" + } + ] + } + }, + "No party has vendor_data": { + "value": { + "count": 0, + "next": null, + "previous": null, + "results": [] + } + } } } } - } - } - }, - "post": { - "summary": "Create standalone report", - "description": "Create a draft FIU report from the standalone Reports page: both case and template are required (unlike the case-scoped create, which can omit template).", - "operationId": "create_standalone_regulatory_report", - "tags": [ - "Regulatory Reports" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "case", - "template" - ], - "properties": { - "case": { - "type": "string", - "format": "uuid" - }, - "template": { - "type": "string", - "format": "uuid" - }, - "report_type": { - "type": "string", - "enum": [ - "SAR", - "STR", - "CTR", - "TTR", - "UTR", - "IFT", - "CBR", - "TFR" - ], - "description": "Required unless a template is given; the template's report_type is used otherwise." - }, - "jurisdiction": { - "type": "string", - "minLength": 2, - "maxLength": 2, - "description": "Required unless a template is given." - }, - "transactions": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - }, - "nullable": true, - "description": "Transactions to attach; defaults to the case's linked transactions." - }, - "rentity_id": { - "type": "string", - "nullable": true - }, - "rentity_branch": { - "type": "string", - "nullable": true - }, - "submission_code": { - "type": "string", - "enum": [ - "E", - "M" - ], - "default": "E" - }, - "entity_reference": { - "type": "string", - "nullable": true - }, - "fiu_ref_number": { - "type": "string", - "nullable": true - }, - "currency_code_local": { - "type": "string", - "minLength": 3, - "maxLength": 3, - "nullable": true - }, - "submission_date": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "reporting_person": { - "type": "object" - }, - "location": { - "type": "object" - }, - "reason": { - "type": "string", - "nullable": true - }, - "action": { - "type": "string", - "nullable": true + "400": { + "description": "`limit` or `offset` is above 1000000. The body is a JSON array of messages, not an object.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "examples": { + "Limit too large": { + "value": [ + "limit must not exceed 1000000." + ] }, - "activity": { - "type": "string", - "nullable": true + "Offset too large": { + "value": [ + "offset must not exceed 1000000." + ] + } + } + } + } + }, + "401": { + "description": "The API key has expired. Only a key created with an expiry date can expire; after that date every request with it answers `401`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "The request is not allowed. A missing or invalid API key gets the generic message (never `401`).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Missing or invalid key": { + "value": { + "detail": "You do not have permission to perform this action." + } }, - "narrative": { - "type": "object", - "properties": { - "who": { - "type": "string" - }, - "what": { - "type": "string" - }, - "when": { - "type": "string" - }, - "where": { - "type": "string" - }, - "why": { - "type": "string" - }, - "how": { - "type": "string" - } + "Scoped key without transactions read": { + "value": { + "detail": "This API key has no read access to transactions." } }, - "report_indicators": { - "type": "array", - "items": { - "type": "string" + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." } }, - "included_fields": { - "type": "array", - "items": { - "type": "string" + "Outside the key's IP list": { + "value": { + "detail": "This API key can't be used from this IP address." } } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"case\": \"c1a5e000-0000-4000-8000-000000000001\", \"template\": \"7ec00000-0000-4000-8000-000000000006\"}'" - } - ], - "responses": { - "201": { - "description": "Draft report created.", + }, + "404": { + "description": "No transaction with this UUID exists in the application (never created, deleted, or owned by another application). A `transaction_id` that is not a UUID does not reach the API: it returns a `404` HTML page instead of this JSON body.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RegulatoryReportDetail" + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Not found": { + "value": { + "detail": "No Transaction matches the given query." + } + } } } } }, - "400": { - "description": "The referenced template was not found.", + "429": { + "description": "More than 600 GET requests per minute with this API key. Wait the number of seconds in `Retry-After`, then retry.", "content": { "application/json": { "schema": { @@ -43710,20 +64134,66 @@ "type": "string" } } + }, + "examples": { + "Throttled": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } + } + } + }, + "headers": { + "Retry-After": { + "description": "Seconds until the current one-minute window resets.", + "schema": { + "type": "integer", + "example": 23 + } + }, + "X-RateLimit-Limit": { + "description": "Requests allowed per minute in this budget.", + "schema": { + "type": "integer" + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the current window.", + "schema": { + "type": "integer" + } + }, + "X-RateLimit-Reset": { + "description": "Unix time when the window resets.", + "schema": { + "type": "integer" } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/": { + "/v3/organization/{organization_id}/application/{application_id}/businesses/{business_session_id}/pdf/": { "get": { - "summary": "Get report", - "description": "Retrieve a regulatory report.", - "operationId": "get_regulatory_report", + "summary": "Download a business session PDF report", + "description": "Render the PDF report of one Business Verification (KYB) session: company registry data, key people and beneficial owners, documents, AML results and the other workflow features, plus the session's tags and review activity. With white-label customization on, the report carries your logo and privacy policy URL. The body is the PDF itself (`application/pdf`), not JSON.\n\nThe session must be `Approved`, `Declined` or `In Review`; any other status returns `403`, so wait for the `status.updated` [webhook](https://docs.didit.me/integration/webhooks) first. The report is rendered on every call and nothing is billed, so store the file instead of fetching it repeatedly. `GET /v3/session/{sessionId}/generate-pdf/` renders the same kind of report by session ID alone.\n\nA PDF report counts as media: a scoped API key needs `businesses: read` and `media: read`, and a key limited to some workflows or to approved sessions cannot call it.", + "operationId": "get_business_session_pdf", + "x-didit-sdk": { + "resource": "businesses", + "method": "generate_session_pdf", + "auth": "api_key", + "server": true, + "scope": "application" + }, "tags": [ - "Regulatory Reports" + "Businesses" ], "parameters": [ { @@ -43734,7 +64204,8 @@ "type": "string", "format": "uuid" }, - "description": "Organization UUID." + "description": "Organization UUID.", + "example": "11111111-2222-3333-4444-555555555555" }, { "name": "application_id", @@ -43744,205 +64215,157 @@ "type": "string", "format": "uuid" }, - "description": "Application UUID." + "description": "Application UUID. Must be the application the API key belongs to: a key used on any other application gets `403`.", + "example": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" }, { - "name": "report_uuid", + "name": "business_session_id", "in": "path", "required": true, + "description": "`session_id` of the Business Verification (KYB) session, as returned when the session was created. A session of another application returns `404`.", "schema": { "type": "string", - "format": "uuid", - "example": "5ec00000-0000-4000-8000-000000000007" + "format": "uuid" }, - "description": "Report UUID." + "example": "22222222-3333-4444-5555-666666666666" } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl --fail -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/businesses/22222222-3333-4444-5555-666666666666/pdf/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n --output business_report.pdf" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\norg_id = '11111111-2222-3333-4444-555555555555'\napp_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'\nbusiness_session_id = '22222222-3333-4444-5555-666666666666'\nresp = requests.get(\n f'https://verification.didit.me/v3/organization/{org_id}/application/{app_id}'\n f'/businesses/{business_session_id}/pdf/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=60,\n)\nresp.raise_for_status()\nwith open(f'business_{business_session_id}.pdf', 'wb') as fh:\n fh.write(resp.content)" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "import { writeFile } from 'node:fs/promises';\n\nconst orgId = '11111111-2222-3333-4444-555555555555';\nconst appId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';\nconst businessSessionId = '22222222-3333-4444-5555-666666666666';\nconst resp = await fetch(\n `https://verification.didit.me/v3/organization/${orgId}/application/${appId}/businesses/${businessSessionId}/pdf/`,\n { headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n);\nif (!resp.ok) throw new Error(`PDF generation failed: ${resp.status}`);\nawait writeFile(`business_${businessSessionId}.pdf`, Buffer.from(await resp.arrayBuffer()));" } ], "responses": { "200": { - "description": "Report detail.", + "description": "The rendered PDF, returned directly as binary `application/pdf`.", + "headers": { + "Content-Disposition": { + "description": "Always `attachment; filename=business_{business_session_id}.pdf`.", + "schema": { + "type": "string", + "example": "attachment; filename=business_22222222-3333-4444-5555-666666666666.pdf" + } + } + }, "content": { - "application/json": { + "application/pdf": { "schema": { - "$ref": "#/components/schemas/RegulatoryReportDetail" + "type": "string", + "format": "binary" } } } - } - } - }, - "patch": { - "summary": "Update report", - "description": "Edit a draft or validated report's fields. Finalized reports cannot be edited (409).", - "operationId": "update_regulatory_report", - "tags": [ - "Regulatory Reports" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "401": { + "description": "The API key has expired. Only a key created with an expiry date can expire; after that date every request with it answers `401`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } }, - { - "name": "report_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "5ec00000-0000-4000-8000-000000000007" - }, - "description": "Report UUID." - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "jurisdiction": { - "type": "string", - "minLength": 2, - "maxLength": 2 - }, - "rentity_id": { - "type": "string", - "nullable": true - }, - "rentity_branch": { - "type": "string", - "nullable": true - }, - "submission_code": { - "type": "string", - "enum": [ - "E", - "M" - ], - "default": "E" - }, - "entity_reference": { - "type": "string", - "nullable": true - }, - "fiu_ref_number": { - "type": "string", - "nullable": true - }, - "currency_code_local": { - "type": "string", - "minLength": 3, - "maxLength": 3, - "nullable": true - }, - "submission_date": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "reporting_person": { - "type": "object" - }, - "location": { - "type": "object" - }, - "reason": { - "type": "string", - "nullable": true + "403": { + "description": "The request is not allowed or the session is not finished. Read `detail`: a session that is not `Approved`, `Declined` or `In Review`, a missing or invalid API key (never `401`), an `application_id` that is not the key's application, a scoped key without `media: read` or `businesses: read`, a key limited to workflows or approved sessions, or a request from outside the key's IP list.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Session not finished": { + "value": { + "detail": "You can only generate a PDF for sessions in review, declined or approved." + } }, - "action": { - "type": "string", - "nullable": true + "Missing or invalid key": { + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } }, - "activity": { - "type": "string", - "nullable": true + "Another application": { + "value": { + "detail": "You do not have permission to perform this action." + } }, - "narrative": { - "type": "object", - "properties": { - "who": { - "type": "string" - }, - "what": { - "type": "string" - }, - "when": { - "type": "string" - }, - "where": { - "type": "string" - }, - "why": { - "type": "string" - }, - "how": { - "type": "string" - } + "Scoped key without media read": { + "value": { + "detail": "This API key has no read access to media." } }, - "report_indicators": { - "type": "array", - "items": { - "type": "string" + "Scoped key without businesses read": { + "value": { + "detail": "This API key has no read access to companies." } }, - "included_fields": { - "type": "array", - "items": { - "type": "string" + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." + } + }, + "Outside the key's IP list": { + "value": { + "detail": "This API key can't be used from this IP address." } } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X PATCH 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"rentity_id\": \"FIU-US-00123\", \"currency_code_local\": \"USD\"}'" - } - ], - "responses": { - "200": { - "description": "Updated report.", + }, + "404": { + "description": "No business session with this ID exists in the application (never created, deleted, or owned by another application). A value that is not a UUID returns a `404` HTML page instead of this JSON body.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RegulatoryReportDetail" + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Not found": { + "value": { + "detail": "Business session not found." + } + } } } } }, - "409": { - "description": "Report is already finalized.", + "429": { + "description": "More than 600 GET requests per minute with this API key. Wait the number of seconds in `Retry-After`, then retry.", "content": { "application/json": { "schema": { @@ -43952,18 +64375,66 @@ "type": "string" } } + }, + "examples": { + "Throttled": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } + } + } + }, + "headers": { + "Retry-After": { + "description": "Seconds until the current one-minute window resets.", + "schema": { + "type": "integer", + "example": 23 + } + }, + "X-RateLimit-Limit": { + "description": "Requests allowed per minute in this budget.", + "schema": { + "type": "integer" + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the current window.", + "schema": { + "type": "integer" + } + }, + "X-RateLimit-Reset": { + "description": "Unix time when the window resets.", + "schema": { + "type": "integer" } } } } - } - }, - "delete": { - "summary": "Delete report", - "description": "Delete a draft or validated report. Finalized reports cannot be deleted (409).", - "operationId": "delete_regulatory_report", + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/pdf/": { + "get": { + "summary": "Download a transaction PDF report", + "description": "Render one monitored transaction as a PDF report: overview and verdict, parties and payment methods, custom properties, alerts and rule runs, screening and Travel Rule results, the network summary, the cost breakdown, up to 50 related transactions and the 20 most recent activity entries. The body is the PDF itself (`application/pdf`), not JSON.\n\nThe report is rendered from the transaction's current data on every call, so request it again after a status change. Nothing is stored and nothing is billed. Rendering can take a few seconds, so use a generous client timeout.\n\nThis endpoint lives on an application-scoped path: put your organization ID and the ID of the application your API key belongs to in the URL ([find your organization ID](https://docs.didit.me/getting-started/api-authentication#find-your-organization-id)). A PDF report counts as media: a scoped API key needs `media: read` and `transactions: read`, and a key limited to some workflows or to approved sessions cannot call it.", + "operationId": "get_transaction_pdf", + "x-didit-sdk": { + "resource": "transactions", + "method": "generate_pdf", + "auth": "api_key", + "server": true, + "scope": "application" + }, "tags": [ - "Regulatory Reports" + "Transactions" ], "parameters": [ { @@ -43974,7 +64445,8 @@ "type": "string", "format": "uuid" }, - "description": "Organization UUID." + "description": "Organization UUID.", + "example": "11111111-2222-3333-4444-555555555555" }, { "name": "application_id", @@ -43984,33 +64456,152 @@ "type": "string", "format": "uuid" }, - "description": "Application UUID." + "description": "Application UUID. Must be the application the API key belongs to: a key used on any other application gets `403`.", + "example": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" }, { - "name": "report_uuid", + "name": "transaction_uuid", "in": "path", "required": true, + "description": "Didit transaction UUID: the `uuid` returned by `POST /v3/transactions/` and `GET /v3/transactions/`. Your own `txn_id` is not accepted.", "schema": { "type": "string", - "format": "uuid", - "example": "5ec00000-0000-4000-8000-000000000007" + "format": "uuid" }, - "description": "Report UUID." + "example": "abcdef12-3456-7890-abcd-ef1234567890" } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X DELETE 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl --fail -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/transactions/abcdef12-3456-7890-abcd-ef1234567890/pdf/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n --output transaction_report.pdf" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\norg_id = '11111111-2222-3333-4444-555555555555'\napp_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'\ntransaction_uuid = 'abcdef12-3456-7890-abcd-ef1234567890'\nresp = requests.get(\n f'https://verification.didit.me/v3/organization/{org_id}/application/{app_id}'\n f'/transactions/{transaction_uuid}/pdf/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=60,\n)\nresp.raise_for_status()\nwith open(f'transaction_{transaction_uuid}.pdf', 'wb') as fh:\n fh.write(resp.content)" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "import { writeFile } from 'node:fs/promises';\n\nconst orgId = '11111111-2222-3333-4444-555555555555';\nconst appId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';\nconst transactionUuid = 'abcdef12-3456-7890-abcd-ef1234567890';\nconst resp = await fetch(\n `https://verification.didit.me/v3/organization/${orgId}/application/${appId}/transactions/${transactionUuid}/pdf/`,\n { headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n);\nif (!resp.ok) throw new Error(`PDF generation failed: ${resp.status}`);\nawait writeFile(`transaction_${transactionUuid}.pdf`, Buffer.from(await resp.arrayBuffer()));" } ], "responses": { - "204": { - "description": "Report deleted." + "200": { + "description": "The rendered PDF, returned directly as binary `application/pdf`.", + "headers": { + "Content-Disposition": { + "description": "Always `attachment; filename=transaction_{transaction_uuid}.pdf`.", + "schema": { + "type": "string", + "example": "attachment; filename=transaction_abcdef12-3456-7890-abcd-ef1234567890.pdf" + } + } + }, + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + } + } + } }, - "409": { - "description": "Report is already finalized.", + "401": { + "description": "The API key has expired. Only a key created with an expiry date can expire; after that date every request with it answers `401`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "The request is not allowed. Read `detail`: a missing or invalid API key (never `401`), an `application_id` that is not the key's application, a scoped key without `media: read` or `transactions: read`, a key limited to workflows or approved sessions, or a request from outside the key's IP list.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Missing or invalid key": { + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + }, + "Another application": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without media read": { + "value": { + "detail": "This API key has no read access to media." + } + }, + "Scoped key without transactions read": { + "value": { + "detail": "This API key has no read access to transactions." + } + }, + "Key limited to workflows or approved sessions": { + "value": { + "detail": "This API key only reaches sessions inside its workflow or status limits, so it can't use this endpoint." + } + }, + "Outside the key's IP list": { + "value": { + "detail": "This API key can't be used from this IP address." + } + } + } + } + } + }, + "404": { + "description": "No transaction with this UUID exists in the application (never created, deleted, or owned by another application). A `transaction_uuid` that is not a UUID does not reach the API: it returns a `404` HTML page instead of this JSON body.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Not found": { + "value": { + "detail": "No Transaction matches the given query." + } + } + } + } + } + }, + "429": { + "description": "More than 600 GET requests per minute with this API key. Wait the number of seconds in `Retry-After`, then retry.", "content": { "application/json": { "schema": { @@ -44020,20 +64611,66 @@ "type": "string" } } + }, + "examples": { + "Throttled": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } + } + } + }, + "headers": { + "Retry-After": { + "description": "Seconds until the current one-minute window resets.", + "schema": { + "type": "integer", + "example": 23 + } + }, + "X-RateLimit-Limit": { + "description": "Requests allowed per minute in this budget.", + "schema": { + "type": "integer" + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the current window.", + "schema": { + "type": "integer" + } + }, + "X-RateLimit-Reset": { + "description": "Unix time when the window resets.", + "schema": { + "type": "integer" } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/validate/": { - "post": { - "summary": "Validate report", - "description": "Check the report against the obligatory goAML header fields (reporting entity ID, submission code, local currency, action taken, submission date), plus at least one attached transaction and a 2-letter jurisdiction, without generating any files. Also stores the result on the report: validation_errors and validated_at are updated, and a non-finalized report's status moves to VALIDATED (no errors) or back to DRAFT (errors).", - "operationId": "validate_regulatory_report", + "/v3/organization/{organization_id}/application/{application_id}/analytics/": { + "get": { + "summary": "Get application analytics", + "description": "Read the numbers behind the Business Console [analytics dashboard](https://docs.didit.me/console/analytics) for a date range. Without `scope` and `widget_keys` the response is the user verification (KYC) summary (`ApplicationAnalyticsSummary`): daily session counts, status totals, conversion rate, the same figures for the preceding period of equal length, locations, demographics, devices, workflow funnels, active verification time, resubmissions, warnings and time series. With `scope` or `widget_keys` it is the widgets of one dashboard tab (`ApplicationAnalyticsWidgetResponse`): `kyc`, `kyb`, `transactions` or `home`.\n\nSession counts are by session creation date (UTC) and reflect each session's current status. They come from daily rollups refreshed in the background, so the newest activity can take a short while to appear. Invalid workflow IDs and versions are ignored rather than rejected. Nothing is billed.\n\nOnly a full-access API key can call this endpoint: every scoped key gets `403`, whatever access it holds, and so does a key of another application. See [API keys](https://docs.didit.me/console/api-keys).", + "operationId": "get_application_analytics", + "x-didit-sdk": { + "resource": "analytics", + "method": "retrieve", + "auth": "api_key", + "server": true, + "scope": "application" + }, "tags": [ - "Regulatory Reports" + "Analytics" ], "parameters": [ { @@ -44044,7 +64681,8 @@ "type": "string", "format": "uuid" }, - "description": "Organization UUID." + "description": "Organization UUID.", + "example": "11111111-2222-3333-4444-555555555555" }, { "name": "application_id", @@ -44054,135 +64692,639 @@ "type": "string", "format": "uuid" }, - "description": "Application UUID." + "description": "Application UUID. Must be the application the API key belongs to: a key used on any other application gets `403`.", + "example": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" }, { - "name": "report_uuid", - "in": "path", - "required": true, + "name": "start_date", + "in": "query", + "required": false, "schema": { "type": "string", - "format": "uuid", - "example": "5ec00000-0000-4000-8000-000000000007" + "format": "date", + "example": "2026-10-01" }, - "description": "Report UUID." + "description": "First day of the range, `YYYY-MM-DD`, inclusive (UTC). Send it together with `end_date`: when either is missing, both are ignored and the range runs from 30 days before today through today." + }, + { + "name": "end_date", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date", + "example": "2026-10-15" + }, + "description": "Last day of the range, `YYYY-MM-DD`, inclusive (UTC). A future date counts as today. The range may not exceed 730 days." + }, + { + "name": "scope", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "home", + "kyc", + "kyb", + "transactions" + ] + }, + "description": "Dashboard tab to return widgets for (case-insensitive). Omit it, together with `widget_keys`, for the KYC summary. Sending only `widget_keys` implies `kyc`. `home` accepts any KYC, KYB or transaction widget and defaults to the application's saved home layout." + }, + { + "name": "widget_keys", + "in": "query", + "required": false, + "schema": { + "type": "string", + "example": "tx_count_total,tx_count_daily" + }, + "description": "Comma-separated widget keys, answered in this order. Keys the scope does not accept are dropped; when none is left you get the scope's defaults. KYC: `kyc_sessions_volume`, `kyc_warning_insights`, `kyc_id_locations`, `kyc_ip_locations`, `kyc_workflow_funnel`, `kyc_average_verification_time`, `kyc_conversion_rate`, `kyc_conversion_by_country`, `kyc_resubmission_insights`, `kyc_original_status_distribution`, `kyc_database_validation_coverage`, `kyc_database_validation_latency`, `kyc_demographics`, `kyc_device_details`. KYB: `kyb_sessions_volume`, `kyb_workflow_funnel`, `kyb_average_verification_time`, `kyb_ip_locations`, `kyb_business_country_distribution`, `kyb_warning_insights`, `kyb_conversion_rate`, `kyb_conversion_by_country`, `kyb_industry_breakdown`, `kyb_resubmission_insights`, `kyb_original_status_distribution`, `kyb_registry_coverage`, `kyb_registry_data_completeness`, `kyb_device_details`. Transactions: `tx_count_total`, `tx_count_daily`, `tx_volume_total_preferred_currency`, `tx_volume_daily_preferred_currency`, `tx_average_amount`, `tx_average_amount_daily`, `tx_transaction_category_breakdown`, `tx_action_type_breakdown`, `tx_status_volume_trend`, `tx_user_ip_or_country_distribution`, `tx_tag_breakdown`, `tx_top_rules_triggered`, `tx_team_member_performance`, `tx_score_histogram`." + }, + { + "name": "workflow_ids", + "in": "query", + "required": false, + "schema": { + "type": "string", + "example": "33333333-4444-5555-6666-777777777777" + }, + "description": "Comma-separated workflow IDs. Limits the KYC summary and the KYC and KYB widgets to those workflows. Values that are not UUIDs are ignored; when none is valid the filter is dropped. The summary also accepts `workflow_id`." + }, + { + "name": "versions", + "in": "query", + "required": false, + "schema": { + "type": "string", + "example": "3,4" + }, + "description": "Comma-separated workflow version numbers. Applies only together with `workflow_ids`; values that are not whole numbers are ignored. `version` is accepted as an alias." + }, + { + "name": "branch_path_ids", + "in": "query", + "required": false, + "schema": { + "type": "string", + "example": "default" + }, + "description": "Comma-separated branch path IDs. Applies only together with `workflow_ids`, and only to KYC figures. Figures are currently recorded only for `default` (all paths combined), so any other value returns empty figures. `branch_path_id` is accepted as an alias." + }, + { + "name": "tag", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Transaction widgets only: comma-separated tag UUIDs or tag names." + }, + { + "name": "transaction_type", + "in": "query", + "required": false, + "schema": { + "type": "string", + "example": "finance" + }, + "description": "Transaction widgets only: comma-separated transaction categories." + }, + { + "name": "action_type", + "in": "query", + "required": false, + "schema": { + "type": "string", + "example": "withdrawal" + }, + "description": "Transaction widgets only: comma-separated action types (case-insensitive)." } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/validate/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/analytics/?start_date=2026-10-01&end_date=2026-10-15' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\norg_id = '11111111-2222-3333-4444-555555555555'\napp_id = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'\nresp = requests.get(\n f'https://verification.didit.me/v3/organization/{org_id}/application/{app_id}/analytics/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n params={'start_date': '2026-10-01', 'end_date': '2026-10-15'},\n timeout=10,\n)\nresp.raise_for_status()\ndata = resp.json()\nprint(data['request_breakdown']['total_approved'], data['conversion_rate'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const orgId = '11111111-2222-3333-4444-555555555555';\nconst appId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';\nconst params = new URLSearchParams({ start_date: '2026-10-01', end_date: '2026-10-15' });\nconst resp = await fetch(\n `https://verification.didit.me/v3/organization/${orgId}/application/${appId}/analytics/?${params}`,\n { headers: { 'x-api-key': process.env.DIDIT_API_KEY } },\n);\nif (!resp.ok) throw new Error(`Analytics failed: ${resp.status}`);\nconst data = await resp.json();\nconsole.log(data.request_breakdown.total_approved, data.conversion_rate);" } ], "responses": { "200": { - "description": "Validation result.", + "description": "The KYC summary, or the requested widgets when `scope` or `widget_keys` is sent.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationAnalyticsSummary" + }, + { + "$ref": "#/components/schemas/ApplicationAnalyticsWidgetResponse" + } + ] + }, + "examples": { + "KYC summary": { + "value": { + "grouping_mode": "day", + "data_points": [ + { + "rounded_date": "2026-10-14", + "total": 12, + "approved": 8, + "declined": 1, + "canceled": 1, + "pending": 2, + "in_review": 1, + "expired": 1, + "abandoned": 0, + "in_progress": 1, + "not_started": 0, + "manually_approved": 1, + "manually_declined": 0 + }, + { + "rounded_date": "2026-10-15", + "total": 10, + "approved": 6, + "declined": 2, + "canceled": 0, + "pending": 2, + "in_review": 2, + "expired": 0, + "abandoned": 0, + "in_progress": 0, + "not_started": 0, + "manually_approved": 0, + "manually_declined": 1 + } + ], + "request_breakdown": { + "total_approved": 14, + "total_in_review": 3, + "total_declined": 3, + "total_canceled": 1, + "total_pending": 4, + "total_expired": 1, + "total_abandoned": 0, + "total_in_progress": 1, + "total_not_started": 0, + "total_manually_approved": 1, + "total_manually_declined": 1 + }, + "conversion_rate": 70.0, + "location_stats": { + "countries": [ + { + "session_count": 15, + "country": "Spain", + "country_code": "ES" + }, + { + "session_count": 7, + "country": "Portugal", + "country_code": "PT" + } + ], + "ip_countries": [ + { + "session_count": 15, + "country": "Spain", + "country_code": "ES" + }, + { + "session_count": 7, + "country": "Portugal", + "country_code": "PT" + } + ], + "id_countries": [ + { + "country": "Spain", + "country_code": "ES", + "session_count": 14 + }, + { + "country": "Portugal", + "country_code": "PT", + "session_count": 6 + } + ], + "conversion_by_country": [ + { + "country": "Spain", + "country_code": "ES", + "finished": 13, + "approved": 10 + }, + { + "country": "Portugal", + "country_code": "PT", + "finished": 6, + "approved": 4 + } + ], + "cities": [ + { + "session_count": 9, + "location": "Madrid, Spain", + "latitude": 40.4168, + "longitude": -3.7038 + } + ] + }, + "demographic_stats": { + "age_distribution": { + "under_18": 0, + "18_24": 5, + "25_34": 8, + "35_44": 4, + "45_64": 2, + "65_plus": 1 + }, + "gender_distribution": { + "M": 11, + "F": 9 + } + }, + "device_details": { + "device_models": [ + { + "device": "Apple", + "count": 12 + }, + { + "device": "Android", + "count": 10 + } + ], + "browsers": [ + { + "browser": "Safari", + "count": 12 + }, + { + "browser": "Chrome", + "count": 10 + } + ], + "operating_systems": [ + { + "os": "iOS", + "count": 12 + }, + { + "os": "Android", + "count": 10 + } + ] + }, + "workflow_funnel_stats": { + "33333333-4444-5555-6666-777777777777": { + "total": 22, + "steps": { + "OCR": { + "reached": 21 + }, + "LIVENESS": { + "reached": 19 + }, + "FACE_MATCH": { + "reached": 19 + }, + "AML": { + "reached": 18 + } + } + } + }, + "verification_time_stats": { + "overall_avg_seconds": 64.0, + "overall_count": 18, + "per_feature": { + "OCR": { + "avg_seconds": 41.2, + "total_seconds": 741.6, + "count": 18 + }, + "LIVENESS": { + "avg_seconds": 22.8, + "total_seconds": 410.4, + "count": 18 + } + }, + "per_sub_step": { + "OCR": { + "front_id_photo": { + "avg_seconds": 18.3, + "total_seconds": 329.4, + "count": 18 + } + } + } + }, + "resubmission_stats": { + "first_pass": 17, + "second": 2, + "third_plus": 1, + "abandoned": 0 + }, + "warning_stats": { + "ID_VERIFICATION": { + "total": 21, + "risks": { + "DOCUMENT_EXPIRED": { + "error": 1, + "warning": 0, + "information": 0 + } + } + }, + "LIVENESS": { + "total": 19, + "risks": { + "LOW_LIVENESS_SCORE": { + "error": 0, + "warning": 2, + "information": 0 + } + } + } + }, + "original_status_stats": { + "approved": 13, + "in_review": 5, + "declined": 2 + }, + "demographic_time_series": [ + { + "date": "2026-10-14", + "age_distribution": { + "under_18": 0, + "18_24": 3, + "25_34": 4, + "35_44": 2, + "45_64": 1, + "65_plus": 0 + }, + "gender_distribution": { + "M": 6, + "F": 4 + } + }, + { + "date": "2026-10-15", + "age_distribution": { + "under_18": 0, + "18_24": 2, + "25_34": 4, + "35_44": 2, + "45_64": 1, + "65_plus": 1 + }, + "gender_distribution": { + "M": 5, + "F": 5 + } + } + ], + "device_time_series": [ + { + "date": "2026-10-14", + "devices": { + "Apple": 7, + "Android": 5 + }, + "browsers": { + "Safari": 7, + "Chrome": 5 + }, + "operating_systems": { + "iOS": 7, + "Android": 5 + } + }, + { + "date": "2026-10-15", + "devices": { + "Apple": 5, + "Android": 5 + }, + "browsers": { + "Safari": 5, + "Chrome": 5 + }, + "operating_systems": { + "iOS": 5, + "Android": 5 + } + } + ], + "verification_time_time_series": [ + { + "date": "2026-10-14", + "overall_avg": 66.1, + "per_feature": { + "OCR": 42.0, + "LIVENESS": 24.1 + } + }, + { + "date": "2026-10-15", + "overall_avg": 61.5, + "per_feature": { + "OCR": 40.2, + "LIVENESS": 21.3 + } + } + ], + "resubmission_time_series": [ + { + "date": "2026-10-14", + "first_pass": 9, + "second": 1, + "third_plus": 0, + "abandoned": 0 + }, + { + "date": "2026-10-15", + "first_pass": 8, + "second": 1, + "third_plus": 1, + "abandoned": 0 + } + ], + "original_status_time_series": [ + { + "date": "2026-10-14", + "approved": 7, + "in_review": 3, + "declined": 1 + }, + { + "date": "2026-10-15", + "approved": 6, + "in_review": 2, + "declined": 1 + } + ], + "previous_data_points": [ + { + "rounded_date": "2026-10-12", + "total": 9, + "approved": 6, + "declined": 1, + "canceled": 1, + "pending": 1 + }, + { + "rounded_date": "2026-10-13", + "total": 11, + "approved": 7, + "declined": 2, + "canceled": 0, + "pending": 2 + } + ], + "previous_request_breakdown": { + "total_approved": 13, + "total_in_review": 2, + "total_declined": 3, + "total_canceled": 1, + "total_pending": 3 + } + } + }, + "Transaction widgets": { + "value": { + "scope": "transactions", + "available_widget_keys": [ + "tx_count_total", + "tx_count_daily", + "tx_volume_total_preferred_currency", + "tx_volume_daily_preferred_currency", + "tx_average_amount", + "tx_average_amount_daily", + "tx_transaction_category_breakdown", + "tx_action_type_breakdown", + "tx_status_volume_trend", + "tx_user_ip_or_country_distribution", + "tx_tag_breakdown", + "tx_top_rules_triggered", + "tx_team_member_performance", + "tx_score_histogram" + ], + "requested_widget_keys": [ + "tx_count_total", + "tx_count_daily" + ], + "widgets": { + "tx_count_total": { + "kind": "metric", + "title": "Transaction count", + "value": 42 + }, + "tx_count_daily": { + "kind": "time_series", + "title": "Transaction count", + "items": [ + { + "date": "2026-10-14", + "value": 20 + }, + { + "date": "2026-10-15", + "value": 22 + } + ], + "previous_items": [ + { + "date": "2026-10-12", + "value": 18 + }, + { + "date": "2026-10-13", + "value": 15 + } + ] + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid date range or scope. The body is an object keyed by the offending parameter, with a string message.", "content": { "application/json": { "schema": { "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "DRAFT", - "VALIDATED", - "FINALIZED" - ] - }, - "is_valid": { - "type": "boolean" - }, - "errors": { - "type": "array", - "description": "Empty when valid.", - "items": { - "type": "object", - "properties": { - "field": { - "type": "string" - }, - "message": { - "type": "string" - } - } - } + "additionalProperties": { + "type": "string" + } + }, + "examples": { + "Bad date format": { + "value": { + "start_date": "Invalid date format. Use YYYY-MM-DD (e.g., 2024-01-15)." + } + }, + "Start after end": { + "value": { + "start_date": "Start date must be before or equal to end date." + } + }, + "Range too long": { + "value": { + "date_range": "Date range cannot exceed 730 days (2 years)." + } + }, + "Unknown scope": { + "value": { + "scope": "Unsupported analytics scope." } } } } } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/finalize/": { - "post": { - "summary": "Finalize report", - "description": "Re-validate, then render the goAML XML and PDF and upload both to S3. On SAR/STR/TFR reports, this also moves the case's transaction alerts from Pending SAR to SAR filed.", - "operationId": "finalize_regulatory_report", - "tags": [ - "Regulatory Reports" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "report_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "5ec00000-0000-4000-8000-000000000007" - }, - "description": "Report UUID." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/finalize/' \\\n -H 'x-api-key: YOUR_API_KEY'" }, - { - "lang": "python", - "label": "Python", - "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/finalize/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=30,\n)\nif resp.status_code == 400:\n print('Validation failed:', resp.json()['errors'])\nelse:\n resp.raise_for_status()\n report = resp.json()\n print(report['status'], report['report_number'])" - } - ], - "responses": { - "200": { - "description": "Report finalized; xml_s3_key and pdf_s3_key are set.", + "401": { + "description": "The API key has expired. Only a key created with an expiry date can expire; after that date every request with it answers `401`.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RegulatoryReportDetail" + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + }, + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } } } } }, - "400": { - "description": "Report failed validation.", + "403": { + "description": "The request is not allowed. Read `detail`: a missing or invalid API key (never `401`), a key of another application, any scoped key, or a request from outside the key's IP list.", "content": { "application/json": { "schema": { @@ -44190,28 +65332,36 @@ "properties": { "detail": { "type": "string" - }, - "errors": { - "type": "array", - "items": { - "type": "object", - "properties": { - "field": { - "type": "string" - }, - "message": { - "type": "string" - } - } - } + } + } + }, + "examples": { + "Missing or invalid key": { + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + }, + "Another application": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key": { + "value": { + "detail": "This API key has no access to this endpoint." + } + }, + "Outside the key's IP list": { + "value": { + "detail": "This API key can't be used from this IP address." } } } } } }, - "409": { - "description": "Report is not in a finalizable state.", + "429": { + "description": "More than 600 GET requests per minute with this API key. Wait the number of seconds in `Retry-After`, then retry.", "content": { "application/json": { "schema": { @@ -44221,1493 +65371,2341 @@ "type": "string" } } + }, + "examples": { + "Throttled": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } + } + } + }, + "headers": { + "Retry-After": { + "description": "Seconds until the current one-minute window resets.", + "schema": { + "type": "integer", + "example": 23 + } + }, + "X-RateLimit-Limit": { + "description": "Requests allowed per minute in this budget.", + "schema": { + "type": "integer" + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the current window.", + "schema": { + "type": "integer" + } + }, + "X-RateLimit-Reset": { + "description": "Unix time when the window resets.", + "schema": { + "type": "integer" } } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/download-url/": { + "/v3/session/{sessionId}/webhooks/": { "get": { - "summary": "Get report download URL", - "description": "Mint a presigned URL for a finalized report's goAML XML or PDF.", - "operationId": "get_regulatory_report_download_url", + "summary": "List session webhook deliveries", + "description": "List every webhook delivery Didit recorded for one verification session, user (KYC) or business (KYB), newest first, as a plain JSON array (not paginated). Each entry is one attempt to one destination: the first send, each automatic [retry](https://docs.didit.me/integration/webhooks#retry-policy) and each resend are separate entries with their own `uuid`, and retries and resends keep the payload's `event_id`. `request_header` and `request_body` are JSON text; parse them to read the signatures and the payload. Delivery records are available for about 30 days, so an empty array means no webhook has been sent for the session yet or every delivery is older than that. A scoped API key needs `sessions: read` and `decisions: read`.", + "operationId": "get_v3_session_webhooks", + "x-didit-sdk": { + "resource": "sessions.webhooks", + "method": "list", + "auth": "api_key", + "server": true + }, "tags": [ - "Regulatory Reports" + "Sessions" ], "parameters": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", + "name": "sessionId", "in": "path", "required": true, + "description": "UUID of the verification session, user (KYC) or business (KYB): the `session_id` returned by `POST /v3/session/`. The server resolves it against both session types.\n\n**Must be a valid UUID.** The route only matches well-formed UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{\"detail\": ...}` envelope shown below.", "schema": { "type": "string", "format": "uuid" }, - "description": "Application UUID." - }, - { - "name": "report_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "5ec00000-0000-4000-8000-000000000007" - }, - "description": "Report UUID." - }, - { - "name": "file", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "pdf", - "xml" - ], - "default": "pdf" - }, - "description": "Which file to download." + "example": "22222222-3333-4444-5555-666666666666" } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/regulatory-reports/5ec00000-0000-4000-8000-000000000007/download-url/?file=xml' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X GET 'https://verification.didit.me/v3/session/22222222-3333-4444-5555-666666666666/webhooks/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import json\nimport os\n\nimport requests\n\nsession_id = '22222222-3333-4444-5555-666666666666'\nresp = requests.get(\n f'https://verification.didit.me/v3/session/{session_id}/webhooks/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nfor delivery in resp.json():\n payload = json.loads(delivery['request_body'])\n print(delivery['created_at'], delivery['webhook_type'], delivery['response_status'], payload['event_id'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const sessionId = '22222222-3333-4444-5555-666666666666';\nconst resp = await fetch(`https://verification.didit.me/v3/session/${sessionId}/webhooks/`, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Request failed: ${resp.status}`);\nconst deliveries = await resp.json();\nfor (const delivery of deliveries) {\n const payload = JSON.parse(delivery.request_body);\n console.log(delivery.created_at, delivery.webhook_type, delivery.response_status, payload.event_id);\n}" } ], "responses": { "200": { - "description": "Presigned download URL.", + "description": "Every delivery recorded for the session, newest first.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionWebhookDelivery" + } + }, + "examples": { + "Failed delivery and its successful retry": { + "value": [ + { + "uuid": "66666666-7777-8888-9999-000000000002", + "session_id": "22222222-3333-4444-5555-666666666666", + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": 200, + "status": "In Progress", + "webhook_type": "status.updated", + "request_header": "{\"Content-Type\": \"application/json\", \"X-Signature\": \"9188851d160b5b3aacfc946e4dde1f061b7e568662518942c927ddaf507f533d\", \"X-Signature-V2\": \"381236af514d3fb538579365ac5a24f2464ce1b8224c6e770c60d8201c43ecc5\", \"X-Signature-Simple\": \"4740131dc38f29e78a8508371979be74967d5307464494bd3148a03b4d6c8716\", \"X-Timestamp\": \"1792056139\", \"User-Agent\": \"DiditWebhook/2.0 +https://didit.me\"}", + "request_body": "{\"application_id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"created_at\":1792056057,\"environment\":\"live\",\"event_id\":\"33333333-4444-5555-6666-777777777777\",\"sandbox_scenario\":null,\"session_id\":\"22222222-3333-4444-5555-666666666666\",\"status\":\"In Progress\",\"timestamp\":1792056139,\"vendor_data\":\"user-123\",\"webhook_type\":\"status.updated\",\"workflow_id\":\"44444444-5555-6666-7777-888888888888\",\"workflow_version\":3}", + "created_at": "2026-10-15T09:22:19.104233Z" + }, + { + "uuid": "66666666-7777-8888-9999-000000000001", + "session_id": "22222222-3333-4444-5555-666666666666", + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": 503, + "status": "In Progress", + "webhook_type": "status.updated", + "request_header": "{\"Content-Type\": \"application/json\", \"X-Signature\": \"a5e02a03404e1337a0198e6be15d34abbd053c658a0e51e5d0e78554213ef29b\", \"X-Signature-V2\": \"927e7cb770c94ce745939bff73f04aa200015f3bc64795ba46c2aaf78a6cedc5\", \"X-Signature-Simple\": \"e37b0d4c716c72bb5ae7127b9cc455d28ab4686e9e22312b4ec2327409a73302\", \"X-Timestamp\": \"1792056078\", \"User-Agent\": \"DiditWebhook/2.0 +https://didit.me\"}", + "request_body": "{\"application_id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"created_at\":1792056057,\"environment\":\"live\",\"event_id\":\"33333333-4444-5555-6666-777777777777\",\"sandbox_scenario\":null,\"session_id\":\"22222222-3333-4444-5555-666666666666\",\"status\":\"In Progress\",\"timestamp\":1792056078,\"vendor_data\":\"user-123\",\"webhook_type\":\"status.updated\",\"workflow_id\":\"44444444-5555-6666-7777-888888888888\",\"workflow_version\":3}", + "created_at": "2026-10-15T09:21:18.412345Z" + } + ] + }, + "No webhook sent yet": { + "value": [] + } + } + } + } + }, + "401": { + "description": "The API key has expired: it was created with an expiry date that has passed. A missing, invalid or revoked key gets `403`, not `401`.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "download_url": { - "type": "string", - "format": "uri" - }, - "file": { + "detail": { "type": "string", - "enum": [ - "pdf", - "xml" - ] + "description": "Human-readable error message." + } + } + }, + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." } } } } } }, - "409": { - "description": "Report is not finalized yet.", + "403": { + "description": "The request is not allowed. No `x-api-key`, or an invalid or revoked key, gets `Authentication credentials were not provided or are invalid.` A key of another application gets `You do not have permission to perform this action.` A scoped key without the access this endpoint needs gets a message naming the missing access, and a key used from outside its IP allowlist gets `This API key can't be used from this IP address.` An unknown `sessionId` returns `404` before the credential is checked.", "content": { "application/json": { "schema": { "type": "object", "properties": { "detail": { - "type": "string" - }, - "status": { - "type": "string" + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Missing or invalid key": { + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + }, + "Key of another application": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without sessions read": { + "value": { + "detail": "This API key has no read access to sessions." + } + }, + "Scoped key without decisions read": { + "value": { + "detail": "This API key has no read access to decisions." + } + }, + "Outside the IP allowlist": { + "value": { + "detail": "This API key can't be used from this IP address." } } } } } - } - } - } - }, - "/v3/transactions/rules/": { - "get": { - "summary": "List rules", - "description": "List the application's transaction-monitoring rules: your custom rules plus any library presets you have installed. Filter by source, category, mode, bundle or free-text search; order with `ordering` (run_count, approved_pct, reviewed_pct, declined_pct, latest_triggered_at, title, created_at, source, each with an optional `-` prefix).", - "operationId": "listTransactionRules", - "tags": [ - "Transactions" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], - "parameters": [ - { - "name": "source", - "in": "query", - "required": false, - "schema": { - "type": "string", - "enum": [ - "PRESET", - "CUSTOM" - ] - }, - "description": "PRESET or CUSTOM" - }, - { - "name": "category", - "in": "query", - "required": false, - "schema": { - "type": "string" - }, - "description": "Filter by rule category" - }, - { - "name": "mode", - "in": "query", - "required": false, - "schema": { - "type": "string", - "enum": [ - "ACTIVE", - "DISABLED", - "TEST" - ] - }, - "description": "ACTIVE, DISABLED or TEST" - }, - { - "name": "bundle", - "in": "query", - "required": false, - "schema": { - "type": "string" - }, - "description": "Filter presets by bundle" - }, - { - "name": "search", - "in": "query", - "required": false, - "schema": { - "type": "string" - }, - "description": "Case-insensitive match against title and description" - }, - { - "name": "ordering", - "in": "query", - "required": false, - "schema": { - "type": "string" - }, - "description": "Comma-separated ordering fields, default source,-run_count" - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer" - }, - "description": "Page size" }, - { - "name": "offset", - "in": "query", - "required": false, - "schema": { - "type": "integer" - }, - "description": "Page offset" - } - ], - "responses": { - "200": { - "description": "Paginated rules.", + "404": { + "description": "The session does not exist or has been deleted (`Not found.`, returned before the credential is checked), or it is outside the workflow or status limits of a scoped key (`Session not found.`).", "content": { "application/json": { "schema": { "type": "object", "properties": { - "count": { - "type": "integer" - }, - "next": { - "type": "string", - "nullable": true - }, - "previous": { + "detail": { "type": "string", - "nullable": true - }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TransactionRuleDetail" - } + "description": "Human-readable error message." } } - } - } - } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/transactions/rules/?source=CUSTOM&ordering=-run_count' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ] - }, - "post": { - "summary": "Create rule", - "description": "Create a CUSTOM transaction-monitoring rule. Titles are unique per application (duplicate -> 400). A `change_status` action targeting `AWAITING_USER` must carry `workflow_id`. Rules created in ACTIVE mode apply to new transactions immediately - the recommended workflow is create in TEST mode, backtest, then switch mode to ACTIVE.", - "operationId": "createTransactionRule", - "tags": [ - "Transactions" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TransactionRuleCreateRequest" - }, - "example": { - "title": "Structuring - inbound (custom)", - "description": "20+ inbound EUR transfers, all under 10,000 EUR, 30 days", - "category": "finance", - "mode": "TEST", - "evaluation_mode": "ALL", - "scope": { - "transaction_types": [ - "finance" - ], - "directions": [ - "INBOUND" - ] }, - "conditions": [ - { - "field": "currency", - "operator": "eq", - "value": "EUR" - }, - { - "field": "amount", - "operator": "lt", - "value": 10000 - } - ], - "aggregation": [ - { - "metric": "count", - "operator": "gte", - "value": 20, - "window": "30d", - "filters": { - "direction": "INBOUND", - "transaction_type": "finance", - "subject_vendor_data": "__current__", - "currency": "EUR" + "examples": { + "Unknown session": { + "value": { + "detail": "Not found." } }, - { - "metric": "max", - "field": "amount", - "operator": "lt", - "value": 10000, - "window": "30d", - "filters": { - "direction": "INBOUND", - "transaction_type": "finance", - "subject_vendor_data": "__current__", - "currency": "EUR" + "Outside the key's limits": { + "value": { + "detail": "Session not found." } } - ], - "actions": [ - { - "type": "add_score", - "value": 35 - } - ] + } } } - } - }, - "responses": { - "201": { - "description": "The created rule.", - "content": { - "application/json": { + }, + "429": { + "description": "Rate limit exceeded: more than 600 GET requests in the current one-minute window for this API key. Wait `Retry-After` seconds before retrying.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", "schema": { - "$ref": "#/components/schemas/TransactionRuleDetail" + "type": "integer", + "minimum": 1, + "example": 30 + } + }, + "X-RateLimit-Limit": { + "description": "Requests allowed in the one-minute window (600).", + "schema": { + "type": "integer", + "example": 600 + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the current window.", + "schema": { + "type": "integer", + "example": 0 + } + }, + "X-RateLimit-Reset": { + "description": "Unix time (seconds) when the current window resets.", + "schema": { + "type": "integer", + "example": 1792056120 + } + }, + "X-RateLimit-Scope": { + "description": "Name of the budget that was exceeded.", + "schema": { + "type": "string", + "example": "generic-get" } } - } - }, - "400": { - "description": "Validation error - duplicate title, unknown operator/metric, malformed window, invalid scope direction, or AWAITING_USER without workflow_id.", + }, "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } }, - "example": { - "title": [ - "A rule with this name already exists." - ] + "examples": { + "Throttled": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } } } } } }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/session/{sessionId}/webhook/{webhook_uuid}": { + "get": { + "summary": "Get session webhook delivery", + "description": "Retrieve one webhook delivery of a verification session: the URL it went to, the HTTP status your endpoint returned, and the exact headers and JSON body Didit sent. `request_header` and `request_body` are JSON text; to check your [signature verification](https://docs.didit.me/integration/webhooks#signature-verification), recompute `X-Signature-V2` from the parsed body with the secret of the destination it was sent to (or the application-level secret for a delivery to the legacy webhook URL). The path has no trailing slash: with one it returns `404`. A scoped API key needs `sessions: read` and `decisions: read`.", + "operationId": "get_v3_session_webhook", + "x-didit-sdk": { + "resource": "sessions.webhooks", + "method": "retrieve", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "UUID of the verification session, user (KYC) or business (KYB): the `session_id` returned by `POST /v3/session/`. The server resolves it against both session types.\n\n**Must be a valid UUID.** The route only matches well-formed UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{\"detail\": ...}` envelope shown below.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "22222222-3333-4444-5555-666666666666" + }, + { + "name": "webhook_uuid", + "in": "path", + "required": true, + "description": "UUID of the delivery: the `uuid` of an entry returned by `GET /v3/session/{sessionId}/webhooks/`. It must belong to the session in the path.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "66666666-7777-8888-9999-000000000001" + } + ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/transactions/rules/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"title\": \"Structuring - inbound (custom)\", \"description\": \"20+ inbound EUR transfers, all under 10,000 EUR, 30 days\", \"category\": \"finance\", \"mode\": \"TEST\", \"evaluation_mode\": \"ALL\", \"scope\": {\"transaction_types\": [\"finance\"], \"directions\": [\"INBOUND\"]}, \"conditions\": [{\"field\": \"currency\", \"operator\": \"eq\", \"value\": \"EUR\"}, {\"field\": \"amount\", \"operator\": \"lt\", \"value\": 10000}], \"aggregation\": [{\"metric\": \"count\", \"operator\": \"gte\", \"value\": 20, \"window\": \"30d\", \"filters\": {\"direction\": \"INBOUND\", \"transaction_type\": \"finance\", \"subject_vendor_data\": \"__current__\", \"currency\": \"EUR\"}}, {\"metric\": \"max\", \"field\": \"amount\", \"operator\": \"lt\", \"value\": 10000, \"window\": \"30d\", \"filters\": {\"direction\": \"INBOUND\", \"transaction_type\": \"finance\", \"subject_vendor_data\": \"__current__\", \"currency\": \"EUR\"}}], \"actions\": [{\"type\": \"add_score\", \"value\": 35}]}'" + "source": "curl -X GET 'https://verification.didit.me/v3/session/22222222-3333-4444-5555-666666666666/webhook/66666666-7777-8888-9999-000000000001' \\\n -H 'x-api-key: YOUR_API_KEY'" }, { "lang": "python", "label": "Python", - "source": "import os\n\nimport requests\n\nrule = {\n \"title\": \"Structuring - inbound (custom)\",\n \"description\": \"20+ inbound EUR transfers, all under 10,000 EUR, 30 days\",\n \"category\": \"finance\",\n \"mode\": \"TEST\",\n \"evaluation_mode\": \"ALL\",\n \"scope\": {\n \"transaction_types\": [\n \"finance\"\n ],\n \"directions\": [\n \"INBOUND\"\n ]\n },\n \"conditions\": [\n {\n \"field\": \"currency\",\n \"operator\": \"eq\",\n \"value\": \"EUR\"\n },\n {\n \"field\": \"amount\",\n \"operator\": \"lt\",\n \"value\": 10000\n }\n ],\n \"aggregation\": [\n {\n \"metric\": \"count\",\n \"operator\": \"gte\",\n \"value\": 20,\n \"window\": \"30d\",\n \"filters\": {\n \"direction\": \"INBOUND\",\n \"transaction_type\": \"finance\",\n \"subject_vendor_data\": \"__current__\",\n \"currency\": \"EUR\"\n }\n },\n {\n \"metric\": \"max\",\n \"field\": \"amount\",\n \"operator\": \"lt\",\n \"value\": 10000,\n \"window\": \"30d\",\n \"filters\": {\n \"direction\": \"INBOUND\",\n \"transaction_type\": \"finance\",\n \"subject_vendor_data\": \"__current__\",\n \"currency\": \"EUR\"\n }\n }\n ],\n \"actions\": [\n {\n \"type\": \"add_score\",\n \"value\": 35\n }\n ]\n}\n\nresp = requests.post(\n 'https://verification.didit.me/v3/transactions/rules/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json=rule,\n timeout=15,\n)\nresp.raise_for_status()\nprint(resp.json()['uuid'], resp.json()['mode'])" + "source": "import json\nimport os\n\nimport requests\n\nsession_id = '22222222-3333-4444-5555-666666666666'\nwebhook_uuid = '66666666-7777-8888-9999-000000000001'\nresp = requests.get(\n f'https://verification.didit.me/v3/session/{session_id}/webhook/{webhook_uuid}', # no trailing slash\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\ndelivery = resp.json()\nheaders = json.loads(delivery['request_header'])\npayload = json.loads(delivery['request_body'])\nprint(delivery['response_status'], headers['X-Signature-V2'], payload['status'])" }, { "lang": "javascript", "label": "JavaScript", - "source": "const rule = {\n \"title\": \"Structuring - inbound (custom)\",\n \"description\": \"20+ inbound EUR transfers, all under 10,000 EUR, 30 days\",\n \"category\": \"finance\",\n \"mode\": \"TEST\",\n \"evaluation_mode\": \"ALL\",\n \"scope\": {\n \"transaction_types\": [\n \"finance\"\n ],\n \"directions\": [\n \"INBOUND\"\n ]\n },\n \"conditions\": [\n {\n \"field\": \"currency\",\n \"operator\": \"eq\",\n \"value\": \"EUR\"\n },\n {\n \"field\": \"amount\",\n \"operator\": \"lt\",\n \"value\": 10000\n }\n ],\n \"aggregation\": [\n {\n \"metric\": \"count\",\n \"operator\": \"gte\",\n \"value\": 20,\n \"window\": \"30d\",\n \"filters\": {\n \"direction\": \"INBOUND\",\n \"transaction_type\": \"finance\",\n \"subject_vendor_data\": \"__current__\",\n \"currency\": \"EUR\"\n }\n },\n {\n \"metric\": \"max\",\n \"field\": \"amount\",\n \"operator\": \"lt\",\n \"value\": 10000,\n \"window\": \"30d\",\n \"filters\": {\n \"direction\": \"INBOUND\",\n \"transaction_type\": \"finance\",\n \"subject_vendor_data\": \"__current__\",\n \"currency\": \"EUR\"\n }\n }\n ],\n \"actions\": [\n {\n \"type\": \"add_score\",\n \"value\": 35\n }\n ]\n};\n\nconst resp = await fetch('https://verification.didit.me/v3/transactions/rules/', {\n method: 'POST',\n headers: {\n 'x-api-key': process.env.DIDIT_API_KEY,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(rule),\n});\nif (!resp.ok) throw new Error(await resp.text());\nconst created = await resp.json();\nconsole.log(created.uuid, created.mode);" - } - ] - } - }, - "/v3/transactions/rules/{rule_uuid}/": { - "parameters": [ - { - "name": "rule_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "The rule's uuid from list or create." - } - ], - "get": { - "summary": "Get rule", - "description": "Retrieve one rule with its full configuration and execution counters.", - "operationId": "getTransactionRule", - "tags": [ - "Transactions" - ], - "security": [ - { - "ApiKeyAuth": [] + "source": "const sessionId = '22222222-3333-4444-5555-666666666666';\nconst webhookUuid = '66666666-7777-8888-9999-000000000001';\n// No trailing slash on this path.\nconst resp = await fetch(`https://verification.didit.me/v3/session/${sessionId}/webhook/${webhookUuid}`, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Request failed: ${resp.status}`);\nconst delivery = await resp.json();\nconst payload = JSON.parse(delivery.request_body);\nconsole.log(delivery.response_status, payload.status);" } ], "responses": { "200": { - "description": "The rule.", + "description": "The delivery, with its request headers and body.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TransactionRuleDetail" + "$ref": "#/components/schemas/SessionWebhookDelivery" + }, + "examples": { + "Connection failure": { + "value": { + "uuid": "66666666-7777-8888-9999-000000000001", + "session_id": "22222222-3333-4444-5555-666666666666", + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": 503, + "status": "In Progress", + "webhook_type": "status.updated", + "request_header": "{\"Content-Type\": \"application/json\", \"X-Signature\": \"a5e02a03404e1337a0198e6be15d34abbd053c658a0e51e5d0e78554213ef29b\", \"X-Signature-V2\": \"927e7cb770c94ce745939bff73f04aa200015f3bc64795ba46c2aaf78a6cedc5\", \"X-Signature-Simple\": \"e37b0d4c716c72bb5ae7127b9cc455d28ab4686e9e22312b4ec2327409a73302\", \"X-Timestamp\": \"1792056078\", \"User-Agent\": \"DiditWebhook/2.0 +https://didit.me\"}", + "request_body": "{\"application_id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"created_at\":1792056057,\"environment\":\"live\",\"event_id\":\"33333333-4444-5555-6666-777777777777\",\"sandbox_scenario\":null,\"session_id\":\"22222222-3333-4444-5555-666666666666\",\"status\":\"In Progress\",\"timestamp\":1792056078,\"vendor_data\":\"user-123\",\"webhook_type\":\"status.updated\",\"workflow_id\":\"44444444-5555-6666-7777-888888888888\",\"workflow_version\":3}", + "created_at": "2026-10-15T09:21:18.412345Z" + } + } } } } }, - "404": { - "description": "No such rule on this application.", + "401": { + "description": "The API key has expired: it was created with an expiry date that has passed. A missing, invalid or revoked key gets `403`, not `401`.", "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/transactions/rules/RULE_UUID/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ] - }, - "patch": { - "summary": "Update rule", - "description": "Partially update a rule. Custom rules accept every configuration field. Library (PRESET) rules accept only `mode` - use it to activate, disable, or switch a preset to TEST; any other field returns 400.", - "operationId": "updateTransactionRule", - "tags": [ - "Transactions" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TransactionRuleUpdateRequest" - }, - "example": { - "mode": "ACTIVE" + }, + "403": { + "description": "The request is not allowed. No `x-api-key`, or an invalid or revoked key, gets `Authentication credentials were not provided or are invalid.` A key of another application gets `You do not have permission to perform this action.` A scoped key without the access this endpoint needs gets a message naming the missing access, and a key used from outside its IP allowlist gets `This API key can't be used from this IP address.` An unknown `sessionId` returns `404` before the credential is checked.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Missing or invalid key": { + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + }, + "Key of another application": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without sessions read": { + "value": { + "detail": "This API key has no read access to sessions." + } + }, + "Scoped key without decisions read": { + "value": { + "detail": "This API key has no read access to decisions." + } + }, + "Outside the IP allowlist": { + "value": { + "detail": "This API key can't be used from this IP address." + } + } + } } } - } - }, - "responses": { - "200": { - "description": "The updated rule.", + }, + "404": { + "description": "The session does not exist or has been deleted (`Not found.`, returned before the credential is checked), or it is outside the workflow or status limits of a scoped key (`Session not found.`). A delivery that does not exist, belongs to another session or is no longer available returns `No WebhookLog matches the given query.`", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TransactionRuleDetail" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Unknown session": { + "value": { + "detail": "Not found." + } + }, + "Outside the key's limits": { + "value": { + "detail": "Session not found." + } + }, + "Delivery not in this session": { + "value": { + "detail": "No WebhookLog matches the given query." + } + } } } } }, - "400": { - "description": "Validation error, or a non-mode field on a library rule.", + "429": { + "description": "Rate limit exceeded: more than 600 GET requests in the current one-minute window for this API key. Wait `Retry-After` seconds before retrying.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + }, + "X-RateLimit-Limit": { + "description": "Requests allowed in the one-minute window (600).", + "schema": { + "type": "integer", + "example": 600 + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the current window.", + "schema": { + "type": "integer", + "example": 0 + } + }, + "X-RateLimit-Reset": { + "description": "Unix time (seconds) when the current window resets.", + "schema": { + "type": "integer", + "example": 1792056120 + } + }, + "X-RateLimit-Scope": { + "description": "Name of the budget that was exceeded.", + "schema": { + "type": "string", + "example": "generic-get" + } + } + }, "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Throttled": { + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } } } } } }, + "security": [ + { + "ApiKeyAuth": [] + } + ] + } + }, + "/v3/session/{sessionId}/webhook/{webhook_uuid}/resend": { + "post": { + "summary": "Resend session webhook delivery", + "description": "Send a recorded session webhook again to the URL it first went to, with the same payload and `event_id` and a fresh `timestamp`, `X-Timestamp` header and signatures. Didit signs it with the current secret of the destination the delivery belongs to (or the application-level secret for a delivery to the legacy webhook URL), waits up to 5 seconds for your endpoint, records the attempt as a new delivery and returns it with `200` whatever your endpoint answered: read `response_status`. A resend is not retried automatically, and every call sends again, so make your endpoint idempotent on `event_id`. The path has no trailing slash and the request has no body. A scoped API key needs `webhooks: write` and `decisions: read`.", + "operationId": "post_v3_session_webhook_resend", + "x-didit-sdk": { + "resource": "sessions.webhooks", + "method": "resend", + "auth": "api_key", + "server": true + }, + "tags": [ + "Sessions" + ], + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "UUID of the verification session, user (KYC) or business (KYB): the `session_id` returned by `POST /v3/session/`. The server resolves it against both session types.\n\n**Must be a valid UUID.** The route only matches well-formed UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{\"detail\": ...}` envelope shown below.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "22222222-3333-4444-5555-666666666666" + }, + { + "name": "webhook_uuid", + "in": "path", + "required": true, + "description": "UUID of the delivery to resend: the `uuid` of an entry returned by `GET /v3/session/{sessionId}/webhooks/`. It must belong to the session in the path.", + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "66666666-7777-8888-9999-000000000001" + } + ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X PATCH 'https://verification.didit.me/v3/transactions/rules/RULE_UUID/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"mode\": \"ACTIVE\"}'" + "source": "curl -X POST 'https://verification.didit.me/v3/session/22222222-3333-4444-5555-666666666666/webhook/66666666-7777-8888-9999-000000000001/resend' \\\n -H 'x-api-key: YOUR_API_KEY'" }, { "lang": "python", "label": "Python", - "source": "import os\n\nimport requests\n\nresp = requests.patch(\n 'https://verification.didit.me/v3/transactions/rules/RULE_UUID/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={'mode': 'ACTIVE'},\n timeout=15,\n)\nresp.raise_for_status()\nprint(resp.json()['mode'])" - } - ] - }, - "delete": { - "summary": "Delete rule", - "description": "Delete a CUSTOM rule. Deletion is immediate and permanent: the rule stops evaluating for new transactions and disappears from listings, while past rule runs on already-evaluated transactions keep their recorded results. Library rules cannot be deleted - uninstall them via the install endpoint instead (they can always be reinstalled from the library).", - "operationId": "deleteTransactionRule", - "tags": [ - "Transactions" - ], - "security": [ + "source": "import os\n\nimport requests\n\nsession_id = '22222222-3333-4444-5555-666666666666'\nwebhook_uuid = '66666666-7777-8888-9999-000000000001'\nresp = requests.post(\n f'https://verification.didit.me/v3/session/{session_id}/webhook/{webhook_uuid}/resend', # no trailing slash\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nresent = resp.json()\nprint(resent['uuid'], resent['response_status'])" + }, { - "ApiKeyAuth": [] + "lang": "javascript", + "label": "JavaScript", + "source": "const sessionId = '22222222-3333-4444-5555-666666666666';\nconst webhookUuid = '66666666-7777-8888-9999-000000000001';\n// No trailing slash on this path.\nconst resp = await fetch(`https://verification.didit.me/v3/session/${sessionId}/webhook/${webhookUuid}/resend`, {\n method: 'POST',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(await resp.text());\nconst resent = await resp.json();\nconsole.log(resent.uuid, resent.response_status);" } ], "responses": { - "204": { - "description": "Deleted." + "200": { + "description": "The new delivery, already attempted. `response_status` is what your endpoint answered this time. The original delivery is unchanged.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionWebhookDelivery" + }, + "examples": { + "Accepted on resend": { + "value": { + "uuid": "66666666-7777-8888-9999-000000000004", + "session_id": "22222222-3333-4444-5555-666666666666", + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": 200, + "status": "In Progress", + "webhook_type": "status.updated", + "request_header": "{\"Content-Type\": \"application/json\", \"X-Signature\": \"b8eb1e376517ee0cad8ba98ff7f8fd35fc72eca231000963a96a335f82328835\", \"X-Signature-V2\": \"9efd2ce9ed1303e1d05273ef9608a59363ed9789a05253db4748b3e8e50cb109\", \"X-Signature-Simple\": \"bf3a9dc04b2c20ab539203b0cd5cca2a10f630bf62d8feb6d68d82af80f64774\", \"X-Timestamp\": \"1792159542\", \"User-Agent\": \"DiditWebhook/2.0 +https://didit.me\"}", + "request_body": "{\"application_id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"created_at\":1792056057,\"environment\":\"live\",\"event_id\":\"33333333-4444-5555-6666-777777777777\",\"sandbox_scenario\":null,\"session_id\":\"22222222-3333-4444-5555-666666666666\",\"status\":\"In Progress\",\"timestamp\":1792159542,\"vendor_data\":\"user-123\",\"webhook_type\":\"status.updated\",\"workflow_id\":\"44444444-5555-6666-7777-888888888888\",\"workflow_version\":3}", + "created_at": "2026-10-16T14:05:42.330981Z" + } + } + } + } + } }, "400": { - "description": "The rule is a library preset - uninstall instead.", + "description": "The delivery could not be resent. Branch on `code`: `webhook_url_missing` means the delivery has no recorded URL and nothing is sent; `webhook_resend_error` means the stored delivery could not be signed again or the attempt could not be completed. After `webhook_resend_error`, `detail` starts with `Error processing webhook resend:` followed by a diagnostic text that can change.", "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + }, + "code": { + "type": "string", + "description": "Stable machine-readable error code. Branch on this, not on `detail`." + } + } }, - "example": { - "detail": "Library rules cannot be deleted. Uninstall them via the rules install endpoint instead." + "examples": { + "No recorded URL": { + "value": { + "detail": "The webhook delivery does not have a request URL configured", + "code": "webhook_url_missing" + } + }, + "Resend failed": { + "value": { + "detail": "Error processing webhook resend: Invalid webhook data format", + "code": "webhook_resend_error" + } + } } } } }, - "404": { - "description": "No such rule on this application.", + "401": { + "description": "The API key has expired: it was created with an expiry date that has passed. A missing, invalid or revoked key gets `403`, not `401`.", "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Expired key": { + "value": { + "detail": "This API key has expired." + } + } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X DELETE 'https://verification.didit.me/v3/transactions/rules/RULE_UUID/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ] - } - }, - "/v3/transactions/rules/backtest/": { - "post": { - "summary": "Backtest rule configuration", - "description": "Evaluate a hypothetical rule configuration against your recent transactions (most recent first, up to the backtest cap) without creating a rule or touching any transaction. Use it before activating a rule to see how often it would have matched and how many subjects it would have affected.", - "operationId": "backtestTransactionRules", - "tags": [ - "Transactions" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TransactionRuleBacktestRequest" - }, - "example": { - "conditions": [ - { - "field": "currency", - "operator": "eq", - "value": "EUR" - }, - { - "field": "amount", - "operator": "lt", - "value": 10000 + }, + "403": { + "description": "The request is not allowed. No `x-api-key`, or an invalid or revoked key, gets `Authentication credentials were not provided or are invalid.` A key of another application gets `You do not have permission to perform this action.` A scoped key without the access this endpoint needs gets a message naming the missing access, and a key used from outside its IP allowlist gets `This API key can't be used from this IP address.` An unknown `sessionId` returns `404` before the credential is checked.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } } - ], - "aggregation": [ - { - "metric": "count", - "operator": "gte", - "value": 20, - "window": "30d", - "filters": { - "direction": "INBOUND", - "transaction_type": "finance", - "subject_vendor_data": "__current__", - "currency": "EUR" + }, + "examples": { + "Missing or invalid key": { + "value": { + "detail": "Authentication credentials were not provided or are invalid." } }, - { - "metric": "max", - "field": "amount", - "operator": "lt", - "value": 10000, - "window": "30d", - "filters": { - "direction": "INBOUND", - "transaction_type": "finance", - "subject_vendor_data": "__current__", - "currency": "EUR" + "Key of another application": { + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without webhooks write": { + "value": { + "detail": "This API key has no write access to webhooks." + } + }, + "Scoped key without decisions read": { + "value": { + "detail": "This API key has no read access to decisions." + } + }, + "Outside the IP allowlist": { + "value": { + "detail": "This API key can't be used from this IP address." + } + } + } + } + } + }, + "404": { + "description": "The session does not exist or has been deleted (`Not found.`, returned before the credential is checked), or it is outside the workflow or status limits of a scoped key (`Session not found.`). A delivery that does not exist, belongs to another session or is no longer available returns `Webhook log not found`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." } } - ], - "evaluation_mode": "ALL", - "scope": { - "transaction_types": [ - "finance" - ], - "directions": [ - "INBOUND" - ] }, - "period_days": 90 + "examples": { + "Unknown session": { + "value": { + "detail": "Not found." + } + }, + "Outside the key's limits": { + "value": { + "detail": "Session not found." + } + }, + "Delivery not in this session": { + "value": { + "detail": "Webhook log not found" + } + } + } } } - } - }, - "responses": { - "200": { - "description": "Backtest statistics.", + }, + "429": { + "description": "Rate limit exceeded: more than 300 write requests (POST, PATCH, PUT, DELETE) in the current one-minute window for this API key. Wait `Retry-After` seconds before retrying.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + }, + "X-RateLimit-Limit": { + "description": "Requests allowed in the one-minute window (300).", + "schema": { + "type": "integer", + "example": 300 + } + }, + "X-RateLimit-Remaining": { + "description": "Requests left in the current window.", + "schema": { + "type": "integer", + "example": 0 + } + }, + "X-RateLimit-Reset": { + "description": "Unix time (seconds) when the current window resets.", + "schema": { + "type": "integer", + "example": 1792056120 + } + }, + "X-RateLimit-Scope": { + "description": "Name of the budget that was exceeded.", + "schema": { + "type": "string", + "example": "generic-write" + } + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TransactionRuleBacktestResult" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } }, - "example": { - "evaluated": 4820, - "matched": 12, - "affected_entities": 4, - "period_days": 90 + "examples": { + "Throttled": { + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." + } + } } } } } }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/transactions/rules/backtest/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"conditions\": [{\"field\": \"currency\", \"operator\": \"eq\", \"value\": \"EUR\"}, {\"field\": \"amount\", \"operator\": \"lt\", \"value\": 10000}], \"aggregation\": [{\"metric\": \"count\", \"operator\": \"gte\", \"value\": 20, \"window\": \"30d\", \"filters\": {\"direction\": \"INBOUND\", \"transaction_type\": \"finance\", \"subject_vendor_data\": \"__current__\", \"currency\": \"EUR\"}}, {\"metric\": \"max\", \"field\": \"amount\", \"operator\": \"lt\", \"value\": 10000, \"window\": \"30d\", \"filters\": {\"direction\": \"INBOUND\", \"transaction_type\": \"finance\", \"subject_vendor_data\": \"__current__\", \"currency\": \"EUR\"}}], \"evaluation_mode\": \"ALL\", \"scope\": {\"transaction_types\": [\"finance\"], \"directions\": [\"INBOUND\"]}, \"period_days\": 90}'" - }, + "security": [ { - "lang": "python", - "label": "Python", - "source": "import os\n\nimport requests\n\nresp = requests.post(\n 'https://verification.didit.me/v3/transactions/rules/backtest/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={\n \"conditions\": [\n {\n \"field\": \"currency\",\n \"operator\": \"eq\",\n \"value\": \"EUR\"\n },\n {\n \"field\": \"amount\",\n \"operator\": \"lt\",\n \"value\": 10000\n }\n ],\n \"aggregation\": [\n {\n \"metric\": \"count\",\n \"operator\": \"gte\",\n \"value\": 20,\n \"window\": \"30d\",\n \"filters\": {\n \"direction\": \"INBOUND\",\n \"transaction_type\": \"finance\",\n \"subject_vendor_data\": \"__current__\",\n \"currency\": \"EUR\"\n }\n },\n {\n \"metric\": \"max\",\n \"field\": \"amount\",\n \"operator\": \"lt\",\n \"value\": 10000,\n \"window\": \"30d\",\n \"filters\": {\n \"direction\": \"INBOUND\",\n \"transaction_type\": \"finance\",\n \"subject_vendor_data\": \"__current__\",\n \"currency\": \"EUR\"\n }\n }\n ],\n \"evaluation_mode\": \"ALL\",\n \"scope\": {\n \"transaction_types\": [\n \"finance\"\n ],\n \"directions\": [\n \"INBOUND\"\n ]\n },\n \"period_days\": 90\n},\n timeout=30,\n)\nresp.raise_for_status()\nstats = resp.json()\nprint(stats['matched'], 'matches across', stats['affected_entities'], 'subjects')" + "ApiKeyAuth": [] } ] } }, - "/v3/transactions/rules/library/": { + "/v3/session/{sessionId}/events/": { "get": { - "summary": "List rule library", - "description": "Browse the full preset rule library (150+ rules) with each entry's complete configuration and an `is_installed` flag for this application. Filter by bundle, category, or free-text search.", - "operationId": "listTransactionRuleLibrary", + "summary": "List session events", + "description": "Read the event timeline of one verification session, newest first by `sent_at`: the screens the user saw, buttons and uploads, failed attempts, status and feature-step changes, and the workflow routing trace. Works for User Verification (KYC) and Business Verification (KYB) session IDs. Events are stored asynchronously, so a new event can take a few seconds to appear. Results are paginated with `limit` and `offset`; follow `next` until it is `null`. A scoped API key needs `sessions: read` and `decisions: read`, and without `media: read` it receives `file_url` as `null`. The routing events are described in [Retrieve session](https://docs.didit.me/sessions-api/retrieve-session).", + "operationId": "get_v3_session_events", + "x-didit-sdk": { + "resource": "sessions", + "method": "list_events", + "auth": "api_key", + "server": true + }, "tags": [ - "Transactions" - ], - "security": [ - { - "ApiKeyAuth": [] - } + "Sessions" ], "parameters": [ { - "name": "bundle", - "in": "query", - "required": false, + "name": "sessionId", + "in": "path", + "required": true, + "description": "UUID of the verification session (the `session_id` returned by `POST /v3/session/`). Accepts both User Verification (KYC) and Business Verification (KYB) session IDs: the server resolves the ID across both session types. Must be a well-formed UUID; a malformed value does not match the route and returns an HTML `404`.", "schema": { - "type": "string" - }, - "description": "Filter by bundle, e.g. finance, aml_ctf, crypto_monitoring, travel_rule" + "type": "string", + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } }, { - "name": "category", + "name": "limit", "in": "query", "required": false, + "description": "Maximum number of events per page. Defaults to 50. A value below 1 or a non-numeric value falls back to 50; a value above 1,000,000 returns `400`.", "schema": { - "type": "string" - }, - "description": "Filter by category" + "type": "integer", + "default": 50, + "minimum": 1, + "maximum": 1000000, + "example": 50 + } }, { - "name": "search", + "name": "offset", "in": "query", "required": false, + "description": "Number of results to skip before the first returned one. Defaults to 0; a negative or non-numeric value falls back to 0, a value above 1,000,000 returns `400`. Prefer following the `next` and `previous` URLs.", "schema": { - "type": "string" - }, - "description": "Case-insensitive match against title and description" + "type": "integer", + "default": 0, + "minimum": 0, + "maximum": 1000000, + "example": 0 + } }, { - "name": "limit", + "name": "ordering", "in": "query", "required": false, + "description": "Optional sort override. The default order is `-sent_at`. Pass a result field name (for example `sent_at`, `created_at`, `event_type`, `feature_step` or `step`) to sort ascending, prefix it with `-` to sort descending, and separate several fields with commas. Unknown fields are ignored.", "schema": { - "type": "integer" - }, - "description": "Page size" + "type": "string", + "example": "sent_at" + } + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/events/?limit=50' \\\n -H 'x-api-key: YOUR_API_KEY'" }, { - "name": "offset", - "in": "query", - "required": false, - "schema": { - "type": "integer" - }, - "description": "Page offset" + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nsession_id = '11111111-2222-3333-4444-555555555555'\nresp = requests.get(\n f'https://verification.didit.me/v3/session/{session_id}/events/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n params={'limit': 50},\n timeout=10,\n)\nresp.raise_for_status()\nfor event in resp.json()['results']:\n print(event['sent_at'], event['event_type'], event['step'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const sessionId = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(`https://verification.didit.me/v3/session/${sessionId}/events/?limit=50`, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Request failed: ${resp.status}`);\nconst page = await resp.json();\nfor (const event of page.results) console.log(event.sent_at, event.event_type, event.step);" } ], "responses": { "200": { - "description": "Paginated library entries.", + "description": "One page of the session's events, newest first.", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "count", + "next", + "previous", + "results" + ], "properties": { "count": { - "type": "integer" + "type": "integer", + "description": "Total number of events of the session." }, "next": { "type": "string", - "nullable": true + "format": "uri", + "nullable": true, + "description": "URL of the next page, or `null` on the last page." }, "previous": { "type": "string", - "nullable": true + "format": "uri", + "nullable": true, + "description": "URL of the previous page, or `null` on the first page." }, "results": { "type": "array", "items": { - "$ref": "#/components/schemas/TransactionRuleLibraryEntry" + "$ref": "#/components/schemas/SessionEvent" } } } + }, + "examples": { + "Completed session": { + "value": { + "count": 3, + "next": null, + "previous": null, + "results": [ + { + "uuid": "aaaaaaaa-1111-2222-3333-444444444444", + "event_type": "SESSION_STATUS_CHANGED", + "created_at": "2026-09-14T10:02:13.904512Z", + "component": null, + "feature_step": null, + "step": null, + "details": { + "old_status": "In Progress", + "current_status": "Approved" + }, + "ip_address": null, + "user_agent": null, + "device_info": null, + "file_url": null, + "sent_at": 1789380131482 + }, + { + "uuid": "bbbbbbbb-1111-2222-3333-444444444444", + "event_type": "FEATURE_STEP_CHANGED", + "created_at": "2026-09-14T10:01:49.210377Z", + "component": null, + "feature_step": null, + "step": null, + "details": { + "old_feature_step": "OCR", + "current_feature_step": "LIVENESS" + }, + "ip_address": null, + "user_agent": null, + "device_info": null, + "file_url": null, + "sent_at": 1789380107103 + }, + { + "uuid": "cccccccc-1111-2222-3333-444444444444", + "event_type": "STEP_RENDERED", + "created_at": "2026-09-14T10:01:00.581930Z", + "component": "DocumentCapture", + "feature_step": "OCR", + "step": "DOCUMENT_CAPTURE_FRONT", + "details": {}, + "ip_address": "203.0.113.24", + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1", + "device_info": { + "device_model": "iPhone" + }, + "file_url": null, + "sent_at": 1789380058776 + } + ] + } + } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/transactions/rules/library/?bundle=finance' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ] - } - }, - "/v3/transactions/rules/install/": { - "post": { - "summary": "Install library rules", - "description": "Install one or more presets onto the application, by `library_keys` or a whole `bundle`. Installed presets evaluate immediately in their default mode; already-installed keys are skipped.", - "operationId": "installTransactionRuleLibrary", - "tags": [ - "Transactions" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TransactionRuleInstallRequest" - }, - "example": { - "library_keys": [ - "structuring-inbound", - "structuring-outbound" - ] + }, + "400": { + "description": "`limit` or `offset` is above 1,000,000. The body is a JSON array with one message.", + "content": { + "application/json": { + "examples": { + "Limit too large": { + "summary": "limit=2000000", + "value": [ + "limit must not exceed 1000000." + ] + }, + "Offset too large": { + "summary": "offset=2000000", + "value": [ + "offset must not exceed 1000000." + ] + } + }, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } } } - } - }, - "responses": { - "200": { - "description": "What was installed.", + }, + "401": { + "description": "The scoped API key has expired. This is the only case that returns `401`: a missing, invalid or revoked key returns `403`.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TransactionRuleInstallResult" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } }, - "example": { - "installed_library_keys": [ - "structuring-inbound", - "structuring-outbound" - ], - "installed_count": 2 + "examples": { + "Expired key": { + "summary": "The key's expiry date has passed", + "value": { + "detail": "This API key has expired." + } + } } } } }, - "400": { - "description": "Neither library_keys nor bundle provided.", + "403": { + "description": "Authentication or authorization failure. Missing, invalid and revoked keys return `403` (not `401`); so do a key of another application, a scoped key without the permission this endpoint needs, and a request from outside the key's IP allowlist. Distinguish them by `detail`.", "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Missing or invalid credentials": { + "summary": "No x-api-key header, or the key failed validation", + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + }, + "Other application": { + "summary": "A valid key that belongs to a different application than the session", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without sessions read": { + "summary": "Scoped key lacks sessions: read", + "value": { + "detail": "This API key has no read access to sessions." + } + }, + "Scoped key without decisions read": { + "summary": "Scoped key lacks decisions: read", + "value": { + "detail": "This API key has no read access to decisions." + } + }, + "IP not allowed": { + "summary": "Request from outside the key's IP allowlist", + "value": { + "detail": "This API key can't be used from this IP address." + } + } } } } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST 'https://verification.didit.me/v3/transactions/rules/install/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"library_keys\": [\"structuring-inbound\", \"structuring-outbound\"]}'" - } - ] - }, - "delete": { - "summary": "Uninstall library rules", - "description": "Remove installed presets from the application, by `library_keys` or a whole `bundle`. Uninstalled presets stop evaluating immediately and can be reinstalled from the library at any time.", - "operationId": "uninstallTransactionRuleLibrary", - "tags": [ - "Transactions" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TransactionRuleInstallRequest" - }, - "example": { - "bundle": "responsible_gaming" - } - } - } - }, - "responses": { - "200": { - "description": "How many presets were removed.", + }, + "404": { + "description": "The session does not exist, was deleted, or is outside a scoped key's workflow or approved-only limits. The session lookup runs before the credential check, so an unknown `sessionId` returns `404` even without a valid key.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TransactionRuleInstallResult" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } }, - "example": { - "uninstalled_count": 5 + "examples": { + "Unknown session": { + "summary": "No session with this ID exists, or it was deleted", + "value": { + "detail": "Not found." + } + }, + "Outside key limits": { + "summary": "The session is outside a scoped key's workflow or approved-only limits", + "value": { + "detail": "Session not found." + } + } } } } }, - "400": { - "description": "Neither library_keys nor bundle provided.", + "429": { + "description": "Rate limit exceeded: 600 GET requests per minute per API key. Wait `Retry-After` seconds before retrying; the `X-RateLimit-*` headers show the budget.", "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Rate limited": { + "summary": "Budget for the current minute exhausted", + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." + } + } + } + } + }, + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 } } } } }, - "x-codeSamples": [ + "security": [ { - "lang": "curl", - "label": "curl", - "source": "curl -X DELETE 'https://verification.didit.me/v3/transactions/rules/install/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"bundle\": \"responsible_gaming\"}'" + "ApiKeyAuth": [] } ] } }, - "/v3/organization/{organization_id}/application/{application_id}/networks/": { + "/v3/session/{sessionId}/aml-screening-history/": { "get": { - "summary": "List networks", - "description": "List fraud networks detected within this application's organization, newest activity first by default. By default dismissed networks are excluded; pass status to include them. Sandbox applications never have networks - clustering excludes sandbox data entirely.", - "operationId": "list_networks", + "summary": "List AML screening history", + "description": "List every completed ongoing-monitoring re-screen of a session's AML check, newest first by `completed_at`, including the runs where nothing changed: alerts, activity entries and webhooks fire only when a result moves, so this history is the evidence that a clean subject was still being screened. Works for KYC and KYB session IDs; pass `node_id` when the session ran more than one AML step. Only ongoing-monitoring re-screens are listed (the first screening is part of the session decision) and runs from before this history existed are not reconstructed, so a session without ongoing monitoring returns an empty list. `count` is exact up to 100 and capped at 100 above that, so follow `next` until it is `null`. A scoped API key needs `decisions: read`; see [Continuous AML monitoring](https://docs.didit.me/core-technology/aml-screening/continuous-monitoring-aml-screening).", + "operationId": "get_v3_session_aml_screening_history", + "x-didit-sdk": { + "resource": "sessions", + "method": "list_aml_screening_history", + "auth": "api_key", + "server": true + }, "tags": [ - "Networks" + "Sessions" ], "parameters": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", + "name": "sessionId", "in": "path", "required": true, + "description": "UUID of the verification session (the `session_id` returned by `POST /v3/session/`). Accepts both User Verification (KYC) and Business Verification (KYB) session IDs: the server resolves the ID across both session types. Must be a well-formed UUID; a malformed value does not match the route and returns an HTML `404`.", "schema": { "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "status", - "in": "query", - "schema": { - "type": "string" - }, - "description": "Comma-separated statuses to include: active,in_review,resolved,dismissed. Omit to exclude dismissed." - }, - { - "name": "signal_type", - "in": "query", - "schema": { - "type": "string" - }, - "description": "Comma-separated signal types: ip_address,device,face,document_number,phone,email,address." - }, - { - "name": "pattern_type", - "in": "query", - "schema": { - "type": "string" - }, - "description": "Comma-separated pattern types, e.g. exact_same_device,same_email." - }, - { - "name": "risk_band", - "in": "query", - "schema": { - "type": "string" - }, - "description": "Comma-separated bands: low,medium,high." - }, - { - "name": "min_size", - "in": "query", - "schema": { - "type": "integer" - }, - "description": "Minimum member count." - }, - { - "name": "max_size", - "in": "query", - "schema": { - "type": "integer" - }, - "description": "Maximum member count." - }, - { - "name": "date_from", - "in": "query", - "schema": { - "type": "string", - "format": "date" - }, - "description": "Lower bound on last_activity_at (ISO 8601 date or datetime)." + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } }, { - "name": "date_to", + "name": "node_id", "in": "query", + "required": false, + "description": "Workflow graph node ID of the AML check. Needed only when the session ran more than one AML step; without it the most recent AML check is used. An unknown node returns `404`.", "schema": { "type": "string", - "format": "date" - }, - "description": "Upper bound on first_activity_at (ISO 8601 date or datetime)." - }, - { - "name": "q", - "in": "query", - "schema": { - "type": "string" - }, - "description": "Free-text search across network name, member names, the network_id (N-0001), and exact pattern_type tokens." - }, - { - "name": "ordering", - "in": "query", - "schema": { - "type": "string" - }, - "description": "One of risk_score, -risk_score, last_activity_at, -last_activity_at, first_activity_at, -first_activity_at, network_number, -network_number. Defaults to -risk_score,-last_activity_at." + "example": "feature_aml_1" + } }, { "name": "limit", "in": "query", + "required": false, + "description": "Maximum number of screening runs per page. Defaults to 50. A value below 1 or a non-numeric value falls back to 50; a value above 1,000,000 returns `400`.", "schema": { "type": "integer", - "default": 100, + "default": 50, "minimum": 1, - "maximum": 500 - }, - "description": "Page size, 1-500. Defaults to 100." + "maximum": 1000000, + "example": 50 + } }, { "name": "offset", "in": "query", + "required": false, + "description": "Number of results to skip before the first returned one. Defaults to 0; a negative or non-numeric value falls back to 0, a value above 1,000,000 returns `400`. Prefer following the `next` and `previous` URLs.", "schema": { "type": "integer", "default": 0, - "minimum": 0 - }, - "description": "Zero-based offset of the first record." + "minimum": 0, + "maximum": 1000000, + "example": 0 + } + }, + { + "name": "ordering", + "in": "query", + "required": false, + "description": "Optional sort override. The default order is `-completed_at`. Pass a result field name (for example `completed_at`, `started_at`, `outcome` or `score`) to sort ascending, prefix it with `-` to sort descending, and separate several fields with commas. Unknown fields are ignored.", + "schema": { + "type": "string", + "example": "completed_at" + } } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/?status=active,in_review&ordering=-risk_score' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X GET 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/aml-screening-history/' \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nsession_id = '11111111-2222-3333-4444-555555555555'\nresp = requests.get(\n f'https://verification.didit.me/v3/session/{session_id}/aml-screening-history/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nfor run in resp.json()['results']:\n print(run['completed_at'], run['outcome'], run['status'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const sessionId = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(`https://verification.didit.me/v3/session/${sessionId}/aml-screening-history/`, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Request failed: ${resp.status}`);\nconst page = await resp.json();\nfor (const run of page.results) console.log(run.completed_at, run.outcome, run.status);" } ], "responses": { "200": { - "description": "Paginated network list with console KPIs.", + "description": "One page of screening runs, newest first.", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "count", + "next", + "previous", + "results" + ], "properties": { "count": { - "type": "integer" + "type": "integer", + "maximum": 100, + "description": "Number of runs, exact up to 100 and capped at 100 above that." }, "next": { - "type": "integer", + "type": "string", + "format": "uri", "nullable": true, - "description": "Offset of the next page, or null on the last page." + "description": "URL of the next page, or `null` on the last page." }, "previous": { - "type": "integer", - "nullable": true + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL of the previous page, or `null` on the first page." }, "results": { "type": "array", "items": { - "$ref": "#/components/schemas/NetworkListItem" + "$ref": "#/components/schemas/AMLScreeningRun" } - }, - "kpis": { - "type": "object", - "description": "Each key is {value, delta} over the results matching the current filters.", - "properties": { - "active": { - "type": "object" - }, - "in_review": { - "type": "object" - }, - "applicants_involved": { - "type": "object" - }, - "detected_this_period": { - "type": "object" + } + } + }, + "examples": { + "Monitored subject": { + "value": { + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "uuid": "dddddddd-1111-2222-3333-444444444444", + "completed_at": "2026-09-14T04:12:33.481207Z", + "started_at": "2026-09-14T04:00:01.002311Z", + "outcome": "NO_CHANGE", + "trigger": "ONGOING_MONITORING", + "provider": "aml_data_source", + "status": "Approved", + "score": 0.0, + "total_hits": 0, + "dataset_freshness": { + "captured_at": "2026-09-14T03:58:02.114820+00:00", + "provider": "aml_data_source", + "catalog_version": "2026-09-13", + "sources": { + "sanctions": { + "last_updated_at": "2026-09-13T21:10:00+00:00", + "oldest_updated_at": "2026-09-11T06:00:00+00:00", + "dataset_count": 120, + "dated_dataset_count": 120 + }, + "pep": { + "last_updated_at": "2026-09-13T08:00:00+00:00", + "oldest_updated_at": null, + "dataset_count": 45, + "dated_dataset_count": 44 + }, + "warnings": { + "last_updated_at": "2026-09-12T18:45:00+00:00", + "oldest_updated_at": "2026-09-01T00:00:00+00:00", + "dataset_count": 30, + "dated_dataset_count": 30 + } + } + } }, - "false_positive_rate": { - "type": "object" + { + "uuid": "eeeeeeee-1111-2222-3333-444444444444", + "completed_at": "2026-09-13T04:11:58.020914Z", + "started_at": "2026-09-13T04:00:00.871022Z", + "outcome": "FAILED", + "trigger": "ONGOING_MONITORING", + "provider": "aml_data_source", + "status": "Approved", + "score": 0.0, + "total_hits": 0, + "dataset_freshness": null } - } + ] + } + } + } + } + } + }, + "400": { + "description": "`limit` or `offset` is above 1,000,000. The body is a JSON array with one message.", + "content": { + "application/json": { + "examples": { + "Limit too large": { + "summary": "limit=2000000", + "value": [ + "limit must not exceed 1000000." + ] + }, + "Offset too large": { + "summary": "offset=2000000", + "value": [ + "offset must not exceed 1000000." + ] + } + }, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "401": { + "description": "The scoped API key has expired. This is the only case that returns `401`: a missing, invalid or revoked key returns `403`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Expired key": { + "summary": "The key's expiry date has passed", + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "Authentication or authorization failure. Missing, invalid and revoked keys return `403` (not `401`); so do a key of another application, a scoped key without the permission this endpoint needs, and a request from outside the key's IP allowlist. Distinguish them by `detail`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Missing or invalid credentials": { + "summary": "No x-api-key header, or the key failed validation", + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + }, + "Other application": { + "summary": "A valid key that belongs to a different application than the session", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without decisions read": { + "summary": "Scoped key lacks decisions: read", + "value": { + "detail": "This API key has no read access to decisions." + } + }, + "IP not allowed": { + "summary": "Request from outside the key's IP allowlist", + "value": { + "detail": "This API key can't be used from this IP address." + } + } + } + } + } + }, + "404": { + "description": "The session does not exist, was deleted or is outside a scoped key's limits, or it has no AML check (for the given `node_id`). The session lookup runs before the credential check, so an unknown `sessionId` returns `404` even without a valid key.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Unknown session": { + "summary": "No session with this ID exists, or it was deleted", + "value": { + "detail": "Not found." + } + }, + "Outside key limits": { + "summary": "The session is outside a scoped key's limits (also returned for a session deleted moments ago)", + "value": { + "detail": "Session not found." + } + }, + "No AML check": { + "summary": "The session never ran an AML step", + "value": { + "detail": "The session does not have an AML check." + } + }, + "No AML check for node": { + "summary": "node_id does not match an AML check of the session", + "value": { + "detail": "The session does not have an AML check for node 'feature_aml_2'." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded: 600 GET requests per minute per API key. Wait `Retry-After` seconds before retrying; the `X-RateLimit-*` headers show the budget.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Rate limited": { + "summary": "Budget for the current minute exhausted", + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." } } } } + }, + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/": { + "/v3/session/{sessionId}/aml-screening-history/export/": { "get": { - "summary": "Get network", - "description": "Retrieve a single network's full detail, including stats, fraud tags, and risk signals.", - "operationId": "get_network", + "summary": "Export AML screening history as CSV", + "description": "Download the same ongoing-monitoring history as `GET /v3/session/{sessionId}/aml-screening-history/` as a CSV file, one line per completed re-screen, newest first, ready to hand to an auditor. The file is streamed whole (no pagination) and starts with a header line; an empty cell means unknown, dataset dates are never estimated, and a value that would start a spreadsheet formula is prefixed with an apostrophe. Works for KYC and KYB session IDs; pass `node_id` when the session ran more than one AML step. Errors are returned as JSON, not CSV. A scoped API key needs `decisions: read` and `media: read`: the response is a file, so a key without `media: read` gets `403`.", + "operationId": "get_v3_session_aml_screening_history_export", + "x-didit-sdk": { + "resource": "sessions", + "method": "export_aml_screening_history", + "auth": "api_key", + "server": true + }, "tags": [ - "Networks" + "Sessions" ], "parameters": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", + "name": "sessionId", "in": "path", "required": true, + "description": "UUID of the verification session (the `session_id` returned by `POST /v3/session/`). Accepts both User Verification (KYC) and Business Verification (KYB) session IDs: the server resolves the ID across both session types. Must be a well-formed UUID; a malformed value does not match the route and returns an HTML `404`.", "schema": { "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } }, { - "name": "network_uuid", - "in": "path", - "required": true, + "name": "node_id", + "in": "query", + "required": false, + "description": "Workflow graph node ID of the AML check. Needed only when the session ran more than one AML step; without it the most recent AML check is used. An unknown node returns `404`.", "schema": { "type": "string", - "format": "uuid", - "example": "n3a5e000-0000-4000-8000-000000000001" - }, - "description": "Network UUID." + "example": "feature_aml_1" + } } ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/n3a5e000-0000-4000-8000-000000000001/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X GET 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/aml-screening-history/export/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -o aml-screening-history.csv" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nsession_id = '11111111-2222-3333-4444-555555555555'\nresp = requests.get(\n f'https://verification.didit.me/v3/session/{session_id}/aml-screening-history/export/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n timeout=10,\n)\nresp.raise_for_status()\nwith open('aml-screening-history.csv', 'wb') as fh:\n fh.write(resp.content)" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "import { writeFile } from 'node:fs/promises';\n\nconst sessionId = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(`https://verification.didit.me/v3/session/${sessionId}/aml-screening-history/export/`, {\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n});\nif (!resp.ok) throw new Error(`Export failed: ${resp.status}`);\nawait writeFile('aml-screening-history.csv', await resp.text());" } ], "responses": { "200": { - "description": "Network detail.", + "description": "The CSV file, streamed as `text/csv` with CRLF line endings. Columns, in order: `screened_at` (when the re-screen finished), `outcome` (`NO_CHANGE`, `CHANGED` or `FAILED`), `provider` (opaque identifier of the AML data source; do not branch on it), `aml_status`, `aml_score`, `total_hits`, `datasets_captured_at`, `datasets_sanctions_last_updated_at`, `datasets_pep_last_updated_at`, `datasets_warnings_last_updated_at`, `datasets_adverse_media_last_updated_at` and `datasets_catalog_version`. Dates are ISO 8601 with a UTC offset. A session with no re-screens yet returns the header line only.", + "headers": { + "Content-Disposition": { + "description": "`attachment; filename=\"aml-screening-history-{sessionId}.csv\"`.", + "schema": { + "type": "string", + "example": "attachment; filename=\"aml-screening-history-11111111-2222-3333-4444-555555555555.csv\"" + } + } + }, + "content": { + "text/csv": { + "schema": { + "type": "string", + "format": "binary" + }, + "example": "screened_at,outcome,provider,aml_status,aml_score,total_hits,datasets_captured_at,datasets_sanctions_last_updated_at,datasets_pep_last_updated_at,datasets_warnings_last_updated_at,datasets_adverse_media_last_updated_at,datasets_catalog_version\r\n2026-09-14T04:12:33.481207+00:00,NO_CHANGE,aml_data_source,Approved,0.0,0,2026-09-14T03:58:02.114820+00:00,2026-09-13T21:10:00+00:00,2026-09-13T08:00:00+00:00,2026-09-12T18:45:00+00:00,,2026-09-13\r\n2026-09-13T04:11:58.020914+00:00,FAILED,aml_data_source,Approved,0.0,0,,,,,,\r\n" + } + } + }, + "401": { + "description": "The scoped API key has expired. This is the only case that returns `401`: a missing, invalid or revoked key returns `403`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Expired key": { + "summary": "The key's expiry date has passed", + "value": { + "detail": "This API key has expired." + } + } + } + } + } + }, + "403": { + "description": "Authentication or authorization failure. Missing, invalid and revoked keys return `403` (not `401`); so do a key of another application, a scoped key without the permission this endpoint needs, and a request from outside the key's IP allowlist. Distinguish them by `detail`.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NetworkDetail" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Missing or invalid credentials": { + "summary": "No x-api-key header, or the key failed validation", + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + }, + "Other application": { + "summary": "A valid key that belongs to a different application than the session", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without decisions read": { + "summary": "Scoped key lacks decisions: read", + "value": { + "detail": "This API key has no read access to decisions." + } + }, + "Scoped key without media read": { + "summary": "Scoped key lacks media: read (the response is a file)", + "value": { + "detail": "This API key has no read access to media." + } + }, + "IP not allowed": { + "summary": "Request from outside the key's IP allowlist", + "value": { + "detail": "This API key can't be used from this IP address." + } + } } } } }, "404": { - "description": "Network not found (or not visible to this application).", + "description": "The session does not exist, was deleted or is outside a scoped key's limits, or it has no AML check (for the given `node_id`). The session lookup runs before the credential check, so an unknown `sessionId` returns `404` even without a valid key.", "content": { "application/json": { "schema": { "type": "object", "properties": { "detail": { - "type": "string" + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Unknown session": { + "summary": "No session with this ID exists, or it was deleted", + "value": { + "detail": "Not found." + } + }, + "Outside key limits": { + "summary": "The session is outside a scoped key's limits (also returned for a session deleted moments ago)", + "value": { + "detail": "Session not found." + } + }, + "No AML check": { + "summary": "The session never ran an AML step", + "value": { + "detail": "The session does not have an AML check." + } + }, + "No AML check for node": { + "summary": "node_id does not match an AML check of the session", + "value": { + "detail": "The session does not have an AML check for node 'feature_aml_2'." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded: 600 GET requests per minute per API key. Wait `Retry-After` seconds before retrying; the `X-RateLimit-*` headers show the budget.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Rate limited": { + "summary": "Budget for the current minute exhausted", + "value": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." } } } } + }, + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/graph/": { - "get": { - "summary": "Get network graph", - "description": "Retrieve the network as a graph of subject nodes (vendor users, vendor businesses, transaction parties) and signal nodes (the shared device/IP/face/document/phone/email/address values linking them), with edges representing memberships.", - "operationId": "get_network_graph", + "/v3/session/{sessionId}/add-images/": { + "patch": { + "summary": "Add an image to a session", + "description": "Attach one extra image or PDF to the ID verification (`model_type=kyc`) or Proof of Address (`model_type=poa`) result of a finished User Verification (KYC) session, for example evidence collected outside the flow. The session must be `Approved`, `Declined`, `In Review` or `Kyc Expired` and carry the result named by `model_type`; Business Verification (KYB) sessions are not supported. Each call appends one file (to the most recent result when the step ran more than once), keeps the existing files and records a `FILE_ADDED` entry in the session activity log; the upload sends no webhook and does not change the session status. The file is compressed on upload and is returned in `extra_files` of the session decision. This endpoint is limited to 10 requests per minute per API key. A scoped API key needs `sessions: write`, and without `media: read` it receives the `extra_files` links as `null`.", + "operationId": "patch_v3_session_add_images", + "x-didit-sdk": { + "resource": "sessions", + "method": "add_images", + "auth": "api_key", + "server": true + }, "tags": [ - "Networks" + "Sessions" ], "parameters": [ { - "name": "organization_id", + "name": "sessionId", "in": "path", "required": true, + "description": "UUID of the User Verification (KYC) session to add the file to (the `session_id` returned by `POST /v3/session/`). A Business Verification (KYB) session ID returns `404`.", "schema": { "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "file", + "model_type" + ], + "properties": { + "file": { + "type": "string", + "format": "binary", + "description": "The image or PDF. Allowed extensions: `jpg`, `jpeg`, `png`, `webp`, `tiff`, `pdf`. At most 15 MB as uploaded; the file is then compressed and rejected with `400` if it is still larger than 2 MB (2,097,152 bytes). Empty files are rejected." + }, + "model_type": { + "type": "string", + "enum": [ + "kyc", + "poa" + ], + "description": "`kyc` adds the file to the ID verification result, `poa` to the Proof of Address result.", + "example": "kyc" + }, + "file_password": { + "type": "string", + "description": "Password of an encrypted PDF. Without it, or with a wrong one, an encrypted PDF returns `400`." + } + } + }, + "example": { + "file": "(binary)", + "model_type": "kyc" + } + } + } + }, + "x-codeSamples": [ { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "lang": "curl", + "label": "curl", + "source": "curl -X PATCH 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/add-images/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'file=@./back-of-id.jpg' \\\n -F 'model_type=kyc'" }, { - "name": "network_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "n3a5e000-0000-4000-8000-000000000001" - }, - "description": "Network UUID." + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nsession_id = '11111111-2222-3333-4444-555555555555'\nwith open('back-of-id.jpg', 'rb') as fh:\n resp = requests.patch(\n f'https://verification.didit.me/v3/session/{session_id}/add-images/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n files={'file': ('back-of-id.jpg', fh, 'image/jpeg')},\n data={'model_type': 'kyc'},\n timeout=10,\n )\nresp.raise_for_status()\nprint(resp.json()['extra_files'])" }, { - "name": "depth", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "1", - "2", - "3", - "all" - ] - }, - "description": "Hops from the focus subject to include. Defaults to all (unbounded)." + "lang": "javascript", + "label": "JavaScript", + "source": "import { readFile } from 'node:fs/promises';\n\nconst sessionId = '11111111-2222-3333-4444-555555555555';\nconst form = new FormData();\nform.append('file', new Blob([await readFile('./back-of-id.jpg')], { type: 'image/jpeg' }), 'back-of-id.jpg');\nform.append('model_type', 'kyc');\n\nconst resp = await fetch(`https://verification.didit.me/v3/session/${sessionId}/add-images/`, {\n method: 'PATCH',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY },\n body: form,\n});\nif (!resp.ok) throw new Error(`Upload failed: ${resp.status}`);\nconst { extra_files: extraFiles } = await resp.json();\nconsole.log(extraFiles);" + } + ], + "responses": { + "200": { + "description": "File added. Returns every extra file of that result, including the new one.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "extra_files" + ], + "properties": { + "extra_files": { + "type": "array", + "description": "Short-lived presigned links to all extra files of the result named by `model_type`, oldest first. Each link is `null` for a scoped API key without `media: read`. Read fresh links from the session decision later.", + "items": { + "type": "string", + "format": "uri", + "nullable": true + } + } + } + }, + "example": { + "extra_files": [ + "https://media.example.com/session/11111111-2222-3333-4444-555555555555/kyc-extra-files/66666666-7777-8888-9999-000000000000.jpg?expires=1789383731&signature=abc123" + ] + } + } + } }, - { - "name": "focus_kind", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "vendor_user", - "vendor_business", - "transaction_party" - ] - }, - "description": "Subject type to center the graph on. Must be supplied together with focus_id; defaults to an arbitrary member when depth is bounded and neither is supplied." + "400": { + "description": "Validation error. Field errors arrive as `{\"\": [\"\"]}`; encrypted-PDF errors as `{\"detail\": \"...\"}`.", + "content": { + "application/json": { + "examples": { + "Missing file": { + "summary": "No file part in the form", + "value": { + "file": [ + "No file was submitted." + ] + } + }, + "Missing model_type": { + "summary": "model_type omitted", + "value": { + "model_type": [ + "This field is required." + ] + } + }, + "Invalid model_type": { + "summary": "model_type is not kyc or poa", + "value": { + "model_type": [ + "\"passport\" is not a valid choice." + ] + } + }, + "File too large": { + "summary": "Upload over 15 MB", + "value": { + "file": [ + "File size should not exceed 15 MB" + ] + } + }, + "Too large after compression": { + "summary": "Still over 2 MB after compression", + "value": { + "file": [ + "File size is too big. Max allowed size is 2097152 bytes." + ] + } + }, + "Encrypted PDF": { + "summary": "Encrypted PDF without file_password", + "value": { + "detail": "The PDF is encrypted. Please upload a decrypted PDF or a photo instead." + } + }, + "Wrong PDF password": { + "summary": "file_password does not open the PDF", + "value": { + "detail": "The PDF password is incorrect. Please provide the correct password." + } + } + } + } + } }, - { - "name": "focus_id", - "in": "query", - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Subject UUID to center the graph on." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/n3a5e000-0000-4000-8000-000000000001/graph/?depth=2' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "200": { - "description": "Network graph.", + "401": { + "description": "The scoped API key has expired. This is the only case that returns `401`: a missing, invalid or revoked key returns `403`.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NetworkGraph" + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Expired key": { + "summary": "The key's expiry date has passed", + "value": { + "detail": "This API key has expired." + } + } } } } }, - "404": { - "description": "Network not found (or not visible to this application).", + "403": { + "description": "Authentication or authorization failure. Missing, invalid and revoked keys return `403` (not `401`); so do a key of another application, a scoped key without the permission this endpoint needs, and a request from outside the key's IP allowlist. Distinguish them by `detail`.", "content": { "application/json": { "schema": { "type": "object", "properties": { "detail": { - "type": "string" + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Missing or invalid credentials": { + "summary": "No x-api-key header, or the key failed validation", + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + }, + "Other application": { + "summary": "A valid key that belongs to a different application than the session", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without sessions write": { + "summary": "Scoped key lacks sessions: write", + "value": { + "detail": "This API key has no write access to sessions." + } + }, + "IP not allowed": { + "summary": "Request from outside the key's IP allowlist", + "value": { + "detail": "This API key can't be used from this IP address." } } } } } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/members/": { - "get": { - "summary": "List network members", - "description": "List the subjects (vendor users, vendor businesses, or transaction parties) that belong to this network.", - "operationId": "list_network_members", - "tags": [ - "Networks" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." }, - { - "name": "network_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "n3a5e000-0000-4000-8000-000000000001" - }, - "description": "Network UUID." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/n3a5e000-0000-4000-8000-000000000001/members/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "200": { - "description": "Network members. Not paginated.", + "404": { + "description": "The session does not exist, was deleted or is outside a scoped key's limits; it is a KYB session or not in `Approved`, `Declined`, `In Review` or `Kyc Expired`; or it has no result of the requested `model_type` (checked before the file is validated). The session lookup runs before the credential check, so an unknown `sessionId` returns `404` even without a valid key.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NetworkMember" - } + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Unknown session": { + "summary": "No session with this ID exists, or it was deleted", + "value": { + "detail": "Not found." + } + }, + "Outside key limits": { + "summary": "The session is outside a scoped key's workflow or approved-only limits", + "value": { + "detail": "Session not found." + } + }, + "Not eligible": { + "summary": "Session not finished yet, or a KYB session ID", + "value": { + "detail": "No Session matches the given query." + } + }, + "No ID verification": { + "summary": "model_type=kyc on a session without ID verification", + "value": { + "detail": "The session does not have KYC data." + } + }, + "No Proof of Address": { + "summary": "model_type=poa on a session without Proof of Address", + "value": { + "detail": "The session does not have POA data." } } } } } }, - "404": { - "description": "Network not found (or not visible to this application).", + "429": { + "description": "Rate limit exceeded: 10 add-images requests per minute per API key (rejected requests count too). The shared budget of 300 write requests per minute also applies. Wait `Retry-After` seconds before retrying; the `X-RateLimit-*` headers show the budget.", "content": { "application/json": { "schema": { "type": "object", "properties": { "detail": { - "type": "string" + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Rate limited": { + "summary": "Budget for the current minute exhausted", + "value": { + "detail": "Session add-images rate limit exceeded. You can make up to 10 requests per minute." } } } } + }, + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/signals/": { - "get": { - "summary": "List network signals", - "description": "List the signals (shared device, IP, face, document number, phone, email, or address values) that link this network's members.", - "operationId": "list_network_signals", + "/v3/session/{sessionId}/update-aml-status/": { + "patch": { + "summary": "Update an AML check status", + "description": "Set the status of a session's AML screening check as a whole (the `status` of an `aml_screenings[]` entry in the session decision), for example `Approved` after your team cleared every hit. Works for KYC and KYB session IDs; pass `node_id` (query parameter or body field) when the session ran more than one AML step, otherwise the most recent AML check is updated. When the status actually changes, an `AML_STATUS_UPDATED` activity entry is recorded and a `data.updated` [webhook](https://docs.didit.me/integration/webhooks) is sent; sending the current status returns `200` and changes nothing. The session status is not recomputed: use `PATCH /v3/session/{sessionId}/update-status/` for the session decision and `PATCH /v3/session/{sessionId}/update-aml-hit-status/` for single hits. A scoped API key needs `decisions: write`.", + "operationId": "patch_v3_session_update_aml_status", + "x-didit-sdk": { + "resource": "sessions", + "method": "update_aml_status", + "auth": "api_key", + "server": true + }, "tags": [ - "Networks" + "Sessions" ], "parameters": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", + "name": "sessionId", "in": "path", "required": true, + "description": "UUID of the verification session (the `session_id` returned by `POST /v3/session/`). Accepts both User Verification (KYC) and Business Verification (KYB) session IDs: the server resolves the ID across both session types. Must be a well-formed UUID; a malformed value does not match the route and returns an HTML `404`.", "schema": { "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "format": "uuid", + "example": "11111111-2222-3333-4444-555555555555" + } }, { - "name": "network_uuid", - "in": "path", - "required": true, + "name": "node_id", + "in": "query", + "required": false, + "description": "Workflow graph node ID of the AML check to update. Needed only when the session ran more than one AML step. Takes precedence over `node_id` in the body.", "schema": { "type": "string", - "format": "uuid", - "example": "n3a5e000-0000-4000-8000-000000000001" - }, - "description": "Network UUID." + "example": "feature_aml_1" + } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ], + "description": "New status of the AML check.", + "example": "Approved" + }, + "node_id": { + "type": "string", + "description": "Workflow graph node ID of the AML check, as an alternative to the query parameter.", + "example": "feature_aml_1" + } + } + }, + "example": { + "status": "Approved" + } + } + } + }, "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/n3a5e000-0000-4000-8000-000000000001/signals/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X PATCH 'https://verification.didit.me/v3/session/11111111-2222-3333-4444-555555555555/update-aml-status/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"status\": \"Approved\"}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import os\n\nimport requests\n\nsession_id = '11111111-2222-3333-4444-555555555555'\nresp = requests.patch(\n f'https://verification.didit.me/v3/session/{session_id}/update-aml-status/',\n headers={'x-api-key': os.environ['DIDIT_API_KEY']},\n json={'status': 'Approved'},\n timeout=10,\n)\nresp.raise_for_status()\nbody = resp.json()\nprint(body['previous_status'], '->', body['status'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const sessionId = '11111111-2222-3333-4444-555555555555';\nconst resp = await fetch(`https://verification.didit.me/v3/session/${sessionId}/update-aml-status/`, {\n method: 'PATCH',\n headers: { 'x-api-key': process.env.DIDIT_API_KEY, 'Content-Type': 'application/json' },\n body: JSON.stringify({ status: 'Approved' }),\n});\nif (!resp.ok) throw new Error(`Update failed: ${resp.status}`);\nconst body = await resp.json();\nconsole.log(body.previous_status, '->', body.status);" } ], "responses": { "200": { - "description": "Network signals. Not paginated.", + "description": "AML check status set (or unchanged when it already had this value).", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "message", + "status", + "previous_status" + ], "properties": { - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NetworkSignal" - } + "message": { + "type": "string", + "description": "Always `AML status updated successfully`." + }, + "status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ], + "description": "Status of the AML check now." + }, + "previous_status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ], + "description": "Status before the call. Equal to `status` when nothing changed." } } + }, + "example": { + "message": "AML status updated successfully", + "status": "Approved", + "previous_status": "In Review" } } } }, - "404": { - "description": "Network not found (or not visible to this application).", + "400": { + "description": "Validation error: `status` missing or not one of the allowed values. The AML check lookup runs first, so a session without an AML check returns `404` even with an invalid body.", + "content": { + "application/json": { + "examples": { + "Missing status": { + "summary": "Empty body or no status", + "value": { + "status": [ + "This field is required." + ] + } + }, + "Invalid status": { + "summary": "status not in the enum", + "value": { + "status": [ + "\"Cleared\" is not a valid choice." + ] + } + } + } + } + } + }, + "401": { + "description": "The scoped API key has expired. This is the only case that returns `401`: a missing, invalid or revoked key returns `403`.", "content": { "application/json": { "schema": { "type": "object", "properties": { "detail": { - "type": "string" + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Expired key": { + "summary": "The key's expiry date has passed", + "value": { + "detail": "This API key has expired." } } } } } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/timeline/": { - "get": { - "summary": "Get network timeline", - "description": "Chronological events for this network: detection, each signal's observation, and lifecycle status changes.", - "operationId": "get_network_timeline", - "tags": [ - "Networks" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "network_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "n3a5e000-0000-4000-8000-000000000001" - }, - "description": "Network UUID." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/n3a5e000-0000-4000-8000-000000000001/timeline/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "200": { - "description": "Timeline events, oldest first. Not paginated.", + "403": { + "description": "Authentication or authorization failure. Missing, invalid and revoked keys return `403` (not `401`); so do a key of another application, a scoped key without the permission this endpoint needs, and a request from outside the key's IP allowlist. Distinguish them by `detail`.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NetworkTimelineEvent" - } + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Missing or invalid credentials": { + "summary": "No x-api-key header, or the key failed validation", + "value": { + "detail": "Authentication credentials were not provided or are invalid." + } + }, + "Other application": { + "summary": "A valid key that belongs to a different application than the session", + "value": { + "detail": "You do not have permission to perform this action." + } + }, + "Scoped key without decisions write": { + "summary": "Scoped key lacks decisions: write", + "value": { + "detail": "This API key has no write access to decisions." + } + }, + "IP not allowed": { + "summary": "Request from outside the key's IP allowlist", + "value": { + "detail": "This API key can't be used from this IP address." } } } @@ -45715,244 +67713,448 @@ } }, "404": { - "description": "Network not found (or not visible to this application).", + "description": "The session does not exist, was deleted or is outside a scoped key's limits, or it has no AML check (for the given `node_id`). The session lookup runs before the credential check, so an unknown `sessionId` returns `404` even without a valid key.", "content": { "application/json": { "schema": { "type": "object", "properties": { "detail": { - "type": "string" + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Unknown session": { + "summary": "No session with this ID exists, or it was deleted", + "value": { + "detail": "Not found." + } + }, + "Outside key limits": { + "summary": "The session is outside a scoped key's limits (also returned for a session deleted moments ago)", + "value": { + "detail": "Session not found." + } + }, + "No AML check": { + "summary": "The session never ran an AML step", + "value": { + "detail": "The session does not have an AML check." + } + }, + "No AML check for node": { + "summary": "node_id does not match an AML check of the session", + "value": { + "detail": "The session does not have an AML check for node 'feature_aml_2'." + } + } + } + } + } + }, + "429": { + "description": "Rate limit exceeded: 300 write (POST, PATCH, DELETE) requests per minute per API key. Wait `Retry-After` seconds before retrying; the `X-RateLimit-*` headers show the budget.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message." + } + } + }, + "examples": { + "Rate limited": { + "summary": "Budget for the current minute exhausted", + "value": { + "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." } } } } + }, + "headers": { + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 30 + } + } } } - } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ] } }, - "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/map/": { + "/v3/status/": { "get": { - "summary": "Get network map", - "description": "Geographic points where this network's members' addresses/locations converge, sourced from the sessions, business sessions, and transactions backing its memberships.", - "operationId": "get_network_map", + "summary": "Get service status (public, unauthenticated)", + "description": "Returns the state of each Didit service (`operational`, `degraded` or `down`), the overall state (the worst of them), and the incidents that are ongoing or were resolved in the last 7 days. Use it to power a status widget or to check Didit's health from your integration.\n\nPublic: send no auth header. The response contains no organization data and is the same for every caller. Responses are cached for 30 seconds, so poll no more than once every 30 seconds.", + "operationId": "get_v3_status", "tags": [ - "Networks" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, - { - "name": "network_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "example": "n3a5e000-0000-4000-8000-000000000001" - }, - "description": "Network UUID." - } + "Status" ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/n3a5e000-0000-4000-8000-000000000001/map/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } + "security": [ + {} ], + "parameters": [], "responses": { "200": { - "description": "Network map.", + "description": "Current service status.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NetworkMap" + "$ref": "#/components/schemas/ServiceStatusFeed" + }, + "examples": { + "One service degraded": { + "summary": "An ongoing incident and one resolved this week", + "value": { + "status": "degraded", + "updated_at": "2026-10-13T09:41:07.512345Z", + "services": [ + { + "key": "service_id_verification", + "name": "ID verification", + "status": "operational" + }, + { + "key": "service_liveness_face_match", + "name": "Liveness and face match", + "status": "operational" + }, + { + "key": "service_aml_screening", + "name": "AML screening", + "status": "operational" + }, + { + "key": "service_kyb_registries", + "name": "KYB registries", + "status": "operational" + }, + { + "key": "service_database_validation", + "name": "Database validation", + "status": "degraded" + }, + { + "key": "service_phone_delivery", + "name": "Phone verification delivery", + "status": "operational" + }, + { + "key": "service_email_delivery", + "name": "Email verification delivery", + "status": "operational" + }, + { + "key": "service_transaction_monitoring", + "name": "Transaction monitoring", + "status": "operational" + } + ], + "incidents": [ + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "service": "service_database_validation", + "service_name": "Database validation", + "state": "degraded", + "status": "ongoing", + "title": "Database validation is degraded in Brazil.", + "body": "Checks against Brazil (CPF) may be slow or fail since 08:55 UTC.", + "affected": "Brazil (CPF)", + "country_name": "Brazil", + "started_at": "2026-10-13T08:55:00Z", + "resolved_at": null, + "duration_minutes": null + }, + { + "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "service": "service_aml_screening", + "service_name": "AML screening", + "state": "down", + "status": "resolved", + "title": "AML screening is back to normal.", + "body": "Recovered at 14:32 UTC after 17 minutes.", + "affected": "", + "country_name": "", + "started_at": "2026-10-11T14:15:00Z", + "resolved_at": "2026-10-11T14:32:00Z", + "duration_minutes": 17 + } + ] + } + }, + "All operational": { + "summary": "No incident in the last 7 days", + "value": { + "status": "operational", + "updated_at": "2026-10-13T09:41:07.512345Z", + "services": [ + { + "key": "service_id_verification", + "name": "ID verification", + "status": "operational" + }, + { + "key": "service_liveness_face_match", + "name": "Liveness and face match", + "status": "operational" + }, + { + "key": "service_aml_screening", + "name": "AML screening", + "status": "operational" + }, + { + "key": "service_kyb_registries", + "name": "KYB registries", + "status": "operational" + }, + { + "key": "service_database_validation", + "name": "Database validation", + "status": "operational" + }, + { + "key": "service_phone_delivery", + "name": "Phone verification delivery", + "status": "operational" + }, + { + "key": "service_email_delivery", + "name": "Email verification delivery", + "status": "operational" + }, + { + "key": "service_transaction_monitoring", + "name": "Transaction monitoring", + "status": "operational" + } + ], + "incidents": [] + } + } } } } }, - "404": { - "description": "Network not found (or not visible to this application).", + "429": { + "description": "More than 600 GET requests in a minute from the same IP address (or the same API key, if you send one). Honor `Retry-After` before retrying.", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "detail" + ], "properties": { "detail": { "type": "string" } } + }, + "example": { + "detail": "GET request rate limit exceeded. You can make up to 600 requests per minute." } } } } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -fsS https://verification.didit.me/v3/status/" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresp = requests.get('https://verification.didit.me/v3/status/', timeout=5)\nresp.raise_for_status()\nfeed = resp.json()\nprint(feed['status'])\nfor service in feed['services']:\n if service['status'] != 'operational':\n print(service['key'], service['status'])" + }, + { + "lang": "javascript", + "label": "JavaScript", + "source": "const feed = await fetch('https://verification.didit.me/v3/status/').then((r) => r.json());\nconsole.log(feed.status);\nconst affected = feed.services.filter((s) => s.status !== 'operational');\nconsole.log(affected.map((s) => `${s.key}: ${s.status}`));" + } + ], + "x-didit-sdk": { + "resource": "system", + "method": "retrieve_status", + "auth": "public", + "server": false } } }, - "/v3/organization/{organization_id}/application/{application_id}/sessions/{session_id}/networks/": { - "get": { - "summary": "List networks for a session", - "description": "List the networks the session's vendor user belongs to. Resolves through the session's vendor_data-linked user.", - "operationId": "list_session_network_memberships", + "/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", "tags": [ - "Networks" + "Sessions" ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, + "security": [ { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, + "ApiKeyAuth": [] + } + ], + "parameters": [ { - "name": "session_id", "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" - }, - "description": "Session UUID." + "format": "uuid", + "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } } ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/sessions/aaaaaaaa-0000-4000-8000-000000000001/networks/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "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": "Networks this entity belongs to. Not paginated.", + "description": "The status changed.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EntityNetworkMembership" - } + "$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 } } } } } }, - "404": { - "description": "Entity not found in this application, or has no linked subject to look up.", + "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": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" + "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." + ] } } } } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/business-sessions/{business_session_id}/networks/": { - "get": { - "summary": "List networks for a business session", - "description": "List the networks the business session's vendor business belongs to.", - "operationId": "list_business_session_network_memberships", - "tags": [ - "Networks" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "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." + } + } + } + } + } }, - { - "name": "business_session_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Business session UUID." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/business-sessions/aaaaaaaa-0000-4000-8000-000000000002/networks/' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ], - "responses": { - "200": { - "description": "Networks this entity belongs to. Not paginated.", + "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": { - "schema": { - "type": "object", - "properties": { - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EntityNetworkMembership" - } + "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." } } } @@ -45960,83 +68162,172 @@ } }, "404": { - "description": "Entity not found in this application, or has no linked subject to look up.", + "description": "No session with this `session_id` exists, or it is a live (non-sandbox) session.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" + "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/organization/{organization_id}/application/{application_id}/users/{vendor_user_uuid}/networks/": { - "get": { - "summary": "List networks for a user", - "description": "List the networks a vendor user belongs to.", - "operationId": "list_user_network_memberships", + "/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": [ - "Networks" + "Sessions" ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, + "security": [ { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, + "SessionTokenAuth": [] + } + ], + "parameters": [ { - "name": "vendor_user_uuid", "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" - }, - "description": "Vendor user UUID." + "format": "uuid", + "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } } ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/users/aaaaaaaa-0000-4000-8000-000000000003/networks/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "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": "Networks this entity belongs to. Not paginated.", + "description": "The status changed.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EntityNetworkMembership" - } + "$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." + ] + } + } + } + } + }, + "403": { + "description": "The `Session-Token` header is missing or invalid.", + "content": { + "application/json": { + "examples": { + "Invalid Session Token": { + "summary": "Missing or invalid Session-Token", + "value": { + "detail": "You do not have permission to perform this action." } } } @@ -46044,167 +68335,349 @@ } }, "404": { - "description": "Entity not found in this application, or has no linked subject to look up.", + "description": "The `Session-Token` belongs to a live (non-sandbox) application, or does not match the `sessionId` in the path.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" + "examples": { + "Not Found": { + "summary": "Live session, or session_id / token mismatch", + "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/sandbox/simulate/ \\\n -H 'Session-Token: YOUR_SESSION_TOKEN' \\\n -H 'Content-Type: application/json' \\\n -d '{\"new_status\": \"Approved\", \"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/sandbox/simulate/\",\n headers={\"Session-Token\": \"YOUR_SESSION_TOKEN\"},\n json={\"new_status\": \"Approved\", \"send_webhooks\": False},\n)\nresponse.raise_for_status()\nprint(response.json())" + } + ], + "x-didit-sdk": { + "resource": "sessions", + "method": "simulate_sandbox", + "auth": "device_token", + "server": false } } }, - "/v3/organization/{organization_id}/application/{application_id}/businesses/{vendor_business_uuid}/networks/": { + "/v3/session/{sessionId}/features/overrides/": { "get": { - "summary": "List networks for a business", - "description": "List the networks a vendor business belongs to.", - "operationId": "list_business_network_memberships", + "summary": "List a session's check overrides", + "description": "Every override of an individual feature (step) status on a User Verification (KYC) or Business Verification (KYB) session, newest first and not paginated: applied ones, ones waiting for the person to finish the flow (`pending_until_flow_finishes`) and cancelled ones, each with who asked, their reason, and whether it applied while the person was still in the flow (`forced`).\n\n`person_in_flow` tells you whether a new override with `defer_until_flow_finishes: true` would wait. `flow_finished_at` is when the person last finished the flow; it is `null` while they are still in it (a resubmission included) and when the session closed before they finished.\n\nRequires the `read:sessions` privilege; a scoped API key needs read access to decisions.", + "operationId": "get_v3_session_feature_overrides", "tags": [ - "Networks" + "Sessions" ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, + "security": [ { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - }, + "ApiKeyAuth": [] + } + ], + "parameters": [ { - "name": "vendor_business_uuid", "in": "path", + "name": "sessionId", "required": true, + "description": "UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs.", "schema": { "type": "string", - "format": "uuid" - }, - "description": "Vendor business UUID." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/businesses/aaaaaaaa-0000-4000-8000-000000000004/networks/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "format": "uuid", + "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } } ], "responses": { "200": { - "description": "Networks this entity belongs to. Not paginated.", + "description": "The session's check overrides, newest first.", "content": { "application/json": { "schema": { "type": "object", + "required": [ + "session_id", + "person_in_flow", + "flow_finished_at", + "overrides" + ], "properties": { - "results": { + "session_id": { + "type": "string", + "format": "uuid", + "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + }, + "person_in_flow": { + "type": "boolean", + "description": "Whether the person is still in the flow, so a new override with `defer_until_flow_finishes: true` would wait.", + "example": true + }, + "flow_finished_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the person last finished the flow. `null` while they are still in it (a resubmission included) and when the session closed before they finished (abandoned, expired or decided mid-flow).", + "example": null + }, + "overrides": { "type": "array", + "description": "Every override of the session, newest first.", "items": { - "$ref": "#/components/schemas/EntityNetworkMembership" + "$ref": "#/components/schemas/FeatureStatusOverride" } } } + }, + "examples": { + "Overrides": { + "summary": "One override waiting, one applied", + "value": { + "session_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "person_in_flow": true, + "flow_finished_at": null, + "overrides": [ + { + "id": "9b2f4c1e-8d3a-4e5f-a6b7-c8d9e0f1a2b3", + "node_id": "feature_liveness_1", + "feature_type": "", + "feature_model": "Face", + "previous_status": "Declined", + "new_status": "Approved", + "state": "pending_until_flow_finishes", + "forced": false, + "reason": "Liveness retried with a reviewer present.", + "requested_by": { + "email": "reviewer@example.com", + "name": "Alex Reviewer", + "type": "CONSOLE_USER" + }, + "requested_at": "2026-10-13T09:15:00Z", + "applied_at": null, + "cancelled_at": null, + "cancel_reason": null + }, + { + "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "node_id": "feature_ocr_1", + "feature_type": "", + "feature_model": "KYC", + "previous_status": "In Review", + "new_status": "Approved", + "state": "applied", + "forced": true, + "reason": "Document manually verified by compliance.", + "requested_by": { + "email": "reviewer@example.com", + "name": "Alex Reviewer", + "type": "CONSOLE_USER" + }, + "requested_at": "2026-10-13T09:15:00Z", + "applied_at": "2026-10-13T09:15:00Z", + "cancelled_at": null, + "cancel_reason": null + } + ] + } + } + } + } + } + }, + "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 `read: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 read: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 read access to decisions." + } + } } } } }, "404": { - "description": "Entity not found in this application, or has no linked subject to look up.", + "description": "No KYC or KYB session with this `session_id` exists.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" + "examples": { + "Not Found": { + "summary": "Unknown session_id", + "value": { + "detail": "Not found." } } } } } } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET \\\n https://verification.didit.me/v3/session/3fa85f64-5717-4562-b3fc-2c963f66afa6/features/overrides/ \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresponse = requests.get(\n \"https://verification.didit.me/v3/session/3fa85f64-5717-4562-b3fc-2c963f66afa6/features/overrides/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n)\nresponse.raise_for_status()\nprint(response.json())" + } + ], + "x-didit-sdk": { + "resource": "sessions", + "method": "list_feature_overrides", + "auth": "api_key", + "server": true } } }, - "/v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/networks/": { - "get": { - "summary": "List networks for a transaction", - "description": "List the networks the transaction's backing KYC/KYB subject belongs to (resolved through the transaction's session or business session).", - "operationId": "list_transaction_network_memberships", + "/v3/session/{sessionId}/features/overrides/{overrideId}/cancel/": { + "post": { + "summary": "Cancel a check override that waits for the flow to finish", + "description": "Cancel an override of a feature (step) status that is still `pending_until_flow_finishes`, so it never applies. The check keeps its status, the cancellation is recorded in the session's activity as `FEATURE_OVERRIDE_CANCELLED`, and no webhook is sent. The response is the override with `state: cancelled` and `cancel_reason: cancelled_by_reviewer`.\n\nAn override that already applied or was cancelled returns `409` with the code `feature_override_not_pending`. Find override IDs with `GET /v3/session/{sessionId}/features/overrides/` or in the `override` returned by the feature status update.\n\nRequires the `write:sessions` privilege; a scoped API key needs write access to decisions.", + "operationId": "post_v3_session_feature_override_cancel", "tags": [ - "Networks" + "Sessions" ], - "parameters": [ + "security": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, + "ApiKeyAuth": [] + } + ], + "parameters": [ { - "name": "application_id", "in": "path", + "name": "sessionId", "required": true, + "description": "UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs.", "schema": { "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "format": "uuid", + "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } }, { - "name": "transaction_uuid", "in": "path", + "name": "overrideId", "required": true, + "description": "The `id` of the override to cancel.", "schema": { "type": "string", - "format": "uuid" - }, - "description": "Transaction UUID." - } - ], - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/transactions/aaaaaaaa-0000-4000-8000-000000000005/networks/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "format": "uuid", + "example": "9b2f4c1e-8d3a-4e5f-a6b7-c8d9e0f1a2b3" + } } ], "responses": { "200": { - "description": "Networks this entity belongs to. Not paginated.", + "description": "The override is cancelled and will never apply.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EntityNetworkMembership" - } + "$ref": "#/components/schemas/FeatureStatusOverride" + }, + "examples": { + "Cancelled": { + "summary": "The cancelled override", + "value": { + "id": "9b2f4c1e-8d3a-4e5f-a6b7-c8d9e0f1a2b3", + "node_id": "feature_liveness_1", + "feature_type": "", + "feature_model": "Face", + "previous_status": "Declined", + "new_status": "Approved", + "state": "cancelled", + "forced": false, + "reason": "Liveness retried with a reviewer present.", + "requested_by": { + "email": "reviewer@example.com", + "name": "Alex Reviewer", + "type": "CONSOLE_USER" + }, + "requested_at": "2026-10-13T09:15:00Z", + "applied_at": null, + "cancelled_at": "2026-10-13T09:20:00Z", + "cancel_reason": "cancelled_by_reviewer" + } + } + } + } + } + }, + "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 decisions." } } } @@ -46212,31 +68685,76 @@ } }, "404": { - "description": "Entity not found in this application, or has no linked subject to look up.", + "description": "No KYC or KYB session with this `session_id` exists, or the session has no override with this `overrideId`.", "content": { "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" + "examples": { + "Not Found": { + "summary": "Unknown session_id", + "value": { + "detail": "Not found." + } + }, + "Override Not Found": { + "summary": "No override with this id on the session", + "value": { + "detail": "Override not found." + } + } + } + } + } + }, + "409": { + "description": "The override already applied or was cancelled.", + "content": { + "application/json": { + "examples": { + "Not Pending": { + "summary": "Only a waiting override can be cancelled", + "value": { + "detail": "This override is no longer pending: it already applied or was cancelled.", + "code": "feature_override_not_pending" } } } } } } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST \\\n https://verification.didit.me/v3/session/3fa85f64-5717-4562-b3fc-2c963f66afa6/features/overrides/9b2f4c1e-8d3a-4e5f-a6b7-c8d9e0f1a2b3/cancel/ \\\n -H 'x-api-key: YOUR_API_KEY'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/session/3fa85f64-5717-4562-b3fc-2c963f66afa6/features/overrides/9b2f4c1e-8d3a-4e5f-a6b7-c8d9e0f1a2b3/cancel/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n)\nresponse.raise_for_status()\nprint(response.json())" + } + ], + "x-didit-sdk": { + "resource": "sessions", + "method": "cancel_feature_override", + "auth": "api_key", + "server": true } } }, - "/v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/parties/{transaction_party_uuid}/networks/": { - "get": { - "summary": "List networks for a transaction party", - "description": "List the networks a transaction party (remitter, beneficiary, or counterparty) belongs to, resolved through the party itself rather than through the transaction's backing session.", - "operationId": "list_transaction_party_network_memberships", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/tags/add/": { + "post": { + "summary": "Add a tag to a network", + "description": "Put one of your application's tags on the network. Returns the network detail, whose `tags` lists your application's tags on the network, with `201` when the tag was added and `200` when it was already on the network. A tag that belongs to another application, or does not exist, returns 404 `Tag not found.`. Requires a full-access API key, or `write:networks` for a console member.", + "operationId": "add_network_tag", "tags": [ "Networks" ], + "security": [ + { + "ApiKeyAuth": [] + } + ], "parameters": [ { "name": "organization_id", @@ -46259,357 +68777,333 @@ "description": "Application UUID." }, { - "name": "transaction_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Transaction UUID." - }, - { - "name": "transaction_party_uuid", + "name": "network_uuid", "in": "path", "required": true, "schema": { "type": "string", - "format": "uuid" + "format": "uuid", + "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }, - "description": "Transaction party UUID." + "description": "Network UUID." } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NetworkTagRequest" + }, + "example": { + "tag_uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7" + } + } + } + }, "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/transactions/aaaaaaaa-0000-4000-8000-000000000005/parties/bbbbbbbb-0000-4000-8000-000000000006/networks/' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/3fa85f64-5717-4562-b3fc-2c963f66afa6/tags/add/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"tag_uuid\": \"7c9e6679-7425-40de-944b-e07fc1f90ae7\"}'" } ], "responses": { - "200": { - "description": "Networks this entity belongs to. Not paginated.", + "201": { + "description": "The tag was added. Returns the network detail.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EntityNetworkMembership" + "$ref": "#/components/schemas/NetworkDetailResponse" + }, + "example": { + "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "network_id": "N-0042", + "name": null, + "status": "in_review", + "risk": { + "score": 82, + "band": "high", + "bar": 0.82 + }, + "pattern_types": [ + "exact_same_device", + "same_email" + ], + "signal_types": [ + "device", + "email" + ], + "member_counts": { + "total": 3, + "users": 3, + "businesses": 0, + "transaction_parties": 0 + }, + "member_status": { + "approved": 1, + "declined": 1, + "in_review": 0, + "not_completed": 1 + }, + "signal_coverage": { + "device": { + "signals": 1, + "subjects": 3, + "coverage": 1.0 + }, + "email": { + "signals": 1, + "subjects": 2, + "coverage": 0.6667 + } + }, + "first_activity_at": "2026-10-01T09:12:44Z", + "last_activity_at": "2026-10-12T16:03:10Z", + "stats": { + "members": { + "total": 3, + "users": 3, + "businesses": 0, + "transaction_parties": 0 + }, + "member_status": { + "approved": 1, + "declined": 1, + "in_review": 0, + "not_completed": 1 + }, + "signals": { + "total": 2, + "by_type": { + "device": { + "signals": 1, + "subjects": 3, + "coverage": 1.0 + }, + "email": { + "signals": 1, + "subjects": 2, + "coverage": 0.6667 + } } } - } - } - } - } - }, - "404": { - "description": "Entity not found in this application, or has no linked subject to look up.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" + }, + "fraud_tags": [], + "risk_signals": { + "residential_proxy_reuse": { + "detected": false, + "signal_count": 0, + "max_subject_count": 0 + }, + "email_intelligence": { + "enriched_signal_count": 1, + "high_risk_count": 0, + "social_profile_count": 0 + }, + "phone_intelligence": { + "enriched_signal_count": 0, + "high_risk_count": 0 } - } + }, + "reviewed_at": "2026-10-12T16:30:00Z", + "reviewed_by": "reviewer@example.com", + "review_reason": "Shared device across three new accounts.", + "notes_count": 0, + "sharing_mode": "global", + "cross_org_available": true, + "cross_org_unavailable_reason": null, + "tags": [ + { + "uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "name": "SUSPICIOUS", + "color": "#6366F1", + "source": "custom", + "added_by": null, + "added_at": "2026-10-13T08:15:00Z" + } + ], + "outcomes": { + "approved": 1, + "declined": 1, + "in_review": 0, + "total": 3 + }, + "patterns": [ + { + "type": "background", + "score": 0.8125, + "members": 3, + "reason": null, + "computed_at": "2026-10-13T08:00:00Z" + } + ] } } } - } - } - } - }, - "/v3/biometric-templates/": { - "get": { - "summary": "List retained biometric templates", - "description": "An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`.\n\nReturns a paginated list of the templates the application currently holds. Purged templates are not listed.", - "operationId": "list_biometric_templates", - "tags": [ - "Biometric Templates" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], - "parameters": [ - { - "in": "query", - "name": "vendor_data", - "required": false, - "description": "Case-insensitive substring match on the owning User's current `vendor_data`.", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "vendor_user_uuid", - "required": false, - "description": "Exact match on the owning User.", - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "in": "query", - "name": "status", - "required": false, - "description": "Lifecycle status. Purged templates are never listed.", - "schema": { - "type": "string", - "enum": [ - "pending", - "active", - "expired", - "held_out_of_use", - "purge_pending", - "purge_failed" - ] - } - }, - { - "in": "query", - "name": "source_type", - "required": false, - "description": "How the template was created.", - "schema": { - "type": "string", - "enum": [ - "session_delete", - "automatic_retention", - "backfill" - ] - } - }, - { - "in": "query", - "name": "retained_from", - "required": false, - "description": "Templates retained at or after this time.", - "schema": { - "type": "string", - "format": "date-time" - } }, - { - "in": "query", - "name": "retained_to", - "required": false, - "description": "Templates retained at or before this time. Must not be earlier than `retained_from` (`400`).", - "schema": { - "type": "string", - "format": "date-time" - } - }, - { - "in": "query", - "name": "ordering", - "required": false, - "description": "Sort order.", - "schema": { - "type": "string", - "enum": [ - "retained_at", - "-retained_at", - "expires_at", - "-expires_at" - ], - "default": "-retained_at" - } - }, - { - "in": "query", - "name": "limit", - "required": false, - "description": "Page size.", - "schema": { - "type": "integer" - } - }, - { - "in": "query", - "name": "offset", - "required": false, - "description": "Pagination offset.", - "schema": { - "type": "integer" - } - } - ], - "responses": { "200": { - "description": "Paginated templates.", + "description": "The tag was already on the network and is left as it is. Returns the network detail.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "count": { - "type": "integer" - }, - "next": { - "type": "string", - "nullable": true + "$ref": "#/components/schemas/NetworkDetailResponse" + }, + "example": { + "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "network_id": "N-0042", + "name": null, + "status": "in_review", + "risk": { + "score": 82, + "band": "high", + "bar": 0.82 + }, + "pattern_types": [ + "exact_same_device", + "same_email" + ], + "signal_types": [ + "device", + "email" + ], + "member_counts": { + "total": 3, + "users": 3, + "businesses": 0, + "transaction_parties": 0 + }, + "member_status": { + "approved": 1, + "declined": 1, + "in_review": 0, + "not_completed": 1 + }, + "signal_coverage": { + "device": { + "signals": 1, + "subjects": 3, + "coverage": 1.0 }, - "previous": { - "type": "string", - "nullable": true + "email": { + "signals": 1, + "subjects": 2, + "coverage": 0.6667 + } + }, + "first_activity_at": "2026-10-01T09:12:44Z", + "last_activity_at": "2026-10-12T16:03:10Z", + "stats": { + "members": { + "total": 3, + "users": 3, + "businesses": 0, + "transaction_parties": 0 + }, + "member_status": { + "approved": 1, + "declined": 1, + "in_review": 0, + "not_completed": 1 }, - "results": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Template id. Randomly generated; not derived from the deleted session or its face." - }, - "vendor_user_uuid": { - "type": "string", - "format": "uuid", - "description": "The owning User." - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "The User's current `vendor_data`, resolved when you read the template (never copied)." - }, - "source_type": { - "type": "string", - "enum": [ - "session_delete", - "automatic_retention", - "backfill" - ], - "description": "How the template was created: a session deletion (API or Console), the application's automatic data-retention window, or a Didit-operated backfill you instructed." - }, - "provenance_reference": { - "type": "string", - "format": "uuid", - "description": "Opaque reference tying the template to its audit trail. It is not the deleted session's id and cannot be resolved to it." - }, - "retention_policy": { - "type": "string", - "enum": [ - "delete_with_session", - "retain_until_user_deleted" - ], - "description": "The application policy at retention time." - }, - "retention_override": { - "type": "boolean", - "nullable": true, - "description": "The `retain_face_embeddings` value sent on the deletion, or `null` when the policy applied." - }, - "retained_at": { - "type": "string", - "format": "date-time" - }, - "expires_at": { - "type": "string", - "format": "date-time", - "description": "When the template expires and is purged automatically. Always set." - }, - "retained_by": { - "type": "string", - "nullable": true, - "description": "Principal that gave the instruction: the application id for API keys, the Console member id for Console actions, or a `service:` identifier for automatic retention. Never an email address." - }, - "retained_by_type": { - "type": "string", - "enum": [ - "API_KEY", - "CONSOLE_USER", - "SYSTEM" - ] - }, - "instruction_class": { - "type": "string", - "enum": [ - "operational_session_delete", - "privacy_erasure" - ] - }, - "instruction_source": { - "type": "string", - "enum": [ - "api", - "console", - "automatic_retention", - "backfill", - "privacy_erasure", - "explicit_template_purge", - "vendor_user_delete", - "expiry", - "reconciliation", - "migration", - "system" - ] - }, - "instruction_id": { - "type": "string", - "description": "The `instruction_id` sent on the deletion, or a Didit-generated id." - }, - "held_out_of_use": { - "type": "boolean", - "description": "`true` only while a legal hold applies. A held template is excluded from all matching." - }, - "status": { - "type": "string", - "enum": [ - "pending", - "active", - "expired", - "held_out_of_use", - "purge_pending", - "purge_failed", - "purged" - ], - "description": "Only `active` templates that are not held, not past `expires_at`, and whose User still exists take part in matching." - }, - "purged_at": { - "type": "string", - "format": "date-time", - "nullable": true - } + "signals": { + "total": 2, + "by_type": { + "device": { + "signals": 1, + "subjects": 3, + "coverage": 1.0 + }, + "email": { + "signals": 1, + "subjects": 2, + "coverage": 0.6667 } } } + }, + "fraud_tags": [], + "risk_signals": { + "residential_proxy_reuse": { + "detected": false, + "signal_count": 0, + "max_subject_count": 0 + }, + "email_intelligence": { + "enriched_signal_count": 1, + "high_risk_count": 0, + "social_profile_count": 0 + }, + "phone_intelligence": { + "enriched_signal_count": 0, + "high_risk_count": 0 + } + }, + "reviewed_at": "2026-10-12T16:30:00Z", + "reviewed_by": "reviewer@example.com", + "review_reason": "Shared device across three new accounts.", + "notes_count": 0, + "sharing_mode": "global", + "cross_org_available": true, + "cross_org_unavailable_reason": null, + "tags": [ + { + "uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "name": "SUSPICIOUS", + "color": "#6366F1", + "source": "custom", + "added_by": null, + "added_at": "2026-10-13T08:15:00Z" + } + ], + "outcomes": { + "approved": 1, + "declined": 1, + "in_review": 0, + "total": 3 + }, + "patterns": [ + { + "type": "background", + "score": 0.8125, + "members": 3, + "reason": null, + "computed_at": "2026-10-13T08:00:00Z" + } + ] + } + } + } + }, + "400": { + "description": "Invalid body: `tag_uuid` is missing or is not a UUID.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tag_uuid": { + "type": "array", + "items": { + "type": "string" + } + } } }, "examples": { - "One active template": { + "Missing tag_uuid": { + "summary": "Missing tag_uuid", "value": { - "count": 1, - "next": null, - "previous": null, - "results": [ - { - "uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", - "vendor_user_uuid": "2f7c1a9e-8b4d-4c6a-9e1f-3d5b7a9c1e2f", - "vendor_data": "user-42", - "source_type": "session_delete", - "provenance_reference": "6b1e8d2f-4a3c-4f9e-8c7d-1a2b3c4d5e6f", - "retention_policy": "retain_until_user_deleted", - "retention_override": null, - "retained_at": "2026-08-28T10:15:42.318402Z", - "expires_at": "2027-08-28T10:15:42.318402Z", - "retained_by": "3c9e2b1f-7d4a-4e8b-9c1d-5f6a7b8c9d0e", - "retained_by_type": "API_KEY", - "instruction_class": "operational_session_delete", - "instruction_source": "api", - "instruction_id": "post-approval-cleanup-2026-08", - "held_out_of_use": false, - "status": "active", - "purged_at": null - } + "tag_uuid": [ + "This field is required." + ] + } + }, + "Invalid tag_uuid": { + "summary": "Invalid tag_uuid", + "value": { + "tag_uuid": [ + "Must be a valid UUID." ] } } @@ -46617,16 +69111,27 @@ } } }, - "400": { - "description": "Invalid filter value, or `retained_to` earlier than `retained_from`. Field-keyed validation errors.", + "401": { + "description": "Missing or invalid credentials.", "content": { "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "description": "Present on coded errors, for example `networks_not_entitled`." + } + } + }, "examples": { - "Bad range": { + "Missing or invalid credentials": { + "summary": "Missing or invalid credentials", "value": { - "retained_to": [ - "Must be on or after retained_from." - ] + "detail": "Authentication credentials were not provided or are invalid." } } } @@ -46634,11 +69139,37 @@ } }, "403": { - "description": "Console user token, or the API key is not allowed. These endpoints accept application API keys only.", + "description": "The API key belongs to another application, or it is a key with restricted access: Networks endpoints need a full-access key. A console member token needs `write:networks`. Also returned with `code: networks_not_entitled` when Networks is not enabled for the organization.", "content": { "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "description": "Present on coded errors, for example `networks_not_entitled`." + } + } + }, "examples": { - "Forbidden": { + "Networks not enabled": { + "summary": "Networks not enabled", + "value": { + "detail": "Networks is disabled for this organization. Contact Didit support if you believe this is an error.", + "code": "networks_not_entitled" + } + }, + "Restricted API key": { + "summary": "Restricted API key", + "value": { + "detail": "This API key has no access to this endpoint." + } + }, + "No permission": { + "summary": "No permission", "value": { "detail": "You do not have permission to perform this action." } @@ -46646,24 +69177,57 @@ } } } + }, + "404": { + "description": "Not found.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "description": "Present on coded errors, for example `networks_not_entitled`." + } + } + }, + "examples": { + "Network not found": { + "summary": "Network not found", + "value": { + "detail": "Network not found." + } + }, + "Tag not found": { + "summary": "Tag not found", + "value": { + "detail": "Tag not found." + } + } + } + } + } } }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/biometric-templates/?vendor_data=user-42&status=active' \\\n -H 'x-api-key: YOUR_API_KEY'" - } - ] + "x-didit-sdk": { + "resource": "networks", + "method": "add_tag", + "auth": "api_key", + "server": true, + "scope": "application" + } } }, - "/v3/biometric-templates/count/": { - "get": { - "summary": "Count retained biometric templates", - "description": "An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`.\n\nReturns the number of templates matching the same filters as the list endpoint. Use it to check what a filtered bulk purge with `delete_all: true` would touch.", - "operationId": "count_biometric_templates", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/tags/remove/": { + "post": { + "summary": "Remove a tag from a network", + "description": "Remove one of your application's tags from the network. Returns the network detail. 404 when the tag is not on the network, or does not belong to your application. Requires a full-access API key, or `write:networks` for a console member.", + "operationId": "remove_network_tag", "tags": [ - "Biometric Templates" + "Networks" ], "security": [ { @@ -46672,110 +69236,239 @@ ], "parameters": [ { - "in": "query", - "name": "vendor_data", - "required": false, - "description": "Case-insensitive substring match on the owning User's current `vendor_data`.", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "vendor_user_uuid", - "required": false, - "description": "Exact match on the owning User.", + "name": "organization_id", + "in": "path", + "required": true, "schema": { "type": "string", "format": "uuid" - } - }, - { - "in": "query", - "name": "status", - "required": false, - "description": "Lifecycle status. Purged templates are never listed.", - "schema": { - "type": "string", - "enum": [ - "pending", - "active", - "expired", - "held_out_of_use", - "purge_pending", - "purge_failed" - ] - } + }, + "description": "Organization UUID." }, { - "in": "query", - "name": "source_type", - "required": false, - "description": "How the template was created.", + "name": "application_id", + "in": "path", + "required": true, "schema": { "type": "string", - "enum": [ - "session_delete", - "automatic_retention", - "backfill" - ] - } + "format": "uuid" + }, + "description": "Application UUID." }, { - "in": "query", - "name": "retained_from", - "required": false, - "description": "Templates retained at or after this time.", + "name": "network_uuid", + "in": "path", + "required": true, "schema": { "type": "string", - "format": "date-time" + "format": "uuid", + "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + }, + "description": "Network UUID." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NetworkTagRequest" + }, + "example": { + "tag_uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7" + } } - }, + } + }, + "x-codeSamples": [ { - "in": "query", - "name": "retained_to", - "required": false, - "description": "Templates retained at or before this time. Must not be earlier than `retained_from` (`400`).", - "schema": { - "type": "string", - "format": "date-time" - } + "lang": "curl", + "label": "curl", + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/networks/3fa85f64-5717-4562-b3fc-2c963f66afa6/tags/remove/' \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"tag_uuid\": \"7c9e6679-7425-40de-944b-e07fc1f90ae7\"}'" } ], "responses": { "200": { - "description": "Count.", + "description": "The tag was removed from the network. Returns the network detail.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NetworkDetailResponse" + }, + "example": { + "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "network_id": "N-0042", + "name": null, + "status": "in_review", + "risk": { + "score": 82, + "band": "high", + "bar": 0.82 + }, + "pattern_types": [ + "exact_same_device", + "same_email" + ], + "signal_types": [ + "device", + "email" + ], + "member_counts": { + "total": 3, + "users": 3, + "businesses": 0, + "transaction_parties": 0 + }, + "member_status": { + "approved": 1, + "declined": 1, + "in_review": 0, + "not_completed": 1 + }, + "signal_coverage": { + "device": { + "signals": 1, + "subjects": 3, + "coverage": 1.0 + }, + "email": { + "signals": 1, + "subjects": 2, + "coverage": 0.6667 + } + }, + "first_activity_at": "2026-10-01T09:12:44Z", + "last_activity_at": "2026-10-12T16:03:10Z", + "stats": { + "members": { + "total": 3, + "users": 3, + "businesses": 0, + "transaction_parties": 0 + }, + "member_status": { + "approved": 1, + "declined": 1, + "in_review": 0, + "not_completed": 1 + }, + "signals": { + "total": 2, + "by_type": { + "device": { + "signals": 1, + "subjects": 3, + "coverage": 1.0 + }, + "email": { + "signals": 1, + "subjects": 2, + "coverage": 0.6667 + } + } + } + }, + "fraud_tags": [], + "risk_signals": { + "residential_proxy_reuse": { + "detected": false, + "signal_count": 0, + "max_subject_count": 0 + }, + "email_intelligence": { + "enriched_signal_count": 1, + "high_risk_count": 0, + "social_profile_count": 0 + }, + "phone_intelligence": { + "enriched_signal_count": 0, + "high_risk_count": 0 + } + }, + "reviewed_at": "2026-10-12T16:30:00Z", + "reviewed_by": "reviewer@example.com", + "review_reason": "Shared device across three new accounts.", + "notes_count": 0, + "sharing_mode": "global", + "cross_org_available": true, + "cross_org_unavailable_reason": null, + "tags": [], + "outcomes": { + "approved": 1, + "declined": 1, + "in_review": 0, + "total": 3 + }, + "patterns": [ + { + "type": "background", + "score": 0.8125, + "members": 3, + "reason": null, + "computed_at": "2026-10-13T08:00:00Z" + } + ] + } + } + } + }, + "400": { + "description": "Invalid body: `tag_uuid` is missing or is not a UUID.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "count": { - "type": "integer", - "minimum": 0 + "tag_uuid": { + "type": "array", + "items": { + "type": "string" + } } } }, "examples": { - "Count": { + "Missing tag_uuid": { + "summary": "Missing tag_uuid", "value": { - "count": 128 + "tag_uuid": [ + "This field is required." + ] + } + }, + "Invalid tag_uuid": { + "summary": "Invalid tag_uuid", + "value": { + "tag_uuid": [ + "Must be a valid UUID." + ] } } } } } }, - "400": { - "description": "Invalid filter value, or `retained_to` earlier than `retained_from`. Field-keyed validation errors.", + "401": { + "description": "Missing or invalid credentials.", "content": { "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "description": "Present on coded errors, for example `networks_not_entitled`." + } + } + }, "examples": { - "Bad range": { + "Missing or invalid credentials": { + "summary": "Missing or invalid credentials", "value": { - "retained_to": [ - "Must be on or after retained_from." - ] + "detail": "Authentication credentials were not provided or are invalid." } } } @@ -46783,11 +69476,37 @@ } }, "403": { - "description": "Console user token, or the API key is not allowed. These endpoints accept application API keys only.", + "description": "The API key belongs to another application, or it is a key with restricted access: Networks endpoints need a full-access key. A console member token needs `write:networks`. Also returned with `code: networks_not_entitled` when Networks is not enabled for the organization.", "content": { "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "description": "Present on coded errors, for example `networks_not_entitled`." + } + } + }, "examples": { - "Forbidden": { + "Networks not enabled": { + "summary": "Networks not enabled", + "value": { + "detail": "Networks is disabled for this organization. Contact Didit support if you believe this is an error.", + "code": "networks_not_entitled" + } + }, + "Restricted API key": { + "summary": "Restricted API key", + "value": { + "detail": "This API key has no access to this endpoint." + } + }, + "No permission": { + "summary": "No permission", "value": { "detail": "You do not have permission to perform this action." } @@ -46795,24 +69514,296 @@ } } } + }, + "404": { + "description": "Not found.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "description": "Present on coded errors, for example `networks_not_entitled`." + } + } + }, + "examples": { + "Network not found": { + "summary": "Network not found", + "value": { + "detail": "Network not found." + } + }, + "Tag not found": { + "summary": "Tag not found", + "value": { + "detail": "Tag not found." + } + }, + "Tag not on the network": { + "summary": "Tag not on the network", + "value": { + "detail": "This tag is not on the network." + } + } + } + } + } } }, + "x-didit-sdk": { + "resource": "networks", + "method": "remove_tag", + "auth": "api_key", + "server": true, + "scope": "application" + } + } + }, + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/remind/": { + "post": { + "summary": "Remind the customer of a request", + "description": "Send a waiting customer request's email again, to the same verified recipient, with the same content. You can remind a request at most once every 24 hours. A reminder changes nothing else: the request keeps its deadline and the case keeps its status. Each reminder is logged on the case as a CUSTOMER_REQUEST event with action `reminded`. The response's `delivery_status` is how this reminder's delivery ended: SENT, SIMULATED (the email was not sent), or UNKNOWN (the email might have been accepted; it is not sent again automatically). `next_reminder_at` is when you can remind this request again. Returns 409 with a `reason` when the customer no longer has the request to answer, and 429 with `retry_at` and a `Retry-After` header when the request was reminded in the last 24 hours. Requires `write:cases`.", + "operationId": "organization_application_cases_requests_remind_create", + "parameters": [ + { + "in": "path", + "name": "organization_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true, + "description": "Organization UUID." + }, + { + "in": "path", + "name": "application_id", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true, + "description": "Application UUID." + }, + { + "in": "path", + "name": "case_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true, + "description": "Case UUID." + }, + { + "in": "path", + "name": "request_uuid", + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true, + "description": "Customer request UUID, from the case's request list." + } + ], + "tags": [ + "Case Management" + ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X GET 'https://verification.didit.me/v3/biometric-templates/count/?status=active' \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X POST 'https://verification.didit.me/v3/organization/11111111-2222-3333-4444-555555555555/application/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/cases/c1a5e000-0000-4000-8000-000000000001/requests/3fa85f64-5717-4562-b3fc-2c963f66afa6/remind/' \\\n -H 'x-api-key: YOUR_API_KEY'" } - ] + ], + "responses": { + "200": { + "description": "The reminder was recorded and its email sent.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "request", + "reminded_at", + "delivery_status", + "next_reminder_at" + ], + "properties": { + "request": { + "$ref": "#/components/schemas/CustomerMessagingCaseRequest" + }, + "reminded_at": { + "type": "string", + "format": "date-time", + "description": "When this reminder was recorded." + }, + "delivery_status": { + "type": "string", + "enum": [ + "SENT", + "SIMULATED", + "UNKNOWN" + ], + "description": "How this reminder's delivery ended. SIMULATED: the email was not sent. UNKNOWN: the email might have been accepted; it is not sent again automatically." + }, + "next_reminder_at": { + "type": "string", + "format": "date-time", + "description": "When you can remind this request again: `reminded_at` plus 24 hours." + } + } + }, + "example": { + "request": { + "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "purpose": "confirm_details", + "language": "en", + "recipient": "customer@example.com", + "expires_at": "2026-10-20T09:00:00Z", + "actor_email": "reviewer@example.com", + "delivery_status": "SENT", + "delivered_at": "2026-10-13T09:00:04Z", + "cancelled_at": null, + "responded_at": null, + "response": null, + "has_attachment": false, + "created_at": "2026-10-13T09:00:00Z" + }, + "reminded_at": "2026-10-14T10:30:00Z", + "delivery_status": "SENT", + "next_reminder_at": "2026-10-15T10:30:00Z" + } + } + } + }, + "400": { + "description": "The case has a resolution pending approval. The body is a list of strings.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "example": [ + "Customer messages require an active case without a pending resolution." + ] + } + } + }, + "403": { + "description": "The caller lacks the required cases permission." + }, + "404": { + "description": "The request or case is unavailable for this caller." + }, + "409": { + "description": "The request no longer waits on the customer, so there is nothing to remind them of. `reason` says why: `answered` (the customer responded), `cancelled`, `expired`, `closed` (the case was resolved after the request went out, even if it was reopened since), `not_actionable` (a review-in-progress update asks nothing), `not_delivered` (the first email has not gone out), or `contact_unavailable` (the verified email is no longer available).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "enum": [ + "case_request_not_waiting" + ] + }, + "reason": { + "type": "string", + "enum": [ + "answered", + "cancelled", + "expired", + "closed", + "not_actionable", + "not_delivered", + "contact_unavailable" + ] + } + } + }, + "example": { + "detail": "This request no longer waits on the customer, so there is nothing to remind them of.", + "code": "case_request_not_waiting", + "reason": "answered" + } + } + } + }, + "429": { + "description": "The request was already reminded in the last 24 hours. `retry_at` and the `Retry-After` header say when you can remind it again.", + "headers": { + "Retry-After": { + "description": "Seconds to wait before you can remind this request again.", + "schema": { + "type": "integer", + "minimum": 1, + "example": 3600 + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": { + "type": "string" + }, + "code": { + "type": "string", + "enum": [ + "case_request_reminder_too_soon" + ] + }, + "retry_at": { + "type": "string", + "format": "date-time" + } + } + }, + "example": { + "detail": "This request was already reminded in the last 24 hours.", + "code": "case_request_reminder_too_soon", + "retry_at": "2026-10-15T10:30:00Z" + } + } + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-didit-sdk": { + "resource": "cases.requests", + "method": "remind", + "auth": "api_key", + "server": true, + "scope": "application" + } } }, - "/v3/biometric-templates/{template_uuid}/": { - "get": { - "summary": "Get a retained biometric template", - "description": "An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`.\n\nReturns one template. The response never includes an image, a vector, the deleted session's id, or identity data.", - "operationId": "get_biometric_template", + "/v3/session/{sessionId}/decision-email-preview/": { + "post": { + "summary": "Preview a session's decision email", + "description": "Render the email the person receives when you set a session to `Approved` or `Declined` with `send_email: true`, without changing the session or sending anything. The response shows the subject and HTML exactly as they are sent, the recipient, the sender, the language, and whether confirming the change sends the email at all (`will_be_sent`, `not_sent_reason`).\n\nThe recipient is the person's own address: the one they verified in an email verification step of the flow, else the one given in `contact_details` when the session was created. The sender is your application's verified sender domain when white label applies (`sender_kind: customer`), else Didit's sender (`sender_kind: didit`).\n\nTo send exactly what you previewed, call [Update Session Status](/sessions-api/update-status) with the same `new_status`, `send_email: true`, `email_address` set to `recipient`, `email_language` set to `language` and `email_preview_token` set to `preview_token`. The receipt expires after 15 minutes. When the recipient, language, session status, sender or email changed since the preview, the update returns `409` before anything changes; preview again and confirm with the new receipt.\n\nWorks for User Verification (KYC) and Business Verification (KYB) sessions in `Approved`, `Declined`, `In Review`, `Kyc Expired`, `Abandoned` or `Resubmitted` status. The status check runs before body validation. Requires the `write:sessions` privilege; a scoped API key needs write access to decisions. Responses use `Cache-Control: no-store`: treat them as sensitive verification content.", + "operationId": "post_v3_session_decision_email_preview", "tags": [ - "Biometric Templates" + "Sessions" ], "security": [ { @@ -46822,159 +69813,251 @@ "parameters": [ { "in": "path", - "name": "template_uuid", + "name": "sessionId", "required": true, - "description": "Template id, as returned in `biometric_template_uuid` by the session deletion endpoints, `biometric_template_id` on a Face Search match, or the list endpoint.", + "description": "UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs.", "schema": { "type": "string", "format": "uuid", - "example": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b" + "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "new_status" + ], + "properties": { + "new_status": { + "type": "string", + "enum": [ + "Approved", + "Declined" + ], + "description": "The decision whose email you want to preview.", + "example": "Approved" + }, + "email_language": { + "type": "string", + "default": "en", + "description": "Language of the email, the same default as Update Session Status. A language that is not offered falls back to English; `language` in the response says which one is used.", + "example": "en" + } + } + }, + "examples": { + "Approved": { + "summary": "Preview the Approved email in English", + "value": { + "new_status": "Approved" + } + }, + "Declined in Spanish": { + "summary": "Preview the Declined email in Spanish", + "value": { + "new_status": "Declined", + "email_language": "es" + } + } + } } } - ], + }, "responses": { "200": { - "description": "The template.", + "description": "Preview only. The session is unchanged and no email is sent.", + "headers": { + "Cache-Control": { + "description": "Always `no-store`: the preview carries verification content and must not be cached.", + "schema": { + "type": "string", + "example": "no-store" + } + } + }, "content": { "application/json": { "schema": { "type": "object", + "required": [ + "recipient", + "recipient_source", + "sender", + "sender_name", + "sender_email", + "sender_kind", + "subject", + "html", + "language", + "will_be_sent", + "not_sent_reason", + "preview_token" + ], "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Template id. Randomly generated; not derived from the deleted session or its face." - }, - "vendor_user_uuid": { - "type": "string", - "format": "uuid", - "description": "The owning User." - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "The User's current `vendor_data`, resolved when you read the template (never copied)." - }, - "source_type": { - "type": "string", - "enum": [ - "session_delete", - "automatic_retention", - "backfill" - ], - "description": "How the template was created: a session deletion (API or Console), the application's automatic data-retention window, or a Didit-operated backfill you instructed." - }, - "provenance_reference": { + "recipient": { "type": "string", - "format": "uuid", - "description": "Opaque reference tying the template to its audit trail. It is not the deleted session's id and cannot be resolved to it." + "description": "The address the email goes to. An empty string when no address is known for the person: then no email can be sent and `preview_token` is empty too. Pass it as `email_address` when you confirm.", + "example": "person@example.com" }, - "retention_policy": { + "recipient_source": { "type": "string", "enum": [ - "delete_with_session", - "retain_until_user_deleted" + "email_verification", + "contact_details", + "" ], - "description": "The application policy at retention time." - }, - "retention_override": { - "type": "boolean", - "nullable": true, - "description": "The `retain_face_embeddings` value sent on the deletion, or `null` when the policy applied." + "description": "Where `recipient` comes from. `email_verification`: the address the person verified in an email verification step of the flow. `contact_details`: the address given in `contact_details` when the session was created. An empty string when there is no address.", + "example": "email_verification" }, - "retained_at": { + "sender": { "type": "string", - "format": "date-time" + "description": "The `From` header the email goes out with.", + "example": "Example App " }, - "expires_at": { + "sender_name": { "type": "string", - "format": "date-time", - "description": "When the template expires and is purged automatically. Always set." + "description": "The display name part of `sender`.", + "example": "Example App" }, - "retained_by": { + "sender_email": { "type": "string", - "nullable": true, - "description": "Principal that gave the instruction: the application id for API keys, the Console member id for Console actions, or a `service:` identifier for automatic retention. Never an email address." + "format": "email", + "description": "The address part of `sender`.", + "example": "no-reply@mail.example.com" }, - "retained_by_type": { + "sender_kind": { "type": "string", "enum": [ - "API_KEY", - "CONSOLE_USER", - "SYSTEM" - ] + "customer", + "didit" + ], + "description": "`customer`: the email goes out from your application's verified sender domain (white label). `didit`: it goes out from Didit's own sender.", + "example": "customer" }, - "instruction_class": { + "subject": { "type": "string", - "enum": [ - "operational_session_delete", - "privacy_erasure" - ] + "description": "The subject line exactly as it is sent.", + "example": "Your verification has been approved in Example App" }, - "instruction_source": { + "html": { "type": "string", - "enum": [ - "api", - "console", - "automatic_retention", - "backfill", - "privacy_erasure", - "explicit_template_purge", - "vendor_user_delete", - "expiry", - "reconciliation", - "migration", - "system" - ] + "description": "The email's HTML exactly as it is sent, with its logo embedded as a data URL. Render it in a sandboxed frame, never directly in your application DOM.", + "example": "..." }, - "instruction_id": { + "language": { "type": "string", - "description": "The `instruction_id` sent on the deletion, or a Didit-generated id." + "description": "The language the email is written in. English (`en`) when the `email_language` you asked for is not offered.", + "example": "en" }, - "held_out_of_use": { + "will_be_sent": { "type": "boolean", - "description": "`true` only while a legal hold applies. A held template is excluded from all matching." + "description": "Whether confirming the change with `send_email: true` sends this email. `false` when `not_sent_reason` is set.", + "example": true }, - "status": { + "not_sent_reason": { "type": "string", + "nullable": true, "enum": [ - "pending", - "active", - "expired", - "held_out_of_use", - "purge_pending", - "purge_failed", - "purged" + "status_unchanged", + "notification_emails_disabled", + "no_recipient", + "sandbox", + null ], - "description": "Only `active` templates that are not held, not past `expires_at`, and whose User still exists take part in matching." + "description": "Why confirming would send no email, or `null` when it will be sent. `status_unchanged`: the session already has this status, so the update changes nothing and sends nothing. `notification_emails_disabled`: the session was created without `contact_details.send_notification_emails: true`, so no decision email goes to the address given in `contact_details`. `no_recipient`: no address is known for the person. `sandbox`: a sandbox session whose recipient has not opted into sandbox email delivery, so the send is only simulated.", + "example": null }, - "purged_at": { + "preview_token": { "type": "string", - "format": "date-time", - "nullable": true + "description": "Signed receipt for this exact email, valid for 15 minutes. Pass it as `email_preview_token` to [Update Session Status](/sessions-api/update-status). An empty string when there is no recipient.", + "example": "eyJkaWdlc3QiOiI0ZjJhIn0:1uExAm:Pl3x4mpleS1gnatureValue000000000000000" } } }, "examples": { - "Active template": { + "Will Be Sent": { + "summary": "The Approved email goes to the address verified in the flow", "value": { - "uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", - "vendor_user_uuid": "2f7c1a9e-8b4d-4c6a-9e1f-3d5b7a9c1e2f", - "vendor_data": "user-42", - "source_type": "session_delete", - "provenance_reference": "6b1e8d2f-4a3c-4f9e-8c7d-1a2b3c4d5e6f", - "retention_policy": "retain_until_user_deleted", - "retention_override": null, - "retained_at": "2026-08-28T10:15:42.318402Z", - "expires_at": "2027-08-28T10:15:42.318402Z", - "retained_by": "3c9e2b1f-7d4a-4e8b-9c1d-5f6a7b8c9d0e", - "retained_by_type": "API_KEY", - "instruction_class": "operational_session_delete", - "instruction_source": "api", - "instruction_id": "post-approval-cleanup-2026-08", - "held_out_of_use": false, - "status": "active", - "purged_at": null + "recipient": "person@example.com", + "recipient_source": "email_verification", + "sender": "Example App ", + "sender_name": "Example App", + "sender_email": "no-reply@mail.example.com", + "sender_kind": "customer", + "subject": "Your verification has been approved in Example App", + "html": "...", + "language": "en", + "will_be_sent": true, + "not_sent_reason": null, + "preview_token": "eyJkaWdlc3QiOiI0ZjJhIn0:1uExAm:Pl3x4mpleS1gnatureValue000000000000000" + } + }, + "No Recipient": { + "summary": "No address is known for the person", + "value": { + "recipient": "", + "recipient_source": "", + "sender": "Example App ", + "sender_name": "Example App", + "sender_email": "no-reply@mail.example.com", + "sender_kind": "customer", + "subject": "Your verification has been declined in Example App", + "html": "...", + "language": "en", + "will_be_sent": false, + "not_sent_reason": "no_recipient", + "preview_token": "" + } + } + } + } + } + }, + "400": { + "description": "The session is not in a status this endpoint accepts (`{\"detail\": ...}`), or the body is invalid (`{\"\": [\"\"]}`).", + "content": { + "application/json": { + "examples": { + "Wrong Current Status": { + "summary": "Session is not in a status this endpoint accepts", + "value": { + "detail": "Only sessions with status Approved, Declined, In Review, Kyc Expired, Abandoned, Resubmitted can be updated. Current status: In Progress" + } + }, + "Invalid new_status": { + "summary": "new_status is not Approved or Declined", + "value": { + "new_status": [ + "\"Resubmitted\" is not a valid choice." + ] + } + }, + "Missing new_status": { + "summary": "Required field omitted", + "value": { + "new_status": [ + "This field is required." + ] + } + } + } + } + } + }, + "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." } } } @@ -46982,27 +70065,41 @@ } }, "403": { - "description": "Console user token, or the API key is not allowed. These endpoints accept application API keys only.", + "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": { - "Forbidden": { + "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 decisions." + } } } } } }, "404": { - "description": "Unknown template, a template of another application, or (on `GET`) a purged template.", + "description": "No KYC or KYB session with this `session_id` exists.", "content": { "application/json": { "examples": { - "Not found": { + "Not Found": { + "summary": "Unknown session_id", "value": { - "detail": "Biometric template not found." + "detail": "Not found." } } } @@ -47014,140 +70111,499 @@ { "lang": "curl", "label": "curl", - "source": "curl -X GET https://verification.didit.me/v3/biometric-templates/9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b/ \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X POST \\\n https://verification.didit.me/v3/session/3fa85f64-5717-4562-b3fc-2c963f66afa6/decision-email-preview/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"new_status\": \"Approved\", \"email_language\": \"en\"}'" + }, + { + "lang": "python", + "label": "Python", + "source": "import requests\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/session/3fa85f64-5717-4562-b3fc-2c963f66afa6/decision-email-preview/\",\n headers={\"x-api-key\": \"YOUR_API_KEY\"},\n json={\"new_status\": \"Approved\", \"email_language\": \"en\"},\n)\nresponse.raise_for_status()\nprint(response.json())" } - ] - }, - "delete": { - "summary": "Purge a retained biometric template", - "description": "An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`.\n\nPurges one template: it is removed from every kind of matching and the stored template is deleted. Irreversible and recorded on the audit trail with the acting principal.\n\n**Retry semantics:** Didit deletes the stored template first and only then marks the template purged. If a biometric store is unavailable the call returns `503`, the template moves to `status: purge_failed` and is already excluded from matching; repeat the same call to finish. Repeating a purge of an already purged template returns `already_purged`.\n\nTemplates are also purged by `POST /v3/users/delete/` for the owning User, by any deletion of that User's sessions with `deletion_instruction: privacy_erasure`, and automatically at `expires_at`.", - "operationId": "purge_biometric_template", + ], + "x-didit-sdk": { + "resource": "sessions", + "method": "preview_decision_email", + "auth": "api_key", + "server": true + } + } + }, + "/v3/travel-rule/join/{token}/": { + "get": { + "summary": "Get a free-join invitation", + "description": "Public, unauthenticated. Shows the invitation a join link is for while it is open: who asked, the address it went to, and the directory values that will be prefilled. The token is the authentication.", + "operationId": "getTravelRuleJoinInvitation", + "x-didit-sdk": { + "resource": "travel_rule.join", + "method": "get_invitation", + "auth": "public", + "server": false + }, "tags": [ - "Biometric Templates" + "Travel Rule" ], "security": [ + {} + ], + "parameters": [ { - "ApiKeyAuth": [] + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The invitation token from the join link." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl https://verification.didit.me/v3/travel-rule/join/{token}/" + } + ], + "responses": { + "200": { + "description": "The invitation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleJoinInvitation" + } + } + } + }, + "404": { + "description": "No invitation has this link." + }, + "410": { + "description": "The invitation is no longer open: expired, already used, or withdrawn." } + } + }, + "post": { + "summary": "Register a free Travel Rule account", + "description": "Public, unauthenticated. Creates a free Travel Rule account for a counterparty VASP that received a request. The email must be the address the invitation went to. A 6-character code is emailed to it; send it to the verify endpoint to finish. Registration attempts are rate limited per address and per IP.", + "operationId": "registerTravelRuleJoin", + "x-didit-sdk": { + "resource": "travel_rule.join", + "method": "register", + "auth": "public", + "server": false + }, + "tags": [ + "Travel Rule" + ], + "security": [ + {} ], "parameters": [ { + "name": "token", "in": "path", - "name": "template_uuid", "required": true, - "description": "Template id, as returned in `biometric_template_uuid` by the session deletion endpoints, `biometric_template_id` on a Face Search match, or the list endpoint.", "schema": { - "type": "string", - "format": "uuid", - "example": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b" + "type": "string" + }, + "description": "The invitation token from the join link." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleJoinRegisterRequest" + }, + "example": { + "email": "compliance@example.com", + "password": "a-long-passphrase" + } } } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/join/{token}/ \\\n -H 'Content-Type: application/json' \\\n -d '{\"email\": \"compliance@example.com\", \"password\": \"a-long-passphrase\"}'" + } ], "responses": { - "200": { - "description": "Purge outcome.", + "202": { + "description": "The account was created and a code was emailed.", "content": { "application/json": { "schema": { "type": "object", "properties": { - "uuid": { + "message": { "type": "string", - "format": "uuid" + "description": "Tells the counterparty to check their email for the verification code." }, - "outcome": { + "email": { "type": "string", - "enum": [ - "purged", - "already_purged" - ] - } - } - }, - "examples": { - "Purged": { - "value": { - "uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", - "outcome": "purged" - } - }, - "Already purged": { - "value": { - "uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", - "outcome": "already_purged" + "format": "email", + "description": "The address the code was sent to." } } } } } }, - "403": { - "description": "Console user token, or the API key is not allowed. These endpoints accept application API keys only.", + "400": { + "description": "The email is not the invited address, or the password or address was refused." + }, + "404": { + "description": "No invitation has this link." + }, + "410": { + "description": "The invitation is no longer open: expired, already used, or withdrawn." + }, + "429": { + "description": "Too many attempts to join. The `X-RateLimit-*` headers name the budget that was spent." + } + } + } + }, + "/v3/travel-rule/join/{token}/verify/": { + "post": { + "summary": "Verify a free Travel Rule account", + "description": "Public, unauthenticated. Spends the emailed code, creates the free organization and answers with the credentials its application uses on the workspace endpoints. Receiving Travel Rule data is free.", + "operationId": "verifyTravelRuleJoin", + "x-didit-sdk": { + "resource": "travel_rule.join", + "method": "verify", + "auth": "public", + "server": false + }, + "tags": [ + "Travel Rule" + ], + "security": [ + {} + ], + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "The invitation token from the join link." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleJoinVerifyRequest" + }, + "example": { + "email": "compliance@example.com", + "code": "123456" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/join/{token}/verify/ \\\n -H 'Content-Type: application/json' \\\n -d '{\"email\": \"compliance@example.com\", \"code\": \"123456\"}'" + } + ], + "responses": { + "201": { + "description": "The organization and application credentials.", "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." - } - } + "schema": { + "$ref": "#/components/schemas/TravelRuleJoinCredentials" } } } }, + "400": { + "description": "The code or email was refused." + }, "404": { - "description": "Unknown template, a template of another application, or (on `GET`) a purged template.", + "description": "No invitation has this link." + }, + "410": { + "description": "The invitation is no longer open: expired, already used, or withdrawn." + }, + "429": { + "description": "Too many attempts to join. The `X-RateLimit-*` headers name the budget that was spent." + } + } + } + }, + "/v3/travel-rule/workspace/": { + "get": { + "summary": "Get the free Travel Rule workspace", + "description": "Reads the free workspace of an application that joined free: directory claim, domain proof, billing and Didit network listing.", + "operationId": "getTravelRuleWorkspace", + "x-didit-sdk": { + "resource": "travel_rule.workspace", + "method": "get", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET https://verification.didit.me/v3/travel-rule/workspace/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "The workspace.", "content": { "application/json": { - "examples": { - "Not found": { - "value": { - "detail": "Biometric template not found." - } - } + "schema": { + "$ref": "#/components/schemas/TravelRuleWorkspace" } } } }, - "503": { - "description": "A biometric store was unavailable while retaining or purging a biometric template. Nothing is deleted; repeat the same request. Retries are idempotent and converge on a single template.", + "404": { + "description": "This application did not join free, so it has no free workspace." + } + } + } + }, + "/v3/travel-rule/workspace/claim/": { + "post": { + "summary": "Claim a directory entry", + "description": "Claims the directory entry this workspace is. When the work email is at a domain the entry lists, the claim is proved and applies at once: the VASP profile and Travel Rule settings are filled from the entry and Travel Rule is turned on, so the next transfer to it routes over the Didit network. Otherwise the claim waits for review (`pending_review`).", + "operationId": "claimTravelRuleWorkspace", + "x-didit-sdk": { + "resource": "travel_rule.workspace", + "method": "claim", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWorkspaceClaimRequest" + }, + "example": { + "vasp_id": "0b0c9d3e-5a53-4a3e-9d6e-1f2f6f6a1c11" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/workspace/claim/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"vasp_id\": \"0b0c9d3e-5a53-4a3e-9d6e-1f2f6f6a1c11\"}'" + } + ], + "responses": { + "200": { + "description": "The workspace with its claim status.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWorkspace" + } + } + } + }, + "400": { + "description": "No directory entry has this id." + }, + "404": { + "description": "This application did not join free, so it has no free workspace." + } + } + } + }, + "/v3/travel-rule/workspace/domain/code/": { + "post": { + "summary": "Email a domain verification code", + "description": "Emails a single-use code to the workspace's work address to prove it controls the email domain. Throttled, and a limited number of wrong attempts is accepted per code. Also returns the TXT record for the DNS alternative.", + "operationId": "sendTravelRuleWorkspaceDomainCode", + "x-didit-sdk": { + "resource": "travel_rule.workspace", + "method": "send_domain_code", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/workspace/domain/code/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "The domain, its status and the TXT record.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWorkspaceDomainCodeResponse" + } + } + } + }, + "404": { + "description": "This application did not join free, so it has no free workspace." + } + } + } + }, + "/v3/travel-rule/workspace/domain/confirm/": { + "post": { + "summary": "Confirm the domain verification code", + "description": "Spends the emailed code and marks the email domain verified.", + "operationId": "confirmTravelRuleWorkspaceDomain", + "x-didit-sdk": { + "resource": "travel_rule.workspace", + "method": "confirm_domain", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWorkspaceDomainConfirmRequest" + }, + "example": { + "code": "123456" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/workspace/domain/confirm/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"code\": \"123456\"}'" + } + ], + "responses": { + "200": { + "description": "The workspace.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } - }, - "examples": { - "Retryable": { - "value": { - "detail": "The biometric-template operation could not finish because an external store is unavailable. The operation is retryable." - } - } + "$ref": "#/components/schemas/TravelRuleWorkspace" } } } + }, + "400": { + "description": "The code is wrong, expired or already used." + }, + "404": { + "description": "This application did not join free, so it has no free workspace." } + } + } + }, + "/v3/travel-rule/workspace/domain/dns/": { + "post": { + "summary": "Verify the domain from its TXT record", + "description": "Checks for the `didit-tr-verify.` TXT record returned by the code endpoint and marks the domain verified when it matches. One of the two proofs, the emailed code or the TXT record, is enough.", + "operationId": "verifyTravelRuleWorkspaceDomainDns", + "x-didit-sdk": { + "resource": "travel_rule.workspace", + "method": "verify_domain_dns", + "auth": "api_key", + "server": true }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], "x-codeSamples": [ { "lang": "curl", "label": "curl", - "source": "curl -X DELETE https://verification.didit.me/v3/biometric-templates/9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b/ \\\n -H 'x-api-key: YOUR_API_KEY'" + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/workspace/domain/dns/ \\\n -H 'x-api-key: YOUR_API_KEY'" } - ] + ], + "responses": { + "200": { + "description": "The workspace.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleWorkspace" + } + } + } + }, + "400": { + "description": "The TXT record was not found or does not match." + }, + "404": { + "description": "This application did not join free, so it has no free workspace." + } + } } }, - "/v3/biometric-templates/delete/": { + "/v3/travel-rule/workspace/billing/": { "post": { - "summary": "Bulk purge retained biometric templates", - "description": "An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`.\n\nPurges many templates in one call, by explicit id list or with `delete_all: true` optionally narrowed by the same filters as the list endpoint. Templates are purged one by one and each result reports `purged` or `already_purged`. If a biometric store is unavailable the call returns `503` at the first failing template: templates purged earlier in the request stay purged, the failing one moves to `purge_failed` (already excluded from matching), and repeating the request finishes the job.\n\n**`delete_all: true` without filters purges every biometric template in the application and cannot be undone.** Run the count endpoint with the same filters first.", - "operationId": "bulk_purge_biometric_templates", + "summary": "Enable outbound billing", + "description": "Turns outbound Travel Rule billing on for the workspace. Receiving stays free. This cannot be turned back off through the API: contact Didit support.", + "operationId": "enableTravelRuleWorkspaceBilling", + "x-didit-sdk": { + "resource": "travel_rule.workspace", + "method": "enable_billing", + "auth": "api_key", + "server": true + }, "tags": [ - "Biometric Templates" + "Travel Rule" ], "security": [ { @@ -47159,22561 +70615,33041 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "template_uuids": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - }, - "minItems": 1, - "description": "Templates to purge. Required unless `delete_all` is `true`. Already purged ids are reported as `already_purged`." - }, - "delete_all": { - "type": "boolean", - "default": false, - "description": "When `true`, purges every template matching the filters below (all of the application's templates when no filter is given). `template_uuids` is ignored. Irreversible." - }, - "vendor_data": { - "type": "string", - "description": "Only with `delete_all`: case-insensitive substring match on the owning User's `vendor_data`." - }, - "vendor_user_uuid": { - "type": "string", - "format": "uuid", - "description": "Only with `delete_all`: exact match on the owning User." - }, - "status": { - "type": "string", - "enum": [ - "pending", - "active", - "expired", - "held_out_of_use", - "purge_pending", - "purge_failed" - ], - "description": "Only with `delete_all`." - }, - "source_type": { - "type": "string", - "enum": [ - "session_delete", - "automatic_retention", - "backfill" - ], - "description": "Only with `delete_all`." - }, - "retained_from": { - "type": "string", - "format": "date-time", - "description": "Only with `delete_all`." - }, - "retained_to": { - "type": "string", - "format": "date-time", - "description": "Only with `delete_all`. Must not be earlier than `retained_from`." - } - } + "$ref": "#/components/schemas/TravelRuleWorkspaceBillingRequest" }, - "examples": { - "By id": { - "value": { - "template_uuids": [ - "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", - "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d" - ] - } - }, - "Every template of one User": { - "value": { - "delete_all": true, - "vendor_user_uuid": "2f7c1a9e-8b4d-4c6a-9e1f-3d5b7a9c1e2f" - } - }, - "Every template in the application": { - "value": { - "delete_all": true - } - } + "example": { + "enabled": true } } } }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/workspace/billing/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"enabled\": true}'" + } + ], "responses": { "200": { - "description": "Per-template purge outcomes.", + "description": "The workspace with `billing_enabled` true.", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "results": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "outcome": { - "type": "string", - "enum": [ - "purged", - "already_purged" - ] - } - } - } - } - } - }, - "examples": { - "Mixed": { - "value": { - "results": [ - { - "uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", - "outcome": "purged" - }, - { - "uuid": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", - "outcome": "already_purged" - } - ] - } - } + "$ref": "#/components/schemas/TravelRuleWorkspace" } } } }, "400": { - "description": "Validation error — nothing is purged.", + "description": "`enabled` was false." + }, + "404": { + "description": "This application did not join free, so it has no free workspace." + } + } + } + }, + "/v3/investigator/summary/": { + "get": { + "operationId": "investigator_summary", + "summary": "Investigator summary", + "description": "Stage, queue counts, reports prepared and decided today, 30-day agreement, prices and the autonomy level of each queue. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`.", + "tags": [ + "Investigator" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "", "content": { "application/json": { - "examples": { - "Missing selector": { - "value": { - "template_uuids": [ - "This field is required unless delete_all is true." - ] - } - }, - "Bad range": { - "value": { - "retained_to": [ - "Must be on or after retained_from." - ] + "schema": { + "type": "object", + "additionalProperties": true, + "properties": { + "stage": { + "type": "string" + }, + "last_report_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "prepared_today": { + "type": "integer" + }, + "ready_to_accept": { + "type": "integer" + }, + "open": { + "type": "integer" + }, + "decided_today": { + "type": "integer" + }, + "automation_rate_month": { + "type": "number", + "nullable": true + }, + "preparing": { + "type": "integer" } } } } } }, + "401": { + "description": "Missing or invalid API key." + }, "403": { - "description": "Console user token, or the API key is not allowed. These endpoints accept application API keys only.", + "description": "The key lacks the scope, the item kind is not readable, or the plan does not include the Investigator." + } + }, + "parameters": [ + { + "in": "query", + "name": "scope", + "schema": { + "type": "string", + "enum": [ + "all", + "identity", + "transactions" + ], + "default": "all" + }, + "description": "`identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened)." + }, + { + "in": "query", + "name": "timezone", + "schema": { + "type": "string" + }, + "description": "IANA time zone (for example `Europe/Madrid`) that \"today\" counts from. UTC by default." + } + ], + "x-didit-sdk": { + "resource": "investigator", + "method": "retrieve_summary", + "auth": "api_key", + "server": true + } + } + }, + "/v3/investigator/queue/": { + "get": { + "operationId": "investigator_queue", + "summary": "List the Investigator queue", + "description": "Open reports, **Ready to accept** first, then by deadline, risk and wait. `counts` covers every kind in scope. `count` is capped at 100. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`.", + "tags": [ + "Investigator" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "", "content": { "application/json": { - "examples": { - "Forbidden": { - "value": { - "detail": "You do not have permission to perform this action." + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "next": { + "type": "string", + "nullable": true + }, + "previous": { + "type": "string", + "nullable": true + }, + "counts": { + "type": "object", + "additionalProperties": true + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InvestigatorQueueRow" + } } } } } } }, - "503": { - "description": "A biometric store was unavailable while retaining or purging a biometric template. Nothing is deleted; repeat the same request. Retries are idempotent and converge on a single template.", + "401": { + "description": "Missing or invalid API key." + }, + "403": { + "description": "The key lacks the scope, the item kind is not readable, or the plan does not include the Investigator." + } + }, + "parameters": [ + { + "in": "query", + "name": "scope", + "schema": { + "type": "string", + "enum": [ + "all", + "identity", + "transactions" + ], + "default": "all" + }, + "description": "`identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened)." + }, + { + "in": "query", + "name": "kind", + "schema": { + "type": "string" + }, + "description": "`session`, `business_session`, `transaction` or `case`, or several separated by commas." + }, + { + "in": "query", + "name": "queue", + "schema": { + "type": "string", + "enum": [ + "kyc", + "kyb", + "aml", + "transactions", + "cases" + ] + } + }, + { + "in": "query", + "name": "recommendation", + "schema": { + "type": "string", + "enum": [ + "approve", + "decline", + "escalate", + "request" + ] + } + }, + { + "in": "query", + "name": "confidence", + "schema": { + "type": "string", + "enum": [ + "high", + "moderate", + "low" + ] + } + }, + { + "in": "query", + "name": "ready", + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "search", + "schema": { + "type": "string" + }, + "description": "Name, company, case title, number, transaction ID or item ID." + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "schema": { + "type": "integer" + } + } + ], + "x-didit-sdk": { + "resource": "investigator", + "method": "list_queue", + "auth": "api_key", + "server": true + } + } + }, + "/v3/investigator/runs/{run_id}/": { + "get": { + "operationId": "investigator_run_retrieve", + "summary": "Get an Investigator report", + "description": "The full report of one run. 404 when the run has no completed report, or its item was deleted. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`.", + "tags": [ + "Investigator" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } - }, - "examples": { - "Retryable": { - "value": { - "detail": "The biometric-template operation could not finish because an external store is unavailable. The operation is retryable." - } - } + "$ref": "#/components/schemas/InvestigatorRunDetail" } } } + }, + "404": { + "description": "Not found, or no report." + }, + "401": { + "description": "Missing or invalid API key." + }, + "403": { + "description": "The key lacks the scope, the item kind is not readable, or the plan does not include the Investigator." } }, - "x-codeSamples": [ + "parameters": [ { - "lang": "curl", - "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/biometric-templates/delete/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\"delete_all\": true, \"vendor_user_uuid\": \"2f7c1a9e-8b4d-4c6a-9e1f-3d5b7a9c1e2f\"}'" + "in": "path", + "name": "run_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } } - ] + ], + "x-didit-sdk": { + "resource": "investigator", + "method": "retrieve_run", + "auth": "api_key", + "server": true + } } }, - "/v3/kyb/shareholders/": { + "/v3/investigator/runs/{run_id}/accept/": { "post": { - "summary": "KYB registry company with shareholders (standalone)", - "operationId": "post_v3_kyb_shareholders", + "operationId": "investigator_run_accept", + "summary": "Accept a recommendation", + "description": "Applies the recommended action through the item's normal path, so your usual webhooks fire. A `narrative` makes it edited and accepted. Restricted keys need the `decisions` scope (write); transaction alerts also need `transactions` (write). Escalating a verification or transaction opens a case.", "tags": [ - "Standalone APIs" + "Investigator" ], "security": [ { "ApiKeyAuth": [] } ], - "parameters": [], - "description": "Retrieve the Lite profile **plus the direct shareholders** (natural persons and corporate entities, with their ownership bands) of one candidate returned by `POST /v3/kyb/search/`.\n\n**Pairing with search.** Pass the candidate's `kyb_response_id` exactly as returned by a search of the same application. The search runs on the Lite registry chain; when the country's ownership data lives at another registry Didit re-finds the company there by registration number, at no extra cost.\n\n**Billing.** Billed at the country's Shareholders price - see [KYB registry pricing](/getting-started/kyb-registry-pricing). When the registry holds no shareholder record for the company (or the country offers no ownership data at all) the Lite profile is delivered and billed instead: read `registry_tier_delivered` and `registry_ownership_status`. The balance is checked against the Shareholders price before any registry call (`403` when short).\n\n**Availability.** v3 only. Every call creates a business session (its id is the `request_id`).\n\n**Pending reports.** When a report is still being prepared, or the ownership section has not been retrieved from the registry yet, the endpoint returns 202 Accepted, kyb_registry.fetch_status=pending, kyb_registry.registry_ownership_status=pending, and registry_tier_delivered=null. Keep request_id and retrieve GET /v3/session/{request_id}/decision/ until registry_checks[0].company.fetch_status is resolved. Didit completes the report automatically and charges the delivered tier once it is ready. Do not repeat the purchase while it is pending; every new POST creates a separate request.", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvestigatorDecision" + } + } + } + }, + "409": { + "description": "Already decided, report not ready, or a guardrail holds the item." + }, + "401": { + "description": "Missing or invalid API key." + }, + "403": { + "description": "The key lacks the scope, the item kind is not readable, or the plan does not include the Investigator." + } + }, + "parameters": [ + { + "in": "path", + "name": "run_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "type": "object", - "required": [ - "kyb_response_id" - ], - "properties": { - "kyb_response_id": { - "type": "string", - "maxLength": 255, - "description": "Candidate handle from `POST /v3/kyb/search/` (`kyb_registry.companies[].kyb_response_id`). Ephemeral — use it shortly after the search that produced it.", - "example": "69aeeb95febb0f1704042259" - }, - "vendor_data": { - "type": "string", - "description": "Your identifier for this company; echoed back and stored with the business session." - }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Free-form JSON stored with the business session and echoed back." - }, - "save_api_request": { - "type": "boolean", - "default": true, - "description": "Must be `true` (the default). KYB registry selects are always persisted so billing and retrieval can be tracked; `false` returns `400`." - } + "$ref": "#/components/schemas/InvestigatorAcceptRequest" + } + } + } + }, + "x-didit-sdk": { + "resource": "investigator", + "method": "accept_run", + "auth": "api_key", + "server": true + } + } + }, + "/v3/investigator/runs/{run_id}/override/": { + "post": { + "operationId": "investigator_run_override", + "summary": "Override a recommendation", + "description": "Takes another action with a category and a reason. Also overrides a decision the Investigator took on its own while it still stands. Restricted keys need the `decisions` scope (write); transaction alerts also need `transactions` (write). Escalating a verification or transaction opens a case.", + "tags": [ + "Investigator" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvestigatorDecision" } } } + }, + "409": { + "description": "Already decided, or a guardrail holds the item." + }, + "401": { + "description": "Missing or invalid API key." + }, + "403": { + "description": "The key lacks the scope, the item kind is not readable, or the plan does not include the Investigator." + } + }, + "parameters": [ + { + "in": "path", + "name": "run_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvestigatorOverrideRequest" + } + } } }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/kyb/shareholders/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"kyb_response_id\": \"69aeeb95febb0f1704042259\",\n \"vendor_data\": \"company-1234\"\n }'" - }, - { - "lang": "python", - "label": "Python", - "source": "import os, time, requests\n\nsearch = requests.post(\n \"https://verification.didit.me/v3/kyb/search/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"country_code\": \"GB\", \"name\": \"Tesco\"},\n timeout=60,\n).json()\nkyb_response_id = search[\"kyb_registry\"][\"companies\"][0][\"kyb_response_id\"]\n\nresp = requests.post(\n \"https://verification.didit.me/v3/kyb/shareholders/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"kyb_response_id\": kyb_response_id, \"vendor_data\": \"company-1234\"},\n timeout=90,\n)\nresp.raise_for_status()\ndata = resp.json()\nrequest_id = data[\"request_id\"]\nprint(\"Keep this request ID:\", request_id)\ncompany = data[\"kyb_registry\"]\nif resp.status_code == 202 or company.get(\"fetch_status\") == \"pending\":\n # This client wait limit does not cancel the report or create a new purchase.\n deadline = time.monotonic() + 300\n while company.get(\"fetch_status\") != \"resolved\" and time.monotonic() < deadline:\n time.sleep(5)\n decision = requests.get(\n f\"https://verification.didit.me/v3/session/{request_id}/decision/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n timeout=30,\n )\n decision.raise_for_status()\n checks = decision.json().get(\"registry_checks\") or []\n if checks:\n company = checks[0].get(\"company\") or company\nif company.get(\"fetch_status\") == \"resolved\":\n print(company[\"company_name\"], company[\"registry_tier_delivered\"])\nelse:\n print(\"Still processing; retrieve this request later:\", request_id)" - }, + "x-didit-sdk": { + "resource": "investigator", + "method": "override_run", + "auth": "api_key", + "server": true + } + } + }, + "/v3/investigator/runs/batch-accept/": { + "post": { + "operationId": "investigator_batch_accept", + "summary": "Batch accept recommendations", + "description": "Accepts up to 50 reports marked Ready to accept. Each is its own decision: one that cannot be accepted is skipped with its reason. Restricted keys need the `decisions` scope (write); transaction alerts also need `transactions` (write). Escalating a verification or transaction opens a case.", + "tags": [ + "Investigator" + ], + "security": [ { - "lang": "javascript", - "label": "JavaScript", - "source": "const res = await fetch('https://verification.didit.me/v3/kyb/shareholders/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n kyb_response_id: '69aeeb95febb0f1704042259',\n vendor_data: 'company-1234',\n }),\n});\nif (!res.ok) throw new Error(`Request failed: ${res.status}`);\nconst data = await res.json();\nconst requestId = data.request_id;\nconsole.log('Keep this request ID:', requestId);\nlet company = data.kyb_registry;\nif (res.status === 202 || company.fetch_status === 'pending') {\n // This client wait limit does not cancel the report or create a new purchase.\n const deadline = Date.now() + 300000;\n while (company.fetch_status !== 'resolved' && Date.now() < deadline) {\n await new Promise(resolve => setTimeout(resolve, 5000));\n const decisionResponse = await fetch(\n `https://verification.didit.me/v3/session/${requestId}/decision/`,\n { headers: { 'x-api-key': 'YOUR_API_KEY' }, signal: AbortSignal.timeout(30000) },\n );\n if (!decisionResponse.ok) throw new Error(`Retrieval failed: ${decisionResponse.status}`);\n const decision = await decisionResponse.json();\n company = decision.registry_checks?.[0]?.company ?? company;\n }\n}\nif (company.fetch_status === 'resolved') {\n console.log(company.company_name, company.registry_tier_delivered);\n} else {\n console.log('Still processing; retrieve this request later:', requestId);\n}" + "ApiKeyAuth": [] } ], "responses": { "200": { - "description": "Company report returned and billed at the delivered tier. Ownership reports still being prepared can return 202; keep request_id to retrieve their saved decision.", + "description": "", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "request_id": { - "type": "string", - "format": "uuid", - "description": "Id of the business session created for this retrieval. The stored record (visible in the console) keeps updating if the registry resolves after the response." - }, - "registry_tier_requested": { - "type": "string", - "enum": [ - "basic", - "shareholders", - "ubo" - ], - "description": "The tier this endpoint sells: `basic` (Lite) for select, `shareholders`, `ubo`." - }, - "registry_tier_delivered": { - "type": "string", - "enum": [ - "basic", - "shareholders", - "ubo", - null - ], - "description": "The tier actually delivered and billed. Lower than requested when the country's registries hold no ownership data, or the registry has no ownership record for this company.", - "nullable": true - }, - "kyb_registry": { - "type": "object", - "description": "Full registry company profile.", - "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Didit's id for this company record." - }, - "node_id": { - "type": "string", - "example": "feature_kyb_registry" - }, - "status": { - "type": "string", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review" - ], - "description": "Didit's assessment of the registry check (e.g. `In Review` when the company is not active in the registry or ownership data could not be derived)." - }, - "registry_status": { - "type": "string", - "nullable": true, - "enum": [ - "active", - "dissolved", - "deregistered", - "see full details", - "authorised", - "appointed representative", - "unauthorised", - "inactive", - "no longer authorised", - "closed", - "struck off", - null - ], - "description": "Normalized company status as reported by the registry." - }, - "data_resolved": { - "type": "boolean", - "description": "`true` once the registry returned the full profile. `false` when the registry was still processing after polling — the stored record completes automatically once the registry resolves." - }, - "company_name": { - "type": "string", - "example": "Tesco PLC" - }, - "registration_number": { - "type": "string", - "nullable": true, - "example": "00445790" - }, - "country_code": { - "type": "string", - "description": "ISO 3166-1 alpha-2.", - "example": "GB" - }, - "region": { - "type": "string", - "nullable": true, - "description": "Subdivision code when the registry is regional (e.g. `CA` for `US-CA`); empty otherwise." - }, - "company_type": { - "type": "string", - "nullable": true, - "example": "Public Limited Company" - }, - "incorporation_date": { - "type": "string", - "format": "date", - "nullable": true, - "example": "1947-11-27" - }, - "registered_address": { - "type": "string", - "nullable": true, - "example": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA" - }, - "tax_number": { - "type": "string", - "nullable": true, - "example": "GB412 8925 93" - }, - "risk_level": { - "type": "string", - "nullable": true, - "description": "Registry-reported risk level, when available." - }, - "verification_status": { - "type": "string", - "nullable": true, - "description": "Registry verification status, when available." - }, - "is_from_registry": { - "type": "boolean", - "description": "Always `true` for this endpoint — the profile was retrieved from an official registry." - }, - "fetch_status": { - "type": "string", - "enum": [ - "pending", - "resolved" - ], - "description": "Registry fetch lifecycle. Mirrored by `data_resolved`." - }, - "alternative_names": { - "type": "string", - "nullable": true, - "description": "Alternative, trading, former or original-language company names as text, when reported." - }, - "nature_of_business": { - "type": "string", - "nullable": true, - "description": "Activity description or classification code, when reported." - }, - "registered_capital": { - "type": "string", - "nullable": true - }, - "registered_capital_amount": { - "type": "string", - "nullable": true, - "description": "Decimal amount as a string, when reported." - }, - "registered_capital_currency": { - "type": "string", - "nullable": true, - "description": "ISO 4217 code, when reported." - }, - "website": { - "type": "string", - "nullable": true, - "example": "https://www.tesco.com/" - }, - "email": { - "type": "string", - "nullable": true, - "example": "customer.service@tesco.co.uk" - }, - "phone": { - "type": "string", - "nullable": true, - "example": "03301231688" - }, - "legal_entity_identifier": { - "type": "string", - "nullable": true, - "description": "LEI, when reported." - }, - "location_of_registration": { - "type": "string", - "nullable": true, - "description": "Registry/jurisdiction the company is registered with, when reported." - }, - "vat_number": { - "type": "string", - "nullable": true, - "description": "EU VAT number, when collected during the hosted registry step or edited afterwards. At select time this is typically not yet set — VAT and tax ID validation runs when the end user submits the registry step in the hosted flow." - }, - "vat_validation_status": { - "type": "string", - "enum": [ - "valid", - "invalid", - "format_valid", - "invalid_format", - "could_not_validate", - "not_applicable" - ], - "description": "Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session." - }, - "vat_validated_number": { - "type": "string", - "nullable": true, - "description": "The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran." - }, - "vat_validation_source": { - "type": "string", - "nullable": true, - "enum": [ - "eu_vies", - "pl_vat_white_list", - "no_brreg", - "ch_uid_register", - "br_receita_federal", - "co_rues", - "cr_hacienda", - "ec_sri", - "py_dnit", - "format_rules", - null - ], - "description": "The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran." - }, - "vat_validated_name": { - "type": "string", - "nullable": true, - "description": "Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." - }, - "vat_validated_address": { - "type": "string", - "nullable": true, - "description": "Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." - }, - "vat_checked_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "Timestamp of the check. `null` when no check ran." - }, - "financial_summary": { - "type": "object", - "nullable": true, - "description": "Registry-reported share/capital details (currency, total shares, share classes), when available." - }, - "officers": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string", - "example": "MELISSA BETHELL" - }, - "designation": { - "type": "string", - "nullable": true, - "description": "Raw registry designation (e.g. `Director`, `Secretary`)." - }, - "role": { - "type": "string", - "description": "Normalized role derived from the designation. Full 17-value model set; registry parsing on this endpoint typically emits director, non_executive_director, secretary, chairman, or company_officer.", - "enum": [ - "director", - "non_executive_director", - "secretary", - "chairman", - "ubo", - "shareholder", - "representative", - "founder", - "legal_advisor", - "authorized_signatory", - "trustee", - "beneficiary", - "company_officer", - "settlor", - "protector", - "investor", - "other" - ] - }, - "nationality": { - "type": "string", - "nullable": true - }, - "is_active": { - "type": "boolean", - "description": "`false` when the registry marks the appointment as resigned." - }, - "kyc_status": { - "type": "string", - "nullable": true, - "enum": [ - "Approved", - "Declined", - "Pending", - null - ], - "description": "Status of an associated KYC session, when one was started for this officer. `null` otherwise." - }, - "kyc_session_url": { - "type": "string", - "nullable": true, - "description": "Verification URL of the associated KYC session, when one exists." - }, - "first_name": { - "type": "string", - "nullable": true, - "description": "Given name when explicitly reported by the registry; not inferred from the full name." - }, - "last_name": { - "type": "string", - "nullable": true, - "description": "Family name when explicitly reported by the registry." - }, - "address": { - "type": "string", - "nullable": true, - "description": "Officer address reported by the registry." - }, - "appointment_date": { - "type": "string", - "nullable": true, - "description": "Date the officer was appointed.", - "format": "date" - }, - "termination_date": { - "type": "string", - "nullable": true, - "description": "Date the appointment ended, when reported.", - "format": "date" - }, - "date_of_birth": { - "type": "string", - "nullable": true, - "description": "Complete registry-reported birth date. Null when only a birth year or month is known.", - "format": "date" - }, - "date_of_birth_year": { - "type": "integer", - "nullable": true, - "minimum": 1, - "maximum": 9999, - "description": "Registry-reported birth year, including when the full date is unavailable." - }, - "date_of_birth_month": { - "type": "integer", - "nullable": true, - "minimum": 1, - "maximum": 12, - "description": "Registry-reported birth month. Null when only the year is known." - } - } - }, - "description": "Directors, secretaries, and other officers extracted from the registry." - }, - "beneficial_owners": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string" - }, - "first_name": { - "type": "string", - "nullable": true - }, - "last_name": { - "type": "string", - "nullable": true - }, - "entity_type": { - "type": "string", - "enum": [ - "person", - "company" - ], - "description": "Whether the owner is a natural person or a corporate entity." - }, - "roles": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Normalized roles (e.g. `ubo`, `shareholder`)." - }, - "ownership_min_shares": { - "type": "string", - "nullable": true, - "description": "Lower bound of the ownership band reported by the registry (e.g. `25%`)." - }, - "ownership_max_shares": { - "type": "string", - "nullable": true, - "description": "Upper bound of the ownership band reported by the registry." - }, - "is_active": { - "type": "boolean" - }, - "kyc_status": { - "type": "string", - "nullable": true, - "enum": [ - "Approved", - "Declined", - "Pending", - null - ] - }, - "kyc_session_url": { - "type": "string", - "nullable": true - }, - "effective_ownership_percent": { - "type": "number", - "nullable": true, - "description": "Parsed ownership percentage (max bound, falling back to min bound)." - }, - "company_name": { - "type": "string", - "nullable": true - }, - "registration_number": { - "type": "string", - "nullable": true - }, - "country_of_birth": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string", - "nullable": true - }, - "nationality": { - "type": "string", - "nullable": true - }, - "designation": { - "type": "string", - "nullable": true - }, - "voting_min_shares": { - "type": "string", - "nullable": true - }, - "voting_max_shares": { - "type": "string", - "nullable": true - }, - "date_of_birth": { - "type": "string", - "nullable": true, - "description": "Complete birth date when reported.", - "format": "date" - }, - "appointment_date": { - "type": "string", - "nullable": true, - "description": "Appointment date when reported.", - "format": "date" - }, - "shareholding": { - "$ref": "#/components/schemas/KYBRegistryShareholding" - } - } + "$ref": "#/components/schemas/InvestigatorBatchAcceptResponse" + } + } + } + }, + "401": { + "description": "Missing or invalid API key." + }, + "403": { + "description": "The key lacks the scope, the item kind is not readable, or the plan does not include the Investigator." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvestigatorBatchAcceptRequest" + } + } + } + }, + "x-didit-sdk": { + "resource": "investigator", + "method": "batch_accept_runs", + "auth": "api_key", + "server": true + } + } + }, + "/v3/investigator/transactions/{transaction_id}/": { + "get": { + "operationId": "investigator_transaction_retrieve", + "summary": "Get a transaction's Investigator review", + "description": "The transaction's latest review, or `investigator: null` when it never had one. Restricted keys need the `decisions` and `transactions` scopes (read).", + "tags": [ + "Investigator" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "transaction_id": { + "type": "string", + "format": "uuid" + }, + "investigator": { + "$ref": "#/components/schemas/InvestigatorBlock" + } + } + } + } + } + }, + "404": { + "description": "Not this application's transaction." + }, + "401": { + "description": "Missing or invalid API key." + }, + "403": { + "description": "The key lacks the scope, the item kind is not readable, or the plan does not include the Investigator." + } + }, + "parameters": [ + { + "in": "path", + "name": "transaction_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "x-didit-sdk": { + "resource": "investigator", + "method": "retrieve_transaction", + "auth": "api_key", + "server": true + } + } + }, + "/v3/investigator/guardrails/": { + "get": { + "operationId": "investigator_guardrails", + "summary": "List the locked guardrails", + "description": "The five guardrails that apply to every queue. They cannot be switched off. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`.", + "tags": [ + "Investigator" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "enum": [ + "sanctions_to_person", + "no_tipping_off", + "no_protected_characteristics", + "never_files_reports", + "every_decision_logged" + ] }, - "description": "Beneficial owners / persons with significant control extracted from the registry. Can be empty when the registry does not expose ownership." - }, - "addresses": { - "type": "array", - "nullable": true, - "description": "Every address the registry states, one canonical shape. Null when it states none.", - "items": { - "$ref": "#/components/schemas/KYBRegistryAddress" - } - }, - "industries": { - "type": "array", - "nullable": true, - "description": "The activities the registry classifies the company under, one canonical shape. Null when it states none.", - "items": { - "$ref": "#/components/schemas/KYBRegistryIndustry" + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "locked": { + "type": "boolean" } - }, - "accounts": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts." - }, - "registry_data": { - "type": "object", - "description": "Raw record returned by the registry source, kept for reviewers. Its shape differs by country and source and is not a stable contract: read the canonical fields (`addresses`, `industries`, `ownership`, `beneficial_owners[].shareholding`) instead." - }, - "user_provided_data": { - "type": "object", - "nullable": true, - "description": "Always `null` on this endpoint (used by the hosted KYB flow when end users edit registry data)." - }, - "confirmed_by_user_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "Always `null` on this endpoint." - }, - "last_console_edit_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "Set when a console reviewer edits the record later." - }, - "is_editable": { - "type": "boolean", - "description": "Whether the registry-sourced data can still be edited in the hosted flow/console." - }, - "registry_tier_delivered": { - "type": "string", - "enum": [ - "basic", - "shareholders", - "ubo", - null - ], - "description": "Same as the top-level `registry_tier_delivered`.", - "nullable": true - }, - "registry_ownership_status": { - "type": "string", - "nullable": true, - "enum": [ - "not_requested", - "delivered", - "unavailable", - "pending", - null - ], - "description": "`delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier." - }, - "ownership": { - "$ref": "#/components/schemas/KYBRegistryOwnership" - }, - "registry_tier": { - "type": "string", - "nullable": true, - "description": "The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`." } } + } + } + } + } + } + }, + "401": { + "description": "Missing or invalid API key." + }, + "403": { + "description": "The key lacks the scope, the item kind is not readable, or the plan does not include the Investigator." + } + }, + "x-didit-sdk": { + "resource": "investigator", + "method": "list_guardrails", + "auth": "api_key", + "server": true + } + } + }, + "/v3/investigator/dashboard/": { + "get": { + "operationId": "investigator_dashboard", + "summary": "Investigator dashboard", + "description": "The last 30 days: reviews, decisions, automation rate, hours saved, spend, overrides, quality samples since go-live, per-queue and weekly figures. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`.", + "tags": [ + "Investigator" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "properties": { + "stage": { + "type": "string" + }, + "window_days": { + "type": "integer" + }, + "reviews": { + "type": "integer" + }, + "decided": { + "type": "integer" + }, + "automated": { + "type": "integer" + }, + "automation_rate": { + "type": "number", + "nullable": true + }, + "hours_saved": { + "type": "number", + "nullable": true + } + } + } + } + } + }, + "401": { + "description": "Missing or invalid API key." + }, + "403": { + "description": "The key lacks the scope, the item kind is not readable, or the plan does not include the Investigator." + } + }, + "parameters": [ + { + "in": "query", + "name": "scope", + "schema": { + "type": "string", + "enum": [ + "all", + "identity", + "transactions" + ], + "default": "all" + }, + "description": "`identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened)." + } + ], + "x-didit-sdk": { + "resource": "investigator", + "method": "retrieve_dashboard", + "auth": "api_key", + "server": true + } + } + }, + "/v3/investigator/performance/": { + "get": { + "operationId": "investigator_performance", + "summary": "Investigator performance", + "description": "Agreement with your team, volume, time saved, confidence calibration, per-queue results, override reasons and disagreements for a period. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`.", + "tags": [ + "Investigator" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "properties": { + "period": { + "type": "integer" + }, + "start": { + "type": "string", + "format": "date" + }, + "end": { + "type": "string", + "format": "date" + }, + "investigated": { + "type": "integer" + } + } + } + } + } + }, + "401": { + "description": "Missing or invalid API key." + }, + "403": { + "description": "The key lacks the scope, the item kind is not readable, or the plan does not include the Investigator." + } + }, + "parameters": [ + { + "in": "query", + "name": "scope", + "schema": { + "type": "string", + "enum": [ + "all", + "identity", + "transactions" + ], + "default": "all" + }, + "description": "`identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened)." + }, + { + "in": "query", + "name": "period", + "schema": { + "type": "string", + "enum": [ + "7", + "30", + "90" + ], + "default": "30" + }, + "description": "Days, ending today." + } + ], + "x-didit-sdk": { + "resource": "investigator", + "method": "retrieve_performance", + "auth": "api_key", + "server": true + } + } + }, + "/v3/investigator/activity/": { + "get": { + "operationId": "investigator_activity", + "summary": "List Investigator activity", + "description": "The append-only audit trail, newest first. A batch accept is one entry. Adds `counts` per group and `actors`. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`.", + "tags": [ + "Investigator" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "next": { + "type": "string", + "nullable": true + }, + "previous": { + "type": "string", + "nullable": true + }, + "counts": { + "type": "object", + "additionalProperties": true + }, + "actors": { + "type": "array", + "items": {} + }, + "results": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "description": "`id`, `at`, `kind`, `group`, `queue`, `run_id`, `item`, `actor`, `agent_version`, `data`, `report_version`, `steps`, `sources`, `batch`." + } + } + } + } + } + } + }, + "401": { + "description": "Missing or invalid API key." + }, + "403": { + "description": "The key lacks the scope, the item kind is not readable, or the plan does not include the Investigator." + } + }, + "parameters": [ + { + "in": "query", + "name": "scope", + "schema": { + "type": "string", + "enum": [ + "all", + "identity", + "transactions" + ], + "default": "all" + }, + "description": "`identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened)." + }, + { + "in": "query", + "name": "group", + "schema": { + "type": "string", + "enum": [ + "agent", + "decisions", + "overrides", + "autonomy" + ] + } + }, + { + "in": "query", + "name": "actor", + "schema": { + "type": "string" + }, + "description": "`agent` for the Investigator, or a person's email." + }, + { + "in": "query", + "name": "since", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "until", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer" + } + }, + { + "in": "query", + "name": "offset", + "schema": { + "type": "integer" + } + } + ], + "x-didit-sdk": { + "resource": "investigator", + "method": "list_activity", + "auth": "api_key", + "server": true + } + } + }, + "/v3/investigator/activity/export/": { + "get": { + "operationId": "investigator_activity_export", + "summary": "Export Investigator activity", + "description": "The same entries as CSV, one row per decision, newest first, up to 10,000 entries. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`.", + "tags": [ + "Investigator" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "", + "content": { + "text/csv": { + "schema": { + "type": "string" + } + } + } + }, + "401": { + "description": "Missing or invalid API key." + }, + "403": { + "description": "The key lacks the scope, the item kind is not readable, or the plan does not include the Investigator." + } + }, + "parameters": [ + { + "in": "query", + "name": "scope", + "schema": { + "type": "string", + "enum": [ + "all", + "identity", + "transactions" + ], + "default": "all" + }, + "description": "`identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened)." + }, + { + "in": "query", + "name": "group", + "schema": { + "type": "string", + "enum": [ + "agent", + "decisions", + "overrides", + "autonomy" + ] + } + }, + { + "in": "query", + "name": "actor", + "schema": { + "type": "string" + }, + "description": "`agent` for the Investigator, or a person's email." + }, + { + "in": "query", + "name": "since", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "in": "query", + "name": "until", + "schema": { + "type": "string", + "format": "date-time" + } + } + ], + "x-didit-sdk": { + "resource": "investigator", + "method": "export_activity", + "auth": "api_key", + "server": true + } + } + }, + "/v3/investigator/settings/": { + "get": { + "operationId": "investigator_settings_retrieve", + "summary": "Get Investigator settings", + "description": "The stage (`none`, `shadow` or `live`) and the monthly spend limit with this month's spend. Restricted keys need the `applications` scope (read).", + "tags": [ + "Investigator" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvestigatorSettings" + } + } + } + }, + "401": { + "description": "Missing or invalid API key." + }, + "403": { + "description": "The key lacks the scope, the item kind is not readable, or the plan does not include the Investigator." + } + }, + "x-didit-sdk": { + "resource": "investigator", + "method": "retrieve_settings", + "auth": "api_key", + "server": true + } + }, + "patch": { + "operationId": "investigator_settings_update", + "summary": "Update Investigator settings", + "description": "Change the stage, the monthly spend limit, or both. Moving past `none` needs a plan that includes the Investigator (Growth or higher); `live` also needs a contract that prices it, if you have one. Restricted keys need the `applications` scope (write).", + "tags": [ + "Investigator" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvestigatorSettings" + } + } + } + }, + "400": { + "description": "Neither field sent, or an invalid value." + }, + "401": { + "description": "Missing or invalid API key." + }, + "403": { + "description": "The key lacks the scope, the item kind is not readable, or the plan does not include the Investigator." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvestigatorSettings" + } + } + } + }, + "x-didit-sdk": { + "resource": "investigator", + "method": "update_settings", + "auth": "api_key", + "server": true + } + } + }, + "/v3/travel-rule/wallet-addresses/import/": { + "post": { + "summary": "Import wallet addresses in bulk", + "description": "Add up to 10,000 wallets to your Travel Rule address book in one call, as JSON or as a CSV file. A wallet can be sent by its address, by the SHA-256 of its lowercased address (`address_hash`) to keep the address private, or both. The import is idempotent: a wallet already in the book is reported as `existing` and left untouched, so the same file can be sent again. Each row is validated on its own, so one bad row never fails the others. A wallet imported by its hash only matches transfers to that address and can be proven with the wallet-ownership widget like any other entry.", + "operationId": "importTravelRuleWalletAddresses", + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "x-didit-sdk": { + "resource": "travel_rule.wallet_addresses", + "method": "bulk_import", + "auth": "api_key", + "server": true + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "required": [ + "entries" + ], + "properties": { + "entries": { + "type": "array", + "minItems": 1, + "maxItems": 10000, + "items": { + "$ref": "#/components/schemas/WalletAddressImportRow" + } + } + } + }, + { + "type": "array", + "minItems": 1, + "maxItems": 10000, + "items": { + "$ref": "#/components/schemas/WalletAddressImportRow" + } + } + ] + }, + "example": { + "entries": [ + { + "address": "0xBeneficiaryWallet01", + "chain": "ETH", + "holder_name": "Ana Diaz", + "holder_vendor_data": "user-042" + }, + { + "address_hash": "4f7d2e1c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e", + "chain": "bitcoin", + "holder_vendor_data": "user-043" + }, + { + "address": "rExchangeSharedAddress01", + "chain": "XRP", + "memo": "104729", + "holder_name": "Ana Diaz" + } + ] + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "file" + ], + "properties": { + "file": { + "type": "string", + "format": "binary", + "description": "A UTF-8 CSV file of at most 5 MB with a header row naming its columns (the row fields or their aliases). Empty cells count as not sent; columns the import does not read are ignored." + } + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl (JSON)", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/wallet-addresses/import/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"entries\": [{\"address\": \"0xBeneficiaryWallet01\", \"chain\": \"ETH\", \"holder_name\": \"Ana Diaz\"}, {\"address\": \"rExchangeSharedAddress01\", \"chain\": \"XRP\", \"memo\": \"104729\"}]}'" + }, + { + "lang": "curl", + "label": "curl (CSV)", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/wallet-addresses/import/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -F 'file=@wallets.csv;type=text/csv'" + } + ], + "responses": { + "200": { + "description": "Every row was created, already in the book, or a repeat of an earlier row.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WalletAddressImportReport" + }, + "example": { + "total": 3, + "created": 2, + "existing": 1, + "duplicate": 0, + "failed": 0, + "rows": [ + { + "row": 1, + "status": "existing", + "entry_id": "11111111-2222-3333-4444-555555555555", + "errors": {}, + "duplicate_of_row": null + }, + { + "row": 2, + "status": "created", + "entry_id": "22222222-3333-4444-5555-666666666666", + "errors": {}, + "duplicate_of_row": null + }, + { + "row": 3, + "status": "created", + "entry_id": "33333333-4444-5555-6666-777777777777", + "errors": {}, + "duplicate_of_row": null + } + ] + } + } + } + }, + "207": { + "description": "At least one row failed; the others were imported. Each failed row names its errors.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WalletAddressImportReport" + }, + "example": { + "total": 2, + "created": 1, + "existing": 0, + "duplicate": 0, + "failed": 1, + "rows": [ + { + "row": 1, + "status": "created", + "entry_id": "44444444-5555-6666-7777-888888888888", + "errors": {}, + "duplicate_of_row": null + }, + { + "row": 2, + "status": "failed", + "entry_id": null, + "errors": { + "address": [ + "Send address, address_hash or both." + ] + }, + "duplicate_of_row": null + } + ] + } + } + } + }, + "400": { + "description": "The request itself cannot be read: both `entries` and a `file`, or neither; more than 10,000 rows; a file over 5 MB, not UTF-8 CSV, without a header row or without rows.", + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "file": [ + "At most 10000 rows per import." + ] + } + } + } + } + } + } + }, + "/v3/travel-rule/counterparty-contacts/": { + "get": { + "summary": "List counterparty contacts", + "description": "The compliance addresses the EMAIL rail may write to for your application: Didit's platform contacts plus your application's own private ones, bounced ones included, newest first. Paginated with `limit` (default 50) and `offset`; `count` stops at 100, so 100 means 100 or more.", + "operationId": "listTravelRuleCounterpartyContacts", + "x-didit-sdk": { + "resource": "travel_rule.counterparty_contacts", + "method": "list", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "counterparty", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "Only the contacts of this directory entry. Must be a UUID; an empty value means no filter." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1 + }, + "description": "Page size, default 50." + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0 + }, + "description": "Rows to skip." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/travel-rule/counterparty-contacts/?counterparty={vasp_id}' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "The contacts.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "next": { + "type": "string", + "nullable": true + }, + "previous": { + "type": "string", + "nullable": true + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TravelRuleCounterpartyContact" + } + } + } + }, + "example": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "uuid": "6c1f9b8e-2d4a-4f7e-8b3c-5a9d0e1f2a3b", + "counterparty": "0d6f3c2a-6a1e-4c55-9a39-1f2b0c7d8e91", + "counterparty_name": "Example Counterparty VASP", + "email": "compliance@counterparty.example", + "name": "Compliance desk", + "source": "application", + "scope": "application", + "created_by": "", + "verified_at": null, + "is_invalid": false, + "invalid_reason": "", + "invalid_detail": "", + "invalid_at": null, + "created_at": "2026-10-19T09:00:00Z" + } + ] + } + } + } + }, + "400": { + "description": "`counterparty` is not a UUID.", + "content": { + "application/json": { + "example": { + "counterparty": [ + "Must be a valid UUID." + ] + } + } + } + } + } + }, + "post": { + "summary": "Add a counterparty contact", + "description": "Adds a private compliance address for one directory entry. Only your application sees and uses it, and the EMAIL rail writes to it before any platform contact. Adding an address the entry already has among your own contacts returns that contact.", + "operationId": "createTravelRuleCounterpartyContact", + "x-didit-sdk": { + "resource": "travel_rule.counterparty_contacts", + "method": "create", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleCounterpartyContactCreate" + }, + "example": { + "counterparty": "0d6f3c2a-6a1e-4c55-9a39-1f2b0c7d8e91", + "email": "compliance@counterparty.example", + "name": "Compliance desk" + } + } + } + }, + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X POST https://verification.didit.me/v3/travel-rule/counterparty-contacts/ \\\n -H 'x-api-key: YOUR_API_KEY' -H 'Content-Type: application/json' \\\n -d '{\"counterparty\": \"0d6f3c2a-6a1e-4c55-9a39-1f2b0c7d8e91\", \"email\": \"compliance@counterparty.example\", \"name\": \"Compliance desk\"}'" + } + ], + "responses": { + "201": { + "description": "The contact was added.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleCounterpartyContact" + }, + "example": { + "uuid": "6c1f9b8e-2d4a-4f7e-8b3c-5a9d0e1f2a3b", + "counterparty": "0d6f3c2a-6a1e-4c55-9a39-1f2b0c7d8e91", + "counterparty_name": "Example Counterparty VASP", + "email": "compliance@counterparty.example", + "name": "Compliance desk", + "source": "application", + "scope": "application", + "created_by": "", + "verified_at": null, + "is_invalid": false, + "invalid_reason": "", + "invalid_detail": "", + "invalid_at": null, + "created_at": "2026-10-19T09:00:00Z" + } + } + } + }, + "400": { + "description": "A field is missing or invalid, or no directory entry has this id.", + "content": { + "application/json": { + "example": { + "counterparty": [ + "No directory entry with this id." + ] + } + } + } + } + } + } + }, + "/v3/travel-rule/counterparty-contacts/{contact_uuid}/": { + "delete": { + "summary": "Delete a counterparty contact", + "description": "Deletes one of your application's own contacts. The EMAIL rail stops writing to it at once. Platform contacts cannot be deleted.", + "operationId": "deleteTravelRuleCounterpartyContact", + "x-didit-sdk": { + "resource": "travel_rule.counterparty_contacts", + "method": "delete", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "contact_uuid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X DELETE https://verification.didit.me/v3/travel-rule/counterparty-contacts/{contact_uuid}/ \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "204": { + "description": "Deleted." + }, + "404": { + "description": "No contact of your application has this id. A platform contact answers 404 too." + } + } + } + }, + "/v3/travel-rule/transfers/": { + "get": { + "summary": "List Travel Rule transfers", + "description": "Your application's Travel Rule transfers, newest first, paginated with `limit` (default 50) and `offset`. Rows carry VASP names, wallets and memos only, never person data. Comma-separated values select several. An unknown value answers 400 naming the parameter, for example `{\"direction\": [\"Unknown value(s): inbound.\"]}`.", + "operationId": "listTravelRuleTransfers", + "x-didit-sdk": { + "resource": "travel_rule.transfers", + "method": "list", + "auth": "api_key", + "server": true + }, + "tags": [ + "Travel Rule" + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "parameters": [ + { + "name": "direction", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "INBOUND, OUTBOUND, or both comma-separated. Case-sensitive." + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "One or more transfer statuses, comma-separated." + }, + { + "name": "rail", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "One or more of INTERNAL, TRP, GTR, TRUST, VERIFYVASP, SYGNA, EMAIL, TEST_VASP, comma-separated." + }, + { + "name": "kind", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "withdrawal, deposit or message, comma-separated." + }, + { + "name": "currency", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "The transaction's currency, case-insensitive." + }, + { + "name": "start_date", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date" + }, + "description": "Transfers created on or after this day (UTC), YYYY-MM-DD." + }, + { + "name": "end_date", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date" + }, + "description": "Transfers created on or before this day (UTC), YYYY-MM-DD." + }, + { + "name": "counterparty", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "uuid" + }, + "description": "The `id` of a directory entry or Didit network member from GET /v3/travel-rule/vasps/: your transfers with that counterparty. A value that is not a valid id answers 400." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1 + }, + "description": "Page size, default 50." + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0 + }, + "description": "Rows to skip." + } + ], + "x-codeSamples": [ + { + "lang": "curl", + "label": "curl", + "source": "curl -X GET 'https://verification.didit.me/v3/travel-rule/transfers/?direction=OUTBOUND&status=AWAITING_COUNTERPARTY' \\\n -H 'x-api-key: YOUR_API_KEY'" + } + ], + "responses": { + "200": { + "description": "A page of transfers.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "next": { + "type": "string", + "nullable": true + }, + "previous": { + "type": "string", + "nullable": true + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TravelRuleTransferRow" + } + } + } + }, + "example": { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": "5b0f6c2e-2a4d-4f0e-9a41-0c3f9a7e1d22", + "transaction_id": "a3c1e8f4-7b62-4d1a-8e0b-2f9d6c4b7a10", + "txn_id": "wd-2026-10-19-0042", + "txn_date": "2026-10-19T08:41:12Z", + "kind": "withdrawal", + "direction": "OUTBOUND", + "status": "AWAITING_COUNTERPARTY", + "rail": "TRP", + "asset": "ETH", + "amount": "1.25", + "originator_vasp": "Origin CASP SL", + "beneficiary_vasp": "Example Counterparty VASP", + "originator_wallet": "0x77ccff0cc624a53cd8b8d8703ced826710211ec7", + "beneficiary_wallet": "0xede2d7e4f5b097e6391ab6bbcf2f47a53f4778cb", + "originator_memo": "", + "beneficiary_memo": "", + "originator_wallet_type": "vasp", + "beneficiary_wallet_type": "vasp", + "matched_transaction_id": null, + "deadline_at": "2026-10-21T08:41:13Z", + "created_at": "2026-10-19T08:41:13Z" + } + ] + } + } + } + }, + "400": { + "description": "A filter value is not valid.", + "content": { + "application/json": { + "example": { + "start_date": [ + "Use the YYYY-MM-DD format." + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "DocumentAIDetectedCode": { + "type": "object", + "required": [ + "type", + "page_number", + "position", + "x", + "y", + "width", + "height", + "page_width", + "page_height", + "raw_payload", + "parsed_payload" + ], + "properties": { + "type": { + "type": "string", + "description": "Detected symbology, for example `QR_CODE`, `CODE_128`, `EAN_13`, `DATA_MATRIX`, or `PDF_417`." + }, + "page_number": { + "type": "integer", + "minimum": 1, + "description": "1-based page number." + }, + "position": { + "type": "array", + "nullable": true, + "description": "Detected polygon in rendered-page pixel coordinates. `null` when no reliable location is available.", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "integer" + } + } + }, + "x": { + "type": "integer", + "nullable": true, + "description": "Left edge of the bounding box in rendered-page pixels." + }, + "y": { + "type": "integer", + "nullable": true, + "description": "Top edge of the bounding box in rendered-page pixels." + }, + "width": { + "type": "integer", + "nullable": true, + "description": "Bounding-box width in rendered-page pixels." + }, + "height": { + "type": "integer", + "nullable": true, + "description": "Bounding-box height in rendered-page pixels." + }, + "page_width": { + "type": "integer", + "description": "Rendered page width in pixels." + }, + "page_height": { + "type": "integer", + "description": "Rendered page height in pixels." + }, + "raw_payload": { + "type": "string", + "description": "Raw decoder payload as a base64 string when available; otherwise an empty string." + }, + "parsed_payload": { + "nullable": true, + "description": "Decoded string or structured value. `null` when the code was located but could not be decoded." + } + } + }, + "SessionImportJob": { + "type": "object", + "required": [ + "uuid", + "status", + "import_type", + "source_format", + "source_filename", + "total_rows", + "processed_rows", + "created_count", + "updated_count", + "skipped_count", + "failed_count" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Import job id. Use this value to poll progress and fetch row errors." + }, + "status": { + "type": "string", + "enum": [ + "PENDING", + "PROCESSING", + "COMPLETED", + "FAILED", + "PAUSED", + "CANCELLED" + ] + }, + "import_type": { + "type": "string", + "enum": [ + "user_verification", + "business_verification", + "status_rules", + "transactions" + ] + }, + "source_format": { + "type": "string", + "enum": [ + "csv", + "ndjson" + ] + }, + "source_filename": { + "type": "string" + }, + "total_rows": { + "type": "integer", + "format": "int64" + }, + "processed_rows": { + "type": "integer", + "format": "int64" + }, + "checkpoint_row": { + "type": "integer", + "format": "int64" + }, + "created_count": { + "type": "integer", + "format": "int64" + }, + "updated_count": { + "type": "integer", + "format": "int64" + }, + "skipped_count": { + "type": "integer", + "format": "int64" + }, + "failed_count": { + "type": "integer", + "format": "int64" + }, + "total_media_bytes": { + "type": "integer", + "format": "int64" + }, + "last_error": { + "type": "string", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "started_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "completed_at": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, + "SessionImportError": { + "type": "object", + "required": [ + "uuid", + "row_number", + "error", + "raw_row" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "row_number": { + "type": "integer", + "format": "int64" + }, + "external_id": { + "type": "string", + "nullable": true + }, + "error": { + "type": "string" + }, + "raw_row": { + "type": "object", + "additionalProperties": true + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "BrandingCustomizationUpdate": { + "type": "object", + "description": "Partial branding update body. Provide only the fields you want to change. Hex colors accept `#RRGGBB` or `#RRGGBBAA`. Custom domain and white-label email are not accepted here.", + "properties": { + "color_primary": { + "type": "string", + "description": "Headers / primary text color." + }, + "color_secondary": { + "type": "string", + "description": "Paragraph / secondary text color." + }, + "color_background": { + "type": "string", + "description": "Page background color." + }, + "color_panel": { + "type": "string", + "description": "Panel / accent color." + }, + "color_panel_10": { + "type": "string", + "description": "Panel color at 10% opacity." + }, + "color_on_panel_1": { + "type": "string", + "description": "Support text color on panels." + }, + "color_on_panel_2": { + "type": "string", + "description": "Pill / badge color on panels." + }, + "color_on_background": { + "type": "string", + "description": "Text color on background." + }, + "color_button_1": { + "type": "string", + "description": "Primary button color." + }, + "color_button_2": { + "type": "string", + "description": "Secondary button color." + }, + "color_button_text_1": { + "type": "string", + "description": "Primary button text color." + }, + "color_button_text_2": { + "type": "string", + "description": "Secondary button text color." + }, + "color_pill_text": { + "type": "string", + "description": "Text color for pills, inputs and selectors." + }, + "border_radius_panel": { + "type": "integer", + "minimum": 0, + "description": "Panel corner radius in pixels." + }, + "border_radius_buttons": { + "type": "integer", + "minimum": 0, + "description": "Button corner radius in pixels." + }, + "font_family": { + "type": "string", + "description": "Google Font family name (e.g. `Inter`, `AR One Sans`)." + }, + "font_weight": { + "type": "string", + "description": "Font weight available for the selected family (e.g. `regular`, `500`, `700`)." + }, + "app_public_name": { + "type": "string", + "nullable": true, + "description": "Public name shown in the verification flow." + }, + "privacy_policy_url": { + "type": "string", + "nullable": true, + "description": "Privacy policy URL shown to the end user." + }, + "skip_welcome_screen": { + "type": "boolean", + "description": "Skip the initial welcome screen." + }, + "disable_login_with_didit": { + "type": "boolean", + "description": "Continue as guest instead of showing the login-with-Didit screen. Cannot be set back to `false` while a custom domain is configured (400)." + }, + "hide_progress_bar": { + "type": "boolean", + "description": "Hide the progress bar during the flow." + }, + "callback_seconds": { + "type": "integer", + "minimum": 0, + "description": "Delay (seconds) before redirecting to the callback URL." + }, + "delete_logo_square": { + "type": "boolean", + "description": "When `true`, removes the current square logo." + }, + "delete_logo_rectangular": { + "type": "boolean", + "description": "When `true`, removes the current rectangular logo." + }, + "delete_logo_rectangular_dark": { + "type": "boolean", + "description": "When `true`, removes the current dark-mode logo, and `logo_rectangular_dark` then reads `null`. If the same multipart request also uploads `image_rectangular_dark`, the upload wins." + }, + "delete_favicon": { + "type": "boolean", + "description": "When `true`, removes the current favicon." + } + } + }, + "BrandingCustomization": { + "type": "object", + "description": "White-label branding for an application: colors, fonts, radii, logo URLs and UI/UX options. Logo URLs are read-only (set them by uploading images). Custom domain and white-label email are not included.", + "properties": { + "color_primary": { + "type": "string" + }, + "color_secondary": { + "type": "string" + }, + "color_background": { + "type": "string" + }, + "color_panel": { + "type": "string" + }, + "color_panel_10": { + "type": "string" + }, + "color_on_panel_1": { + "type": "string" + }, + "color_on_panel_2": { + "type": "string" + }, + "color_on_background": { + "type": "string" + }, + "color_button_1": { + "type": "string" + }, + "color_button_2": { + "type": "string" + }, + "color_button_text_1": { + "type": "string" + }, + "color_button_text_2": { + "type": "string" + }, + "color_pill_text": { + "type": "string" + }, + "border_radius_panel": { + "type": "integer" + }, + "border_radius_buttons": { + "type": "integer" + }, + "font_family": { + "type": "string" + }, + "font_weight": { + "type": "string" + }, + "font_url": { + "type": "string", + "nullable": true, + "description": "Read-only. Resolved Google Fonts stylesheet URL for the selected family." + }, + "logo_square": { + "type": "string", + "nullable": true, + "description": "Read-only URL of the square logo. Upload via `image_square`." + }, + "logo_rectangular": { + "type": "string", + "nullable": true, + "description": "Read-only URL of the rectangular logo. Upload via `image_rectangular`." + }, + "logo_rectangular_dark": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Read-only. Temporary signed URL of the rectangular logo for dark mode, shown on a dark background. `null` when no dark logo is set or rectangular logos are not shown for your account. Upload it with `image_rectangular_dark`." + }, + "favicon": { + "type": "string", + "nullable": true, + "description": "Read-only URL of the favicon. Upload via `image_favicon`." + }, + "app_public_name": { + "type": "string", + "nullable": true + }, + "privacy_policy_url": { + "type": "string", + "nullable": true + }, + "skip_welcome_screen": { + "type": "boolean" + }, + "disable_login_with_didit": { + "type": "boolean" + }, + "hide_progress_bar": { + "type": "boolean" + }, + "callback_seconds": { + "type": "integer" + } + } + }, + "WebhookEvent": { + "type": "string", + "description": "Supported webhook event name. Use these exact strings in `subscribed_events`; unsupported values are rejected.", + "enum": [ + "status.updated", + "data.updated", + "user.status.updated", + "user.data.updated", + "business.status.updated", + "business.data.updated", + "activity.created", + "transaction.created", + "transaction.status.updated", + "travel_rule.status.updated", + "workflow.compliance_changed", + "auth.signup", + "auth.login", + "auth.login_failed", + "auth.password_reset_requested", + "auth.password_reset_completed", + "auth.mfa_changed", + "auth.new_device", + "auth.account_locked", + "investigator.report.ready", + "investigator.decision.automated", + "investigator.decision.overridden", + "investigator.decision.contested", + "investigator.question.raised", + "investigator.automation.paused", + "investigator.rfi.answered", + "investigator.rfi.overdue", + "investigator.drift.detected", + "investigator.sla.breached", + "investigator.qa.failed", + "investigator.external.report.ready" + ] + }, + "WebhookSubscribedEvents": { + "type": "array", + "description": "Event filter for this webhook destination. Didit delivers only webhooks whose event type exactly matches one of these values — there is no wildcard subscription. When sent, the list must contain at least one valid event (an explicit `[]` is rejected with 400); a destination whose list is empty (field omitted at create) receives nothing. On update the list is replaced wholesale, never merged.", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/components/schemas/WebhookEvent" + }, + "example": [ + "status.updated", + "data.updated" + ] + }, + "PhoneVerificationSendResponse": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Session id of the verification. A `Retry` send returns the same `request_id` as the original send. This id appears in the Business Console, is returned again by a finalized `POST /v3/phone/check/`, and can be passed to `GET /v3/session/{sessionId}/decision/`." + }, + "status": { + "type": "string", + "enum": [ + "Success", + "Retry", + "Blocked" + ], + "description": "`Success` — OTP sent to a new verification. `Retry` — OTP re-sent for the pending verification created by a previous send. `Blocked` — the anti-fraud layer refused to send; the verification is immediately finalized as `Declined` and nothing is billed." + }, + "reason": { + "type": "string", + "nullable": true, + "description": "Why a send was `Blocked` (e.g., `repeated_attempts`, `suspicious`, `spam`). `null` on `Success` and `Retry`." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Echo of the `vendor_data` stored on the session (from the first send)." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Echo of the `metadata` stored on the session (from the first send)." + } + } + }, + "PhoneVerificationCheckResponse": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "On `Approved`/`Declined`: the session id of the matched verification — identical to the `request_id` returned by `POST /v3/phone/send/`. On `Failed` and `Expired or Not Found`: a random one-off UUID that cannot be looked up later." + }, + "status": { + "type": "string", + "enum": [ + "Approved", + "Declined", + "Failed", + "Expired or Not Found" + ], + "description": "`Approved` — correct code, no declining risk. `Declined` — terminal: a declining risk matched or the attempt budget (3) was exhausted. `Failed` — wrong code, attempts remaining. `Expired or Not Found` — no pending verification for this number in the last 5 minutes." + }, + "message": { + "type": "string", + "description": "Human-readable explanation of the outcome, including the number of attempts remaining after a wrong code." + }, + "phone": { + "type": "object", + "nullable": true, + "description": "Full phone report. Present (non-null) only on finalized outcomes (`Approved`/`Declined`); `null` on `Failed` and absent on `Expired or Not Found`.", + "properties": { + "status": { + "type": "string", + "enum": [ + "Approved", + "Declined", + "In Review", + "Not Finished", + "Expired" + ], + "description": "Final status of the phone verification. Matches the top-level `status` for finalized checks." + }, + "phone_number_prefix": { + "type": "string", + "description": "Country calling code with leading `+` (e.g., `+1`).", + "example": "+1" + }, + "phone_number": { + "type": "string", + "description": "National number without the country calling code.", + "example": "4155552671" + }, + "full_number": { + "type": "string", + "description": "Complete number in E.164 format.", + "example": "+14155552671" + }, + "country_code": { + "type": "string", + "nullable": true, + "description": "ISO 3166-1 alpha-2 country of the number.", + "example": "US" + }, + "country_name": { + "type": "string", + "description": "Full country name derived from `country_code`.", + "example": "United States" + }, + "carrier": { + "type": "object", + "description": "Current network serving the number, from the carrier lookup run at finalization.", + "properties": { + "name": { + "type": "string", + "description": "Carrier name. Empty string when the lookup could not identify it." + }, + "type": { + "type": "string", + "description": "Line type reported by the lookup: `mobile`, `fixed_line`, `voip`, `isp`, `vpn`, `toll_free`, `premium_rate`, `pager`, `payphone`, `satellite`, `service`, `shared_cost`, `short_codes_commercial`, `calling_cards`, `local_rate`, `universal_access`, `voice_mail`, `other`, or `unknown`." + } + } + }, + "is_disposable": { + "type": "boolean", + "description": "`true` when the number belongs to a temporary/burner number service." + }, + "is_virtual": { + "type": "boolean", + "description": "`true` when the line type is virtual (`voip`, `isp`, or `vpn`)." + }, + "verification_method": { + "type": "string", + "description": "Channel that actually delivered the OTP once delivery is confirmed (e.g., `whatsapp`, `sms`); until then, the requested channel." + }, + "verification_attempts": { + "type": "integer", + "description": "Number of OTP sends for this verification (1, or 2 after a retry)." + }, + "verified_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the correct code was entered. `null` if the verification was never approved." + }, + "warnings": { + "type": "array", + "description": "Risk warnings recorded during the verification (duplicate, disposable, VoIP, attempts exceeded, blocklist, etc.). Empty array when there are none.", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "description": "Feature that produced the warning. Always `PHONE` here." }, - "vendor_data": { + "risk": { "type": "string", - "nullable": true, - "description": "Echo of the `vendor_data` you sent." + "description": "Machine-readable risk code." }, - "metadata": { + "additional_data": { "type": "object", "nullable": true, - "description": "Echo of the `metadata` you sent." + "description": "Risk-specific context (e.g., the duplicated session id for duplicate risks). `null` when the risk carries no extra data." }, - "created_at": { + "log_type": { "type": "string", - "format": "date-time" - } - } - }, - "examples": { - "Resolved company": { - "summary": "Full registry profile (officer list truncated for brevity)", - "value": { - "request_id": "9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e", - "kyb_registry": { - "uuid": "f1e2d3c4-b5a6-4789-9c0d-1e2f3a4b5c6d", - "node_id": "feature_kyb_registry", - "status": "In Review", - "registry_status": "active", - "data_resolved": true, - "company_name": "Tesco PLC", - "registration_number": "00445790", - "country_code": "GB", - "region": "", - "company_type": "Public Limited Company", - "incorporation_date": "1947-11-27", - "registered_address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", - "tax_number": "GB412 8925 93", - "risk_level": "low risk", - "verification_status": "verified", - "is_from_registry": true, - "fetch_status": "resolved", - "alternative_names": "TESCO STORES (HOLDINGS) PUBLIC LIMITED COMPANY", - "nature_of_business": null, - "registered_capital": null, - "registered_capital_amount": null, - "registered_capital_currency": null, - "website": "https://www.tesco.com/", - "email": "customer.service@tesco.co.uk", - "phone": "03301231688", - "legal_entity_identifier": null, - "location_of_registration": null, - "financial_summary": { - "currency": "Pound (£)", - "total_number_of_shares": "8174378721", - "meta_detail": { - "Class_Of_Share": "Ordinary" - } - }, - "officers": [ - { - "uuid": "a1b2c3d4-e5f6-4789-9a0b-1c2d3e4f5a6b", - "name": "CHRISTOPHER JON TAYLOR", - "designation": "Secretary", - "role": "secretary", - "nationality": null, - "is_active": true, - "kyc_status": null, - "kyc_session_url": null - }, - { - "uuid": "b2c3d4e5-f6a7-4890-8b1c-2d3e4f5a6b7c", - "name": "MELISSA BETHELL", - "designation": "Director", - "role": "director", - "nationality": "British", - "is_active": true, - "kyc_status": null, - "kyc_session_url": null - } - ], - "beneficial_owners": [], - "addresses": [ - { - "address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", - "type": "legal_entity_registry_address", - "description": "registered office address" - } - ], - "industries": null, - "accounts": { - "last_account": "26 February 2025", - "next_account": "26 February 2026", - "due_by_date": "26 August 2026" - }, - "registry_data": { - "name": "Tesco PLC", - "registration_number": "00445790", - "status": "active", - "type": "Public Limited Company", - "incorporation_date": "1947-11-27", - "tax_number": "GB412 8925 93", - "country_code": "GB", - "fetch_status": "resolved", - "industries_detail": [ - { - "code": "47110", - "name": "Retail Sale In Non-specialised Stores With Food, Beverages Or Tobacco Predominating", - "description": null, - "meta_detail": [] - } - ] - }, - "user_provided_data": null, - "confirmed_by_user_at": null, - "last_console_edit_at": null, - "is_editable": true - }, - "vendor_data": "company-1234", - "metadata": null, - "created_at": "2026-06-11T10:45:00.000000+00:00" - } - } - } - } - } - }, - "400": { - "description": "Validation error. Field-level problems use DRF's standard envelope.", - "content": { - "application/json": { - "examples": { - "Missing kyb_response_id": { - "summary": "`kyb_response_id` is required", - "value": { - "kyb_response_id": [ - "This field is required." - ] - } - }, - "save_api_request=false rejected": { - "summary": "Selects are always persisted", - "value": { - "save_api_request": [ - "KYB registry select must be saved so billing and retrieval can be tracked." - ] + "enum": [ + "error", + "warning", + "information" + ], + "description": "Severity. `error` risks decline the verification, `warning` flags it for review, `information` is recorded without affecting the status." + }, + "short_description": { + "type": "string", + "description": "Human-readable one-line summary of the risk." + }, + "long_description": { + "type": "string", + "description": "Human-readable explanation of the risk." } } } - } - } - }, - "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any screening or provider call happens.", - "content": { - "application/json": { - "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", - "value": { - "detail": "You do not have permission to perform this action." - } - }, - "Not enough credits": { - "summary": "Organization balance cannot cover the call", - "value": { - "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" + }, + "lifecycle": { + "type": "array", + "description": "Chronological audit trail of the verification. Event `type` is one of `PHONE_VERIFICATION_MESSAGE_SENT`, `PHONE_VERIFICATION_RETRY_MESSAGE_SENT`, `PHONE_VERIFICATION_BLOCKED`, `PHONE_DELIVERY_DELIVERED`, `PHONE_DELIVERY_UNDELIVERABLE`, `VALID_CODE_ENTERED`, `INVALID_CODE_ENTERED`, `PHONE_VERIFICATION_APPROVED`, `PHONE_VERIFICATION_DECLINED`, `PHONE_VERIFICATION_IN_REVIEW`, `PHONE_VERIFICATION_EXPIRED`.", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Event type." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "When the event happened." + }, + "details": { + "type": "object", + "nullable": true, + "description": "Event-specific payload. Send events: `{status, reason, channel, actual_channel}` (`actual_channel` is filled once delivery is confirmed). Delivery events: `{channel, status}`. Code-entry events: `{code_tried, status}`. Final-status events: `null`, or `{reason}` when declined or in review." + }, + "fee": { + "type": "number", + "description": "USD amount billed for this event. Non-zero only on send events (the delivery price); 0 for code entries, delivery confirmations, and status events." } } } - } - } - }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", - "content": { - "application/json": { - "examples": { - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." - } - } - }, - "schema": { + }, + "matches": { + "type": "array", + "description": "Other sessions of your application (KYC, KYB, or API) where the same phone number was used by a different user (different `vendor_data`). Up to 5 entries, a synthetic list-entry blocklist match first when present (session-derived matches keep query order). Empty array when there are no matches.", + "items": { "type": "object", "properties": { - "detail": { - "type": "string" + "session_id": { + "type": "string", + "format": "uuid", + "description": "Session id of the other verification that used this phone number." + }, + "session_number": { + "type": "integer", + "description": "Human-friendly sequential number of that session." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "`vendor_data` of that session." + }, + "verification_date": { + "type": "string", + "description": "When that session was created (`YYYY-MM-DDTHH:MM:SSZ`)." + }, + "phone_number": { + "type": "string", + "description": "The matched phone number." + }, + "status": { + "type": "string", + "description": "Status of that session (e.g., `Approved`, `Declined`, `In Progress`)." + }, + "is_blocklisted": { + "type": "boolean", + "description": "Whether the phone number is on your application's blocklist." + }, + "api_service": { + "type": "string", + "nullable": true, + "description": "For API-type sessions, the standalone API that created it (e.g., `PHONE_VERIFICATION`); `null` for regular verification sessions." + }, + "source": { + "type": "string", + "description": "Where the match was found. Usually `session`; a blocklist hit via an organization list entry prepends a synthetic match with `source: \"list_entry\"` and null `session_id`/`session_number`." } } } + }, + "phone_social": { + "$ref": "#/components/schemas/PhoneSocialFootprint" } } }, - "202": { - "description": "Report accepted and still processing. When a report is still being prepared, the endpoint returns 202 Accepted, kyb_registry.fetch_status=pending, and registry_tier_delivered=null. Keep request_id and retrieve GET /v3/session/{request_id}/decision/ until registry_checks[0].company.fetch_status is resolved. Didit completes the report automatically and charges the delivered tier once it is ready. Do not repeat the purchase while it is pending; every new POST creates a separate request.", - "content": { - "application/json": { - "schema": { - "$ref": "#/paths/~1v3~1kyb~1shareholders~1/post/responses/200/content/application~1json/schema" - }, - "examples": { - "Pending (registry still resolving)": { - "summary": "Ownership report is still processing; retrieve the saved decision", - "value": { - "request_id": "9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e", - "kyb_registry": { - "uuid": "7c0a4f7e-2b1d-4f6a-9c3e-8d5b2a1f0e9d", - "node_id": "feature_kyb_registry", - "status": "Not Finished", - "registry_status": null, - "data_resolved": false, - "company_name": "Example Holdings Limited", - "registration_number": "12345678", - "country_code": "HK", - "region": null, - "company_type": null, - "incorporation_date": null, - "registered_address": null, - "tax_number": null, - "risk_level": null, - "verification_status": null, - "is_from_registry": true, - "fetch_status": "pending", - "alternative_names": null, - "nature_of_business": null, - "registered_capital": null, - "registered_capital_amount": null, - "registered_capital_currency": null, - "website": null, - "email": null, - "phone": null, - "legal_entity_identifier": null, - "location_of_registration": null, - "financial_summary": {}, - "officers": [], - "beneficial_owners": [], - "addresses": null, - "industries": null, - "accounts": null, - "registry_data": {}, - "user_provided_data": null, - "confirmed_by_user_at": null, - "last_console_edit_at": null, - "is_editable": true, - "registry_tier_delivered": null, - "registry_ownership_status": "pending" - }, - "vendor_data": "company-1234", - "metadata": null, - "created_at": "2026-06-11T10:45:00.000000+00:00", - "registry_tier_delivered": null, - "registry_tier_requested": "shareholders" - } - } - } - } - } + "vendor_data": { + "type": "string", + "nullable": true, + "description": "`vendor_data` of the matched verification's session. `null` on `Expired or Not Found`." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "`metadata` of the matched verification's session. `null` on `Expired or Not Found`." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of this check response." } } - } - }, - "/v3/kyb/ubo/": { - "post": { - "summary": "KYB registry company with ultimate beneficial owners (standalone)", - "operationId": "post_v3_kyb_ubo", - "tags": [ - "Standalone APIs" - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], - "parameters": [], - "description": "Retrieve the Lite profile, the direct shareholders **and the ultimate beneficial owners** resolved through the ownership chain (`registry_data.ubo_resolution` carries the corporate parent chain) of one candidate returned by `POST /v3/kyb/search/`.\n\n**Pairing with search.** Pass the candidate's `kyb_response_id` exactly as returned by a search of the same application. The search runs on the Lite registry chain; when the country's ownership data lives at another registry Didit re-finds the company there by registration number, at no extra cost.\n\n**Billing.** Billed at the country's UBO price - see [KYB registry pricing](/getting-started/kyb-registry-pricing). The tier delivered is the highest the country and the registry can serve: Shareholders when the resolved report names no natural person as a beneficial owner, Lite when there is no ownership record at all; `registry_tier_delivered` names what was billed. The balance is checked against the UBO price before any registry call (`403` when short).\n\n**Availability.** v3 only. Every call creates a business session (its id is the `request_id`).\n\n**Pending reports.** When a report is still being prepared, or the ownership section has not been retrieved from the registry yet, the endpoint returns 202 Accepted, kyb_registry.fetch_status=pending, kyb_registry.registry_ownership_status=pending, and registry_tier_delivered=null. Keep request_id and retrieve GET /v3/session/{request_id}/decision/ until registry_checks[0].company.fetch_status is resolved. Didit completes the report automatically and charges the delivered tier once it is ready. Do not repeat the purchase while it is pending; every new POST creates a separate request.", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "kyb_response_id" - ], - "properties": { - "kyb_response_id": { - "type": "string", - "maxLength": 255, - "description": "Candidate handle from `POST /v3/kyb/search/` (`kyb_registry.companies[].kyb_response_id`). Ephemeral — use it shortly after the search that produced it.", - "example": "69aeeb95febb0f1704042259" - }, - "vendor_data": { - "type": "string", - "description": "Your identifier for this company; echoed back and stored with the business session." - }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Free-form JSON stored with the business session and echoed back." - }, - "save_api_request": { - "type": "boolean", - "default": true, - "description": "Must be `true` (the default). KYB registry selects are always persisted so billing and retrieval can be tracked; `false` returns `400`." - } - } - } - } - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "curl", - "source": "curl -X POST https://verification.didit.me/v3/kyb/ubo/ \\\n -H 'x-api-key: YOUR_API_KEY' \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"kyb_response_id\": \"69aeeb95febb0f1704042259\",\n \"vendor_data\": \"company-1234\"\n }'" + }, + "EmailVerificationSendResponse": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Session id of the verification. A `Retry` send returns the same `request_id` as the original send. This id appears in the Business Console, is returned again by a finalized `POST /v3/email/check/`, and can be passed to `GET /v3/session/{sessionId}/decision/`." }, - { - "lang": "python", - "label": "Python", - "source": "import os, time, requests\n\nsearch = requests.post(\n \"https://verification.didit.me/v3/kyb/search/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"country_code\": \"GB\", \"name\": \"Tesco\"},\n timeout=60,\n).json()\nkyb_response_id = search[\"kyb_registry\"][\"companies\"][0][\"kyb_response_id\"]\n\nresp = requests.post(\n \"https://verification.didit.me/v3/kyb/ubo/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n json={\"kyb_response_id\": kyb_response_id, \"vendor_data\": \"company-1234\"},\n timeout=90,\n)\nresp.raise_for_status()\ndata = resp.json()\nrequest_id = data[\"request_id\"]\nprint(\"Keep this request ID:\", request_id)\ncompany = data[\"kyb_registry\"]\nif resp.status_code == 202 or company.get(\"fetch_status\") == \"pending\":\n # This client wait limit does not cancel the report or create a new purchase.\n deadline = time.monotonic() + 300\n while company.get(\"fetch_status\") != \"resolved\" and time.monotonic() < deadline:\n time.sleep(5)\n decision = requests.get(\n f\"https://verification.didit.me/v3/session/{request_id}/decision/\",\n headers={\"x-api-key\": os.environ[\"DIDIT_API_KEY\"]},\n timeout=30,\n )\n decision.raise_for_status()\n checks = decision.json().get(\"registry_checks\") or []\n if checks:\n company = checks[0].get(\"company\") or company\nif company.get(\"fetch_status\") == \"resolved\":\n print(company[\"company_name\"], company[\"registry_tier_delivered\"])\nelse:\n print(\"Still processing; retrieve this request later:\", request_id)" + "status": { + "type": "string", + "enum": [ + "Success", + "Retry", + "Undeliverable" + ], + "description": "`Success` — OTP emailed to a new verification (billed). `Retry` — fresh OTP emailed for the pending verification created by a previous send (free). `Undeliverable` — the address failed deliverability validation or the message could not be sent; the verification is immediately finalized as `Declined` and nothing is billed." }, - { - "lang": "javascript", - "label": "JavaScript", - "source": "const res = await fetch('https://verification.didit.me/v3/kyb/ubo/', {\n method: 'POST',\n headers: {\n 'x-api-key': 'YOUR_API_KEY',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n kyb_response_id: '69aeeb95febb0f1704042259',\n vendor_data: 'company-1234',\n }),\n});\nif (!res.ok) throw new Error(`Request failed: ${res.status}`);\nconst data = await res.json();\nconst requestId = data.request_id;\nconsole.log('Keep this request ID:', requestId);\nlet company = data.kyb_registry;\nif (res.status === 202 || company.fetch_status === 'pending') {\n // This client wait limit does not cancel the report or create a new purchase.\n const deadline = Date.now() + 300000;\n while (company.fetch_status !== 'resolved' && Date.now() < deadline) {\n await new Promise(resolve => setTimeout(resolve, 5000));\n const decisionResponse = await fetch(\n `https://verification.didit.me/v3/session/${requestId}/decision/`,\n { headers: { 'x-api-key': 'YOUR_API_KEY' }, signal: AbortSignal.timeout(30000) },\n );\n if (!decisionResponse.ok) throw new Error(`Retrieval failed: ${decisionResponse.status}`);\n const decision = await decisionResponse.json();\n company = decision.registry_checks?.[0]?.company ?? company;\n }\n}\nif (company.fetch_status === 'resolved') {\n console.log(company.company_name, company.registry_tier_delivered);\n} else {\n console.log('Still processing; retrieve this request later:', requestId);\n}" + "reason": { + "type": "string", + "nullable": true, + "enum": [ + "email_can_not_be_delivered", + null + ], + "description": "`email_can_not_be_delivered` when `status` is `Undeliverable`; `null` otherwise." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Echo of the `vendor_data` stored on the session (from the first send)." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Echo of the `metadata` stored on the session (from the first send)." } - ], - "responses": { - "200": { - "description": "Company report returned and billed at the delivered tier. Ownership reports still being prepared can return 202; keep request_id to retrieve their saved decision.", - "content": { - "application/json": { - "schema": { + } + }, + "EmailVerificationCheckResponse": { + "type": "object", + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "On `Approved`/`Declined`: the session id of the matched verification — identical to the `request_id` returned by `POST /v3/email/send/`. On `Failed` and `Expired or Not Found`: a random one-off UUID that cannot be looked up later." + }, + "status": { + "type": "string", + "enum": [ + "Approved", + "Declined", + "Failed", + "Expired or Not Found" + ], + "description": "`Approved` — correct code, no declining risk. `Declined` — terminal: a declining risk matched or the attempt budget (3) was exhausted. `Failed` — wrong code, attempts remaining. `Expired or Not Found` — no pending verification for this address in the last 5 minutes." + }, + "message": { + "type": "string", + "description": "Human-readable explanation of the outcome, including the number of attempts remaining after a wrong code." + }, + "email": { + "type": "object", + "nullable": true, + "description": "Full email report. Present (non-null) only on finalized outcomes (`Approved`/`Declined`); `null` on `Failed` and absent on `Expired or Not Found`.", + "properties": { + "status": { + "type": "string", + "enum": [ + "Approved", + "Declined", + "In Review", + "Not Finished", + "Expired" + ], + "description": "Final status of the email verification. Matches the top-level `status` for finalized checks." + }, + "email": { + "type": "string", + "format": "email", + "description": "The verified email address.", + "example": "alice@example.com" + }, + "is_breached": { + "type": "boolean", + "description": "`true` when the address appears in known data breaches (breach intelligence lookup run at finalization)." + }, + "breaches": { + "type": "array", + "description": "Up to 5 most recent known breaches containing this address. Empty array (or omitted) when `is_breached` is `false`.", + "items": { "type": "object", "properties": { - "request_id": { - "type": "string", - "format": "uuid", - "description": "Id of the business session created for this retrieval. The stored record (visible in the console) keeps updating if the registry resolves after the response." - }, - "registry_tier_requested": { - "type": "string", - "enum": [ - "basic", - "shareholders", - "ubo" - ], - "description": "The tier this endpoint sells: `basic` (Lite) for select, `shareholders`, `ubo`." - }, - "registry_tier_delivered": { - "type": "string", - "enum": [ - "basic", - "shareholders", - "ubo", - null - ], - "description": "The tier actually delivered and billed. Lower than requested when the country's registries hold no ownership data, when the registry has no ownership record for this company, or when the resolved report of a `ubo` request names no natural person as a beneficial owner.", - "nullable": true - }, - "kyb_registry": { - "type": "object", - "description": "Full registry company profile.", - "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Didit's id for this company record." - }, - "node_id": { - "type": "string", - "example": "feature_kyb_registry" - }, - "status": { - "type": "string", - "enum": [ - "Not Finished", - "Approved", - "Declined", - "In Review" - ], - "description": "Didit's assessment of the registry check (e.g. `In Review` when the company is not active in the registry or ownership data could not be derived)." - }, - "registry_status": { - "type": "string", - "nullable": true, - "enum": [ - "active", - "dissolved", - "deregistered", - "see full details", - "authorised", - "appointed representative", - "unauthorised", - "inactive", - "no longer authorised", - "closed", - "struck off", - null - ], - "description": "Normalized company status as reported by the registry." - }, - "data_resolved": { - "type": "boolean", - "description": "`true` once the registry returned the full profile. `false` when the registry was still processing after polling — the stored record completes automatically once the registry resolves." - }, - "company_name": { - "type": "string", - "example": "Tesco PLC" - }, - "registration_number": { - "type": "string", - "nullable": true, - "example": "00445790" - }, - "country_code": { - "type": "string", - "description": "ISO 3166-1 alpha-2.", - "example": "GB" - }, - "region": { - "type": "string", - "nullable": true, - "description": "Subdivision code when the registry is regional (e.g. `CA` for `US-CA`); empty otherwise." - }, - "company_type": { - "type": "string", - "nullable": true, - "example": "Public Limited Company" - }, - "incorporation_date": { - "type": "string", - "format": "date", - "nullable": true, - "example": "1947-11-27" - }, - "registered_address": { - "type": "string", - "nullable": true, - "example": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA" - }, - "tax_number": { - "type": "string", - "nullable": true, - "example": "GB412 8925 93" - }, - "risk_level": { - "type": "string", - "nullable": true, - "description": "Registry-reported risk level, when available." - }, - "verification_status": { - "type": "string", - "nullable": true, - "description": "Registry verification status, when available." - }, - "is_from_registry": { - "type": "boolean", - "description": "Always `true` for this endpoint — the profile was retrieved from an official registry." - }, - "fetch_status": { - "type": "string", - "enum": [ - "pending", - "resolved" - ], - "description": "Registry fetch lifecycle. Mirrored by `data_resolved`." - }, - "alternative_names": { - "type": "string", - "nullable": true, - "description": "Alternative, trading, former or original-language company names as text, when reported." - }, - "nature_of_business": { - "type": "string", - "nullable": true, - "description": "Activity description or classification code, when reported." - }, - "registered_capital": { - "type": "string", - "nullable": true - }, - "registered_capital_amount": { - "type": "string", - "nullable": true, - "description": "Decimal amount as a string, when reported." - }, - "registered_capital_currency": { - "type": "string", - "nullable": true, - "description": "ISO 4217 code, when reported." - }, - "website": { - "type": "string", - "nullable": true, - "example": "https://www.tesco.com/" - }, - "email": { - "type": "string", - "nullable": true, - "example": "customer.service@tesco.co.uk" - }, - "phone": { - "type": "string", - "nullable": true, - "example": "03301231688" - }, - "legal_entity_identifier": { - "type": "string", - "nullable": true, - "description": "LEI, when reported." - }, - "location_of_registration": { - "type": "string", - "nullable": true, - "description": "Registry/jurisdiction the company is registered with, when reported." - }, - "vat_number": { - "type": "string", - "nullable": true, - "description": "EU VAT number, when collected during the hosted registry step or edited afterwards. At select time this is typically not yet set — VAT and tax ID validation runs when the end user submits the registry step in the hosted flow." - }, - "vat_validation_status": { - "type": "string", - "enum": [ - "valid", - "invalid", - "format_valid", - "invalid_format", - "could_not_validate", - "not_applicable" - ], - "description": "Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session." - }, - "vat_validated_number": { - "type": "string", - "nullable": true, - "description": "The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran." - }, - "vat_validation_source": { - "type": "string", - "nullable": true, - "enum": [ - "eu_vies", - "pl_vat_white_list", - "no_brreg", - "ch_uid_register", - "br_receita_federal", - "co_rues", - "cr_hacienda", - "ec_sri", - "py_dnit", - "format_rules", - null - ], - "description": "The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran." - }, - "vat_validated_name": { - "type": "string", - "nullable": true, - "description": "Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." - }, - "vat_validated_address": { - "type": "string", - "nullable": true, - "description": "Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." - }, - "vat_checked_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "Timestamp of the check. `null` when no check ran." - }, - "financial_summary": { - "type": "object", - "nullable": true, - "description": "Registry-reported share/capital details (currency, total shares, share classes), when available." - }, - "officers": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string", - "example": "MELISSA BETHELL" - }, - "designation": { - "type": "string", - "nullable": true, - "description": "Raw registry designation (e.g. `Director`, `Secretary`)." - }, - "role": { - "type": "string", - "description": "Normalized role derived from the designation. Full 17-value model set; registry parsing on this endpoint typically emits director, non_executive_director, secretary, chairman, or company_officer.", - "enum": [ - "director", - "non_executive_director", - "secretary", - "chairman", - "ubo", - "shareholder", - "representative", - "founder", - "legal_advisor", - "authorized_signatory", - "trustee", - "beneficiary", - "company_officer", - "settlor", - "protector", - "investor", - "other" - ] - }, - "nationality": { - "type": "string", - "nullable": true - }, - "is_active": { - "type": "boolean", - "description": "`false` when the registry marks the appointment as resigned." - }, - "kyc_status": { - "type": "string", - "nullable": true, - "enum": [ - "Approved", - "Declined", - "Pending", - null - ], - "description": "Status of an associated KYC session, when one was started for this officer. `null` otherwise." - }, - "kyc_session_url": { - "type": "string", - "nullable": true, - "description": "Verification URL of the associated KYC session, when one exists." - }, - "first_name": { - "type": "string", - "nullable": true, - "description": "Given name when explicitly reported by the registry; not inferred from the full name." - }, - "last_name": { - "type": "string", - "nullable": true, - "description": "Family name when explicitly reported by the registry." - }, - "address": { - "type": "string", - "nullable": true, - "description": "Officer address reported by the registry." - }, - "appointment_date": { - "type": "string", - "nullable": true, - "description": "Date the officer was appointed.", - "format": "date" - }, - "termination_date": { - "type": "string", - "nullable": true, - "description": "Date the appointment ended, when reported.", - "format": "date" - }, - "date_of_birth": { - "type": "string", - "nullable": true, - "description": "Complete registry-reported birth date. Null when only a birth year or month is known.", - "format": "date" - }, - "date_of_birth_year": { - "type": "integer", - "nullable": true, - "minimum": 1, - "maximum": 9999, - "description": "Registry-reported birth year, including when the full date is unavailable." - }, - "date_of_birth_month": { - "type": "integer", - "nullable": true, - "minimum": 1, - "maximum": 12, - "description": "Registry-reported birth month. Null when only the year is known." - } - } - }, - "description": "Directors, secretaries, and other officers extracted from the registry." - }, - "beneficial_owners": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string" - }, - "first_name": { - "type": "string", - "nullable": true - }, - "last_name": { - "type": "string", - "nullable": true - }, - "entity_type": { - "type": "string", - "enum": [ - "person", - "company" - ], - "description": "Whether the owner is a natural person or a corporate entity." - }, - "roles": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Normalized roles (e.g. `ubo`, `shareholder`)." - }, - "ownership_min_shares": { - "type": "string", - "nullable": true, - "description": "Lower bound of the ownership band reported by the registry (e.g. `25%`)." - }, - "ownership_max_shares": { - "type": "string", - "nullable": true, - "description": "Upper bound of the ownership band reported by the registry." - }, - "is_active": { - "type": "boolean" - }, - "kyc_status": { - "type": "string", - "nullable": true, - "enum": [ - "Approved", - "Declined", - "Pending", - null - ] - }, - "kyc_session_url": { - "type": "string", - "nullable": true - }, - "effective_ownership_percent": { - "type": "number", - "nullable": true, - "description": "Parsed ownership percentage (max bound, falling back to min bound)." - }, - "company_name": { - "type": "string", - "nullable": true - }, - "registration_number": { - "type": "string", - "nullable": true - }, - "country_of_birth": { - "type": "string", - "nullable": true - }, - "email": { - "type": "string", - "nullable": true - }, - "nationality": { - "type": "string", - "nullable": true - }, - "designation": { - "type": "string", - "nullable": true - }, - "voting_min_shares": { - "type": "string", - "nullable": true - }, - "voting_max_shares": { - "type": "string", - "nullable": true - }, - "date_of_birth": { - "type": "string", - "nullable": true, - "description": "Complete birth date when reported.", - "format": "date" - }, - "appointment_date": { - "type": "string", - "nullable": true, - "description": "Appointment date when reported.", - "format": "date" - }, - "shareholding": { - "$ref": "#/components/schemas/KYBRegistryShareholding" - } - } - }, - "description": "Beneficial owners / persons with significant control extracted from the registry. Can be empty when the registry does not expose ownership." - }, - "addresses": { - "type": "array", - "nullable": true, - "description": "Every address the registry states, one canonical shape. Null when it states none.", - "items": { - "$ref": "#/components/schemas/KYBRegistryAddress" - } - }, - "industries": { - "type": "array", - "nullable": true, - "description": "The activities the registry classifies the company under, one canonical shape. Null when it states none.", - "items": { - "$ref": "#/components/schemas/KYBRegistryIndustry" - } - }, - "accounts": { - "type": "object", - "nullable": true, - "additionalProperties": true, - "description": "Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts." + "name": { + "type": "string", + "description": "Breach identifier." + }, + "domain": { + "type": "string", + "description": "Domain of the breached service." + }, + "breach_date": { + "type": "string", + "format": "date", + "description": "Date the breach occurred." + }, + "breach_emails_count": { + "type": "integer", + "description": "Total number of accounts exposed in the breach." + }, + "description": { + "type": "string", + "description": "Human-readable description of the breach (may contain HTML markup)." + }, + "logo_path": { + "type": "string", + "description": "URL of the breached service's logo." + }, + "data_classes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Categories of data exposed, in snake_case (e.g., `email_addresses`, `passwords`)." + }, + "is_verified": { + "type": "boolean", + "description": "Whether the breach has been verified as legitimate." + } + } + } + }, + "is_disposable": { + "type": "boolean", + "description": "`true` when the domain belongs to a known disposable/temporary-mail provider." + }, + "is_undeliverable": { + "type": "boolean", + "description": "`true` when the address cannot receive mail (failed syntax or DNS/MX validation, or the message bounced)." + }, + "verification_attempts": { + "type": "integer", + "description": "Number of OTP sends for this verification (1, or 2 after a retry)." + }, + "verified_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the correct code was entered. `null` if the verification was never approved." + }, + "warnings": { + "type": "array", + "description": "Risk warnings recorded during the verification (duplicate, breached, disposable, undeliverable, attempts exceeded, blocklist, etc.). Empty array when there are none.", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "description": "Feature that produced the warning. Always `EMAIL` here." + }, + "risk": { + "type": "string", + "description": "Machine-readable risk code." + }, + "additional_data": { + "type": "object", + "nullable": true, + "description": "Risk-specific context (e.g., the duplicated session id for duplicate risks). `null` when the risk carries no extra data." + }, + "log_type": { + "type": "string", + "enum": [ + "error", + "warning", + "information" + ], + "description": "Severity. `error` risks decline the verification, `warning` flags it for review, `information` is recorded without affecting the status." + }, + "short_description": { + "type": "string", + "description": "Human-readable one-line summary of the risk." + }, + "long_description": { + "type": "string", + "description": "Human-readable explanation of the risk." + } + } + } + }, + "lifecycle": { + "type": "array", + "description": "Chronological audit trail of the verification. Event `type` is one of `EMAIL_VERIFICATION_MESSAGE_SENT`, `EMAIL_VERIFICATION_RETRY_MESSAGE_SENT`, `VALID_CODE_ENTERED`, `INVALID_CODE_ENTERED`, `EMAIL_VERIFICATION_APPROVED`, `EMAIL_VERIFICATION_DECLINED`, `EMAIL_VERIFICATION_IN_REVIEW`, `EMAIL_VERIFICATION_EXPIRED`.", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Event type." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "When the event happened." + }, + "details": { + "type": "object", + "nullable": true, + "description": "Event-specific payload. Send events: `{status, reason}`. Code-entry events: `{code_tried, status}`. Final-status events: `null`, or `{reason}` when declined or in review." + }, + "fee": { + "type": "number", + "description": "USD amount billed for this event. Non-zero only on the first successful send; 0 for retries, code entries, and status events." + } + } + } + }, + "matches": { + "type": "array", + "description": "Other sessions of your application (KYC, KYB, or API) where the same email address was used by a different user (different `vendor_data`). Up to 5 entries; a synthetic list-entry blocklist match is prepended when present (and only when no session-derived blocklisted email exists) — session-derived matches keep created_at order. Empty array when there are no matches.", + "items": { + "type": "object", + "properties": { + "session_id": { + "type": "string", + "format": "uuid", + "description": "Session id of the other verification that used this email address." + }, + "session_number": { + "type": "integer", + "description": "Human-friendly sequential number of that session." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "`vendor_data` of that session." + }, + "verification_date": { + "type": "string", + "description": "When that session was created (`YYYY-MM-DDTHH:MM:SSZ`)." + }, + "email": { + "type": "string", + "description": "The matched email address." + }, + "status": { + "type": "string", + "description": "Status of that session (e.g., `Approved`, `Declined`, `In Progress`)." + }, + "is_blocklisted": { + "type": "boolean", + "description": "Whether the email address is on your application's blocklist." + }, + "api_service": { + "type": "string", + "nullable": true, + "description": "For API-type sessions, the standalone API that created it (e.g., `PHONE_VERIFICATION`); `null` for regular verification sessions." + }, + "source": { + "type": "string", + "description": "Where the match was found. Usually `session`; a blocklist hit via an organization list entry prepends a synthetic match with `source: \"list_entry\"` and null `session_id`/`session_number`." + } + } + } + }, + "email_social": { + "$ref": "#/components/schemas/EmailSocialFootprint" + } + } + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "`vendor_data` of the matched verification's session. `null` on `Expired or Not Found`." + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "`metadata` of the matched verification's session. `null` on `Expired or Not Found`." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of this check response." + } + } + }, + "WorkflowListItem": { + "type": "object", + "description": "One workflow version in the list view.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Per-version UUID. Use as `{settings_uuid}` for get/update/delete, and as `workflow_id` when creating sessions." + }, + "workflow_id": { + "type": "string", + "format": "uuid", + "description": "Stable group ID shared by every version of the same workflow. Equals `uuid` for version 1." + }, + "workflow_label": { + "type": "string", + "description": "Display name for the workflow." + }, + "workflow_type": { + "type": "string", + "nullable": true, + "description": "Base type for simple workflows: `kyc`, `kyb`, `adaptive_age_verification`, `biometric_authentication`, etc. `null` for graph-based workflows without a KYB feature." + }, + "is_default": { + "type": "boolean", + "description": "Whether this is the default workflow for new sessions." + }, + "is_archived": { + "type": "boolean" + }, + "is_white_label_enabled": { + "type": "boolean", + "description": "Whether white-label customization applies to sessions created with this workflow." + }, + "total_price": { + "type": "number", + "description": "Fixed price per verification in USD (JSON number, excludes variable-priced features such as DATABASE_VALIDATION and PHONE_VERIFICATION)." + }, + "min_price": { + "type": "number", + "description": "Cheapest fixed price across workflow-graph paths, in USD." + }, + "max_price": { + "type": "number", + "description": "Most expensive fixed price across workflow-graph paths, in USD." + }, + "features": { + "type": "string", + "description": "Enabled features as a single `\" + \"`-joined string, e.g. `\"OCR + LIVENESS + FACE_MATCH\"`. Split on `\" + \"` to get a list." + }, + "is_simple_workflow": { + "type": "boolean", + "description": "True when the workflow has no custom workflow graph." + }, + "is_editable": { + "type": "boolean", + "description": "True when this version is a `draft` (drafts are editable in place; published versions are immutable in the Console)." + }, + "workflow_url": { + "type": "string", + "format": "uri", + "description": "Public shareable verification URL for this workflow (`https://verify.didit.me/u/`)." + }, + "max_retry_attempts": { + "type": "integer", + "description": "Maximum retry attempts allowed after a declined verification (0-10)." + }, + "retry_window_days": { + "type": "integer", + "nullable": true, + "description": "Rolling window in days used to count retries (1-365). `null` = all-time limit." + }, + "session_expiration_time": { + "type": "integer", + "description": "Seconds before an unfinished session expires (3600-2419200)." + }, + "version": { + "type": "integer", + "description": "Version number within the `workflow_id` group, starting at 1." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "published" + ], + "description": "Version status. New sessions use the latest `published` version of a group." + }, + "has_draft": { + "type": "boolean", + "description": "True when any version in this `workflow_id` group is currently a draft." + }, + "created_by": { + "type": "object", + "nullable": true, + "description": "Who created this version: `{name, email, type}` where `type` is `user` (Console user, `email` set) or `api_key` (`name` is the client id, `email` is null). `null` when no creator was recorded.", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string", + "nullable": true + }, + "type": { + "type": "string", + "enum": [ + "user", + "api_key" + ] + } + } + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + }, + "SimpleWorkflowRequest": { + "type": "object", + "description": "Create a simple linear workflow. The API converts this feature list into a node-based workflow graph internally. STRICT field whitelist: any key not listed here (including `workflow_type`) is rejected with a 400.", + "required": [ + "features" + ], + "properties": { + "workflow_label": { + "type": "string", + "maxLength": 50, + "default": "ID Verification", + "description": "Display name for the workflow (max 50 characters). Defaults to `\"ID Verification\"` when omitted — always set it so workflows stay distinguishable.", + "example": "Standard KYC" + }, + "is_default": { + "type": "boolean", + "description": "Set this workflow as the default for new sessions." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "published" + ], + "description": "Omit this field to create a published workflow ready for sessions. Use `draft` only when you want to save without publishing." + }, + "features": { + "type": "array", + "minItems": 1, + "description": "Verification features in execution order. The API links each item to the next one and adds one final status node automatically.", + "items": { + "$ref": "#/components/schemas/SimpleWorkflowFeature" + } + }, + "is_white_label_enabled": { + "type": "boolean", + "description": "Enable white-label customization for sessions created with this workflow.", + "default": false + }, + "is_desktop_allowed": { + "type": "boolean", + "description": "Allow the verification flow to run on desktop browsers.", + "default": false + }, + "max_retry_attempts": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "default": 3, + "description": "Maximum retry attempts allowed after a declined verification. `0` blocks the user after the first decline." + }, + "retry_window_days": { + "type": "integer", + "minimum": 1, + "maximum": 365, + "nullable": true, + "default": 7, + "description": "Rolling window in days used to count retries. `null` enforces an all-time limit." + }, + "face_liveness_max_attempts": { + "type": "integer", + "minimum": 1, + "maximum": 3, + "default": 3, + "description": "Maximum liveness submissions per session. Default 3: one initial attempt plus two retries. This value is also copied into generated liveness and age-estimation workflow nodes unless the feature-level config overrides it." + }, + "face_match_max_attempts": { + "type": "integer", + "minimum": 1, + "maximum": 3, + "default": 3, + "description": "Maximum face-match submissions per session. Default 3: one initial attempt plus two retries. This value is also copied into generated face-match workflow nodes unless the feature-level config overrides it." + }, + "session_expiration_time": { + "type": "integer", + "minimum": 3600, + "maximum": 2419200, + "default": 604800, + "description": "Seconds before an unfinished session expires. Minimum 1 hour (3600), maximum 4 weeks (2419200)." + } + } + }, + "SimpleWorkflowUpdateRequest": { + "type": "object", + "description": "Update workflow metadata or replace the full linear feature list. STRICT field whitelist: any key not listed here is rejected with a 400.", + "properties": { + "workflow_label": { + "type": "string", + "maxLength": 50, + "description": "Display name for the workflow (max 50 characters)." + }, + "is_default": { + "type": "boolean", + "description": "Set this workflow as the default for new sessions." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "published" + ], + "description": "Controls versioning. Omit to update the current version in place (drafts auto-publish). `published` on an already-published version creates and returns a NEW version. `draft` is only valid while the version is still a draft." + }, + "features": { + "type": "array", + "minItems": 1, + "description": "If provided, this replaces the full feature order and configuration.", + "items": { + "$ref": "#/components/schemas/SimpleWorkflowFeature" + } + }, + "is_white_label_enabled": { + "type": "boolean", + "description": "Enable white-label customization for sessions created with this workflow." + }, + "is_desktop_allowed": { + "type": "boolean", + "description": "Allow the verification flow to run on desktop browsers." + }, + "max_retry_attempts": { + "type": "integer", + "minimum": 0, + "maximum": 10, + "description": "Maximum retry attempts allowed after a declined verification. `0` blocks the user after the first decline." + }, + "retry_window_days": { + "type": "integer", + "minimum": 1, + "maximum": 365, + "nullable": true, + "description": "Rolling window in days used to count retries. `null` enforces an all-time limit." + }, + "face_liveness_max_attempts": { + "type": "integer", + "minimum": 1, + "maximum": 3, + "description": "Maximum liveness submissions per session. This value is also copied into generated liveness and age-estimation workflow nodes unless the feature-level config overrides it." + }, + "face_match_max_attempts": { + "type": "integer", + "minimum": 1, + "maximum": 3, + "description": "Maximum face-match submissions per session. This value is also copied into generated face-match workflow nodes unless the feature-level config overrides it." + }, + "session_expiration_time": { + "type": "integer", + "minimum": 3600, + "maximum": 2419200, + "description": "Seconds before an unfinished session expires. Minimum 1 hour (3600), maximum 4 weeks (2419200)." + } + } + }, + "SimpleWorkflowFeature": { + "type": "object", + "required": [ + "feature" + ], + "properties": { + "feature": { + "type": "string", + "description": "Feature to run. Use the uppercase values shown here.", + "enum": [ + "OCR", + "NFC", + "LIVENESS", + "FACE_MATCH", + "PROOF_OF_ADDRESS", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "PHONE_VERIFICATION", + "EMAIL_VERIFICATION", + "DATABASE_VALIDATION", + "AML", + "IP_ANALYSIS", + "AGE_ESTIMATION", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + "SIGNATURE" + ] + }, + "config": { + "type": "object", + "description": "Feature-specific settings. For `QUESTIONNAIRE`, include `questionnaire_uuid` with the `questionnaire_id` returned by the questionnaire create endpoint. For `OCR`, `methods` selects which ID Verification methods each country may use (document capture, non-doc lookup against a government or other authoritative source, and digital identity wallets); omit it and every country is document capture only. For `SIGNATURE`, see [Electronic Signature settings](/management-api/workflows/feature-configs#electronic-signature): `signature_level` accepts `aes` only for now, and workflow documents and their fields are managed in the console, so through the API send documents and fields per session with `signature_documents_source: \"session\"`.", + "additionalProperties": true + }, + "label": { + "type": "string", + "description": "Optional internal label for this workflow node." + } + } + }, + "ReviewItem": { + "type": "object", + "description": "One entry in a session's review / activity feed. Status changes carry `previous_status`, `new_status`, `previous_value`, `new_value`, and `changed_fields`; comments carry `comment` and (for Console comments) `mentioned_emails`; the remaining fields are `null` or empty when they do not apply to the activity type.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Unique identifier of this feed entry.", + "example": "de0561bc-5583-4409-9171-1a31a4c8a5f4" + }, + "activity_type": { + "type": "string", + "enum": [ + "COMMENT", + "STATUS_UPDATED", + "KYC_DATA_UPDATED", + "POA_DATA_UPDATED", + "FILE_ADDED", + "FILE_REMOVED", + "AML_ONGOING_MATCH", + "AML_STATUS_UPDATED", + "AML_HIT_STATUS_UPDATED", + "KYB_DATA_UPDATED", + "KYB_DOCUMENT_UPLOADED", + "TAG_ADDED", + "TAG_REMOVED", + "FEATURE_OVERRIDE_PENDING", + "FEATURE_OVERRIDE_APPLIED", + "FEATURE_OVERRIDE_CANCELLED" + ], + "description": "What kind of activity this entry records. `STATUS_UPDATED` rows come from manual decisions (`PATCH /v3/session/{session_id}/update-status/`) and from entries added via `POST …/reviews/` (which default to this type even when no status is recorded). `COMMENT` rows come from Console reviewers; Console detail-page views are also stored as `COMMENT` rows flagged with `metadata.system_event = \"DETAIL_VIEWED\"`. The remaining types track data edits, file changes, tags, and AML monitoring activity. STATUS_UPDATED rows are also written by system actors (e.g. AML ongoing monitoring with actor_type SYSTEM, and automatic feature-status updates), not only by the update-status endpoint and manual review posts. `FEATURE_OVERRIDE_PENDING`, `FEATURE_OVERRIDE_APPLIED` and `FEATURE_OVERRIDE_CANCELLED` record an override of one check's status (`PATCH /v3/session/{session_id}/features/{node_id}/update-status/`): requested to wait for the flow to finish, applied, or cancelled. They carry the check's status in `previous_value` / `new_value` (`{\"status\": ...}`), leave `previous_status` and `new_status` `null`, and name the override in `metadata` (`feature_override_id`, `node_id`, `feature_model`, `forced`, `state`, `cancel_reason`). The entry for the request carries the reviewer's `comment`; an override applied when the flow finishes is recorded with `actor_type` `SYSTEM`.", + "example": "STATUS_UPDATED" + }, + "actor_display": { + "type": "string", + "description": "Human-readable author label, resolved in order: actor's full name → Console account identifier → local part of `actor_email` → `\"API Client\"` (for `API_KEY` actors) → `\"System\"` (for `SYSTEM` actors) → `\"Unknown\"`. Entries created via `POST …/reviews/` always display `\"Unknown\"` because that endpoint records no actor attribution.", + "example": "API Client" + }, + "actor_email": { + "type": "string", + "nullable": true, + "description": "Email of the Console user who performed the action. `null` for API-key and system actors.", + "example": null + }, + "actor_type": { + "type": "string", + "enum": [ + "API_KEY", + "CONSOLE_USER", + "SYSTEM" + ], + "description": "Who performed the action: an API key, a Console user, or Didit itself (`SYSTEM`, e.g. AML ongoing monitoring). Entries created via `POST …/reviews/` carry the default `CONSOLE_USER` regardless of the caller.", + "example": "API_KEY" + }, + "new_status": { + "type": "string", + "nullable": true, + "enum": [ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User" + ], + "description": "Session status recorded by this entry (for status updates). `null` for comments and data-change entries.", + "example": "Approved" + }, + "previous_status": { + "type": "string", + "nullable": true, + "enum": [ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User" + ], + "description": "Status the session had before the change. Populated on `STATUS_UPDATED` entries written by the update-status endpoint; `null` on entries added via `POST …/reviews/`. Also populated on system-written STATUS_UPDATED rows.", + "example": "In Review" + }, + "comment": { + "type": "string", + "nullable": true, + "description": "Free-text note attached to the entry, or `null`.", + "example": "Document re-checked manually; address matches." + }, + "mentioned_emails": { + "type": "array", + "items": { + "type": "string", + "format": "email" + }, + "description": "Console users `@email`-mentioned in the comment. Always empty for entries created via this API — mention processing is a Console-only feature.", + "example": [] + }, + "previous_value": { + "type": "object", + "nullable": true, + "description": "Snapshot of the changed data before the action, e.g. `{\"status\": \"In Review\"}` on a status change. `null` when nothing was changed.", + "example": { + "status": "In Review" + } + }, + "new_value": { + "type": "object", + "nullable": true, + "description": "Snapshot of the changed data after the action, e.g. `{\"status\": \"Approved\"}`. `null` when nothing was changed.", + "example": { + "status": "Approved" + } + }, + "changed_fields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Names of the fields modified by this activity (e.g. `[\"status\"]`). Empty for comments and for entries added via `POST …/reviews/`.", + "example": [ + "status" + ] + }, + "metadata": { + "type": "object", + "nullable": true, + "description": "Extra context for system-generated entries. Console detail-page views carry `{\"system_event\": \"DETAIL_VIEWED\", \"detail_type\": \"verification\"}` so they can be filtered out of notification logic. `null` for ordinary entries.", + "example": null + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the entry was written, ISO 8601 UTC.", + "example": "2026-06-12T00:19:31.918735Z" + } + } + }, + "QuestionnaireListItem": { + "type": "object", + "description": "One questionnaire version in the list view.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Per-version UUID. Use as `{questionnaire_uuid}` for get/update/delete and as `questionnaire_uuid` in a workflow's QUESTIONNAIRE feature config." + }, + "title": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + }, + "default_language": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "is_simple_questionnaire": { + "type": "boolean", + "description": "True for linear questionnaires created through this API; false for graph-based questionnaires built in the Console." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "question_types": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Uppercase element types of the answerable questions, in creation order (e.g. `SHORT_TEXT`, `MULTIPLE_CHOICE`, `FILE_UPLOAD`). Structural `SECTION_HEADER` and `SEPARATOR` elements are excluded. One entry per question, so values can repeat." + }, + "workflow_names": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Names of workflows using this questionnaire. Currently always `[]` on this endpoint (populated only in the Console listing)." + }, + "questionnaire_group_id": { + "type": "string", + "format": "uuid", + "description": "Stable identifier that groups all versions of the same questionnaire." + }, + "version": { + "type": "integer", + "description": "Version number within the group, starting at 1." + }, + "status": { + "type": "string", + "enum": [ + "draft", + "published" + ] + }, + "published_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When this version was published. `null` for drafts." + }, + "is_editable": { + "type": "boolean", + "description": "True when this version is a `draft`." + } + } + }, + "SimpleQuestionnaireRequest": { + "type": "object", + "description": "Create a simple linear questionnaire. The API converts `form_elements` into the internal questionnaire graph.", + "required": [ + "title", + "languages", + "form_elements" + ], + "properties": { + "title": { + "type": "string", + "description": "Internal name for the questionnaire." + }, + "description": { + "type": "string", + "nullable": true + }, + "languages": { + "type": "array", + "description": "Languages included in every label. Must include `en`.", + "items": { + "type": "string" + }, + "example": [ + "en" + ] + }, + "default_language": { + "type": "string", + "description": "Default language. Must be included in `languages`.", + "example": "en" + }, + "is_active": { + "type": "boolean", + "default": true + }, + "status": { + "type": "string", + "enum": [ + "draft", + "published" + ], + "description": "Omit this field to create a published questionnaire ready for workflows." + }, + "form_elements": { + "type": "array", + "minItems": 1, + "description": "Questions in the exact order users should see them. Branching and conditional fields are not supported.", + "items": { + "$ref": "#/components/schemas/SimpleQuestionnaireElement" + } + } + } + }, + "SimpleQuestionnaireUpdateRequest": { + "type": "object", + "description": "Update questionnaire metadata or replace the full linear `form_elements` list.", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + }, + "default_language": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "status": { + "type": "string", + "enum": [ + "draft", + "published" + ] + }, + "form_elements": { + "type": "array", + "minItems": 1, + "description": "If provided, this replaces the full linear question list.", + "items": { + "$ref": "#/components/schemas/SimpleQuestionnaireElement" + } + } + } + }, + "SimpleQuestionnaireElement": { + "type": "object", + "required": [ + "element_type", + "label" + ], + "properties": { + "id": { + "type": "string", + "description": "Optional stable question id. If omitted, the API generates `q1`, `q2`, etc. Use lowercase letters, numbers, and underscores for readability.", + "example": "source_of_funds" + }, + "element_type": { + "type": "string", + "description": "Question type. The API accepts these lowercase values and their uppercase equivalents.", + "enum": [ + "short_text", + "long_text", + "dropdown", + "single_choice", + "multiple_choice", + "number", + "image", + "file_upload", + "time", + "email", + "address", + "phone", + "country", + "date_picker", + "consent", + "section_header", + "separator", + "heading", + "paragraph" + ] + }, + "label": { + "type": "object", + "description": "Question label by language code, for example `{ \"en\": \"Source of funds\" }`.", + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "object", + "nullable": true, + "additionalProperties": { + "type": "string" + } + }, + "placeholder": { + "type": "object", + "nullable": true, + "additionalProperties": { + "type": "string" + } + }, + "is_required": { + "type": "boolean", + "default": false + }, + "options": { + "type": "array", + "description": "Use for `dropdown`, `single_choice`, and `multiple_choice`. If `value` is omitted, the API generates one from the option label.", + "items": { + "type": "object", + "required": [ + "label" + ], + "properties": { + "value": { + "type": "string" + }, + "label": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "max_files": { + "type": "integer", + "minimum": 1, + "maximum": 5, + "description": "Use with `file_upload` or `image`." + }, + "validation": { + "type": "object", + "nullable": true, + "description": "Optional format validation for `short_text`, `long_text`, or `number`. Enforced while the user fills the field (they cannot proceed until it passes) and re-checked by the API on submission.", + "properties": { + "pattern": { + "type": "string", + "description": "Regular expression (ECMA-262 subset) the value must fully match. For `short_text` and `long_text`. Do not add ^/$ anchors; the whole value is matched." + }, + "min_length": { + "type": "integer", + "minimum": 0, + "description": "Minimum number of characters. For `short_text` and `long_text`." + }, + "max_length": { + "type": "integer", + "minimum": 0, + "description": "Maximum number of characters. For `short_text` and `long_text`." + }, + "min": { + "type": "number", + "description": "Minimum numeric value. For `number`." + }, + "max": { + "type": "number", + "description": "Maximum numeric value. For `number`." + }, + "error_message": { + "type": "object", + "description": "Translated message shown when validation fails, keyed by language code.", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "QuestionnaireDetail": { + "type": "object", + "description": "Full questionnaire detail returned from GET, POST (create) and PATCH (update) endpoints.", + "properties": { + "questionnaire_id": { + "type": "string", + "format": "uuid", + "description": "Unique identifier of the questionnaire. Use this id when referencing the questionnaire from a workflow or in subsequent update/delete requests." + }, + "title": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + }, + "default_language": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "is_simple_questionnaire": { + "type": "boolean" + }, + "graph": { + "type": "object", + "description": "Graph structure with start_node and nodes map." + }, + "sections": { + "type": "array", + "description": "Questionnaire content grouped into sections (derived from the graph).", + "items": { + "type": "object" + } + }, + "questionnaire_group_id": { + "type": "string", + "format": "uuid", + "description": "Stable identifier that groups all versions of the same questionnaire." + }, + "version": { + "type": "integer" + }, + "status": { + "type": "string", + "enum": [ + "draft", + "published" + ] + }, + "published_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "is_editable": { + "type": "boolean", + "description": "True when the questionnaire can still be edited in place (draft versions)." + } + } + }, + "UserListItem": { + "type": "object", + "description": "A verified user.", + "properties": { + "didit_internal_id": { + "type": "string", + "format": "uuid", + "description": "Didit's stable internal UUID for this user." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your unique identifier for this user (passed when creating sessions). This can be null when no vendor identifier was supplied." + }, + "display_name": { + "type": "string", + "nullable": true, + "description": "Custom display name set by you" + }, + "full_name": { + "type": "string", + "nullable": true, + "description": "Full name extracted from verified documents" + }, + "date_of_birth": { + "type": "string", + "format": "date", + "nullable": true + }, + "effective_name": { + "type": "string", + "nullable": true, + "description": "Best available name: display_name if set, otherwise full_name" + }, + "status": { + "type": "string", + "enum": [ + "ACTIVE", + "FLAGGED", + "BLOCKED" + ], + "description": "Lifecycle status of the user record (NOT a session status). `ACTIVE` is the default, `FLAGGED` marks the user for manual attention, `BLOCKED` prevents new sessions for this `vendor_data`." + }, + "portrait_image_url": { + "type": "string", + "nullable": true, + "description": "Presigned URL of the user's portrait photo (expires after a few hours)" + }, + "session_count": { + "type": "integer", + "description": "Total number of verification sessions for this user" + }, + "approved_count": { + "type": "integer", + "description": "Number of approved sessions" + }, + "declined_count": { + "type": "integer", + "description": "Number of declined sessions" + }, + "in_review_count": { + "type": "integer", + "description": "Number of sessions in review" + }, + "issuing_states": { + "type": "array", + "items": { + "type": "string" + }, + "description": "ISO 3166-1 alpha-3 codes of issuing countries seen on this user's approved ID documents, e.g. `[\"USA\", \"ESP\"]`. Empty array when none." + }, + "approved_emails": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Verified email addresses collected from this user's approved sessions, e.g. `[\"john@example.com\"]`." + }, + "approved_phones": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Verified phone numbers collected from this user's approved sessions, e.g. `[\"+14155551234\"]`." + }, + "features": { + "type": "object", + "description": "Aggregated per-feature status across all of this user's sessions. Possible keys: `ID_VERIFICATION`, `NFC`, `LIVENESS`, `FACE_MATCH`, `POA`, `QUESTIONNAIRE`, `EMAIL_VERIFICATION`, `PHONE`, `AML`, `IP_ANALYSIS`, `AGE_ESTIMATION`, `DATABASE_VALIDATION`. Possible values: `Approved`, `Declined`, `In Review`, `Not Finished`, `Resub Requested`." + }, + "features_list": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "description": "Same data as `features`, as an ordered array of `{feature, status}` objects." + }, + "last_session_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Timestamp of the most recent session" + }, + "first_session_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Timestamp of the first session" + }, + "last_activity_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Timestamp of the most recent activity on this user (session, transaction, status change, data edit, etc.)." + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string" + } + } + }, + "description": "Tags assigned to this user" + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "BlocklistItem": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "face", + "document", + "phone", + "email" + ] + }, + "session_id": { + "type": "string", + "format": "uuid" + }, + "session_number": { + "type": "integer" + }, + "blocklisted_at": { + "type": "string", + "format": "date-time" + }, + "face_image": { + "type": "string", + "nullable": true + }, + "document_type": { + "type": "string", + "nullable": true + }, + "document_number": { + "type": "string", + "nullable": true + }, + "phone_number": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "nullable": true + } + } + }, + "UserDetailItem": { + "type": "object", + "description": "Full user detail. Extends UserListItem with metadata, comments, and updated_at.", + "allOf": [ + { + "$ref": "#/components/schemas/UserListItem" + }, + { + "type": "object", + "properties": { + "metadata": { + "type": "object", + "description": "Custom metadata JSON you attached to this user. Defaults to `{}`." + }, + "tags": { + "type": "array", + "description": "Tag assignments. NOTE: on detail responses each entry is a tag *link* object (`{uuid, tag: {...}, added_by_email, added_by_name, created_at}`), unlike the flat `{uuid, name, color}` shape used on list responses.", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "UUID of the tag assignment (not the tag itself)." + }, + "tag": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" }, - "registry_data": { - "type": "object", - "description": "Raw record returned by the registry source, kept for reviewers. Its shape differs by country and source and is not a stable contract: read the canonical fields (`addresses`, `industries`, `ownership`, `beneficial_owners[].shareholding`) instead." + "name": { + "type": "string" }, - "user_provided_data": { - "type": "object", - "nullable": true, - "description": "Always `null` on this endpoint (used by the hosted KYB flow when end users edit registry data)." + "color": { + "type": "string" }, - "confirmed_by_user_at": { + "description": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "Always `null` on this endpoint." + "nullable": true }, - "last_console_edit_at": { + "source": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "Set when a console reviewer edits the record later." + "enum": [ + "didit", + "custom" + ] }, - "is_editable": { - "type": "boolean", - "description": "Whether the registry-sourced data can still be edited in the hosted flow/console." + "created_at": { + "type": "string", + "format": "date-time" }, - "registry_tier_delivered": { + "updated_at": { + "type": "string", + "format": "date-time" + } + } + }, + "added_by_email": { + "type": "string", + "nullable": true + }, + "added_by_name": { + "type": "string", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + } + }, + "comments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "comment_type": { + "type": "string", + "enum": [ + "COMMENT", + "STATUS_CHANGED", + "METADATA_UPDATED", + "TAG_ADDED", + "TAG_REMOVED", + "PROFILE_UPDATED" + ] + }, + "comment": { + "type": "string", + "nullable": true + }, + "actor_email": { + "type": "string", + "nullable": true + }, + "actor_name": { + "type": "string", + "nullable": true + }, + "previous_status": { + "type": "string", + "nullable": true, + "enum": [ + "ACTIVE", + "FLAGGED", + "BLOCKED", + null + ] + }, + "new_status": { + "type": "string", + "nullable": true, + "enum": [ + "ACTIVE", + "FLAGGED", + "BLOCKED", + null + ] + }, + "previous_value": { + "type": "object", + "nullable": true, + "description": "Previous field values for PROFILE_UPDATED entries." + }, + "new_value": { + "type": "object", + "nullable": true, + "description": "New field values for PROFILE_UPDATED entries." + }, + "changed_fields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Names of the fields changed by a PROFILE_UPDATED entry." + }, + "metadata": { + "type": "object", + "nullable": true + }, + "mentioned_emails": { + "type": "array", + "items": { + "type": "string" + } + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "description": "Activity log and comments for this user (status changes, profile edits, manual notes)." + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + } + ] + }, + "BusinessListItem": { + "type": "object", + "description": "A verified business.", + "properties": { + "didit_internal_id": { + "type": "string", + "format": "uuid", + "description": "Didit's stable internal UUID for this business." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your unique identifier for this business (passed when creating sessions). This can be null when no vendor identifier was supplied." + }, + "display_name": { + "type": "string", + "nullable": true, + "description": "Custom display name set by you" + }, + "legal_name": { + "type": "string", + "nullable": true, + "description": "Official legal name from registry or manual entry" + }, + "registration_number": { + "type": "string", + "nullable": true, + "description": "Company registration or incorporation number" + }, + "country_code": { + "type": "string", + "nullable": true, + "description": "Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `US`)." + }, + "effective_name": { + "type": "string", + "nullable": true, + "description": "Best available name: display_name if set, otherwise legal_name" + }, + "status": { + "type": "string", + "enum": [ + "ACTIVE", + "FLAGGED", + "BLOCKED" + ], + "description": "Lifecycle status of the business record (NOT a session status). `ACTIVE` is the default, `FLAGGED` marks it for manual attention, `BLOCKED` prevents new sessions for this `vendor_data`." + }, + "session_count": { + "type": "integer", + "description": "Total number of verification sessions for this business" + }, + "approved_count": { + "type": "integer", + "description": "Number of approved sessions" + }, + "declined_count": { + "type": "integer", + "description": "Number of declined sessions" + }, + "in_review_count": { + "type": "integer", + "description": "Number of sessions in review" + }, + "features": { + "type": "object", + "description": "Aggregated per-feature status across all of this business's KYB sessions. Currently the only key is `KYB` (the registry company check status). Possible values: `Approved`, `Declined`, `In Review`, `Not Finished`, `Resub Requested`." + }, + "features_list": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "description": "Same data as `features`, as an ordered array of `{feature, status}` objects." + }, + "last_session_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Timestamp of the most recent session" + }, + "first_session_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Timestamp of the first session" + }, + "last_activity_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Timestamp of the most recent activity (status change, session update, etc.)" + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string" + } + } + }, + "description": "Tags assigned to this business" + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "BusinessDetailItem": { + "type": "object", + "description": "Full business detail. Extends BusinessListItem with metadata and updated_at.", + "allOf": [ + { + "$ref": "#/components/schemas/BusinessListItem" + }, + { + "type": "object", + "properties": { + "metadata": { + "type": "object", + "description": "Custom metadata JSON you attached to this business. Defaults to `{}`." + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + } + ] + }, + "TransactionDetail": { + "type": "object", + "description": "Full monitoring record for one transaction, as returned by `GET /v3/transactions/{transaction_id}/` and `POST /v3/transactions/`.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "Didit-stable transaction identifier. Use as `{transaction_id}` for follow-up calls." + }, + "environment": { + "type": "string", + "enum": [ + "live", + "sandbox" + ], + "description": "Environment of the application that owns the transaction. A `sandbox` transaction is stored but never evaluated, screened, notified or billed, so its empty `rule_runs` means \"not evaluated\"." + }, + "transaction_number": { + "type": "integer", + "description": "Application-scoped sequential transaction number shown in Console (e.g. `4123`)." + }, + "txn_id": { + "type": "string", + "description": "The `transaction_id` you supplied at create time (max 128 chars). Unique per application." + }, + "txn_date": { + "type": "string", + "format": "date-time", + "description": "When the transaction occurred (from `transaction_at`; defaults to submission time)." + }, + "zone_id": { + "type": "string", + "nullable": true, + "description": "IANA time zone identifier provided at create time." + }, + "transaction_type": { + "type": "string", + "description": "Top-level category as stored: `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet`, `gamblingLimitChange`, `gamblingBonusChange`, `auditTrailEvent`. Note multi-word values are echoed back in camelCase even when submitted in snake_case." + }, + "action_type": { + "type": "string", + "description": "Sub-type within the category (e.g. `deposit`, `withdrawal`, `transfer`). Defaults to the category when not supplied." + }, + "direction": { + "type": "string", + "enum": [ + "INBOUND", + "OUTBOUND" + ], + "description": "Direction relative to the subject. Stored uppercase regardless of the casing submitted (`in`/`out`/`inbound`/`outbound` are accepted on input)." + }, + "status": { + "type": "string", + "enum": [ + "APPROVED", + "IN_REVIEW", + "DECLINED", + "AWAITING_USER" + ], + "description": "Current monitoring verdict. Transactions are created `APPROVED`; rules may flip them to `IN_REVIEW`/`DECLINED` synchronously." + }, + "source": { + "type": "string", + "enum": [ + "LIVE", + "IMPORTED" + ], + "description": "How the record originated: `LIVE` for transactions submitted through the API or SDK, `IMPORTED` for transactions loaded through the console import." + }, + "amount": { + "type": "string", + "description": "Transaction amount as a decimal string with trailing zeros stripped (e.g. `\"1500\"`, `\"0.5\"`, `\"0.123456789012345678\"`). Up to 18 decimal places." + }, + "currency": { + "type": "string", + "description": "Currency code of `amount`. Any ISO 4217 alphabetic code is accepted (e.g. `EUR`, `USD`, `NGN`), as is a crypto asset symbol (e.g. `BTC`). Prefer a currently-circulating code. Not every accepted code has an exchange rate: when none is available for the code (for example the ISO accounting unit `XSU`, or a crypto symbol Didit does not price), automatic conversion fails and `preferred_currency_amount` is returned null unless the request supplied a pre-converted `amount_in_default_currency`." + }, + "currency_type": { + "type": "string", + "nullable": true, + "description": "`fiat` or `crypto`, as submitted in `currency_kind`." + }, + "amount_in_default_currency": { + "type": "string", + "nullable": true, + "description": "Pre-converted amount you supplied, as a decimal string with trailing zeros stripped." + }, + "default_currency_code": { + "type": "string", + "nullable": true + }, + "preferred_currency_amount": { + "type": "string", + "nullable": true, + "description": "`amount` converted to the application's preferred currency, when available." + }, + "preferred_currency_code": { + "type": "string", + "nullable": true + }, + "payment_details": { + "type": "string", + "nullable": true, + "description": "Free-text payment reference or memo from the submission." + }, + "payment_txn_id": { + "type": "string", + "nullable": true, + "description": "External payment system reference (e.g. blockchain transaction hash) from `payment_reference_id`." + }, + "score": { + "type": "integer", + "description": "Risk score accumulated by rules (typically 0–100). Higher = riskier." + }, + "severity": { + "type": "string", + "nullable": true, + "enum": [ + "UNKNOWN", + "LOW", + "MEDIUM", + "HIGH", + "CRITICAL" + ], + "description": "Categorical risk severity set by rules/providers (`UNKNOWN`, `LOW`, `MEDIUM`, `HIGH`, `CRITICAL`). `null` until something sets it." + }, + "decision_reason_code": { + "type": "string", + "nullable": true, + "description": "Machine-readable reason for the current `status`." + }, + "decision_reason_label": { + "type": "string", + "nullable": true, + "description": "Human-readable label for `decision_reason_code`." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Convenience copy of the applicant's `vendor_data`." + }, + "metadata": { + "type": "object", + "description": "Snapshot of the `subject` and `counterparty` payloads as submitted at create time." + }, + "props": { + "type": "object", + "description": "The `custom_properties` you supplied at create time. Each key is addressable in rule conditions as `custom_values.`." + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "source": { + "type": "string", + "enum": [ + "didit", + "custom" + ], + "description": "Whether the tag is a Didit preset or a custom tag." + } + } + }, + "description": "Tags attached to the transaction (manually or by rules)." + }, + "parties": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "role": { + "type": "string", + "enum": [ + "APPLICANT", + "REMITTER", + "BENEFICIARY", + "COUNTERPARTY" + ], + "description": "Party role. The create endpoint assigns `APPLICANT` (the subject) and `COUNTERPARTY`." + }, + "entity_type": { + "type": "string", + "enum": [ + "individual", + "company", + "external", + "unhostedWallet" + ] + }, + "kind": { + "type": "string", + "enum": [ + "USER", + "BUSINESS", + "EXTERNAL" + ], + "description": "`USER`/`BUSINESS` parties link back to a Didit-owned entity via `vendor_data`. `EXTERNAL` parties have no Didit entity behind them." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your internal user/business identifier." + }, + "full_name": { + "type": "string", + "nullable": true + }, + "first_name": { + "type": "string", + "nullable": true + }, + "last_name": { + "type": "string", + "nullable": true + }, + "country_code": { + "type": "string", + "nullable": true, + "description": "Country code from the submitted `address.country` (typically ISO 3166-1 alpha-3)." + }, + "dob": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Date of birth from the submitted `date_of_birth`." + }, + "address": { + "type": "object", + "description": "Address object as submitted. `{}` when omitted." + }, + "institution_info": { + "type": "object", + "description": "Institution details as submitted. `{}` when omitted." + }, + "device": { + "type": "object", + "description": "Normalized device context, including server-side IP enrichment under network_context (geolocation, VPN/data-center flags) when an IP was provided.", + "properties": { + "fingerprint": { + "type": "string", + "description": "Unique device fingerprint." + }, + "persistent_device_id": { + "type": "string", + "description": "Stable cross-session device identifier (v2 fingerprint schema)." + }, + "user_agent": { + "type": "string", + "description": "Browser or app user agent string." + }, + "browser_family": { + "type": "string", + "description": "Browser family (e.g. Chrome, Firefox, Safari)." + }, + "browser_version": { + "type": "string", + "description": "Browser version string." + }, + "platform": { + "type": "string", + "description": "Platform identifier (web, ios, android, macos, windows, linux)." + }, + "os_family": { + "type": "string", + "description": "Operating system family (e.g. iOS, Android, Windows)." + }, + "os_version": { + "type": "string", + "description": "Operating system version string." + }, + "accept_language": { + "type": "string", + "description": "Accept-Language header value." + }, + "session_id": { + "type": "string", + "description": "Session identifier (stored in metadata)." + }, + "session_age_ms": { + "type": "number", + "description": "Session age in milliseconds." + }, + "network_context": { + "type": "object", + "description": "Network context with IP address and geo-location.", + "properties": { + "ip_address": { "type": "string", - "enum": [ - "basic", - "shareholders", - "ubo", - null - ], - "description": "Same as the top-level `registry_tier_delivered`.", - "nullable": true + "description": "Client IP address." }, - "registry_ownership_status": { + "ip_country_code": { "type": "string", - "nullable": true, - "enum": [ - "not_requested", - "delivered", - "unavailable", - "pending", - null - ], - "description": "`delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier." + "description": "Geo-resolved country of the IP (ISO 3166-1 alpha-3)." }, - "ownership": { - "$ref": "#/components/schemas/KYBRegistryOwnership" + "latitude": { + "type": "number", + "description": "Geographic latitude of the IP." }, - "registry_tier": { - "type": "string", - "nullable": true, - "description": "The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`." + "longitude": { + "type": "number", + "description": "Geographic longitude of the IP." } } + } + } + }, + "external_party_snapshot": { + "type": "object", + "nullable": true, + "description": "Frozen copy of the party data for `EXTERNAL` parties. `null` while the party is linked to a Didit user or business." + } + } + }, + "description": "Transaction parties (applicant, counterparty)." + }, + "payment_methods": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "payment_method_type": { + "type": "string", + "enum": [ + "bank_card", + "bank_account", + "e_wallet", + "crypto_wallet", + "unhosted_wallet", + "other" + ] + }, + "label": { + "type": "string", + "nullable": true + }, + "fingerprint": { + "type": "string", + "nullable": true, + "description": "SHA-256 over `type:account_id:issuing_country` — stable identifier to correlate reuse of the same instrument." + }, + "account_id": { + "type": "string", + "nullable": true, + "description": "Account identifier (IBAN, wallet address, card token, …)." + }, + "issuing_country": { + "type": "string", + "nullable": true + }, + "owner_kind": { + "type": "string", + "enum": [ + "USER", + "BUSINESS", + "EXTERNAL" + ] + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your identifier for the owning user/business." + }, + "owner_name": { + "type": "string", + "nullable": true + }, + "institution_info": { + "type": "object" + }, + "details": { + "type": "object", + "description": "Raw `payment_method` object as submitted." + }, + "external_owner_snapshot": { + "type": "object", + "nullable": true + }, + "role": { + "type": "string", + "enum": [ + "SOURCE", + "DESTINATION", + "FUNDING", + "BENEFICIARY" + ], + "description": "How this method was used in the transaction. The create endpoint assigns `SOURCE`/`DESTINATION` from the direction (outbound: subject = SOURCE; inbound: subject = DESTINATION)." + } + } + }, + "description": "Payment methods linked to the transaction." + }, + "documents": { + "type": "array", + "description": "Files attached to the transaction from the console. `[]` when none.", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "label": { + "type": "string", + "nullable": true + }, + "note": { + "type": "string", + "nullable": true + }, + "uploaded_by": { + "type": "string", + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "file_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Short-lived signed download URL; fetch it when needed rather than storing it." + }, + "original_filename": { + "type": "string", + "nullable": true + }, + "file_size": { + "type": "integer", + "nullable": true + } + } + } + } + } + } + }, + "activities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "activity_type": { + "type": "string", + "description": "E.g. `TRANSACTION_CREATED`, `TRANSACTION_NOTE_ADDED`, `TRANSACTION_STATUS_CHANGED`." + }, + "source": { + "type": "string" + }, + "status": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "metadata": { + "type": "object" + }, + "occurred_at": { + "type": "string", + "format": "date-time" + } + } + }, + "description": "Activity log entries (creation, notes, manual reviews, status changes)." + }, + "alerts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "severity": { + "type": "string", + "enum": [ + "UNKNOWN", + "LOW", + "MEDIUM", + "HIGH", + "CRITICAL" + ] + }, + "status": { + "type": "string", + "enum": [ + "OPEN", + "INVESTIGATING", + "AWAITING_USER", + "PENDING_SAR", + "SAR_FILED", + "RESOLVED", + "DISMISSED" + ] + }, + "source": { + "type": "string", + "enum": [ + "RULE", + "PROVIDER", + "MANUAL" + ] + }, + "metadata": { + "type": "object" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "due_at": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, + "description": "Alerts raised by rules and providers." + }, + "rule_runs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "rule": { + "type": "string", + "format": "uuid", + "description": "UUID of the rule that ran." + }, + "rule_title": { + "type": "string" + }, + "rule_description": { + "type": "string", + "nullable": true + }, + "rule_category": { + "type": "string" + }, + "matched": { + "type": "boolean", + "description": "Whether the rule's conditions matched this transaction. A run is recorded only for matched rules, so live evaluations always carry `true`." + }, + "is_test": { + "type": "boolean", + "description": "True when the rule ran in TEST mode (no effect on status/score)." + }, + "mode": { + "type": "string", + "enum": [ + "ACTIVE", + "DISABLED", + "TEST" + ] + }, + "severity": { + "type": "string", + "nullable": true + }, + "score_delta": { + "type": "integer", + "description": "Score added by this rule's `add_score` actions." + }, + "status_target": { + "type": "string", + "nullable": true, + "description": "Status set by a `change_status` action, if any." + }, + "action_summary": { + "type": "string", + "nullable": true, + "description": "Summary of the actions the rule executed, joined with `; ` (e.g. `score +30; change_status:IN_REVIEW; add_tag:high_risk_country`). Parts: `score +N`, `change_status:`, `add_tag:`, `add_note`, `add_to_list`, `open_case`. `null` when the rule executed no action." + }, + "metadata": { + "type": "object", + "description": "Evaluation detail: `scope_match`, `conditions[]` and `aggregations[]`. Each condition entry carries `field`, `operator`, `expected_value`, the `current_value` read from the transaction, `matched`, `group_index` and `group_logic`. Each aggregation entry (velocity / count / sum) carries `metric`, `field`, `operator`, `expected_value`, the `computed_value` the aggregate evaluated to, `window`, `filters`, `matched` and `sample_references`; deferred async aggregations have `computed_value: null` and `deferred: true`. A nested condition group is one `conditions[]` entry with `operator` `any_of` or `all_of`, `field` null, and its members' results under its own `conditions`." + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "description": "Per-rule execution results — which rule fired, with what score impact." + }, + "provider_results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "provider": { + "type": "string" + }, + "result_type": { + "type": "string", + "enum": [ + "FIAT_MONITORING", + "WALLET_SCREENING", + "TRANSACTION_SCREENING", + "TRAVEL_RULE", + "NETWORK_GRAPH", + "CUSTOM" + ] + }, + "status": { + "type": "string" + }, + "severity": { + "type": "string", + "nullable": true + }, + "score": { + "type": "integer" + }, + "summary": { + "type": "string", + "nullable": true + }, + "payload": { + "type": "object", + "description": "Raw provider response payload." + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "description": "Raw provider payloads (AML screening, blockchain analytics, etc.)." + }, + "travel_rule": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/TransactionTravelRule" + } + ], + "description": "Travel Rule transfer or check. Populated when `travel_rule_details` was submitted, and also when Travel Rule is enabled and the transaction was created with `transaction_category: travelRule` (Didit initiates the exchange itself, even without `travel_rule_details`). `null` when no transfer or check exists. On an inbound transfer, the counterparty's `originator_data` and `beneficiary_data` stay masked (`data_masked: true`) until wallet ownership is confirmed." + }, + "network_snapshot": { + "type": "object", + "nullable": true, + "description": "Graph snapshot of related transactions/parties used by the rules engine. Either the `network_snapshot` submitted with the transaction or the graph Didit builds from a successful AML wallet screening (which replaces a submitted one). `null` when neither exists.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "nodes": { + "type": "array", + "items": { + "type": "object" + } + }, + "edges": { + "type": "array", + "items": { + "type": "object" + } + }, + "metrics": { + "type": "object" + } + } + }, + "remediation": { + "type": "object", + "nullable": true, + "description": "Remediation session offered to the user when re-verification is required. Superseded by `action_required`, which is the canonical block; kept for backward compatibility.", + "properties": { + "session_id": { + "type": "string" + }, + "session_token": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "status": { + "type": "string" + } + } + }, + "action_required": { + "type": "object", + "nullable": true, + "description": "Pending end-user action required to complete this transaction, or null when there is none. The Didit SDKs auto-launch the action by default and refresh the transaction when it finishes. Two variants, discriminated by `type`: `verification_session` (a hosted verification session created by a rule action) and `wallet_ownership` (a wallet-ownership widget session auto-minted for a Travel Rule transfer that needs proof of wallet control, gated by the `auto_wallet_verification` Travel Rule setting). When both could apply, `wallet_ownership` takes precedence because it blocks the Travel Rule exchange.", + "oneOf": [ + { + "$ref": "#/components/schemas/TransactionVerificationSessionAction" + }, + { + "$ref": "#/components/schemas/TransactionWalletOwnershipAction" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "verification_session": "#/components/schemas/TransactionVerificationSessionAction", + "wallet_ownership": "#/components/schemas/TransactionWalletOwnershipAction" + } + } + }, + "cost_breakdown": { + "type": "object", + "nullable": true, + "description": "Cost of this transaction: `total_price` and `items`, one per charge, each with `usage_type`, `price`, `details` and `created_at`. A re-screen of the same transaction is a new item." + } + } + }, + "TravelRuleSettingsDetail": { + "type": "object", + "properties": { + "is_enabled": { + "type": "boolean", + "description": "Whether the managed Travel Rule negotiation flow is active for outbound `travelRule` transactions on this application." + }, + "jurisdiction": { + "type": "string", + "maxLength": 8, + "description": "Your VASP's operating jurisdiction code. Default `\"EU\"`. Didit maps it to the regime it stamps on your Travel Rule transactions as `travel_rule_regime` (for example `EU` or an EU member code becomes `eu`), which transaction rules can key on. Other Didit customers see it as your jurisdiction in the VASP directory once the Didit network lists your profile." + }, + "name_matching_strictness": { + "type": "string", + "enum": [ + "NONE", + "STRICT", + "CONTAINS", + "DEFAULT", + "FUZZY" + ], + "description": "How strictly beneficiary names must match before a transfer can reach COMPLETED. `CONTAINS` accepts one name whose words all appear in the other." + }, + "confirmation_timeout_hours": { + "type": "integer", + "minimum": 1, + "description": "Hours an AWAITING_COUNTERPARTY transfer may remain unconfirmed before the periodic sweeper expires it. Default 48." + }, + "timeout_outcome": { + "type": "string", + "enum": [ + "HOLD", + "REJECT", + "PROCEED" + ], + "description": "What happens to the underlying transaction when a transfer expires. Default HOLD. sunrise_policy and non_responsive_policy take precedence when they apply and are set." + }, + "threshold_amount": { + "type": "string", + "description": "Decimal string, in your application's default currency. Minimum transfer amount that triggers a managed exchange; transfers below it resolve straight to NOT_APPLICABLE. Didit compares the transaction amount converted to your default currency, or the `amount_in_default_currency` you send; when no converted amount is available, the threshold does not apply. Defaults to \"0.00\" (EU TFR applies no minimum amount)." + }, + "ownership_validation_mode": { + "type": "string", + "enum": [ + "FULL", + "SIMPLIFIED", + "AUTOMATED" + ], + "description": "How much proof a wallet's ownership takes. `FULL` always needs a proof (signed message, test deposit or reviewed screenshot), never the end user's word. `SIMPLIFIED` accepts a self-declaration below `simplified_validation_threshold` and a proof above it. `AUTOMATED` lets the address book and earlier proofs answer on their own, self-declarations included while `allow_self_declaration` is true. Default `AUTOMATED`." + }, + "simplified_validation_threshold": { + "type": "string", + "description": "Decimal string, default \"0.00\". In `SIMPLIFIED` mode a transfer below this amount (in the threshold currency) accepts a self-declaration. A transfer with no amount in that currency counts as above it." + }, + "data_sharing_fields": { + "$ref": "#/components/schemas/TravelRuleDataSharingFields" + }, + "matching_rules": { + "$ref": "#/components/schemas/TravelRuleMatchingRules" + }, + "legal_name": { + "type": "string", + "description": "Your VASP's legal name. Required (non-blank) to set is_enabled to true." + }, + "lei": { + "type": "string", + "description": "Your Legal Entity Identifier, surfaced in the VASP directory." + }, + "compliance_email": { + "type": "string", + "format": "email", + "description": "Compliance contact email for your VASP profile. The Didit network lists your profile only once this address is confirmed through an emailed link (POST /v3/travel-rule/settings/compliance-email/confirmation/); changing it needs a new confirmation." + }, + "is_discoverable": { + "type": "boolean", + "description": "Whether your VASP profile can be listed in the Didit network: found in GET /v3/travel-rule/vasps/ by other Didit customers and reached on the INTERNAL rail. Listing also needs a live application, Travel Rule on, a legal name and a confirmed compliance email; didit_network says which are missing. Default true." + }, + "inbound_auto_accept": { + "type": "boolean", + "description": "When true, an inbound TRP inquiry whose beneficiary wallet is a verified address-book entry with a matching name is approved automatically. When false, that inquiry waits in ON_HOLD for you to approve or deny it with the ownership endpoint. INTERNAL-rail and after-deposit transfers are not affected. Default true." + }, + "allow_self_declaration": { + "type": "boolean", + "description": "When true, the wallet-ownership widget offers self-declaration as a fallback proof method. Default true." + }, + "allow_screenshot_proof": { + "type": "boolean", + "description": "When true, the wallet-ownership widget offers screenshot upload as an ownership evidence method. Default true." + }, + "auto_wallet_verification": { + "type": "boolean", + "description": "When true, submitting an outbound Travel Rule transfer that needs end-user proof of wallet control auto-mints a wallet-ownership widget session bound to the transaction and exposes it in the transaction's action_required block. Default true." + }, + "vasp_attribution_enabled": { + "type": "boolean", + "description": "When true, an outbound Travel Rule transfer that no rail can route is attributed through blockchain analytics: the destination wallet is resolved to its owning VASP, the entity is recorded on the transfer and in the VASP directory, and the rails are re-run once with the attributed name. Default true." + }, + "counterparty_screening_enabled": { + "type": "boolean", + "description": "Opt-in sanctions and adverse-media screening of counterparty VASPs, billed per screening at the Company AML Screening price. When on, Didit screens the counterparty an outbound exchange reaches (reusing a screening younger than 30 days) and you can screen one with POST /v3/travel-rule/vasps/{vasp_id}/screening/. Default false: nothing is screened or billed." + }, + "sunrise_policy": { + "type": "string", + "enum": [ + "", + "HOLD", + "REJECT", + "PROCEED" + ], + "description": "What an expired transfer does to its transaction when the counterparty is a directory entry in a jurisdiction on sunrise_jurisdictions, which does not enforce the Travel Rule yet: HOLD moves it to review, REJECT declines it, PROCEED leaves its status untouched. Empty (the default) follows timeout_outcome. Applies before non_responsive_policy." + }, + "sunrise_jurisdictions": { + "type": "array", + "items": { + "type": "string" + }, + "readOnly": true, + "description": "Read-only. The countries (ISO 3166 alpha-2) Didit treats as in a Travel Rule sunrise period, where sunrise_policy applies. Currently CO, KH and VN. Any other jurisdiction, and an empty or unknown one, follows timeout_outcome." + }, + "non_responsive_policy": { + "type": "string", + "enum": [ + "", + "HOLD", + "REJECT", + "PROCEED" + ], + "description": "What an expired transfer does to its transaction when the counterparty is a directory entry whose responsiveness is UNRESPONSIVE: HOLD, REJECT or PROCEED, as for timeout_outcome. Empty (the default) follows timeout_outcome. sunrise_policy applies first when both fit." + }, + "travel_address": { + "type": "string", + "description": "Read-only. Your VASP's own travel address - the encoded inbound TRP inquiry endpoint other VASPs use to reach you. Empty while `is_enabled` is false." + }, + "disabled_networks": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "GTR", + "TRUST", + "VERIFYVASP", + "SYGNA" + ] + }, + "description": "Networks opted out of platform membership. Does not disable your connected own memberships or provision unavailable networks.", + "default": [] + }, + "networks": { + "type": "object", + "readOnly": true, + "description": "Effective membership configuration for each network. No network, including GTR, is included by default. This is not a live health or delivery check.", + "properties": { + "gtr": { + "type": "object", + "properties": { + "rail": { + "type": "string", + "enum": [ + "GTR" + ] + }, + "mode": { + "type": "string", + "enum": [ + "own", + "didit", + "off", + "unavailable" + ], + "description": "own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available." + }, + "platform_available": { + "type": "boolean", + "description": "Whether platform membership is configured; not a live health check." + }, + "connected": { + "type": "boolean", + "description": "Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange." + } + } + }, + "trust": { + "type": "object", + "properties": { + "rail": { + "type": "string", + "enum": [ + "TRUST" + ] + }, + "mode": { + "type": "string", + "enum": [ + "own", + "didit", + "off", + "unavailable" + ], + "description": "own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available." + }, + "platform_available": { + "type": "boolean", + "description": "Whether platform membership is configured; not a live health check." + }, + "connected": { + "type": "boolean", + "description": "Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange." + } + } + }, + "verifyvasp": { + "type": "object", + "properties": { + "rail": { + "type": "string", + "enum": [ + "VERIFYVASP" + ] + }, + "mode": { + "type": "string", + "enum": [ + "own", + "didit", + "off", + "unavailable" + ], + "description": "own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available." + }, + "platform_available": { + "type": "boolean", + "description": "Whether platform membership is configured; not a live health check." + }, + "connected": { + "type": "boolean", + "description": "Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange." + } + } + }, + "sygna": { + "type": "object", + "properties": { + "rail": { + "type": "string", + "enum": [ + "SYGNA" + ] + }, + "mode": { + "type": "string", + "enum": [ + "own", + "didit", + "off", + "unavailable" + ], + "description": "own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available." + }, + "platform_available": { + "type": "boolean", + "description": "Whether platform membership is configured; not a live health check." + }, + "connected": { + "type": "boolean", + "description": "Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange." + } + } + } + } + }, + "didit_network": { + "type": "object", + "readOnly": true, + "description": "Whether the Didit network lists your VASP profile, so other Didit customers find it in the directory and reach it on the INTERNAL rail, and each condition it still misses.", + "properties": { + "listed": { + "type": "boolean" + }, + "missing": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "travel_rule_off", + "sandbox_application", + "not_discoverable", + "legal_name_missing", + "compliance_email_missing", + "compliance_email_unconfirmed", + "excluded_by_staff", + "internal_organization" + ] + }, + "description": "Empty when listed. compliance_email_unconfirmed: send the confirmation with POST /v3/travel-rule/settings/compliance-email/confirmation/ and open the emailed link. excluded_by_staff: Didit marked the application as a test, demo or internal one." + }, + "compliance_email_confirmed": { + "type": "boolean", + "description": "Whether the current compliance_email was confirmed through the emailed link." + } + } + } + } + }, + "TravelRuleSettingsUpdate": { + "type": "object", + "properties": { + "is_enabled": { + "type": "boolean" + }, + "legal_name": { + "type": "string", + "maxLength": 255 + }, + "lei": { + "type": "string", + "maxLength": 20 + }, + "jurisdiction": { + "type": "string", + "maxLength": 8 + }, + "name_matching_strictness": { + "type": "string", + "enum": [ + "NONE", + "STRICT", + "CONTAINS", + "DEFAULT", + "FUZZY" + ], + "description": "`CONTAINS` accepts one name whose words all appear in the other." + }, + "confirmation_timeout_hours": { + "type": "integer", + "minimum": 1 + }, + "timeout_outcome": { + "type": "string", + "enum": [ + "HOLD", + "REJECT", + "PROCEED" + ] + }, + "compliance_email": { + "type": "string", + "format": "email", + "description": "Compliance contact email for your VASP profile. The Didit network lists your profile only once this address is confirmed through an emailed link (POST /v3/travel-rule/settings/compliance-email/confirmation/); changing it needs a new confirmation." + }, + "is_discoverable": { + "type": "boolean", + "description": "Whether your VASP profile can be listed in the Didit network: found in GET /v3/travel-rule/vasps/ by other Didit customers and reached on the INTERNAL rail. Listing also needs a live application, Travel Rule on, a legal name and a confirmed compliance email; didit_network says which are missing. Default true." + }, + "threshold_amount": { + "type": "string", + "description": "Decimal string, zero or greater, in your application's default currency. Minimum transfer amount that triggers a managed exchange." + }, + "ownership_validation_mode": { + "type": "string", + "enum": [ + "FULL", + "SIMPLIFIED", + "AUTOMATED" + ], + "description": "See the response field of the same name." + }, + "simplified_validation_threshold": { + "type": "string", + "description": "Decimal string, 0 or more." + }, + "data_sharing_fields": { + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleDataSharingFields" + } + ], + "description": "Merged per entity type: an entity type you leave out keeps its current choice. A choice that cannot meet the originator obligations of your `jurisdiction` regime (EU TFR or FATF Recommendation 16), an empty list included, is refused with `400`." + }, + "matching_rules": { + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleMatchingRules" + } + ], + "description": "Merged per entity type, and per field inside it. A field that entity type does not compare (for example `date_of_birth` for `company`) is refused with `400`." + }, + "inbound_auto_accept": { + "type": "boolean" + }, + "allow_self_declaration": { + "type": "boolean" + }, + "allow_screenshot_proof": { + "type": "boolean", + "description": "When true, the wallet-ownership widget offers screenshot upload as an ownership evidence method. Default true." + }, + "auto_wallet_verification": { + "type": "boolean" + }, + "vasp_attribution_enabled": { + "type": "boolean" + }, + "counterparty_screening_enabled": { + "type": "boolean", + "description": "Opt-in sanctions and adverse-media screening of counterparty VASPs, billed per screening at the Company AML Screening price. When on, Didit screens the counterparty an outbound exchange reaches (reusing a screening younger than 30 days) and you can screen one with POST /v3/travel-rule/vasps/{vasp_id}/screening/. Default false: nothing is screened or billed." + }, + "sunrise_policy": { + "type": "string", + "enum": [ + "", + "HOLD", + "REJECT", + "PROCEED" + ], + "description": "HOLD, REJECT, PROCEED, or empty to follow timeout_outcome. See the response field of the same name." + }, + "non_responsive_policy": { + "type": "string", + "enum": [ + "", + "HOLD", + "REJECT", + "PROCEED" + ], + "description": "HOLD, REJECT, PROCEED, or empty to follow timeout_outcome. See the response field of the same name." + }, + "disabled_networks": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "GTR", + "TRUST", + "VERIFYVASP", + "SYGNA" + ] + }, + "description": "Networks opted out of platform membership. Does not disable your connected own memberships or provision unavailable networks.", + "default": [] + } + }, + "description": "All fields optional; PUT applies a partial update." + }, + "WalletAddressEntry": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "address": { + "type": "string", + "description": "The wallet address; `\"\"` for an entry imported by its hash only. Unique per application, chain and memo, comparing the address without regard to case. After you delete an entry, you can register the same address again." + }, + "chain": { + "type": "string", + "description": "Chain token for the address, for example `ETH` or `BTC`. When Didit routes or registers a transfer, it matches address book entries on the wallet address and, when the transfer carries a chain, on this value too, ignoring case. A transfer's chain is `beneficiary_data.chain`, filled from the transaction's crypto chain or currency symbol (for example `ETH`) when you do not send it, or the `chain` of an after-deposit registration, so use the same token everywhere. An entry with an empty chain matches any chain. A Lightning payee (a node id or a Lightning Address) registered on bitcoin is stored lowercased on `LIGHTNING`, where Lightning transfers are matched." + }, + "holder_vendor_data": { + "type": "string", + "description": "Your internal identifier for the holder." + }, + "holder_name": { + "type": "string", + "description": "Name of the wallet holder, used for the beneficiary name match." + }, + "entity_type": { + "type": "string", + "description": "Free text, defaults to \"individual\"." + }, + "is_ownership_verified": { + "type": "boolean", + "description": "Read-only. Whether ownership of this address has been proven." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "travel_address": { + "type": "string", + "description": "The counterparty VASP's travel address for this wallet, if known. Lets Didit route this beneficiary's transfers over the TRP rail, for the assets TRP carries (BTC, ETH, and USDC on a named network)." + }, + "address_hash": { + "type": "string", + "description": "Read-only. SHA-256 of the lowercased address, in lowercase hex. An entry imported by its hash only is matched on it." + }, + "memo": { + "type": "string", + "description": "The destination tag or memo that is part of the wallet on a memo network (XRP, XLM, EOS, ATOM, HBAR); `\"\"` for the whole address. An entry with a memo matches only transfers naming that memo; one without matches the whole address." + }, + "receiving_travel_address": { + "type": "string", + "readOnly": true, + "description": "Your per-wallet travel address for this wallet: give it to the sending VASP so its TRP inquiry names this wallet. Empty for a wallet recorded as another VASP's, while Travel Rule is off, and before your VASP profile is saved." + } + } + }, + "WalletAddressEntryCreate": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string", + "maxLength": 128, + "description": "The wallet address. An XRP X-address is stored as its classic address and its tag, so it matches transfers that name either form." + }, + "chain": { + "type": "string", + "maxLength": 32, + "description": "Chain token for the address, for example `ETH` or `BTC`. When Didit routes or registers a transfer, it matches address book entries on the wallet address and, when the transfer carries a chain, on this value too, ignoring case. A transfer's chain is `beneficiary_data.chain`, filled from the transaction's crypto chain or currency symbol (for example `ETH`) when you do not send it, or the `chain` of an after-deposit registration, so use the same token everywhere. An entry with an empty chain matches any chain." + }, + "holder_vendor_data": { + "type": "string", + "maxLength": 255 + }, + "holder_name": { + "type": "string", + "maxLength": 255 + }, + "entity_type": { + "type": "string", + "default": "individual", + "maxLength": 16 + }, + "self_declared": { + "type": "boolean", + "default": false, + "description": "Write-only. When true, the entry is created already ownership-verified via a SELF_DECLARATION proof. Returns 400 when `allow_self_declaration` is off in your Travel Rule settings." + }, + "travel_address": { + "type": "string", + "description": "Optional. The counterparty VASP's travel address for this wallet, enabling TRP routing for the assets TRP carries (BTC, ETH, and USDC on a named network). An invalid travel address returns 400.", + "maxLength": 512 + }, + "memo": { + "type": "string", + "maxLength": 256, + "description": "Optional. The destination tag or memo that is part of the wallet. XRP: a destination tag, a whole number from 0 to 4294967295 (a JSON number is accepted). XLM: a text memo of up to 28 bytes, a 64-bit id or a 32-byte hash in hex. EOS: up to 256 bytes. ATOM: up to 256 characters. HBAR: up to 100 bytes. Bitcoin, EVM, Solana, Tron, Litecoin, Bitcoin Cash, Dogecoin and Lightning wallets take no memo: one there is a `400`. Alias: `destinationTag` or `destination_tag`." + } + } + }, + "WalletAddressEntryUpdate": { + "type": "object", + "properties": { + "holder_name": { + "type": "string", + "maxLength": 255 + }, + "holder_vendor_data": { + "type": "string", + "maxLength": 255 + }, + "entity_type": { + "type": "string", + "maxLength": 16 + }, + "travel_address": { + "type": "string", + "maxLength": 512 + } + }, + "description": "PATCH is a partial update; the response echoes only holder_name, holder_vendor_data, entity_type, travel_address, receiving_travel_address, label, and owner_type." + }, + "TravelRuleTransferDetail": { + "type": "object", + "description": "Full transfer object returned by the ownership-confirmation and finish/cancel endpoints. While an inbound transfer's wallet ownership is unconfirmed, the counterparty's `originator_data` and `beneficiary_data` come back masked (the first letter of each word is kept; the wallet, chain and VASP routing stay readable) and `data_masked` is true.", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string", + "enum": [ + "AWAITING_COUNTERPARTY", + "ON_HOLD", + "COMPLETED", + "FINISHED", + "CANCELLED", + "EXPIRED", + "COUNTERPARTY_MISMATCHED_DATA", + "COUNTERPARTY_VASP_NOT_FOUND", + "COUNTERPARTY_VASP_NOT_REACHABLE", + "UNCONFIRMED_OWNERSHIP", + "COUNTERPARTY_VASP_GENERAL_DECLINE", + "NOT_ENOUGH_COUNTERPARTY_DATA", + "NOT_APPLICABLE" + ] + }, + "direction": { + "type": "string", + "enum": [ + "INBOUND", + "OUTBOUND" + ] + }, + "rail": { + "type": "string", + "nullable": true, + "enum": [ + "INTERNAL", + "TRP", + "GTR", + "TRUST", + "VERIFYVASP", + "SYGNA", + "EMAIL", + "TEST_VASP", + null + ], + "description": "The rail the transfer was routed over, or null when no rail was selected (for example below the threshold, a self-hosted counterparty wallet, an after-deposit registration, or no reachable counterparty). INTERNAL means both sides are Didit applications; EMAIL means the counterparty was reached by an emailed pickup link. TEST_VASP means the Didit Test VASP answered a sandbox application's transfer. Over GTR, VERIFYVASP and SYGNA, Didit reads the counterparty's answer from the network's response or by polling it every 5 minutes. TRUST only sends, so a TRUST transfer stays AWAITING_COUNTERPARTY until it expires. A network rail is used only when you have connected your own membership for that network. It is chosen after the transaction create returns, so it is null in that response." + }, + "protocol": { + "type": "string", + "nullable": true + }, + "ivms_version": { + "type": "string", + "example": "ivms101.2023" + }, + "required": { + "type": "boolean" + }, + "ownership_confirmed": { + "type": "boolean", + "nullable": true + }, + "deadline_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "timeline": { + "type": "array", + "items": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "at": { + "type": "string", + "format": "date-time" + }, + "detail": { + "type": "string" + }, + "name_match": { + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleNameMatch" + } + ], + "nullable": true, + "description": "The name comparison this entry decided, next to `detail`. Absent on entries that compared no name." + }, + "reason": { + "type": "string", + "description": "Present only on the entry of a transition a failed delivery caused: `email_undeliverable` or `delivery_failed`, the value the transfer's `reason` reports." + } + } + } + }, + "counterparty_vasp": { + "type": "object", + "nullable": true, + "properties": { + "name": { + "type": "string" + }, + "lei": { + "type": "string" + }, + "dd_score": { + "type": "integer", + "description": "Due-diligence score from 0 to 100, where higher is better. Same scale as in the VASP directory." + }, + "dd_score_breakdown": { + "type": "object", + "description": "Per-component contributions to dd_score, one object per component as in the VASP directory (jurisdiction, verification, protocol, lei, history, essentials, responsiveness), each {points, max, reason, detail}. {} until the counterparty has been assessed.", + "additionalProperties": { + "type": "object", + "properties": { + "points": { + "type": "number", + "description": "Points awarded for this component." }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Echo of the `vendor_data` you sent." + "max": { + "type": "integer", + "description": "Maximum points this component can award." }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Echo of the `metadata` you sent." + "reason": { + "type": "string", + "description": "Machine-readable code of the component's verdict." }, - "created_at": { + "detail": { "type": "string", - "format": "date-time" - } - } - }, - "examples": { - "Resolved company": { - "summary": "Full registry profile (officer list truncated for brevity)", - "value": { - "request_id": "9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e", - "kyb_registry": { - "uuid": "f1e2d3c4-b5a6-4789-9c0d-1e2f3a4b5c6d", - "node_id": "feature_kyb_registry", - "status": "In Review", - "registry_status": "active", - "data_resolved": true, - "company_name": "Tesco PLC", - "registration_number": "00445790", - "country_code": "GB", - "region": "", - "company_type": "Public Limited Company", - "incorporation_date": "1947-11-27", - "registered_address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", - "tax_number": "GB412 8925 93", - "risk_level": "low risk", - "verification_status": "verified", - "is_from_registry": true, - "fetch_status": "resolved", - "alternative_names": "TESCO STORES (HOLDINGS) PUBLIC LIMITED COMPANY", - "nature_of_business": null, - "registered_capital": null, - "registered_capital_amount": null, - "registered_capital_currency": null, - "website": "https://www.tesco.com/", - "email": "customer.service@tesco.co.uk", - "phone": "03301231688", - "legal_entity_identifier": null, - "location_of_registration": null, - "financial_summary": { - "currency": "Pound (£)", - "total_number_of_shares": "8174378721", - "meta_detail": { - "Class_Of_Share": "Ordinary" - } - }, - "officers": [ - { - "uuid": "a1b2c3d4-e5f6-4789-9a0b-1c2d3e4f5a6b", - "name": "CHRISTOPHER JON TAYLOR", - "designation": "Secretary", - "role": "secretary", - "nationality": null, - "is_active": true, - "kyc_status": null, - "kyc_session_url": null - }, - { - "uuid": "b2c3d4e5-f6a7-4890-8b1c-2d3e4f5a6b7c", - "name": "MELISSA BETHELL", - "designation": "Director", - "role": "director", - "nationality": "British", - "is_active": true, - "kyc_status": null, - "kyc_session_url": null - } - ], - "beneficial_owners": [], - "addresses": [ - { - "address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", - "type": "legal_entity_registry_address", - "description": "registered office address" - } - ], - "industries": null, - "accounts": { - "last_account": "26 February 2025", - "next_account": "26 February 2026", - "due_by_date": "26 August 2026" - }, - "registry_data": { - "name": "Tesco PLC", - "registration_number": "00445790", - "status": "active", - "type": "Public Limited Company", - "incorporation_date": "1947-11-27", - "tax_number": "GB412 8925 93", - "country_code": "GB", - "fetch_status": "resolved", - "industries_detail": [ - { - "code": "47110", - "name": "Retail Sale In Non-specialised Stores With Food, Beverages Or Tobacco Predominating", - "description": null, - "meta_detail": [] - } - ] - }, - "user_provided_data": null, - "confirmed_by_user_at": null, - "last_console_edit_at": null, - "is_editable": true - }, - "vendor_data": "company-1234", - "metadata": null, - "created_at": "2026-06-11T10:45:00.000000+00:00" + "description": "Human-readable reason for the awarded points." } } } + }, + "dd_assessed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the due-diligence assessment last ran. Null until the counterparty has been assessed." + } + } + }, + "originator_data": { + "type": "object", + "description": "Originator party. For an inbound transfer the counterparty's values are masked until ownership is confirmed." + }, + "beneficiary_data": { + "type": "object", + "description": "Beneficiary party. For an inbound transfer the counterparty's values are masked until ownership is confirmed." + }, + "name_match": { + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleNameMatch" + } + ], + "nullable": true, + "description": "The name comparison that decided the transfer, or null until one ran. While the parties are masked, and on the other customer's copy of an INTERNAL exchange, `transfer_name`, `compared_name`, `matched_names` and `mismatched_names` are hidden. `score` and `matched` can disagree: `matched` follows the strictness, `score` is the similarity of the two full names." + }, + "data_match": { + "$ref": "#/components/schemas/TravelRuleDataMatch" + }, + "mismatched_fields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Shortcut for `data_match.mismatched_fields`. Empty when nothing differs." + }, + "data_unmasked": { + "$ref": "#/components/schemas/TravelRuleDataUnmasked" + }, + "data_masked": { + "type": "boolean", + "description": "True while the counterparty's personal data is masked in this response." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "delivery": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleDelivery" } + ], + "description": "Where the latest message this transfer owes the other side stands. Null when the transfer never owed one (for example below `threshold_amount`, a declared self-hosted wallet, or a sunrise registration)." + }, + "discovery": { + "type": "array", + "description": "How Didit looked for the counterparty, one entry per step that ran. Empty when no discovery ran, for example on an inbound transfer.", + "items": { + "$ref": "#/components/schemas/TravelRuleDiscoveryStep" + } + }, + "reason": { + "type": "string", + "description": "Why a `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE` transfer is stuck; `\"\"` for any other status. A failed delivery decides it first, on `COUNTERPARTY_VASP_NOT_REACHABLE` only: `email_undeliverable` (the mail provider refused the compliance email and no address is left to try: when it was sent, on a reminder, or as a permanent bounce reported later) or `delivery_failed` (the rail that was delivering failed for good: a network rail refused the message or ran out of retries, a `TRP` identity host did not answer, or the counterparty's `TRP` host did not accept the inquiry). Otherwise the `discovery` trace decides it: `awaiting_end_user` (no step named a counterparty Didit can reach), `directory_match_no_route` (a step named a directory VASP that has no route), `picked_member_without_proven_wallet` (the end user picked a Didit customer that has not proven control of the wallet), or `no_directory_match` when the trace gives no other reason. A resend moves the transfer on, so the reason clears. See [Why a transfer is stuck](/transaction-monitoring/travel-rule#why-a-transfer-is-stuck)." + }, + "next_actions": { + "type": "array", + "description": "What moves the transfer on; `[]` for any status but `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE`. `email_undeliverable` offers `update_compliance_email`, then `resend`. `delivery_failed` offers `resend`. A discovery reason offers `select_counterparty_vasp`, `declare_self_hosted`, `add_vasp_hint` and `resend`.", + "items": { + "$ref": "#/components/schemas/TravelRuleNextAction" } }, - "400": { - "description": "Validation error. Field-level problems use DRF's standard envelope.", - "content": { - "application/json": { - "examples": { - "Missing kyb_response_id": { - "summary": "`kyb_response_id` is required", - "value": { - "kyb_response_id": [ - "This field is required." - ] - } - }, - "save_api_request=false rejected": { - "summary": "Selects are always persisted", - "value": { - "save_api_request": [ - "KYB registry select must be saved so billing and retrieval can be tracked." - ] - } - } + "chain_details": { + "$ref": "#/components/schemas/TravelRuleChainDetails" + } + } + }, + "TravelRuleOwnershipConfirmRequest": { + "type": "object", + "required": [ + "confirmed" + ], + "properties": { + "confirmed": { + "type": "boolean", + "description": "true to confirm ownership, false to deny it." + }, + "wallet_address": { + "type": "string", + "default": "", + "description": "Inbound TRP transfers: the payment address Didit returns to the originating VASP when you approve. Defaults to the matched address book entry's address, then beneficiary_data.wallet_address. On a declared self-hosted wallet transfer that names no counterparty wallet, the wallet whose ownership you verified; without it the request is rejected with 400. Ignored for other transfers." + } + } + }, + "TravelRuleTransferPatchRequest": { + "type": "object", + "description": "Provide exactly one of: payment_txn_id (finish), action=cancel, action=resend, action=update_compliance_email with compliance_email, or action=update_originator_wallet with wallet_address.", + "properties": { + "payment_txn_id": { + "type": "string", + "description": "The on-chain transaction hash. Requires the transfer to be an outbound transfer in COMPLETED status." + }, + "action": { + "type": "string", + "enum": [ + "cancel", + "resend", + "update_compliance_email", + "update_originator_wallet" + ], + "description": "\"cancel\" cancels a non-terminal transfer. \"resend\" re-runs routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA. \"update_compliance_email\" replaces the compliance email the request goes to, on an outbound transfer in one of those three statuses; send \"resend\" afterwards. \"update_originator_wallet\" records the wallet the funds actually left from (`wallet_address`, `memo`)." + }, + "compliance_email": { + "type": "string", + "format": "email", + "maxLength": 254, + "description": "Required with `action: update_compliance_email`. Kept as this transfer's `beneficiary_data.counterparty_vasp_email`: it belongs to this transfer only and joins no directory entry and no contact list." + }, + "wallet_address": { + "type": "string", + "maxLength": 128, + "description": "With `action: update_originator_wallet`: the wallet the funds left from. When the transfer's UTXO inputs are known it must be one of them." + }, + "memo": { + "type": "string", + "maxLength": 256, + "description": "With `action: update_originator_wallet`: the wallet's memo or tag on a memo network. Optional." + } + } + }, + "VaspDirectoryEntry": { + "type": "object", + "description": "A VASP of the directory or a member of the Didit network, with the routes your application has to it. A customer is a member of the Didit network only while its application is live, its Travel Rule is on, its profile is discoverable, has a legal name and a confirmed compliance email, and Didit has not marked the application as a test, demo or internal one. Each row also carries the shared due-diligence assessment (dd_score, risk_level, responsiveness, essential_checks) and your application's own decision, entity screening and questionnaires, which no other customer sees.", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "The entry's id, for GET /v3/travel-rule/vasps/{vasp_id}/." + }, + "kind": { + "type": "string", + "enum": [ + "directory", + "didit" + ], + "description": "directory for an entry of the shared directory; didit for a member of the Didit network." + }, + "name": { + "type": "string" + }, + "lei": { + "type": "string" + }, + "jurisdiction": { + "type": "string", + "description": "ISO 3166 country code, as the source gives it." + }, + "domains": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The VASP's web domains. Always empty for Didit network members." + }, + "reachable": { + "type": "boolean", + "description": "Whether your application has an available route to this VASP, or to one of its entities for a brand. Always true for a Didit network member, except on a sandbox application, where every row is false." + }, + "aliases": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": [ + "trading_name", + "other_legal_name", + "network_name", + "attribution", + "merged", + "staff" + ], + "description": "trading_name, other_legal_name (from the global LEI register), network_name (the name on a Travel Rule network), attribution (the entity name blockchain analytics gives the VASP's wallets), merged (the name of a duplicate entry merged into this one) or staff." } } - } + }, + "description": "Other names the VASP is found by." }, - "403": { - "description": "Permission denied. Returned when the `x-api-key` header is missing, malformed, revoked, or belongs to another environment — and also when the calling organization's balance cannot cover the call. Authentication failures return `403` with `{\"detail\": ...}`; this API never returns `401`. Credit shortfalls return `403` with `{\"error\": ...}` before any screening or provider call happens.", - "content": { - "application/json": { - "examples": { - "Missing or invalid API key": { - "summary": "No `x-api-key` header, or the key is invalid/revoked", - "value": { - "detail": "You do not have permission to perform this action." - } - }, - "Not enough credits": { - "summary": "Organization balance cannot cover the call", - "value": { - "error": "You don't have enough credits to perform this request. Please top up at https://business.didit.me" - } - } + "networks": { + "type": "array", + "items": { + "type": "object", + "description": "A current listing of this VASP in a Travel Rule network's member directory. A TRISA listing says who the VASP is on TRISA; Didit has no TRISA rail, so it is never a route.", + "properties": { + "network": { + "type": "string", + "enum": [ + "GTR", + "TRUST", + "VERIFYVASP", + "SYGNA", + "TRISA" + ] + }, + "code": { + "type": "string", + "description": "The VASP's identifier on that network." + }, + "listed_name": { + "type": "string", + "description": "The name the network lists the VASP under." + }, + "country": { + "type": "string", + "description": "The country the network lists, when it gives one." + }, + "verified_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When Didit staff verified the listing. Null for listings read from a public directory." + }, + "last_seen_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the source last showed the listing. A listing the source has not shown for three weeks is removed." + }, + "source": { + "type": "string", + "enum": [ + "trisa_directory", + "staff" + ], + "description": "Where the listing comes from: the public TRISA directory, or Didit staff." } } } }, - "429": { - "description": "Rate limit exceeded. All POST/PATCH/DELETE endpoints share a budget of 300 write requests per minute per API key. The response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers.", - "content": { - "application/json": { - "examples": { - "Write rate limit exceeded": { - "summary": "More than 300 write requests in the current minute", - "value": { - "detail": "Write request rate limit exceeded. You can make up to 300 requests per minute." - } - } + "routes": { + "type": "array", + "items": { + "type": "object", + "description": "One way your application can deliver Travel Rule data to this VASP, in the order dispatch tries them. A network route needs a membership on that network: your own (connected in the Marketplace) or Didit's once Didit holds one. Without it the route is listed as unavailable with the reason, never as a way to reach the VASP.", + "properties": { + "rail": { + "type": "string", + "enum": [ + "INTERNAL", + "TRP", + "GTR", + "TRUST", + "VERIFYVASP", + "SYGNA", + "EMAIL" + ] }, - "schema": { - "type": "object", - "properties": { - "detail": { - "type": "string" - } - } + "status": { + "type": "string", + "enum": [ + "available", + "unavailable" + ] + }, + "reason": { + "type": "string", + "enum": [ + "", + "no_membership", + "network_switched_off", + "sandbox_application" + ], + "description": "Why an unavailable route cannot be used: no_membership (neither you nor Didit holds a membership on that network), network_switched_off (you turned that network off in your settings) or sandbox_application (a sandbox application sends only to the Didit Test VASP, so no route is available to it). Empty when the route is available." + }, + "via": { + "type": "string", + "enum": [ + "", + "own_membership", + "didit_membership", + "directory", + "contact", + "didit_network" + ], + "description": "How an available route is reached: own_membership or didit_membership for a network rail, directory for a TRP endpoint the directory verified, contact for the EMAIL rail, didit_network for a Didit network member. Empty when the route is unavailable." } } } }, - "202": { - "description": "Report accepted and still processing. When a report is still being prepared, the endpoint returns 202 Accepted, kyb_registry.fetch_status=pending, and registry_tier_delivered=null. Keep request_id and retrieve GET /v3/session/{request_id}/decision/ until registry_checks[0].company.fetch_status is resolved. Didit completes the report automatically and charges the delivered tier once it is ready. Do not repeat the purchase while it is pending; every new POST creates a separate request.", - "content": { - "application/json": { - "schema": { - "$ref": "#/paths/~1v3~1kyb~1ubo~1/post/responses/200/content/application~1json/schema" - }, - "examples": { - "Pending (registry still resolving)": { - "summary": "Ownership report is still processing; retrieve the saved decision", - "value": { - "request_id": "9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e", - "kyb_registry": { - "uuid": "7c0a4f7e-2b1d-4f6a-9c3e-8d5b2a1f0e9d", - "node_id": "feature_kyb_registry", - "status": "Not Finished", - "registry_status": null, - "data_resolved": false, - "company_name": "Example Holdings Limited", - "registration_number": "12345678", - "country_code": "HK", - "region": null, - "company_type": null, - "incorporation_date": null, - "registered_address": null, - "tax_number": null, - "risk_level": null, - "verification_status": null, - "is_from_registry": true, - "fetch_status": "pending", - "alternative_names": null, - "nature_of_business": null, - "registered_capital": null, - "registered_capital_amount": null, - "registered_capital_currency": null, - "website": null, - "email": null, - "phone": null, - "legal_entity_identifier": null, - "location_of_registration": null, - "financial_summary": {}, - "officers": [], - "beneficial_owners": [], - "addresses": null, - "industries": null, - "accounts": null, - "registry_data": {}, - "user_provided_data": null, - "confirmed_by_user_at": null, - "last_console_edit_at": null, - "is_editable": true, - "registry_tier_delivered": null, - "registry_ownership_status": "pending" - }, - "vendor_data": "company-1234", - "metadata": null, - "created_at": "2026-06-11T10:45:00.000000+00:00", - "registry_tier_delivered": null, - "registry_tier_requested": "ubo" - } - } - } + "umbrella": { + "type": "object", + "nullable": true, + "description": "The brand this entry is a regulated entity of. Null when it is not part of one.", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" } } - } - } - } - }, - "/v3/id-verification/wallets/": { - "get": { - "operationId": "list_wallets", - "description": "Every Digital ID wallet with its start endpoint, countries, required fields and whether this application can start it in this environment now. Listed does not mean live.", - "summary": "List Digital ID wallets", - "tags": [ - "Digital ID wallets" - ], - "security": [ - { - "ApiKeyAuth": [] }, - { - "BearerAuth": [] - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletCatalogResponse" + "entities": { + "type": "array", + "items": { + "type": "object", + "description": "A regulated entity of a brand: the legal entity that receives a transfer.", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "lei": { + "type": "string" + }, + "jurisdiction": { + "type": "string" + }, + "reachable": { + "type": "boolean", + "description": "Whether your application has an available route to it." } } }, - "description": "" + "description": "For a brand, the regulated entities that receive its transfers. A transfer naming the brand goes to the reachable entity in the jurisdiction it states (beneficiary_data.counterparty_vasp_jurisdiction), else to the entity blockchain analytics attribution linked to the destination wallet, else to the only reachable entity when no jurisdiction is stated. Two entities that fit equally are never guessed between." }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiClientCredentialsForbiddenErrorMessage" + "contacts": { + "type": "array", + "items": { + "type": "object", + "description": "A compliance contact the EMAIL rail can write to.", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "email": { + "type": "string", + "format": "email" + }, + "name": { + "type": "string" + }, + "source": { + "type": "string", + "enum": [ + "staff", + "application", + "learned" + ], + "description": "staff: curated by Didit; application: added by your application; learned: an address that answered a Travel Rule request through its response link." + }, + "scope": { + "type": "string", + "enum": [ + "platform", + "application" + ], + "description": "platform contacts are used for every Didit customer's transfers; application contacts are private to your application, which sees only its own." + }, + "verified_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When someone at this address answered a request. Null when it has not yet." } } }, - "description": "Missing or invalid credentials (x-api-key or Authorization: Bearer)." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request GET \\\n --url https://verification.didit.me/v3/id-verification/wallets/ \\\n --header 'x-api-key: YOUR_API_KEY'" + "description": "The platform's contacts for this VASP and your application's own." }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"]\n}\n\nresponse = requests.get(\n \"https://verification.didit.me/v3/id-verification/wallets/\",\n headers=headers,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ] - } - }, - "/v3/id-verification/wallets/verifications/{request_id}/": { - "get": { - "operationId": "get_wallet_verification", - "description": "Status and result of a wallet verification: the normalized identity and the wallet's own attributes once verified, the error code once declined, the next action while pending. A verification past expires_at is decided timeout.", - "summary": "Get a wallet verification", - "parameters": [ - { - "in": "path", - "name": "request_id", - "schema": { + "dd_status": { + "type": "string", + "enum": [ + "NOT_STARTED", + "IN_PROGRESS", + "COMPLETED", + "REJECTED" + ], + "description": "Didit's verification status of the entry, scored by the verification component of dd_score. Always COMPLETED for a Didit network member." + }, + "dd_score": { + "type": "integer", + "nullable": true, + "minimum": 0, + "maximum": 100, + "description": "The shared due-diligence score of a directory entry, 0 to 100, where higher is safer (see dd_score_semantic): the sum of the points in dd_score_breakdown. Null for a Didit network member, which is not scored. An entry not assessed yet (dd_assessed_at null) shows 0 with an empty risk_level, so read risk_level rather than the raw score." + }, + "reachable_rails": { + "type": "array", + "items": { "type": "string", - "format": "uuid" + "enum": [ + "INTERNAL", + "TRP", + "GTR", + "TRUST", + "VERIFYVASP", + "SYGNA", + "EMAIL" + ] }, - "description": "The request_id the start endpoint returned.", - "required": true - } - ], - "tags": [ - "Digital ID wallets" - ], - "security": [ - { - "ApiKeyAuth": [] + "description": "Rails recorded on the entry. Read routes for what your application can use today." }, - { - "BearerAuth": [] - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + "is_didit_customer": { + "type": "boolean", + "description": "true for a member of the Didit network (dd_status is always COMPLETED, dd_score null and risk_level empty for these) and for a directory entry linked to a Didit customer." + }, + "dd_score_breakdown": { + "type": "object", + "description": "One object per component of dd_score. Components, their weight (max) and their reason codes: jurisdiction, 25 (jurisdiction_not_high_risk, jurisdiction_high_risk, jurisdiction_unknown); verification, 15 (dd_status_completed, dd_status_in_progress, dd_status_not_started, dd_status_rejected); protocol, 15 (native_rail, email_only, no_rail); lei, 10 (lei_valid, lei_missing); history, 10 (history, no_history); essentials, 15 (essential_checks, no_essential_checks); responsiveness, 10 (responsive, unresponsive, responsiveness_unknown). Empty for a Didit network member.", + "additionalProperties": { + "type": "object", + "properties": { + "points": { + "type": "number", + "description": "Points the component contributes, rounded to one decimal." }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "ftn", - "name": "Finnish Trust Network", - "issuing_authority": "Finnish banks and mobile operators (FTN)" - }, - "country": "FIN", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" + "max": { + "type": "integer", + "description": "The component's weight: the points it contributes at best." + }, + "reason": { + "type": "string", + "description": "Machine-readable code of the component's verdict." + }, + "detail": { + "type": "string", + "description": "One-line English explanation." } } - }, - "description": "" + } }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiClientCredentialsForbiddenErrorMessage" - } - } - }, - "description": "Missing or invalid credentials (x-api-key or Authorization: Bearer)." + "dd_assessed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When dd_score, risk_level and responsiveness were last computed: after an exchange with the entry completes, finishes, expires, mismatches or is declined, when Didit refreshes or curates the entry, and in the weekly directory refresh. Null for a Didit network member." }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error not_found: no wallet verification of this application has this request_id." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request GET \\\n --url https://verification.didit.me/v3/id-verification/wallets/verifications/REQUEST_ID/ \\\n --header 'x-api-key: YOUR_API_KEY'" + "dd_score_semantic": { + "type": "string", + "enum": [ + "higher_is_safer" + ], + "description": "Which way dd_score points: always higher_is_safer." }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"]\n}\n\nresponse = requests.get(\n \"https://verification.didit.me/v3/id-verification/wallets/verifications/REQUEST_ID/\",\n headers=headers,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ] - } - }, - "/v3/id-verification/wallets/verifications/{request_id}/poll/": { - "post": { - "operationId": "poll_wallet_verification", - "description": "Smart-ID and Mobile-ID: ask the provider whether the user confirmed, refused or let the prompt lapse, and verify the signed identity when they confirmed. Redirect wallets complete through the user's browser, so for them this only reads the verification back. Answers the verification as it is afterwards.", - "summary": "Poll a wallet verification", - "parameters": [ - { - "in": "path", - "name": "request_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "The request_id the start endpoint returned.", - "required": true - } - ], - "tags": [ - "Digital ID wallets" - ], - "security": [ - { - "ApiKeyAuth": [] + "risk_level": { + "type": "string", + "enum": [ + "LOW", + "MEDIUM", + "HIGH", + "" + ], + "description": "The band dd_score falls in: LOW (70 or more), MEDIUM (40 to 69) or HIGH (below 40). Empty for a Didit network member and for an entry not assessed yet. Filter on it with risk. Your own flagged screening never changes this shared value: it makes counterparty_risk_level HIGH in your rules instead." + }, + "responsiveness": { + "type": "object", + "description": "Whether the VASP answers Travel Rule requests: the share of the requests Didit customers started towards it in the last 90 days that it answered. Only requests sent over a network or protocol rail, or by email to a shared address (the entry's compliance address or a platform contact), count; a request sent to an address a customer typed, or to a customer's private contact, never does. An expired request is unanswered; a completed exchange, a decline or a data mismatch is an answer; a cancelled request or an unreachable rail is neither. Always UNKNOWN with zero counts for a Didit network member.", + "properties": { + "status": { + "type": "string", + "enum": [ + "RESPONSIVE", + "UNRESPONSIVE", + "UNKNOWN" + ], + "description": "UNKNOWN with fewer than 5 decided requests in the window; UNRESPONSIVE when less than 10 percent of them were answered; RESPONSIVE otherwise." + }, + "answered": { + "type": "integer", + "description": "Decided requests in the window that the VASP answered." + }, + "total": { + "type": "integer", + "description": "Decided requests in the window: answered plus expired." + }, + "computed_at": { + "type": "string", + "format": "date-time", + "nullable": true + } + } }, - { - "BearerAuth": [] - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "smart_id", - "name": "Smart-ID", - "issuing_authority": "SK ID Solutions" + "essential_checks": { + "type": "object", + "description": "The essential-checks report of a directory entry, built only from the platform's verified sources: registration (an official register lists the VASP: the EU's interim MiCA register, the public TRISA directory or the global LEI register), licence (a MiCA authorisation is on record and current), domain (the entry's domain matches its TRP endpoint or the website its register gives) and lei (its LEI record is active, issued and consistent with the entry's names and country). The essentials component of dd_score is the share of decided checks that pass. Empty until the weekly directory refresh first checks the entry, and always empty for a Didit network member.", + "properties": { + "level": { + "type": "string", + "enum": [ + "essential" + ] + }, + "checked_at": { + "type": "string", + "format": "date-time" + }, + "checks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "registration", + "licence", + "domain", + "lei" + ] }, - "country": "EST", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "in_app", - "simulated": true, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "confirm_in_app", - "verification_code": "1234", - "poll_interval_seconds": 2 + "status": { + "type": "string", + "enum": [ + "pass", + "fail", + "unknown" + ], + "description": "unknown when no source on record can decide the check." }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" + "reason": { + "type": "string", + "description": "Machine-readable code of the verdict." + }, + "detail": { + "type": "string", + "description": "One-line English explanation." + } + } } + }, + "passed": { + "type": "integer" + }, + "failed": { + "type": "integer" + }, + "unknown": { + "type": "integer" + }, + "total": { + "type": "integer" } - }, - "description": "" + } }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiClientCredentialsForbiddenErrorMessage" - } + "decision": { + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleCounterpartyDecision" } - }, - "description": "Missing or invalid credentials (x-api-key or Authorization: Bearer)." + ], + "nullable": true, + "description": "Your application's decision on this VASP, or for an entity with no decision of its own its brand's (inherited_from names the brand). Null when there is none. Private to your application." }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } + "entity_screening": { + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleCounterpartyScreening" } - }, - "description": "error not_found: no wallet verification of this application has this request_id." + ], + "nullable": true, + "description": "Your application's latest sanctions and adverse-media screening of this VASP. Null when you never screened it. Private to your application." }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } + "questionnaires": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TravelRuleCounterpartyQuestionnaire" }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." + "description": "Your application's due-diligence questionnaires with this VASP, newest first. Private to your application." } }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/verifications/REQUEST_ID/poll/ \\\n --header 'x-api-key: YOUR_API_KEY'" + "example": { + "id": "0d6f3c2a-6a1e-4c55-9a39-1f2b0c7d8e91", + "kind": "directory", + "name": "Example Counterparty VASP", + "lei": "529900EXAMPLECPTY001", + "jurisdiction": "DE", + "domains": [ + "counterparty.example" + ], + "reachable": true, + "aliases": [ + { + "name": "Example Counterparty", + "kind": "trading_name" + } + ], + "networks": [ + { + "network": "GTR", + "code": "EXAMPLE01", + "listed_name": "Example Counterparty VASP", + "country": "DE", + "verified_at": "2026-10-12T09:30:00Z", + "last_seen_at": "2026-10-12T09:30:00Z", + "source": "staff" + } + ], + "routes": [ + { + "rail": "GTR", + "status": "unavailable", + "reason": "no_membership", + "via": "" + }, + { + "rail": "EMAIL", + "status": "available", + "reason": "", + "via": "contact" + } + ], + "umbrella": null, + "entities": [], + "contacts": [ + { + "id": "6c1f9b8e-2d4a-4f7e-8b3c-5a9d0e1f2a3b", + "email": "travelrule@counterparty.example", + "name": "Travel Rule desk", + "source": "staff", + "scope": "platform", + "verified_at": null + } + ], + "dd_status": "NOT_STARTED", + "dd_score": 62, + "dd_score_breakdown": { + "jurisdiction": { + "points": 25.0, + "max": 25, + "reason": "jurisdiction_not_high_risk", + "detail": "DEU is not on the high-risk jurisdiction list" + }, + "verification": { + "points": 3.8, + "max": 15, + "reason": "dd_status_not_started", + "detail": "due diligence status NOT_STARTED" + }, + "protocol": { + "points": 6.0, + "max": 15, + "reason": "email_only", + "detail": "reachable over the email fallback only" + }, + "lei": { + "points": 10.0, + "max": 10, + "reason": "lei_valid", + "detail": "valid LEI on record" + }, + "history": { + "points": 5.0, + "max": 10, + "reason": "no_history", + "detail": "no exchange history" + }, + "essentials": { + "points": 7.5, + "max": 15, + "reason": "no_essential_checks", + "detail": "no decided essential check yet" + }, + "responsiveness": { + "points": 5.0, + "max": 10, + "reason": "responsiveness_unknown", + "detail": "0 decided recent requests, too few to judge" + } + }, + "dd_assessed_at": "2026-10-12T09:30:00Z", + "dd_score_semantic": "higher_is_safer", + "risk_level": "MEDIUM", + "responsiveness": { + "status": "UNKNOWN", + "answered": 0, + "total": 0, + "computed_at": "2026-10-12T09:30:00Z" + }, + "essential_checks": {}, + "reachable_rails": [ + "GTR", + "EMAIL" + ], + "is_didit_customer": false, + "decision": { + "decision": "APPROVED", + "reason": "Licensed in DE, questionnaire reviewed", + "decided_by": "", + "updated_at": "2026-10-14T10:05:00Z", + "inherited_from": null + }, + "entity_screening": { + "id": "7a1d2c3b-4e5f-4a6b-8c7d-9e0f1a2b3c4d", + "status": "Approved", + "total_hits": 0, + "score": null, + "screened_at": "2026-10-14T09:58:00Z", + "fresh": true, + "flagged": false + }, + "questionnaires": [ + { + "id": "8b7a6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d", + "status": "ANSWERED", + "form_version": "1", + "sent_to": "travelrule@counterparty.example", + "sent_at": "2026-09-02T08:00:00Z", + "expires_at": "2026-10-02T08:00:00Z", + "answered_at": "2026-09-04T15:20:00Z", + "valid_until": "2027-09-04T15:20:00Z", + "current": true, + "answers": { + "legal_name": "Example Counterparty VASP, incorporated in the example jurisdiction.", + "licensing": "Crypto-asset service provider authorised under MiCA by its national regulator, licence active.", + "jurisdictions": "EU and EEA. We do not serve the United States.", + "aml_program": "Written AML/CFT policy, annual independent review, last completed in March 2026.", + "compliance_officer": "Head of Compliance, reachable at compliance@counterparty.example.", + "sanctions_screening": "Customers at onboarding and daily; counterparties on every transfer.", + "transaction_monitoring": "Rule-based monitoring with analyst review; suspicious activity reports filed with the FIU.", + "travel_rule": "TRP and email; we answer requests within one business day.", + "custody": "Segregated customer wallets held with a qualified custodian, insured.", + "ownership": "No shareholder holds 25 percent or more; parent company Example Holding Group.", + "record_keeping": "Five-year record-keeping for Travel Rule and transaction data, as our regulator requires.", + "data_protection": "Encrypted at rest and in transit, stored in the EU." + } + } + ] + } + }, + "TravelRuleCounterpartyDecision": { + "type": "object", + "description": "Your application's decision on a counterparty VASP. Private to your application: it never changes the shared directory entry and another customer never sees it. It acts only through your transaction rules (the counterparty_dd_status rule field and the travel-rule-counterparty-blocked and travel-rule-counterparty-restricted presets); it never changes how a transfer is routed.", + "properties": { + "decision": { + "type": "string", + "enum": [ + "APPROVED", + "RESTRICTED", + "BLOCKED" + ], + "description": "APPROVED: your team cleared the counterparty. RESTRICTED: transfers to it may go ahead after review. BLOCKED: your team refused it." }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"]\n}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/verifications/REQUEST_ID/poll/\",\n headers=headers,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" + "reason": { + "type": "string", + "maxLength": 255, + "description": "Free-text reason you gave, empty when none." + }, + "decided_by": { + "type": "string", + "description": "The console user who made or last changed the decision. Empty when an API key made it." + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "inherited_from": { + "type": "object", + "nullable": true, + "description": "The brand whose decision this entity follows because you made none on the entity itself. Null when the decision is the counterparty's own.", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + } + } } - ] - } - }, - "/v3/id-verification/wallets/verifications/{request_id}/cancel/": { - "post": { - "operationId": "cancel_wallet_verification", - "description": "Withdraw a pending verification: it is declined as cancelled and never billed. A decided one is returned unchanged. Smart-ID and Mobile-ID: a cancel that arrives while a poll is checking the sign-in waits a few seconds for that poll, then declines it, or returns it verified if the user had already confirmed. A verification the user is completing right now cannot be cancelled (409 verification_in_progress with Retry-After): retry after that many seconds to read its outcome.", - "summary": "Cancel a wallet verification", - "parameters": [ - { - "in": "path", - "name": "request_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "The request_id the start endpoint returned.", - "required": true + } + }, + "TravelRuleCounterpartyDecisionResponse": { + "type": "object", + "properties": { + "decision": { + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleCounterpartyDecision" + } + ], + "nullable": true, + "description": "The decision that applies to the counterparty for your application: its own, else (for an entity of a brand) the brand's. Null when there is none." } + } + }, + "TravelRuleCounterpartyDecisionRequest": { + "type": "object", + "required": [ + "decision" ], - "tags": [ - "Digital ID wallets" - ], - "security": [ - { - "ApiKeyAuth": [] + "properties": { + "decision": { + "type": "string", + "enum": [ + "APPROVED", + "RESTRICTED", + "BLOCKED" + ] }, - { - "BearerAuth": [] + "reason": { + "type": "string", + "maxLength": 255, + "description": "Optional. Replaces the previous reason; omit it to clear it." } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "ftn", - "name": "Finnish Trust Network", - "issuing_authority": "Finnish banks and mobile operators (FTN)" - }, - "country": "FIN", - "status": "Declined", - "outcome": "cancelled", - "error": "cancelled_by_customer", - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": null, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + } + }, + "TravelRuleCounterpartyScreening": { + "type": "object", + "description": "One sanctions and adverse-media screening of the counterparty entity, run for and billed to your application. Private to your application: another customer never sees it.", + "properties": { + "id": { + "type": "string", + "format": "uuid" }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiClientCredentialsForbiddenErrorMessage" - } - } - }, - "description": "Missing or invalid credentials (x-api-key or Authorization: Bearer)." + "status": { + "type": "string", + "enum": [ + "Approved", + "In Review", + "Declined" + ], + "description": "Approved: no possible match needs attention. In Review or Declined: a possible sanctions or adverse-media match." }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error not_found: no wallet verification of this application has this request_id." + "total_hits": { + "type": "integer", + "description": "How many possible matches the screening returned." }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } + "score": { + "type": "number", + "nullable": true, + "description": "The highest risk score among the possible matches. Null when nothing matched." + }, + "screened_at": { + "type": "string", + "format": "date-time" + }, + "fresh": { + "type": "boolean", + "description": "Whether the screening is younger than fresh_days (30 days). A fresh screening is reused instead of billed again." + }, + "flagged": { + "type": "boolean", + "description": "true while the screening is fresh and its status is In Review or Declined. While your latest screening is flagged and counterparty screening stays on, counterparty_risk_level is HIGH in your rules." + } + } + }, + "TravelRuleCounterpartyScreeningStatus": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Your counterparty_screening_enabled Travel Rule setting. Off by default; nothing is screened or billed while it is off." + }, + "price": { + "type": "string", + "description": "Decimal string, in USD: what one screening costs your organization, the Company AML Screening price." + }, + "fresh_days": { + "type": "integer", + "description": "How many days a screening stays fresh and is reused." + }, + "screening": { + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleCounterpartyScreening" } - }, - "description": "error verification_in_progress: the user is completing this sign-in right now. The Retry-After header says when to retry; the retry returns the decided verification." + ], + "nullable": true, + "description": "Your latest screening of the counterparty. Null when you never screened it." } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/verifications/REQUEST_ID/cancel/ \\\n --header 'x-api-key: YOUR_API_KEY'" + } + }, + "TravelRuleCounterpartyScreeningRequest": { + "type": "object", + "properties": { + "refresh": { + "type": "boolean", + "default": false, + "description": "true screens again (and bills again) even when a fresh screening exists." + } + } + }, + "TravelRuleCounterpartyScreeningResult": { + "type": "object", + "properties": { + "outcome": { + "type": "string", + "enum": [ + "screened", + "reused" + ], + "description": "screened: a new screening ran and was billed (201). reused: a fresh screening was returned and nothing was billed (200)." }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"]\n}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/verifications/REQUEST_ID/cancel/\",\n headers=headers,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" + "screening": { + "$ref": "#/components/schemas/TravelRuleCounterpartyScreening" } - ] - } - }, - "/v3/id-verification/wallets/mitid/": { - "post": { - "operationId": "start_mitid_wallet_verification", - "description": "Start a MitID sign-in (Danish Agency for Digital Government; DNK) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start MitID verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { + } + }, + "TravelRuleCounterpartyQuestionnaire": { + "type": "object", + "description": "One due-diligence questionnaire your application sent the counterparty: Didit's standard questionnaire, emailed to a compliance address on record with a single-use answer link. Private to your application.", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string", + "enum": [ + "SENT", + "ANSWERED", + "EXPIRED" + ], + "description": "SENT: waiting for answers. ANSWERED: the counterparty answered every question. EXPIRED: the answer link closed unanswered (its deadline passed, or a newer questionnaire replaced it), or answers passed valid_until." + }, + "form_version": { + "type": "string", + "description": "Version of Didit's standard questionnaire the answers belong to. Currently 1." + }, + "sent_to": { + "type": "string", + "format": "email" + }, + "sent_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "expires_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "The answer-by deadline, 30 days after sending. The link stops working after it." + }, + "answered_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "valid_until": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "How long the answers stay current: 365 days after answering. Null until answered." + }, + "current": { + "type": "boolean", + "description": "true while the questionnaire is ANSWERED and valid_until has not passed." + }, + "answers": { + "type": "object", + "additionalProperties": { "type": "string" }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." + "description": "The answers keyed by question id: legal_name, licensing, jurisdictions, aml_program, compliance_officer, sanctions_screening, transaction_monitoring, travel_rule, custody, ownership, record_keeping, data_protection. Each answer is up to 2,000 characters. Empty until answered." } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiMitidWalletVerificationStartRequest" - }, - "example": { - "country": "DNK", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiMitidWalletVerificationStartRequest" - } + } + }, + "TravelRuleCounterpartyQuestionnaireRequest": { + "type": "object", + "properties": { + "sent_to": { + "type": "string", + "format": "email", + "description": "Optional. A compliance address on record for the counterparty: for a directory entry its compliance address or a contact your application sees, for a Didit network member its confirmed compliance email. Default: the address a Travel Rule email to the counterparty goes to (your own contact first, then a platform contact, then the entry's compliance address)." + } + } + }, + "TravelRuleCounterpartyQuestionnaireList": { + "type": "object", + "properties": { + "questionnaires": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TravelRuleCounterpartyQuestionnaire" }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiMitidWalletVerificationStartRequest" - } - } + "description": "Newest first." + } + } + }, + "TravelRulePickupInfo": { + "type": "object", + "description": "The pending exchange behind an emailed pickup link, as the counterparty's response page shows it.", + "properties": { + "reference": { + "type": "string", + "format": "uuid", + "description": "The transfer's uuid, as the sending VASP's console shows it." }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "requested_at": { + "type": "string", + "format": "date-time" }, - { - "BearerAuth": [] - } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "mitid", - "name": "MitID", - "issuing_authority": "Danish Agency for Digital Government" - }, - "country": "DNK", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + "originator_vasp_name": { + "type": "string" + }, + "originator_vasp_lei": { + "type": "string" + }, + "recipient_vasp_name": { + "type": "string", + "description": "The counterparty VASP the request is addressed to." + }, + "language": { + "type": "string", + "description": "The language of the request email and the response page, from the counterparty's jurisdiction." }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "amount": { + "type": "string" }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "currency": { + "type": "string" }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "fiat_amount": { + "type": "string", + "description": "The converted value the sender gave, or empty." }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } + "fiat_currency": { + "type": "string" + }, + "chain": { + "type": "string" + }, + "beneficiary_wallet": { + "type": "string" + }, + "status": { + "type": "string" + }, + "ivms_version": { + "type": "string" + }, + "deadline_at": { + "type": "string", + "format": "date-time", + "description": "When the link stops answering." + }, + "reminders_sent": { + "type": "integer" + }, + "contact": { + "type": "string", + "description": "The `?contact=` value of the link, echoed back; empty when the link has none." + }, + "decline_reasons": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "data_mismatch", + "not_our_customer", + "wallet_not_hosted", + "sanctions", + "risk_appetite", + "jurisdiction", + "insufficient_data", + "other" + ] }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "description": "The values `decline_reason` accepts." }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } + "join_invitation": { + "type": "object", + "description": "Whether the counterparty holds a live invitation to join Didit for free. The invitation link travels only in the email to the invited address, so `url` is empty here.", + "properties": { + "eligible": { + "type": "boolean" + }, + "url": { + "type": "string" } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/mitid/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"DNK\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + } }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'DNK',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/mitid/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" + "originator_data": { + "type": "object", + "description": "The originator fields the sending VASP shares (`data_sharing_fields`). The name and the wallet are always included." + }, + "beneficiary_data": { + "type": "object" + }, + "ivms_request": { + "type": "object", + "description": "The same request as an IVMS 101 object. Left out while `data_masked` is true." + }, + "data_masked": { + "type": "boolean", + "description": "Always false on this endpoint: an email request is outbound, and the sender's own user is never masked." } + } + }, + "TravelRulePickupRespondRequest": { + "type": "object", + "required": [ + "decision" ], - "x-didit-wallet-id": "mitid" - } - }, - "/v3/id-verification/wallets/bankid-se/": { - "post": { - "operationId": "start_bankid_se_wallet_verification", - "description": "Start a BankID (SWE) sign-in (Finansiell ID-Teknik / bank consortium; SWE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start BankID (SWE) verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." + "properties": { + "decision": { + "type": "string", + "enum": [ + "accept", + "decline" + ] + }, + "beneficiary_name": { + "type": "string", + "description": "The beneficiary's name as your records hold it, run through the sending VASP's name rule. `ivms_response` takes precedence when it names the beneficiary.", + "maxLength": 255 + }, + "decline_reason": { + "type": "string", + "enum": [ + "", + "data_mismatch", + "not_our_customer", + "wallet_not_hosted", + "sanctions", + "risk_appetite", + "jurisdiction", + "insufficient_data", + "other" + ], + "description": "Why you decline. Recorded on the sending VASP's transfer timeline. Empty (the default) when you give no reason." + }, + "decline_detail": { + "type": "string", + "maxLength": 512, + "description": "Free text; required when `decision` is `decline` and `decline_reason` is `other`." + }, + "ivms_response": { + "type": "object", + "description": "Your full IVMS 101 response. Its beneficiary name wins over `beneficiary_name`." + }, + "contact": { + "type": "string", + "format": "uuid", + "description": "The contact the link was sent to, from the link's `?contact=` value. When the body leaves it out, the `contact` query parameter is used." } + } + }, + "TravelRuleInboundRequest": { + "type": "object", + "required": [ + "chain", + "txid", + "wallet_address", + "amount", + "currency" ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiBankidSeWalletVerificationStartRequest" + "properties": { + "chain": { + "type": "string", + "description": "Chain the deposit settled on, for example `ETH`. Stored lowercase and compared case-insensitively with your wallet address book entries, so use the same chain token there.", + "maxLength": 32 + }, + "txid": { + "type": "string", + "description": "On-chain transaction hash. Deduped together with wallet_address.", + "maxLength": 128 + }, + "wallet_address": { + "type": "string", + "description": "The destination wallet you received the deposit on. An XRP X-address is split into its classic address and tag.", + "maxLength": 128 + }, + "amount": { + "type": "string", + "description": "Deposit amount as a decimal string, zero or greater, with up to 18 decimal places." + }, + "currency": { + "type": "string", + "description": "Deposit asset, e.g. \"USDC\".", + "maxLength": 16 + }, + "originator_data": { + "type": "object", + "description": "Originator details from the sending VASP, if known, as a flat object such as `{\"name\": \"...\"}`. Didit reads name (or full_name), first_name, last_name and vendor_data from it.", + "additionalProperties": true + }, + "beneficiary_data": { + "type": "object", + "description": "Beneficiary details (your customer), as a flat object such as `{\"name\": \"...\"}`. Didit matches name against the holder_name of the matched address book entry, and sets wallet_address and chain from the request.", + "additionalProperties": true + }, + "originating_vasp": { + "type": "object", + "description": "Optional description of the sending VASP. It stays on this transfer only (`metadata.originating_vasp`, and the transfer's originator VASP in GET /v3/travel-rule/transfers/); it never creates, edits or links a shared VASP directory entry.", + "properties": { + "name": { + "type": "string", + "maxLength": 255 }, - "example": { - "country": "SWE", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiBankidSeWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiBankidSeWalletVerificationStartRequest" + "lei": { + "type": "string", + "maxLength": 20, + "description": "Optional. A valid 20-character ISO 17442 LEI, or empty." + }, + "travel_address": { + "type": "string", + "description": "The sender's travel address, recorded on this transfer.", + "maxLength": 512 } } }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "memo": { + "type": "string", + "maxLength": 256, + "description": "Optional. The deposit's destination tag or memo on a memo network: on a shared deposit address it is what tells your customers apart, and the address-book entry for that memo is matched. XRP: a destination tag, a whole number from 0 to 4294967295 (a JSON number is accepted). XLM: a text memo of up to 28 bytes, a 64-bit id or a 32-byte hash in hex. EOS: up to 256 bytes. ATOM: up to 256 characters. HBAR: up to 100 bytes. Bitcoin, EVM, Solana, Tron, Litecoin, Bitcoin Cash, Dogecoin and Lightning wallets take no memo: one there is a `400`. Alias: `destinationTag` or `destination_tag`." }, - { - "BearerAuth": [] + "utxo": { + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleUtxo" + } + ], + "description": "Optional, UTXO chains only. The deposit transaction's inputs and outputs: the sender's wallet is the declared `originator_data.wallet_address`, else the largest input." + } + } + }, + "TravelRuleInboundResponse": { + "type": "object", + "properties": { + "transaction_id": { + "type": "string", + "format": "uuid", + "description": "Didit transaction UUID for the inbound deposit." + }, + "txn_id": { + "type": "string", + "description": "txn_id of the inbound transaction: Didit-generated when Didit created the transaction, or your own when an unclaimed deposit was reused." + }, + "created": { + "type": "boolean", + "description": "true when this call registered the transfer (HTTP 201); false when a transfer for the same txid and wallet_address already existed (HTTP 200)." + }, + "travel_rule": { + "$ref": "#/components/schemas/TravelRuleTransferDetail" } + } + }, + "TravelRuleTestVaspInboundRequest": { + "type": "object", + "required": [ + "beneficiary_wallet_address" ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "bankid_se", - "name": "BankID", - "issuing_authority": "Finansiell ID-Teknik / bank consortium" - }, - "country": "SWE", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + "properties": { + "beneficiary_wallet_address": { + "type": "string", + "description": "The destination wallet address. The Didit Test VASP sends to it, matched against your wallet address book on the chain of the asset. A wallet with no matching entry is accepted and the inbound transaction is booked ON_HOLD." }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "scenario": { + "type": "string", + "enum": [ + "accept", + "name_mismatch" + ], + "default": "accept", + "description": "What the Didit Test VASP declares the beneficiary to be. `accept` declares the holder's own name; `name_mismatch` declares a name no holder agrees with. The decision itself is always yours." }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "chain": { + "type": "string", + "default": "ETH", + "description": "Chain of the asset." }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "amount": { + "type": "string", + "default": "1", + "description": "Amount as a decimal string." }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "currency": { + "type": "string", + "default": "ETH", + "description": "Asset code." }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." + "reference": { + "type": "string", + "description": "Optional idempotency key. A repeat with the same value returns the transfer the first call booked." } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/bankid-se/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"SWE\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + } + }, + "TravelRuleTestVaspInboundResponse": { + "type": "object", + "properties": { + "transaction_id": { + "type": "string", + "format": "uuid", + "description": "Didit transaction UUID of the inbound transfer." }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'SWE',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/bankid-se/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" + "txn_id": { + "type": "string", + "description": "Didit-generated txn_id." + }, + "scenario": { + "type": "string", + "description": "The scenario that ran." + }, + "travel_rule_status": { + "type": "string", + "description": "The status your application's own decision resolved to." + }, + "detail": { + "type": "string", + "description": "Why the transfer resolved to that status." + }, + "test_vasp": { + "type": "boolean", + "description": "Always true: the sender was the Didit Test VASP." + }, + "travel_rule": { + "$ref": "#/components/schemas/TravelRuleTransferDetail" } - ], - "x-didit-wallet-id": "bankid_se" - } - }, - "/v3/id-verification/wallets/bankid-no/": { - "post": { - "operationId": "start_bankid_no_wallet_verification", - "description": "Start a BankID (NOR) sign-in (BankID BankAxept AS; NOR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start BankID (NOR) verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." + } + }, + "TravelRuleWidgetSessionCreate": { + "type": "object", + "properties": { + "wallet_address": { + "type": "string", + "description": "The wallet address to verify. Optional; when empty, the address must be supplied when a proof is started.", + "maxLength": 128 + }, + "chain": { + "type": "string", + "description": "Chain token for the address, for example `ETH`, `BTC` or `SOL`. Use the same token as in your wallet address book. Message signing and the Satoshi test are offered only for a supported chain.", + "maxLength": 32 + }, + "holder_name": { + "type": "string", + "description": "Name of the wallet holder, used for the beneficiary name match.", + "maxLength": 255 + }, + "vendor_data": { + "type": "string", + "description": "Your internal identifier for the holder, carried onto the created address-book entry.", + "maxLength": 255 + }, + "transaction_id": { + "type": "string", + "format": "uuid", + "description": "Optional. Didit transaction UUID of a Travel Rule transfer to advance when the proof verifies.", + "nullable": true + }, + "satoshi_deposit_address": { + "type": "string", + "description": "Optional. A deposit address you control; supplying it enables the SATOSHI_TEST proof method.", + "maxLength": 128 + }, + "callback_url": { + "type": "string", + "description": "Optional. Absolute http(s) URL where the widget returns the customer after completion.", + "maxLength": 512 + }, + "expires_in_minutes": { + "type": "integer", + "description": "Optional. Lifetime of the minted link, in minutes. Automatically extended to cover the Satoshi test window when SATOSHI_TEST is offered.", + "default": 60, + "minimum": 5, + "maximum": 10080 } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiBankidNoWalletVerificationStartRequest" - }, - "example": { - "country": "NOR", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiBankidNoWalletVerificationStartRequest" - } + } + }, + "TravelRuleWidgetSessionResponse": { + "type": "object", + "properties": { + "widget_session_id": { + "type": "string", + "format": "uuid" + }, + "token": { + "type": "string", + "description": "Opaque session token embedded in the widget URL." + }, + "url": { + "type": "string", + "description": "The hosted wallet-ownership widget URL. Open it verbatim in the system browser (never inside a mobile SDK webview)." + }, + "expires_at": { + "type": "string", + "format": "date-time" + } + } + }, + "TravelRuleWidgetContext": { + "type": "object", + "properties": { + "vasp_name": { + "type": "string", + "description": "Legal name from your VASP profile, shown to the customer in the widget. Empty when no profile exists." + }, + "wallet_address": { + "type": "string" + }, + "chain": { + "type": "string" + }, + "holder_name": { + "type": "string" + }, + "allowed_methods": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "MESSAGE_SIGNING", + "SCREENSHOT", + "SATOSHI_TEST", + "SELF_DECLARATION" + ] }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiBankidNoWalletVerificationStartRequest" - } + "description": "Proof methods available for this session. MESSAGE_SIGNING requires a supported chain family, SATOSHI_TEST requires a satoshi_deposit_address on the session, SCREENSHOT requires the allow_screenshot_proof setting, and SELF_DECLARATION requires the allow_self_declaration setting and an `ownership_validation_mode` that accepts a declaration for the linked transfer (never in `FULL`; in `SIMPLIFIED` only below `simplified_validation_threshold`). Initiating a SELF_DECLARATION the mode does not accept answers 403." + }, + "transfer_status": { + "type": "string", + "nullable": true, + "description": "Status of the linked Travel Rule transfer. Null when the session was minted without a transaction_id." + }, + "callback_url": { + "type": "string", + "description": "Where the widget returns the customer after completion. Empty when not set." + }, + "expires_at": { + "type": "string", + "format": "date-time" + }, + "completed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Non-null once a proof has been verified." + }, + "proofs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TravelRuleWidgetProofSummary" } }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "max_attempts": { + "type": "integer", + "description": "Rejected wallet proofs this session allows." }, - { - "BearerAuth": [] + "attempts_used": { + "type": "integer", + "description": "Rejected proofs so far." + }, + "attempts_remaining": { + "type": "integer", + "description": "When 0, starting another proof answers 409 and you mint a new widget session." + }, + "customization": { + "type": "object", + "nullable": true, + "description": "The application's whitelabel customization (colors, logo, font family) when white label is enabled - the widget renders with the same branding as the verification flow. Null otherwise." } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "bankid_no", - "name": "BankID", - "issuing_authority": "BankID BankAxept AS" - }, - "country": "NOR", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + } + }, + "TravelRuleWidgetProofSummary": { + "type": "object", + "properties": { + "proof_id": { + "type": "string", + "format": "uuid" }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "method": { + "type": "string", + "enum": [ + "MESSAGE_SIGNING", + "SATOSHI_TEST", + "SCREENSHOT", + "SELF_DECLARATION" + ] }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "status": { + "type": "string", + "enum": [ + "PENDING", + "VERIFIED", + "REJECTED", + "EXPIRED" + ], + "description": "PENDING on a SATOSHI_TEST proof means the deposit has been seen on-chain but has not yet reached enough confirmations; a background job automatically rechecks it and moves it to VERIFIED once it confirms, with no resubmission needed. REJECTED means a genuinely wrong deposit was submitted (absent, wrong sender, or wrong amount). EXPIRED means the proof's own test window closed before a valid deposit was seen." }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "wallet_address": { + "type": "string" }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "chain": { + "type": "string" }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/bankid-no/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"NOR\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + "deposit_address": { + "type": "string", + "nullable": true, + "description": "Deposit address the customer must send the Satoshi test amount to. Present only on a SATOSHI_TEST proof." }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'NOR',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/bankid-no/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "bankid_no" - } - }, - "/v3/id-verification/wallets/vipps/": { - "post": { - "operationId": "start_vipps_wallet_verification", - "description": "Start a Vipps sign-in (Vipps MobilePay / BankID NO; NOR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start Vipps verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." - } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiVippsWalletVerificationStartRequest" - }, - "example": { - "country": "NOR", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiVippsWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiVippsWalletVerificationStartRequest" - } - } + "expected_amount": { + "type": "string", + "nullable": true, + "description": "Exact on-chain amount, in the deposit's native currency, the Satoshi test deposit must match. Present only on a SATOSHI_TEST proof." }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "expires_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Deadline for this proof's own Satoshi test window. Present only on a SATOSHI_TEST proof; the widget session's own expires_at is extended to cover it when SATOSHI_TEST is offered." }, - { - "BearerAuth": [] + "challenge": { + "type": "string", + "nullable": true, + "description": "Didit-issued message the customer signs with their wallet. Present only on a MESSAGE_SIGNING proof." + }, + "rejection_reason": { + "type": "string", + "description": "Why the proof was rejected. Present only when `status` is REJECTED." + }, + "created_at": { + "type": "string", + "format": "date-time" } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "vipps", - "name": "Vipps", - "issuing_authority": "Vipps MobilePay / BankID NO" - }, - "country": "NOR", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + } + }, + "CaseSubject": { + "type": "object", + "nullable": true, + "description": "The single user or business the case is anchored to.", + "properties": { + "type": { + "type": "string", + "enum": [ + "user", + "business" + ] }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "uuid": { + "type": "string", + "format": "uuid" }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "name": { + "type": "string" }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "external_id": { + "type": "string", + "nullable": true, + "description": "The subject's vendor_data." }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "country": { + "type": "string", + "nullable": true, + "description": "Three-letter country code of the subject (ISO 3166-1 alpha-3), or null when it is not known. For a business, its country of incorporation. For a person, the issuing country of an identity document from their verifications; a person with documents from several countries shows one of them, and ICAO document codes such as `XXA` can appear.", + "example": "ESP" }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." + "portrait": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Short-lived signed URL of the person's portrait image. Always null for a business, and null for a person without a portrait. The URL expires, so download the image when you read the case instead of storing the URL." } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/vipps/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"NOR\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + } + }, + "CaseAssignee": { + "type": "object", + "nullable": true, + "properties": { + "uuid": { + "type": "string", + "format": "uuid" }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'NOR',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/vipps/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "vipps" - } - }, - "/v3/id-verification/wallets/buypass/": { - "post": { - "operationId": "start_buypass_wallet_verification", - "description": "Start a Buypass ID sign-in (Buypass AS; NOR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start Buypass ID verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." - } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiBuypassWalletVerificationStartRequest" - }, - "example": { - "country": "NOR", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiBuypassWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiBuypassWalletVerificationStartRequest" - } - } + "full_name": { + "type": "string" }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "email": { + "type": "string", + "format": "email" + } + } + }, + "CaseBlueprintSummary": { + "type": "object", + "nullable": true, + "properties": { + "uuid": { + "type": "string", + "format": "uuid" }, - { - "BearerAuth": [] + "name": { + "type": "string", + "maxLength": 24 } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "buypass", - "name": "Buypass ID", - "issuing_authority": "Buypass AS" - }, - "country": "NOR", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" + } + }, + "CaseContentConfig": { + "type": "object", + "description": "The six togglable sections that drive which tabs and fields appear on the case page. Unknown sections or keys are rejected.", + "properties": { + "checklist": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "optional": { + "type": "boolean", + "description": "When true, a case can be resolved with incomplete checklist items." + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "maxLength": 255 + }, + "order": { + "type": "integer" + } + } } } - }, - "description": "" + } }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } + "aml_control": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "financial": { + "type": "boolean" + }, + "identity": { + "type": "boolean" } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + } }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } + "identity": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "personal_information": { + "type": "boolean" + }, + "applicant_tags": { + "type": "boolean" + }, + "contact_information": { + "type": "boolean" + }, + "risk_overview": { + "type": "boolean" + }, + "poi_documents": { + "type": "boolean" + }, + "poa_documents": { + "type": "boolean" } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + } }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } + "verifications": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "summary": { + "type": "boolean" } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + } }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } + "reporting": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "fiu_reports": { + "type": "boolean" } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + } }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } + "financial": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "payment_methods": { + "type": "boolean" + }, + "transactions": { + "type": "boolean" } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." + } } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/buypass/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"NOR\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + } + }, + "CaseNote": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'NOR',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/buypass/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "buypass" - } - }, - "/v3/id-verification/wallets/itsme/": { - "post": { - "operationId": "start_itsme_wallet_verification", - "description": "Start a itsme sign-in (Belgian Mobile ID; BEL, LUX, NLD) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start itsme verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." - } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiItsmeWalletVerificationStartRequest" - }, - "example": { - "country": "BEL", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiItsmeWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiItsmeWalletVerificationStartRequest" - } + "comment": { + "type": "string" + }, + "actor_email": { + "type": "string", + "format": "email", + "nullable": true + }, + "actor_name": { + "type": "string", + "nullable": true + }, + "mentioned_emails": { + "type": "array", + "items": { + "type": "string", + "format": "email" } }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "is_internal": { + "type": "boolean" }, - { - "BearerAuth": [] - } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + "attachments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "s3_key": { + "type": "string" }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "itsme", - "name": "itsme", - "issuing_authority": "Belgian Mobile ID" - }, - "country": "BEL", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" + "filename": { + "type": "string" + }, + "file_size": { + "type": "integer" + }, + "url": { + "type": "string", + "format": "uri" } } - }, - "description": "" + } }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "tags": { + "type": "array", + "items": { + "type": "string" + } }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "created_at": { + "type": "string", + "format": "date-time" + } + } + }, + "CaseEvent": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "event_type": { + "type": "string", + "enum": [ + "CREATED", + "STATUS_CHANGED", + "ASSIGNED", + "UNASSIGNED", + "RESOLVED", + "REOPENED", + "LINK_ADDED", + "LINK_REMOVED", + "NOTE_ADDED", + "NOTE_PINNED", + "NOTE_UNPINNED", + "PRIORITY_CHANGED", + "DUE_DATE_CHANGED", + "TAG_ADDED", + "TAG_REMOVED", + "ESCALATED", + "TRANSFERRED", + "CHECKLIST_COMPLETED", + "REPORT_CREATED", + "REPORT_FINALIZED", + "SUBMITTED_FOR_APPROVAL", + "APPROVAL_GRANTED", + "APPROVAL_REJECTED", + "ENTITY_ACTION", + "APPROVAL_INVALIDATED", + "CUSTOMER_REQUEST", + "STAGE_CHANGED", + "TEAM_CHANGED", + "CHECKER_ASSIGNED", + "CHECKER_UNASSIGNED" + ] }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "actor_email": { + "type": "string", + "format": "email", + "nullable": true }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." + "actor_name": { + "type": "string", + "nullable": true + }, + "previous_status": { + "type": "string", + "enum": [ + "OPEN", + "AWAITING_USER", + "RESOLVED" + ], + "nullable": true + }, + "new_status": { + "type": "string", + "enum": [ + "OPEN", + "AWAITING_USER", + "RESOLVED" + ], + "nullable": true + }, + "metadata": { + "type": "object" + }, + "created_at": { + "type": "string", + "format": "date-time" } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/itsme/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"BEL\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + } + }, + "CaseLink": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'BEL',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/itsme/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" + "linked_entity_type": { + "type": "string", + "enum": [ + "session", + "business_session", + "transaction" + ], + "nullable": true + }, + "linked_entity_uuid": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "linked_entity_summary": { + "type": "object", + "nullable": true, + "description": "session_id/session_number/status for session and business_session links; uuid/txn_id/status for transaction links." + }, + "created_at": { + "type": "string", + "format": "date-time" } - ], - "x-didit-wallet-id": "itsme" - } - }, - "/v3/id-verification/wallets/idin/": { - "post": { - "operationId": "start_idin_wallet_verification", - "description": "Start a iDIN sign-in (Dutch banks (Currence iDIN); NLD) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start iDIN verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." + } + }, + "CaseChecklistItem": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "label": { + "type": "string", + "maxLength": 255 + }, + "is_completed": { + "type": "boolean" + }, + "completed_by": { + "type": "string", + "format": "email", + "nullable": true + }, + "completed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "order": { + "type": "integer" } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiIdinWalletVerificationStartRequest" - }, - "example": { - "country": "NLD", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiIdinWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiIdinWalletVerificationStartRequest" - } + } + }, + "CaseBlueprintListItem": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "maxLength": 24 + }, + "description": { + "type": "string", + "nullable": true + }, + "assignment_method": { + "type": "string", + "enum": [ + "MANUAL", + "AUTOMATIC" + ] + }, + "assignee_pool": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" } }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "max_active_cases": { + "type": "integer", + "nullable": true }, - { - "BearerAuth": [] - } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "idin", - "name": "iDIN", - "issuing_authority": "Dutch banks (Currence iDIN)" - }, - "country": "NLD", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + "escalation_enabled": { + "type": "boolean" }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "transfer_enabled": { + "type": "boolean" }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "require_resolution_note": { + "type": "boolean" }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "is_active": { + "type": "boolean" }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "preset_key": { + "type": "string", + "nullable": true }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/idin/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"NLD\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + "created_at": { + "type": "string", + "format": "date-time" }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'NLD',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/idin/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "idin" - } - }, - "/v3/id-verification/wallets/ftn/": { - "post": { - "operationId": "start_ftn_wallet_verification", - "description": "Start a Finnish Trust Network sign-in (Finnish banks and mobile operators (FTN); FIN) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start Finnish Trust Network verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." + "version": { + "type": "integer", + "readOnly": true + }, + "default_deadline_hours": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 8760 } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiFtnWalletVerificationStartRequest" - }, - "example": { - "country": "FIN", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiFtnWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiFtnWalletVerificationStartRequest" - } + } + }, + "CaseBlueprintDetail": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "maxLength": 24 + }, + "description": { + "type": "string", + "nullable": true + }, + "assignment_method": { + "type": "string", + "enum": [ + "MANUAL", + "AUTOMATIC" + ] + }, + "assignee_pool": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" } }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "max_active_cases": { + "type": "integer", + "nullable": true }, - { - "BearerAuth": [] - } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "ftn", - "name": "Finnish Trust Network", - "issuing_authority": "Finnish banks and mobile operators (FTN)" - }, - "country": "FIN", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + "escalation_enabled": { + "type": "boolean" }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "transfer_enabled": { + "type": "boolean" }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "require_resolution_note": { + "type": "boolean" }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "is_active": { + "type": "boolean" }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "preset_key": { + "type": "string", + "nullable": true }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/ftn/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"FIN\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + "created_at": { + "type": "string", + "format": "date-time" }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'FIN',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/ftn/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "ftn" - } - }, - "/v3/id-verification/wallets/personalausweis/": { - "post": { - "operationId": "start_personalausweis_wallet_verification", - "description": "Start a Personalausweis sign-in (Bundesministerium des Innern (eID); DEU) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start Personalausweis verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." - } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiPersonalausweisWalletVerificationStartRequest" - }, - "example": { - "country": "DEU", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiPersonalausweisWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiPersonalausweisWalletVerificationStartRequest" - } - } + "escalation_blueprint": { + "type": "string", + "format": "uuid", + "nullable": true }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "escalation_blueprint_name": { + "type": "string", + "nullable": true }, - { - "BearerAuth": [] - } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "personalausweis", - "name": "Personalausweis", - "issuing_authority": "Bundesministerium des Innern (eID)" - }, - "country": "DEU", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" + "four_eyes_checker_blueprint": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "four_eyes_checker_blueprint_name": { + "type": "string", + "nullable": true + }, + "content_config": { + "type": "object", + "description": "The six togglable sections that drive which tabs and fields appear on the case page. Unknown sections or keys are rejected.", + "properties": { + "checklist": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "optional": { + "type": "boolean", + "description": "When true, a case can be resolved with incomplete checklist items." + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "maxLength": 255 + }, + "order": { + "type": "integer" + } + } + } + } + } + }, + "aml_control": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "financial": { + "type": "boolean" + }, + "identity": { + "type": "boolean" + } + } + }, + "identity": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "personal_information": { + "type": "boolean" }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" + "applicant_tags": { + "type": "boolean" }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" + "contact_information": { + "type": "boolean" + }, + "risk_overview": { + "type": "boolean" + }, + "poi_documents": { + "type": "boolean" + }, + "poa_documents": { + "type": "boolean" + } } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" + }, + "verifications": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "summary": { + "type": "boolean" + } } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + }, + "reporting": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "fiu_reports": { + "type": "boolean" + } } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." - }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + }, + "financial": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "payment_methods": { + "type": "boolean" + }, + "transactions": { + "type": "boolean" + } } } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + } }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + "creation_sources": { + "type": "array", + "description": "Connectors currently routing new cases into this blueprint.", + "items": { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "aml", + "rule", + "workflow" + ] + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" } } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/personalausweis/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"DEU\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + } }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'DEU',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/personalausweis/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "personalausweis" - } - }, - "/v3/id-verification/wallets/frejaid/": { - "post": { - "operationId": "start_frejaid_wallet_verification", - "description": "Start a Freja eID sign-in (Freja eID Group; SWE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start Freja eID verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." - } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiFrejaidWalletVerificationStartRequest" - }, - "example": { - "country": "SWE", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" + "receives_escalation_from": { + "type": "array", + "description": "Other blueprints whose Escalation target points at this one.", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiFrejaidWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiFrejaidWalletVerificationStartRequest" + "name": { + "type": "string" + } } } }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "updated_at": { + "type": "string", + "format": "date-time" }, - { - "BearerAuth": [] + "version": { + "type": "integer", + "readOnly": true + }, + "default_deadline_hours": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 8760 } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "frejaid", - "name": "Freja eID", - "issuing_authority": "Freja eID Group" - }, - "country": "SWE", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + } + }, + "CaseListItem": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "case_number": { + "type": "string", + "nullable": true, + "description": "Auto-assigned, e.g. CS-0001." }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "title": { + "type": "string" }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "status": { + "type": "string", + "enum": [ + "OPEN", + "AWAITING_USER", + "RESOLVED" + ] }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "resolution": { + "type": "string", + "enum": [ + "FALSE_POSITIVE", + "VALID_THREAT", + "INCONCLUSIVE", + "REQUIREMENTS_NOT_MET", + "REQUIREMENTS_SATISFIED" + ], + "nullable": true }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } + "pending_resolution": { + "type": "object", + "nullable": true, + "description": "Set only while a 4-eyes resolution is staged on the checker blueprint, awaiting approve or reject-approval.", + "properties": { + "resolution": { + "type": "string", + "enum": [ + "FALSE_POSITIVE", + "VALID_THREAT", + "INCONCLUSIVE", + "REQUIREMENTS_NOT_MET", + "REQUIREMENTS_SATISFIED" + ] + }, + "resolution_notes": { + "type": "string", + "nullable": true + }, + "maker_email": { + "type": "string", + "format": "email", + "nullable": true, + "description": "Email of the officer who submitted the resolution. Null when an API key submitted it: that resolution has no recorded maker, so approve and reject-approval return 403 for every caller." + }, + "maker_name": { + "type": "string", + "nullable": true + }, + "maker_user_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Member user uuid of the officer who submitted the resolution. Null when an API key submitted it. That member cannot be named as `checker`." + }, + "maker_blueprint_id": { + "type": "string", + "format": "uuid" + }, + "staged_at": { + "type": "string", + "format": "date-time" } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/frejaid/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"SWE\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + } }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'SWE',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/frejaid/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "frejaid" - } - }, - "/v3/id-verification/wallets/uae-pass/": { - "post": { - "operationId": "start_uae_pass_wallet_verification", - "description": "Start a UAE PASS sign-in (UAE Digital Government Authority; ARE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start UAE PASS verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { + "checker": { + "$ref": "#/components/schemas/CaseChecker" + }, + "priority": { + "type": "string", + "enum": [ + "LOW", + "MEDIUM", + "HIGH" + ] + }, + "source": { + "type": "string", + "enum": [ + "MANUAL", + "AML_SCREENING", + "TRANSACTION_RULE", + "WORKFLOW_BUILDER" + ] + }, + "tags": { + "type": "array", + "items": { "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." - } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiUaePassWalletVerificationStartRequest" - }, - "example": { - "country": "ARE", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiUaePassWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiUaePassWalletVerificationStartRequest" - } } }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "subject": { + "$ref": "#/components/schemas/CaseSubject" }, - { - "BearerAuth": [] - } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "uae_pass", - "name": "UAE PASS", - "issuing_authority": "UAE Digital Government Authority" - }, - "country": "ARE", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + "assigned_to": { + "$ref": "#/components/schemas/CaseAssignee" + }, + "assigned_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "team": { + "$ref": "#/components/schemas/CaseTeamSummary" + }, + "due_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "resolved_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "created_by_email": { + "type": "string", + "nullable": true + }, + "created_by_name": { + "type": "string", + "nullable": true }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "blueprint": { + "$ref": "#/components/schemas/CaseBlueprintSummary" }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "links_count": { + "type": "integer" }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "notes_count": { + "type": "integer" }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "created_at": { + "type": "string", + "format": "date-time" }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/uae-pass/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"ARE\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + "updated_at": { + "type": "string", + "format": "date-time" }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'ARE',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/uae-pass/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "uae_pass" - } - }, - "/v3/id-verification/wallets/govbr/": { - "post": { - "operationId": "start_govbr_wallet_verification", - "description": "Start a gov.br sign-in (Governo Federal do Brasil; BRA) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start gov.br verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." - } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiGovbrWalletVerificationStartRequest" - }, - "example": { - "country": "BRA", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiGovbrWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiGovbrWalletVerificationStartRequest" - } - } + "application_id": { + "type": "string", + "format": "uuid" }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "application_name": { + "type": "string" }, - { - "BearerAuth": [] + "organization_id": { + "type": "string", + "format": "uuid" + }, + "organization_name": { + "type": "string" } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "govbr", - "name": "gov.br", - "issuing_authority": "Governo Federal do Brasil" - }, - "country": "BRA", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + } + }, + "CaseDetail": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "case_number": { + "type": "string", + "nullable": true }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "title": { + "type": "string" }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "status": { + "type": "string", + "enum": [ + "OPEN", + "AWAITING_USER", + "RESOLVED" + ] }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "resolution": { + "type": "string", + "enum": [ + "FALSE_POSITIVE", + "VALID_THREAT", + "INCONCLUSIVE", + "REQUIREMENTS_NOT_MET", + "REQUIREMENTS_SATISFIED" + ], + "nullable": true }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/govbr/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"BRA\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + "resolution_notes": { + "type": "string", + "nullable": true }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'BRA',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/govbr/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "govbr" - } - }, - "/v3/id-verification/wallets/oneid/": { - "post": { - "operationId": "start_oneid_wallet_verification", - "description": "Start a OneID sign-in (OneID (UK bank-verified identity); GBR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start OneID verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { + "priority": { + "type": "string", + "enum": [ + "LOW", + "MEDIUM", + "HIGH" + ] + }, + "source": { + "type": "string", + "enum": [ + "MANUAL", + "AML_SCREENING", + "TRANSACTION_RULE", + "WORKFLOW_BUILDER" + ] + }, + "tags": { + "type": "array", + "items": { "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." - } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiOneidWalletVerificationStartRequest" - }, - "example": { - "country": "GBR", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiOneidWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiOneidWalletVerificationStartRequest" - } } }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "subject": { + "$ref": "#/components/schemas/CaseSubject" }, - { - "BearerAuth": [] - } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "oneid", - "name": "OneID", - "issuing_authority": "OneID (UK bank-verified identity)" - }, - "country": "GBR", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + "assigned_to": { + "$ref": "#/components/schemas/CaseAssignee" }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "assigned_at": { + "type": "string", + "format": "date-time", + "nullable": true }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "team": { + "$ref": "#/components/schemas/CaseTeamSummary" }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "due_at": { + "type": "string", + "format": "date-time", + "nullable": true }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "resolved_at": { + "type": "string", + "format": "date-time", + "nullable": true }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/oneid/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"GBR\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + "resolved_by_email": { + "type": "string", + "nullable": true }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'GBR',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/oneid/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "oneid" - } - }, - "/v3/id-verification/wallets/govuk-wallet/": { - "post": { - "operationId": "start_govuk_wallet_wallet_verification", - "description": "Start a GOV.UK Wallet sign-in (UK Government Digital Service; GBR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start GOV.UK Wallet verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." - } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiGovukWalletWalletVerificationStartRequest" + "pending_resolution": { + "type": "object", + "nullable": true, + "description": "Set only while a 4-eyes resolution is staged on the checker blueprint, awaiting approve or reject-approval.", + "properties": { + "resolution": { + "type": "string", + "enum": [ + "FALSE_POSITIVE", + "VALID_THREAT", + "INCONCLUSIVE", + "REQUIREMENTS_NOT_MET", + "REQUIREMENTS_SATISFIED" + ] + }, + "resolution_notes": { + "type": "string", + "nullable": true + }, + "maker_email": { + "type": "string", + "format": "email", + "nullable": true, + "description": "Email of the officer who submitted the resolution. Null when an API key submitted it: that resolution has no recorded maker, so approve and reject-approval return 403 for every caller." + }, + "maker_name": { + "type": "string", + "nullable": true + }, + "maker_user_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Member user uuid of the officer who submitted the resolution. Null when an API key submitted it. That member cannot be named as `checker`." + }, + "maker_blueprint_id": { + "type": "string", + "format": "uuid" }, - "example": { - "country": "GBR", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiGovukWalletWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiGovukWalletWalletVerificationStartRequest" + "staged_at": { + "type": "string", + "format": "date-time" } } }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "checker": { + "$ref": "#/components/schemas/CaseChecker" }, - { - "BearerAuth": [] - } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "govuk_wallet", - "name": "GOV.UK Wallet", - "issuing_authority": "UK Government Digital Service" - }, - "country": "GBR", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + "metadata": { + "type": "object" }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "blueprint": { + "$ref": "#/components/schemas/CaseBlueprintDetail" }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } + "notes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseNote" }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "description": "Newest 50 notes." }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } + "events": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseEvent" }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "description": "Newest 100 events." }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "links": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseLink" + } }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/govuk-wallet/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"GBR\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + "links_count": { + "type": "integer" }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'GBR',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/govuk-wallet/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "govuk_wallet" - } - }, - "/v3/id-verification/wallets/smart-id/": { - "post": { - "operationId": "start_smart_id_wallet_verification", - "description": "Start a Smart-ID sign-in (SK ID Solutions; EST, LVA, LTU, BEL) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=confirm_in_app with the comparison code to show the user; the user confirms it and enters their PIN in the app (never collect the PIN). Poll POST /v3/id-verification/wallets/verifications/{request_id}/poll/ every poll_interval_seconds, or rely on the webhook: Didit polls in the background too. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start Smart-ID verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." - } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiSmartIdWalletVerificationStartRequest" - }, - "example": { - "country": "EST", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "personal_code": "00000000000" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiSmartIdWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiSmartIdWalletVerificationStartRequest" - } + "notes_count": { + "type": "integer" + }, + "checklist_items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseChecklistItem" } }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "created_at": { + "type": "string", + "format": "date-time" }, - { - "BearerAuth": [] + "updated_at": { + "type": "string", + "format": "date-time" } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "smart_id", - "name": "Smart-ID", - "issuing_authority": "SK ID Solutions" - }, - "country": "EST", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "in_app", - "simulated": true, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "confirm_in_app", - "verification_code": "1234", - "poll_interval_seconds": 2 - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + } + }, + "ReportTemplateDetail": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "name": { + "type": "string" }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "jurisdiction": { + "type": "string", + "minLength": 2, + "maxLength": 2, + "description": "ISO 3166-1 alpha-2 country code." }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "report_type": { + "type": "string", + "enum": [ + "SAR", + "STR", + "CTR", + "TTR", + "UTR", + "IFT", + "CBR", + "TFR" + ] }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "report_type_display": { + "type": "string" }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/smart-id/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"EST\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"personal_code\": \"00000000000\"\n}'" + "field_config": { + "type": "object", + "description": "Reusable goAML header defaults. Template-level obligatory fields: rentity_id, submission_code, and currency_code_local (3-letter). Optional: enabled_fields (which optional report blocks to include) plus reporting_person, location, reason, and action defaults. Per-filing header fields (action, submission_date, transactions, jurisdiction) are enforced when the report is validated or finalized, not on the template." }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'EST',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'personal_code': '00000000000'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/smart-id/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "smart_id" - } - }, - "/v3/id-verification/wallets/mobile-id/": { - "post": { - "operationId": "start_mobile_id_wallet_verification", - "description": "Start a Mobile-ID sign-in (SK ID Solutions with the national mobile operators; EST, LTU) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=confirm_in_app with the comparison code to show the user; the user confirms it and enters their PIN in the app (never collect the PIN). Poll POST /v3/id-verification/wallets/verifications/{request_id}/poll/ every poll_interval_seconds, or rely on the webhook: Didit polls in the background too. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start Mobile-ID verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiMobileIdWalletVerificationStartRequest" - }, - "example": { - "country": "EST", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "personal_code": "00000000000", - "phone_number": "+37200000000" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiMobileIdWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiMobileIdWalletVerificationStartRequest" - } - } + } + }, + "RegulatoryReportDetail": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "case_uuid": { + "type": "string", + "format": "uuid" }, - { - "BearerAuth": [] - } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "mobile_id", - "name": "Mobile-ID", - "issuing_authority": "SK ID Solutions with the national mobile operators" - }, - "country": "EST", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "in_app", - "simulated": true, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "confirm_in_app", - "verification_code": "1234", - "poll_interval_seconds": 2 - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + "case_number": { + "type": "string", + "nullable": true + }, + "report_type": { + "type": "string", + "enum": [ + "SAR", + "STR", + "CTR", + "TTR", + "UTR", + "IFT", + "CBR", + "TFR" + ] + }, + "report_type_display": { + "type": "string" + }, + "report_code": { + "type": "string", + "description": "goAML report code derived from report_type, e.g. STR, CTR, BCR." + }, + "report_number": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string", + "enum": [ + "DRAFT", + "VALIDATED", + "FINALIZED" + ] + }, + "status_display": { + "type": "string" + }, + "jurisdiction": { + "type": "string", + "minLength": 2, + "maxLength": 2 + }, + "rentity_id": { + "type": "string", + "nullable": true, + "description": "Reporting entity ID issued by the FIU." }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "rentity_branch": { + "type": "string", + "nullable": true }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "submission_code": { + "type": "string", + "enum": [ + "E", + "M" + ] }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "entity_reference": { + "type": "string", + "nullable": true }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "fiu_ref_number": { + "type": "string", + "nullable": true }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/mobile-id/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"EST\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"personal_code\": \"00000000000\",\n \"phone_number\": \"+37200000000\"\n}'" + "currency_code_local": { + "type": "string", + "minLength": 3, + "maxLength": 3, + "nullable": true }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'EST',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'personal_code': '00000000000',\n 'phone_number': '+37200000000'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/mobile-id/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "mobile_id" - } - }, - "/v3/id-verification/wallets/idcard-ee/": { - "post": { - "operationId": "start_idcard_ee_wallet_verification", - "description": "Start a Estonian ID-card sign-in (Police and Border Guard Board (Estonia); EST) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start Estonian ID-card verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." - } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiIdcardEeWalletVerificationStartRequest" + "submission_date": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "reporting_person": { + "type": "object" + }, + "location": { + "type": "object" + }, + "reason": { + "type": "string", + "nullable": true + }, + "action": { + "type": "string", + "nullable": true + }, + "activity": { + "type": "string", + "nullable": true + }, + "narrative": { + "type": "object", + "description": "Five Ws narrative sections.", + "properties": { + "who": { + "type": "string" }, - "example": { - "country": "EST", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiIdcardEeWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiIdcardEeWalletVerificationStartRequest" + "what": { + "type": "string" + }, + "when": { + "type": "string" + }, + "where": { + "type": "string" + }, + "why": { + "type": "string" + }, + "how": { + "type": "string" } } }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "report_indicators": { + "type": "array", + "items": { + "type": "string" + } }, - { - "BearerAuth": [] - } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + "included_fields": { + "type": "array", + "items": { + "type": "string" + } + }, + "transactions": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "validation_errors": { + "type": "array", + "description": "Result of the most recent validation; each entry is a {field, message} object.", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string" }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "idcard_ee", - "name": "Estonian ID-card", - "issuing_authority": "Police and Border Guard Board (Estonia)" - }, - "country": "EST", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" + "message": { + "type": "string" } } - }, - "description": "" + } }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "validated_at": { + "type": "string", + "format": "date-time", + "nullable": true }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "finalized_at": { + "type": "string", + "format": "date-time", + "nullable": true }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "created_by_email": { + "type": "string", + "nullable": true }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "created_by_name": { + "type": "string", + "nullable": true }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/idcard-ee/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"EST\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + "created_at": { + "type": "string", + "format": "date-time" }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'EST',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/idcard-ee/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "idcard_ee" - } - }, - "/v3/id-verification/wallets/eparaksts-mobile/": { - "post": { - "operationId": "start_eparaksts_mobile_wallet_verification", - "description": "Start a eParaksts mobile sign-in (LVRTC (Latvian State Radio and Television Centre); LVA) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start eParaksts mobile verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." + "updated_at": { + "type": "string", + "format": "date-time" } + } + }, + "TransactionRuleCondition": { + "type": "object", + "required": [ + "field", + "operator" ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiEparakstsMobileWalletVerificationStartRequest" - }, - "example": { - "country": "LVA", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiEparakstsMobileWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiEparakstsMobileWalletVerificationStartRequest" - } - } + "properties": { + "field": { + "type": "string", + "description": "Transaction field path to evaluate. Core fields: amount, currency, direction, action_type, score, severity, transaction_type, payment_details, preferred_currency_amount (falls back to amount_in_default_currency), default_currency_code, subject_country, counterparty_country, subject_vendor_data, counterparty_vendor_data, subject_device_fingerprint, counterparty_device_fingerprint, subject_browser_family, subject_browser_version, subject_platform, subject_accept_language, subject_session_age_ms, subject_ip_country, counterparty_ip_country, subject_ip_address, counterparty_ip_address, subject_payment_method_type, subject_payment_method_country, subject_payment_method_fingerprint, subject_payment_method_account_id (and the counterparty_payment_method_* equivalents), travel_rule_status, travel_rule_required, travel_rule_obligations_count, counterparty_dd_status (your decision on the Travel Rule counterparty: APPROVED, RESTRICTED or BLOCKED, an entity following its brand's), counterparty_risk_level (LOW, MEDIUM or HIGH; HIGH while your own counterparty screening is flagged), counterparty_jurisdiction (the counterparty VASP's country), counterparty_responsiveness (RESPONSIVE, UNRESPONSIVE or UNKNOWN), subject_days_since_previous_transaction, tags, and custom_values. for any key submitted in custom_properties on transaction create. Country-code fields accept ISO-2 or ISO-3; both sides are normalized to ISO-3 before comparison." }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "operator": { + "type": "string", + "enum": [ + "eq", + "ne", + "gt", + "gte", + "lt", + "lte", + "in", + "not_in", + "contains", + "not_contains", + "contains_any", + "regex", + "fuzzy_match", + "exists", + "is_not_empty", + "is_not_null", + "not_exists", + "is_empty", + "is_null" + ], + "description": "Comparison operator. in/not_in test membership (value may be a list or a single scalar). contains/not_contains are case-insensitive substring checks against a string value. contains_any takes a list of strings and matches when any is a case-insensitive substring of the field value. regex matches the value pattern against the field. fuzzy_match compares against the value string with the required `score` threshold (0-100). gt/gte/lt/lte never match when either side is missing. exists/is_not_empty/is_not_null (aliases) and not_exists/is_empty/is_null (aliases) are presence checks that ignore `value`." }, - { - "BearerAuth": [] + "value": { + "description": "Comparison value. Its shape depends on the operator and value_type; omit for the presence-check operators." + }, + "value_type": { + "type": "string", + "enum": [ + "list", + "field", + "relative_date" + ], + "description": "Optional value interpretation. `list`: value is a List UUID and the list's entries become the comparison value. `field`: value is another field path, compared field-to-field. `relative_date`: value is a relative-date object, e.g. {\"direction\": \"past\", \"unit\": \"days\", \"amount\": 30}." + }, + "score": { + "type": "number", + "minimum": 0, + "maximum": 100, + "description": "Required when operator is fuzzy_match: the 0-100 similarity threshold." + }, + "group_index": { + "type": "integer", + "description": "Optional grouped logic. When any condition carries group_index, conditions sharing an index form a group and grouped evaluation replaces evaluation_mode." + }, + "group_logic": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "default": "AND", + "description": "How conditions combine WITHIN a group." + }, + "groups_logic": { + "type": "string", + "enum": [ + "AND", + "OR" + ], + "default": "OR", + "description": "How group outcomes combine ACROSS groups. Read from the first condition." } + }, + "description": "A condition reads only the transaction being evaluated - it never filters the aggregation window." + }, + "TransactionRuleAggregation": { + "type": "object", + "required": [ + "value" ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "eparaksts_mobile", - "name": "eParaksts mobile", - "issuing_authority": "LVRTC (Latvian State Radio and Television Centre)" - }, - "country": "LVA", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + "properties": { + "metric": { + "type": "string", + "enum": [ + "count", + "sum", + "max", + "min", + "avg", + "distinct_count" + ], + "default": "count", + "description": "How the matching historical transactions are aggregated. unique_count is accepted as an alias of distinct_count." + }, + "field": { + "type": "string", + "default": "amount", + "description": "Field aggregated by sum/max/min/avg/distinct_count. Ignored for count." }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "operator": { + "type": "string", + "enum": [ + "eq", + "ne", + "gt", + "gte", + "lt", + "lte" + ], + "default": "eq", + "description": "Comparison applied to the computed metric." }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "value": { + "type": "number", + "description": "Threshold the computed metric is compared against." }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } + "window": { + "type": "string", + "pattern": "^\\d+(m|h|d)$", + "default": "1d", + "description": "Look-back window ending at the transaction's txn_date: m (MINUTES, not months), h (hours) or d (days), e.g. 30m, 24h, 7d, 30d." + }, + "filters": { + "type": "object", + "additionalProperties": true, + "description": "Narrows which historical transactions are aggregated. Keys are field paths from the condition field catalog; a value of \"__current__\" resolves to the same field's value on the transaction being evaluated (e.g. {\"subject_vendor_data\": \"__current__\"} scopes the window to the same subject), and a list value means membership. Filters compare by equality or membership ONLY - there is no lt/gte filter, so a range such as \"historical transactions under 10,000\" cannot be expressed here. Put the range on the metric instead (metric max with operator lt keeps every transaction in the window below the threshold)." + } + }, + "description": "Velocity check over the application's historical transactions inside the window. Every aggregation entry must match for the rule to match, regardless of evaluation_mode. An aggregation constrains only the window; the transaction being evaluated is constrained by scope and conditions, so repeat direction, currency or transaction_type on both halves when the rule promises them." + }, + "TransactionRuleScope": { + "type": "object", + "properties": { + "transaction_types": { + "type": "array", + "items": { + "type": "string" }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "description": "Restrict to transaction types, e.g. finance, kyc, travel_rule, user_event. Empty or missing = no restriction." }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } + "directions": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "INBOUND", + "OUTBOUND" + ] }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "description": "Restrict to transaction directions (case-insensitive on write)." }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } + "action_types": { + "type": "array", + "items": { + "type": "string" }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." + "description": "Restrict to your action_type values, e.g. withdrawal, deposit." } }, - "x-codeSamples": [ + "description": "Which transactions the rule applies to. Only these three keys are accepted." + }, + "TransactionRuleAction": { + "oneOf": [ { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/eparaksts-mobile/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"LVA\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + "$ref": "#/components/schemas/TransactionRuleActionAddScore" }, { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'LVA',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/eparaksts-mobile/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "eparaksts_mobile" - } - }, - "/v3/id-verification/wallets/bankid-cz/": { - "post": { - "operationId": "start_bankid_cz_wallet_verification", - "description": "Start a Bank iD sign-in (Bankovní identita, a.s.; CZE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start Bank iD verification", - "parameters": [ + "$ref": "#/components/schemas/TransactionRuleActionChangeStatus" + }, { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." - } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiBankidCzWalletVerificationStartRequest" - }, - "example": { - "country": "CZE", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiBankidCzWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiBankidCzWalletVerificationStartRequest" - } - } + "$ref": "#/components/schemas/TransactionRuleActionAddTags" }, - "required": true - }, - "security": [ { - "ApiKeyAuth": [] + "$ref": "#/components/schemas/TransactionRuleActionAddNote" }, { - "BearerAuth": [] + "$ref": "#/components/schemas/TransactionRuleActionAddToList" + }, + { + "$ref": "#/components/schemas/TransactionRuleActionOpenCase" } ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "bankid_cz", - "name": "Bank iD", - "issuing_authority": "Bankovní identita, a.s." - }, - "country": "CZE", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + "discriminator": { + "propertyName": "type", + "mapping": { + "add_score": "#/components/schemas/TransactionRuleActionAddScore", + "change_status": "#/components/schemas/TransactionRuleActionChangeStatus", + "add_tags": "#/components/schemas/TransactionRuleActionAddTags", + "add_note": "#/components/schemas/TransactionRuleActionAddNote", + "add_to_list": "#/components/schemas/TransactionRuleActionAddToList", + "open_case": "#/components/schemas/TransactionRuleActionOpenCase" + } + }, + "description": "One rule action. A rule can carry several; all are applied together when it matches. Rules in TEST mode record what they would have done without applying any action." + }, + "TransactionRuleCreateRequest": { + "type": "object", + "required": [ + "title", + "category", + "mode" + ], + "properties": { + "title": { + "type": "string", + "description": "Rule name. Unique per application among non-deleted rules; duplicates return 400." }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "description": { + "type": "string", + "nullable": true }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "category": { + "type": "string", + "description": "Risk theme used for grouping and reporting, e.g. finance, aml_ctf, anomaly_detection, fatf, device_intelligence, crypto_monitoring, travel_rule, responsible_gaming, e_commerce." }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "mode": { + "type": "string", + "enum": [ + "ACTIVE", + "DISABLED", + "TEST" + ], + "description": "ACTIVE evaluates live. TEST evaluates and records rule runs but never touches score or status - the recommended starting mode. DISABLED is skipped." }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "evaluation_mode": { + "type": "string", + "enum": [ + "ALL", + "ANY" + ], + "default": "ALL", + "description": "Whether every condition must match (ALL) or at least one (ANY). Ignored when conditions use group_index." }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." + "scope": { + "$ref": "#/components/schemas/TransactionRuleScope" + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionRuleCondition" + } + }, + "aggregation": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionRuleAggregation" + } + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionRuleAction" + } + }, + "metadata": { + "type": "object", + "description": "Free-form metadata stored on the rule." } }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/bankid-cz/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"CZE\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + "description": "Creates a CUSTOM rule. Rules in ACTIVE mode take effect immediately for new transactions - create in TEST mode and backtest first." + }, + "TransactionRuleUpdateRequest": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Rule name. Unique per application among non-deleted rules; duplicates return 400." }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'CZE',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/bankid-cz/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "bankid_cz" - } - }, - "/v3/id-verification/wallets/mojeid/": { - "post": { - "operationId": "start_mojeid_wallet_verification", - "description": "Start a MojeID sign-in (CZ.NIC; CZE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start MojeID verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." + "description": { + "type": "string", + "nullable": true + }, + "category": { + "type": "string", + "description": "Risk theme used for grouping and reporting, e.g. finance, aml_ctf, anomaly_detection, fatf, device_intelligence, crypto_monitoring, travel_rule, responsible_gaming, e_commerce." + }, + "mode": { + "type": "string", + "enum": [ + "ACTIVE", + "DISABLED", + "TEST" + ], + "description": "ACTIVE evaluates live. TEST evaluates and records rule runs but never touches score or status - the recommended starting mode. DISABLED is skipped." + }, + "evaluation_mode": { + "type": "string", + "enum": [ + "ALL", + "ANY" + ], + "default": "ALL", + "description": "Whether every condition must match (ALL) or at least one (ANY). Ignored when conditions use group_index." + }, + "scope": { + "$ref": "#/components/schemas/TransactionRuleScope" + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionRuleCondition" + } + }, + "aggregation": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionRuleAggregation" + } + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionRuleAction" + } + }, + "metadata": { + "type": "object", + "description": "Free-form metadata stored on the rule." } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiMojeidWalletVerificationStartRequest" - }, - "example": { - "country": "CZE", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiMojeidWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiMojeidWalletVerificationStartRequest" - } + }, + "description": "Partial update. Custom rules accept every field; library (PRESET) rules accept only mode - anything else returns 400." + }, + "TransactionRuleDetail": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "title": { + "type": "string", + "description": "Rule name. Unique per application among non-deleted rules; duplicates return 400." + }, + "description": { + "type": "string", + "nullable": true + }, + "category": { + "type": "string", + "description": "Risk theme used for grouping and reporting, e.g. finance, aml_ctf, anomaly_detection, fatf, device_intelligence, crypto_monitoring, travel_rule, responsible_gaming, e_commerce." + }, + "mode": { + "type": "string", + "enum": [ + "ACTIVE", + "DISABLED", + "TEST" + ], + "description": "ACTIVE evaluates live. TEST evaluates and records rule runs but never touches score or status - the recommended starting mode. DISABLED is skipped." + }, + "evaluation_mode": { + "type": "string", + "enum": [ + "ALL", + "ANY" + ], + "default": "ALL", + "description": "Whether every condition must match (ALL) or at least one (ANY). Ignored when conditions use group_index." + }, + "scope": { + "$ref": "#/components/schemas/TransactionRuleScope" + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionRuleCondition" + } + }, + "aggregation": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionRuleAggregation" + } + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionRuleAction" } }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "metadata": { + "type": "object", + "description": "Free-form metadata stored on the rule." + }, + "severity": { + "type": "string", + "enum": [ + "UNKNOWN", + "LOW", + "MEDIUM", + "HIGH", + "CRITICAL" + ], + "nullable": true, + "description": "Informational severity carried by library rules; not settable on custom rules." + }, + "source": { + "type": "string", + "enum": [ + "PRESET", + "CUSTOM" + ], + "description": "PRESET rules come from the library (install/uninstall, mode-only edits); CUSTOM rules are fully yours." + }, + "library_key": { + "type": "string", + "nullable": true, + "description": "Stable library identifier; null for custom rules." + }, + "latest_triggered_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "run_count": { + "type": "integer", + "description": "How many transactions matched this rule." }, - { - "BearerAuth": [] - } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "mojeid", - "name": "MojeID", - "issuing_authority": "CZ.NIC" - }, - "country": "CZE", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + "approved_count": { + "type": "integer" }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "reviewed_count": { + "type": "integer" }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "declined_count": { + "type": "integer" }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "awaiting_user_count": { + "type": "integer" }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "approved_pct": { + "type": "number" }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/mojeid/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"CZE\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + "reviewed_pct": { + "type": "number" }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'CZE',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/mojeid/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "mojeid" - } - }, - "/v3/id-verification/wallets/diia/": { - "post": { - "operationId": "start_diia_wallet_verification", - "description": "Start a Diia sign-in (Ministry of Digital Transformation of Ukraine; UKR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start Diia verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." - } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiDiiaWalletVerificationStartRequest" - }, - "example": { - "country": "UKR", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiDiiaWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiDiiaWalletVerificationStartRequest" - } - } + "declined_pct": { + "type": "number" }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "created_at": { + "type": "string", + "format": "date-time" }, - { - "BearerAuth": [] + "updated_at": { + "type": "string", + "format": "date-time" } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "diia", - "name": "Diia", - "issuing_authority": "Ministry of Digital Transformation of Ukraine" - }, - "country": "UKR", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } + } + }, + "TransactionRuleBacktestRequest": { + "type": "object", + "properties": { + "conditions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionRuleCondition" }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "default": [] }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } + "aggregation": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionRuleAggregation" }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "default": [] }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "evaluation_mode": { + "type": "string", + "enum": [ + "ALL", + "ANY" + ], + "default": "ALL" }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "scope": { + "$ref": "#/components/schemas/TransactionRuleScope" }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." + "period_days": { + "type": "integer", + "minimum": 1, + "maximum": 365, + "default": 90 } }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/diia/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"UKR\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + "description": "Hypothetical rule configuration to evaluate against recent transactions. Read-only and free: nothing is created or modified. Send the same conditions, aggregation, evaluation_mode AND scope as the rule you intend to activate. Omitted conditions, aggregation and scope add no constraints of their own; dropping scope can include directions or transaction types the saved rule skips. Omitted evaluation_mode defaults to ALL (which can reduce matches for an ANY rule), and omitted period_days defaults to 90 days, not unlimited history." + }, + "TransactionRuleBacktestResult": { + "type": "object", + "properties": { + "evaluated": { + "type": "integer", + "description": "Transactions evaluated (most recent first, capped)." }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'UKR',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/diia/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "diia" - } - }, - "/v3/id-verification/wallets/franceconnect/": { - "post": { - "operationId": "start_franceconnect_wallet_verification", - "description": "Start a FranceConnect sign-in (DINUM (French state); FRA) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start FranceConnect verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." - } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiFranceconnectWalletVerificationStartRequest" - }, - "example": { - "country": "FRA", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiFranceconnectWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiFranceconnectWalletVerificationStartRequest" - } - } + "matched": { + "type": "integer", + "description": "Transactions the configuration would have matched." }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "affected_entities": { + "type": "integer", + "description": "Distinct subjects (by vendor_data) among the matches." }, - { - "BearerAuth": [] + "period_days": { + "type": "integer" } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "franceconnect", - "name": "FranceConnect", - "issuing_authority": "DINUM (French state)" - }, - "country": "FRA", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + } + }, + "TransactionRuleLibraryEntry": { + "type": "object", + "properties": { + "library_key": { + "type": "string" }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "title": { + "type": "string" }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "description": { + "type": "string", + "nullable": true }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "category": { + "type": "string" }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "evaluation_mode": { + "type": "string", + "enum": [ + "ALL", + "ANY" + ] + }, + "scope": { + "$ref": "#/components/schemas/TransactionRuleScope" + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionRuleCondition" + } + }, + "aggregation": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionRuleAggregation" + } + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionRuleAction" + } + }, + "metadata": { + "type": "object" + }, + "bundle": { + "type": "string", + "nullable": true, + "description": "Functional family, e.g. finance, aml_ctf, crypto_monitoring, travel_rule, responsible_gaming." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/franceconnect/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"FRA\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + "industries": { + "type": "array", + "items": { + "type": "string" + } }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'FRA',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/franceconnect/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" + "source": { + "type": "string" + }, + "is_installed": { + "type": "boolean", + "description": "Whether this preset is currently installed on the application." } - ], - "x-didit-wallet-id": "franceconnect" - } - }, - "/v3/id-verification/wallets/audkenni/": { - "post": { - "operationId": "start_audkenni_wallet_verification", - "description": "Start a Auðkenni sign-in (Auðkenni (Icelandic electronic ID); ISL) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start Auðkenni verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { + } + }, + "TransactionRuleInstallRequest": { + "type": "object", + "properties": { + "library_keys": { + "type": "array", + "items": { "type": "string" }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." + "description": "Specific presets to install or uninstall." + }, + "bundle": { + "type": "string", + "description": "Install or uninstall every preset in this bundle instead." } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiAudkenniWalletVerificationStartRequest" - }, - "example": { - "country": "ISL", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiAudkenniWalletVerificationStartRequest" - } + }, + "description": "Provide library_keys OR bundle." + }, + "TransactionRuleInstallResult": { + "type": "object", + "properties": { + "installed_library_keys": { + "type": "array", + "items": { + "type": "string" }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiAudkenniWalletVerificationStartRequest" - } - } + "description": "POST (install) only: presets installed by this call." }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] + "installed_count": { + "type": "integer", + "description": "POST (install) only." }, - { - "BearerAuth": [] + "uninstalled_count": { + "type": "integer", + "description": "DELETE (uninstall) only: presets removed by this call." } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "audkenni", - "name": "Auðkenni", - "issuing_authority": "Auðkenni (Icelandic electronic ID)" - }, - "country": "ISL", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + } + }, + "WalletScreeningResult": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Provider that performed the screening (e.g. `merklescience`, `crystal`)." }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "screening_type": { + "type": "string", + "enum": [ + "WALLET_SCREENING" + ], + "description": "Always `WALLET_SCREENING` for this endpoint." }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } + "risk_score": { + "type": "integer", + "description": "Normalised 0-100 risk score. Higher means greater exposure to risky entities. Merkle Science grades an address 0-5, which maps to 0, 15, 35, 55, 75 or 95." + }, + "severity": { + "type": "string", + "enum": [ + "UNKNOWN", + "LOW", + "MEDIUM", + "HIGH", + "CRITICAL" + ], + "description": "Risk bucket derived from `risk_score`: `0-9` UNKNOWN, `10-39` LOW, `40-69` MEDIUM, `70-89` HIGH, `90-100` CRITICAL. `UNKNOWN` is the lowest band, not a separate no-data state: `risk_score` 0 (the common clean-address case) means no adverse assessment, while a non-zero score in the 1-9 range is a real but sub-LOW signal - read the `risk_score`, not just the band. Never treat `UNKNOWN` as an affirmative low-risk or clear rating; do not display it as a pass." + }, + "status": { + "type": "string", + "enum": [ + "SCREENED", + "PENDING", + "ERROR" + ], + "description": "Screening outcome status." + }, + "summary": { + "type": "string", + "description": "Human-readable summary of the screening result." + }, + "wallet_address": { + "type": "string", + "description": "The screened address, echoed back." + }, + "transaction_hash": { + "type": "string", + "nullable": true, + "description": "Transaction screening field, shared with the result shape. Always `null` on a wallet screening result." + }, + "blockchain": { + "type": "string", + "description": "The blockchain that was screened." + }, + "block_timestamp": { + "type": "string", + "nullable": true, + "description": "Transaction screening field, shared with the result shape. Always `null` on a wallet screening result." + }, + "total_value_usd": { + "type": "number", + "nullable": true, + "description": "Transaction screening field, shared with the result shape. Always `null` on a wallet screening result." + }, + "fee_usd": { + "type": "number", + "nullable": true, + "description": "Transaction screening field, shared with the result shape. Always `null` on a wallet screening result." + }, + "sender_rows": { + "type": "array", + "items": { + "type": "object" }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "description": "Transaction screening field, shared with the result shape. Always empty on a wallet screening result." }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } + "receiver_rows": { + "type": "array", + "items": { + "type": "object" }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "description": "Transaction screening field, shared with the result shape. Always empty on a wallet screening result." }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } + "transaction_flows": { + "type": "array", + "items": { + "type": "object" }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "description": "Transaction screening field, shared with the result shape. Always empty on a wallet screening result." }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + "transaction_flows_status": { + "type": "string", + "nullable": true, + "description": "Transaction screening field, shared with the result shape. Always `null` on a wallet screening result." + }, + "transaction_flows_error": { + "type": "string", + "nullable": true, + "description": "Transaction screening field, shared with the result shape. Always `null` on a wallet screening result." + }, + "source_of_funds": { + "type": "array", + "description": "Where the address received funds from, attributed by entity. Each entry is an exposure breakdown.", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "description": "Normalised risk category, e.g. `exchange_licensed`, `dex`, `defi`, `payment_processor`, `mixer`, `darknet_market`, `sanctioned`, `stolen_funds`, `high_risk_exchange`." + }, + "percentage": { + "type": "number", + "description": "Share of the direction's total exposure, 0-100." + }, + "amount_usd": { + "type": "number", + "nullable": true, + "description": "USD value attributed to this entity." + }, + "entity_name": { + "type": "string", + "nullable": true, + "description": "Name of the attributed entity, for example an exchange, a mixer or a sanctioned group." + }, + "entity_type": { + "type": "string", + "nullable": true, + "description": "Provider entity type (e.g. `Exchange`, `DeFi`, `Sanctions`, `Theft`)." + }, + "entity_subtype": { + "type": "string", + "nullable": true, + "description": "More granular subtype (e.g. `Mandatory KYC and AML`, `OFAC`, `Yield Aggregator`)." + }, + "hops": { + "type": "integer", + "nullable": true, + "description": "Number of hops to the entity (1 for a direct counterparty), when the provider reports one. Always `null` for Merkle Science, which reports only whether exposure is direct or indirect: use `exposure_type`." + }, + "description": { + "type": "string", + "nullable": true, + "description": "`entity_type / entity_subtype`, as the provider labels the entity." + }, + "is_direct": { + "type": "boolean", + "description": "True when `exposure_type` is `direct`." + }, + "exposure_direction": { + "type": "string", + "enum": [ + "incoming", + "outgoing", + "connected" + ], + "description": "Whether the funds came from (`incoming`) or went to (`outgoing`) this entity." + }, + "exposure_type": { + "type": "string", + "enum": [ + "direct", + "indirect" + ], + "description": "`direct` (1 hop) or `indirect` (multi-hop) exposure." + }, + "provider_entity_id": { + "type": "string", + "nullable": true, + "description": "The provider's own identifier for the entity." + }, + "provider_entity_type_id": { + "type": "string", + "nullable": true, + "description": "The provider's own identifier for `entity_type`." + }, + "provider_entity_subtype_id": { + "type": "string", + "nullable": true, + "description": "The provider's own identifier for `entity_subtype`." + }, + "country": { + "type": "string", + "nullable": true, + "description": "Primary ISO 3166-1 alpha-2 country of the entity. Empty or `null` when the provider does not know it." + }, + "raw_amount": { + "type": "number", + "nullable": true, + "description": "Amount in the asset's own unit, when the provider reports it." + }, + "raw_currency": { + "type": "string", + "nullable": true, + "description": "Unit of `raw_amount`, when the provider reports it." } } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/audkenni/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"ISL\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + } }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'ISL',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/audkenni/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "audkenni" - } - }, - "/v3/id-verification/wallets/connectid/": { - "post": { - "operationId": "start_connectid_wallet_verification", - "description": "Start a ConnectID sign-in (Australian Payments Plus; AUS) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start ConnectID verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." - } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiConnectidWalletVerificationStartRequest" - }, - "example": { - "country": "AUS", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" + "destination_of_funds": { + "type": "array", + "description": "Where the address sent funds to, attributed by entity. Same item shape as `source_of_funds` with `exposure_direction` = `outgoing`.", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "description": "Normalised risk category, e.g. `exchange_licensed`, `dex`, `defi`, `payment_processor`, `mixer`, `darknet_market`, `sanctioned`, `stolen_funds`, `high_risk_exchange`." + }, + "percentage": { + "type": "number", + "description": "Share of the direction's total exposure, 0-100." + }, + "amount_usd": { + "type": "number", + "nullable": true, + "description": "USD value attributed to this entity." + }, + "entity_name": { + "type": "string", + "nullable": true, + "description": "Name of the attributed entity, for example an exchange, a mixer or a sanctioned group." + }, + "entity_type": { + "type": "string", + "nullable": true, + "description": "Provider entity type (e.g. `Exchange`, `DeFi`, `Sanctions`, `Theft`)." + }, + "entity_subtype": { + "type": "string", + "nullable": true, + "description": "More granular subtype (e.g. `Mandatory KYC and AML`, `OFAC`, `Yield Aggregator`)." + }, + "hops": { + "type": "integer", + "nullable": true, + "description": "Number of hops to the entity (1 for a direct counterparty), when the provider reports one. Always `null` for Merkle Science, which reports only whether exposure is direct or indirect: use `exposure_type`." + }, + "description": { + "type": "string", + "nullable": true, + "description": "`entity_type / entity_subtype`, as the provider labels the entity." + }, + "is_direct": { + "type": "boolean", + "description": "True when `exposure_type` is `direct`." + }, + "exposure_direction": { + "type": "string", + "enum": [ + "incoming", + "outgoing", + "connected" + ], + "description": "Whether the funds came from (`incoming`) or went to (`outgoing`) this entity." + }, + "exposure_type": { + "type": "string", + "enum": [ + "direct", + "indirect" + ], + "description": "`direct` (1 hop) or `indirect` (multi-hop) exposure." + }, + "provider_entity_id": { + "type": "string", + "nullable": true, + "description": "The provider's own identifier for the entity." + }, + "provider_entity_type_id": { + "type": "string", + "nullable": true, + "description": "The provider's own identifier for `entity_type`." + }, + "provider_entity_subtype_id": { + "type": "string", + "nullable": true, + "description": "The provider's own identifier for `entity_subtype`." }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiConnectidWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiConnectidWalletVerificationStartRequest" + "country": { + "type": "string", + "nullable": true, + "description": "Primary ISO 3166-1 alpha-2 country of the entity. Empty or `null` when the provider does not know it." + }, + "raw_amount": { + "type": "number", + "nullable": true, + "description": "Amount in the asset's own unit, when the provider reports it." + }, + "raw_currency": { + "type": "string", + "nullable": true, + "description": "Unit of `raw_amount`, when the provider reports it." + } } } }, - "required": true - }, - "security": [ - { - "ApiKeyAuth": [] - }, - { - "BearerAuth": [] - } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" + "counterparty_connections": { + "type": "array", + "description": "Entities the provider attributes the screened address to. For Merkle Science these come from the address's own owner and user tags, so the same entity can appear twice, and they carry no fund flows: `received_usd`, `sent_usd`, `received_hops` and `sent_hops` are `null` and `percentage` is 0. Do not route on `risk_level` or `is_direct` here: use `sanctions_hit`, `dominant_risk_category` and `risk_factors[].is_high_risk` for the verdict, and `exposure_type` in `source_of_funds`/`destination_of_funds` for directness.", + "items": { + "type": "object", + "properties": { + "entity_name": { + "type": "string", + "nullable": true, + "description": "Attributed entity name." }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "connectid", - "name": "ConnectID", - "issuing_authority": "Australian Payments Plus" - }, - "country": "AUS", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" + "entity_type": { + "type": "string", + "nullable": true, + "description": "Provider entity type." + }, + "entity_subtype": { + "type": "string", + "nullable": true, + "description": "Provider entity subtype." + }, + "risk_level": { + "type": "string", + "description": "Lowercase and provider-dependent. For Merkle Science it is the address's own 0-5 risk grade (`no_risk`, `low`, `medium_low`, `medium`, `high`, `critical`), raised to `high` when the entity's category is high risk; it is not a separate grade of the entity." + }, + "categories": { + "type": "array", + "items": { + "type": "string" }, - "created_at": "2026-01-01T10:00:00Z" + "description": "Normalised risk categories of the entity." + }, + "received_usd": { + "type": "number", + "nullable": true, + "description": "USD received from the entity, when the provider reports it." + }, + "sent_usd": { + "type": "number", + "nullable": true, + "description": "USD sent to the entity, when the provider reports it." + }, + "received_hops": { + "type": "integer", + "nullable": true, + "description": "Hops to the entity on the receiving side, when the provider reports them." + }, + "sent_hops": { + "type": "integer", + "nullable": true, + "description": "Hops to the entity on the sending side, when the provider reports them." + }, + "percentage": { + "type": "number", + "description": "Share of the address's exposure, when the provider reports it; otherwise 0." + }, + "is_direct": { + "type": "boolean", + "description": "Always `true` for Merkle Science, whose connections are attributions rather than counterparties." + }, + "country": { + "type": "string", + "nullable": true, + "description": "Country of the entity, when known." + }, + "provider_entity_id": { + "type": "string", + "nullable": true, + "description": "The provider's own identifier for the entity." } } - }, - "description": "" + } }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } + "network_nodes": { + "type": "array", + "items": { + "type": "object" }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "description": "Exposure graph nodes for transaction screening. Empty on a wallet screening result." }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } + "network_edges": { + "type": "array", + "items": { + "type": "object" }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "description": "Exposure graph edges for transaction screening. Empty on a wallet screening result." }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "sanctions_hit": { + "type": "boolean", + "description": "True if the address has direct or indirect sanctions exposure." }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "pep_counterparty": { + "type": "boolean", + "description": "Politically exposed person exposure. `null` when no PEP finding is available, which is every result today because no provider reports one; `false` only for an explicit negative screening result. Do not read `null` as a completed PEP check.", + "nullable": true }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/connectid/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"AUS\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + "dominant_risk_category": { + "type": "string", + "nullable": true, + "description": "Highest-weighted high-risk category, or `null` when none is dominant (e.g. `sanctioned`, `mixer`, `stolen_funds`)." }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'AUS',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/connectid/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "connectid" - } - }, - "/v3/id-verification/wallets/eudi/": { - "post": { - "operationId": "start_eudi_wallet_verification", - "description": "Start a EUDI Wallet sign-in (The user's own member state; the issuer differs per country; AUT, BEL, BGR, HRV, CYP, CZE, DNK, EST, FIN, FRA, DEU, GRC, HUN, IRL, ITA, LVA, LTU, LUX, MLT, NLD, POL, PRT, ROU, SVK, SVN, ESP, SWE, ISL, LIE, NOR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider.", - "summary": "Start EUDI Wallet verification", - "parameters": [ - { - "in": "header", - "name": "Idempotency-Key", - "schema": { - "type": "string" - }, - "description": "Optional retry-safety key (max 255 characters). Retrying with the same key, wallet and body returns the original verification without starting a second sign-in; the same key with another wallet or body returns 409 idempotency_key_reused." - } - ], - "tags": [ - "Digital ID wallets" - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiEudiWalletVerificationStartRequest" - }, - "example": { - "country": "AUT", - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" + "risk_factors": { + "type": "array", + "description": "Ranked explanation of `risk_score`: what produced this verdict. Providers grade an address on their own model and return only a verdict (Merkle Science returns an integer 0-5), so the score alone is not actionable. Ordered high-risk first, then by the value that moved through each driver, capped at the five strongest; `source_of_funds` and `destination_of_funds` always carry the complete breakdown. A high `risk_score` with no `is_high_risk` entry means the provider reported no sanctions or attributed-entity match and graded the address purely on its own exposure model - `summary` states this explicitly.", + "items": { + "type": "object", + "properties": { + "source": { + "type": "string", + "enum": [ + "attribution", + "exposure" + ], + "description": "`attribution` is what the provider says the address itself is (its own tag or cluster); `exposure` is where its funds actually moved." }, - "return_url": "https://app.example.com/identity/complete" - } - }, - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiEudiWalletVerificationStartRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiEudiWalletVerificationStartRequest" + "category": { + "type": "string", + "description": "Normalised risk category, same vocabulary as the exposure breakdown." + }, + "label": { + "type": "string", + "description": "The provider's own wording for the driver (e.g. `Gambling / Crypto only`), which is what you would also see in the provider's console." + }, + "entity_name": { + "type": "string", + "nullable": true, + "description": "Attributed entity behind the driver, for example a gambling site or an exchange." + }, + "direction": { + "type": "string", + "enum": [ + "incoming", + "outgoing", + "connected", + "unknown", + "attribution" + ], + "description": "Direction of the fund flow, `attribution` when the driver is the address's own tag, or `unknown` when the provider did not report a direction." + }, + "exposure_type": { + "type": "string", + "enum": [ + "direct", + "indirect", + "unknown", + "attribution" + ], + "description": "`direct` (1 hop) or `indirect` (multi-hop), `attribution` for the address's own tag, or `unknown` when the provider did not state how many hops away the counterparty is. Treat `unknown` as unclassified, not as `direct`." + }, + "amount_usd": { + "type": "number", + "format": "float", + "nullable": true, + "description": "USD value attributed to this driver." + }, + "percentage": { + "type": "number", + "format": "float", + "description": "Share of the address's TOTAL screened volume, both directions combined - unlike `percentage` inside `source_of_funds`/`destination_of_funds`, which is normalised per direction. Comparable and summable across the whole list." + }, + "is_high_risk": { + "type": "boolean", + "description": "Whether the category is one Didit treats as high risk (`sanctioned`, `mixer`, `stolen_funds`, `ransomware`, `scam`, `darknet_market`, `terrorist_financing`, `child_exploitation`, `hacked_exchange`, `high_risk_exchange`, `seized_funds`)." + }, + "description": { + "type": "string", + "description": "One-line rendering of the driver, e.g. `9.01% indirect outgoing exposure to Gambling / Crypto only (Example Gambling)`." + } } } }, - "required": true + "raw_response": { + "type": "object", + "additionalProperties": true, + "description": "The provider's own payload. Its shape is provider-specific and can change without notice: read the normalised fields instead of parsing it." + }, + "report_signature": { + "type": "string", + "description": "Opaque HMAC-SHA256 signature over this exact result, scoped to your application. Not meaningful on its own — repost the full response body, unmodified and including this field, to `POST /v3/wallet-screening/pdf/` to render it as a PDF. The PDF endpoint rejects the request with `400` if `report_signature` is missing or if any signed field (e.g. `risk_score`, `severity`, `sanctions_hit`) was edited before repost." + } }, - "security": [ - { - "ApiKeyAuth": [] + "required": [ + "provider", + "screening_type", + "risk_score", + "severity", + "status", + "summary", + "wallet_address", + "blockchain", + "sanctions_hit", + "dominant_risk_category", + "source_of_funds", + "destination_of_funds", + "counterparty_connections", + "report_signature" + ] + }, + "NetworkRiskDisplay": { + "type": "object", + "properties": { + "score": { + "type": "integer", + "description": "Risk score, 0-100." }, - { - "BearerAuth": [] + "band": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ], + "description": "Band the score renders as. high >= 70, medium >= 40, otherwise low." + }, + "bar": { + "type": "number", + "format": "float", + "description": "Normalized 0-1 value for rendering a risk bar." } - ], - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResponse" - }, - "example": { - "request_id": "00000000-0000-4000-8000-000000000001", - "wallet_verification": { - "wallet": { - "id": "eudi", - "name": "EUDI Wallet", - "issuing_authority": "The user's own member state; the issuer differs per country" - }, - "country": "AUT", - "status": "Not Finished", - "outcome": "pending", - "error": null, - "interaction": "redirect", - "simulated": false, - "expires_at": "2026-01-01T10:10:00Z", - "next_action": { - "type": "redirect", - "url": "https://wallet.example.com/authorize" - }, - "identity": null, - "attributes": null, - "level_of_assurance": null, - "verified_at": null, - "signature_valid": null, - "credential_type": "person_identification" - }, - "vendor_data": "your-customer-reference", - "metadata": { - "reference": "example" - }, - "created_at": "2026-01-01T10:00:00Z" - } - } - }, - "description": "" + } + }, + "NetworkNodeTag": { + "type": "object", + "properties": { + "id": { + "type": "string" }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartBadRequest" - } - } - }, - "description": "Field validation errors ({field: [messages]}), or error return_url_invalid, return_url_not_allowed, invalid_personal_code, invalid_phone_number or idempotency_key_invalid." + "name": { + "type": "string" }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationStartForbidden" - } - } - }, - "description": "Missing or invalid credentials ({detail}), or error wallet_unavailable, insufficient_credits or contract_rate_missing." + "color": { + "type": "string", + "nullable": true, + "description": "Null for guard tags, whose color token the renderer owns." }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error idempotency_key_reused or idempotency_key_in_progress (with Retry-After)." + "source": { + "type": "string", + "enum": [ + "subject_tag", + "session_tag", + "transaction_tag", + "signal_guard" + ], + "description": "Where the tag came from: a tag on the user/business, a tag on a session in this network, a tag on a transaction a party belongs to, or a signal guard condition (e.g. hub, datacenter IP)." }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_provider_unavailable: the provider could not start; nothing was created." + "category": { + "type": "string", + "enum": [ + "customer", + "quality" + ], + "nullable": true, + "description": "customer for customer-defined tags, quality for signal-quality guard tags. Null when not applicable." + } + } + }, + "NetworkListItem": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "network_id": { + "type": "string", + "description": "Human-readable per-organization sequential id, e.g. N-0001." + }, + "name": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string", + "enum": [ + "active", + "in_review", + "resolved", + "dismissed" + ], + "description": "Network lifecycle status. Distinct from the per-applicant outcome_status inside a network (approved/in_review/declined/not_completed)." }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - } - }, - "description": "error wallet_not_configured: this environment cannot reach the wallet." - } - }, - "x-codeSamples": [ - { - "lang": "curl", - "label": "cURL", - "source": "curl --request POST \\\n --url https://verification.didit.me/v3/id-verification/wallets/eudi/ \\\n --header 'x-api-key: YOUR_API_KEY' \\\n --header 'Idempotency-Key: your-unique-attempt-key' \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"country\": \"AUT\",\n \"vendor_data\": \"your-customer-reference\",\n \"metadata\": {\n \"reference\": \"example\"\n },\n \"return_url\": \"https://app.example.com/identity/complete\"\n}'" + "risk": { + "$ref": "#/components/schemas/NetworkRiskDisplay" }, - { - "lang": "python", - "label": "Python", - "source": "import os\nimport requests\n\nheaders = {\n \"x-api-key\": os.environ[\"DIDIT_API_KEY\"],\n \"Idempotency-Key\": \"your-unique-attempt-key\"\n}\npayload = {'country': 'AUT',\n 'vendor_data': 'your-customer-reference',\n 'metadata': {'reference': 'example'},\n 'return_url': 'https://app.example.com/identity/complete'}\n\nresponse = requests.post(\n \"https://verification.didit.me/v3/id-verification/wallets/eudi/\",\n headers=headers,\n json=payload,\n timeout=30,\n)\nresponse.raise_for_status()\nprint(response.json())\n" - } - ], - "x-didit-wallet-id": "eudi" - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/teams/": { - "get": { - "summary": "List review teams", - "description": "Requires `list:cases` for the requested application.", - "operationId": "list_case_teams", - "tags": [ - "Case Assignment" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { + "pattern_types": { + "type": "array", + "items": { "type": "string", - "format": "uuid" + "enum": [ + "exact_same_device", + "similar_device", + "same_ip_address", + "same_address", + "similar_selfie_backgrounds", + "similar_poa_documents", + "same_document_number", + "same_phone_number", + "same_email" + ] }, - "description": "Organization UUID." + "description": "The clustering patterns detected in this network." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { + "signal_types": { + "type": "array", + "items": { "type": "string", - "format": "uuid" + "enum": [ + "ip_address", + "device", + "face", + "document_number", + "phone", + "email", + "address" + ] }, - "description": "Application UUID." + "description": "The distinct signal types present in this network." + }, + "member_counts": { + "type": "object", + "description": "Counts of members by category (e.g. total, per outcome_status)." + }, + "member_status": { + "type": "object", + "description": "Breakdown of members by outcome_status." + }, + "signal_coverage": { + "type": "object", + "description": "Per signal_type coverage of this network's members." + }, + "first_activity_at": { + "type": "string", + "format": "date-time", + "nullable": true }, + "last_activity_at": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, + "NetworkDetail": { + "allOf": [ { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - } + "$ref": "#/components/schemas/NetworkListItem" }, { - "name": "offset", - "in": "query", - "schema": { - "type": "integer", - "minimum": 0, - "maximum": 1000000, - "default": 0 + "type": "object", + "properties": { + "stats": { + "type": "object", + "description": "Aggregate stats for the network (e.g. member totals)." + }, + "fraud_tags": { + "type": "array", + "items": { + "type": "object" + }, + "description": "Fraud-pattern tags surfaced on the network." + }, + "risk_signals": { + "type": "object", + "description": "Risk-signal summaries: residential_proxy_reuse, email_intelligence, phone_intelligence." + }, + "reviewed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "reviewed_by": { + "type": "string", + "nullable": true + }, + "review_reason": { + "type": "string", + "nullable": true + } } } ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaseReviewTeam" - } - }, - "count": { - "type": "integer", - "description": "Matching count, capped at 100. Follow next to continue beyond the cap." - }, - "next": { - "type": "string", - "nullable": true - }, - "previous": { - "type": "string", - "nullable": true - } - } - } - } - } - }, - "400": { - "description": "Invalid input." + "properties": { + "notes_count": { + "type": "integer", + "minimum": 0, + "readOnly": true, + "description": "Number of private analyst notes in this investigation." + } + } + }, + "NetworkGraphNode": { + "type": "object", + "description": "A subject node or a signal node, distinguished by kind. Subject nodes (kind: vendor_user | vendor_business | transaction_party) carry id, kind, subject, node_status (approved | in_review | declined | not_completed), and tags. Signal nodes (kind: signal) carry id, kind, uuid, signal_type, subject_count, coverage, is_hub, is_datacenter_ip, is_high_collision_device, node_status (signal_active | signal_guarded), tags, risk_evidence, first_seen_at, and last_seen_at.", + "properties": { + "id": { + "type": "string" }, - "403": { - "description": "Insufficient permission." + "kind": { + "type": "string", + "enum": [ + "vendor_user", + "vendor_business", + "transaction_party", + "signal" + ] } } }, - "post": { - "summary": "Create review team", - "description": "Requires `write:cases` for the requested application.", - "operationId": "create_case_teams", - "tags": [ - "Case Assignment" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." + "NetworkGraphEdge": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "source": { + "type": "string", + "description": "Subject node id." + }, + "target": { + "type": "string", + "description": "Signal node id." + }, + "subject_kind": { + "type": "string", + "enum": [ + "vendor_user", + "vendor_business", + "transaction_party" + ] + }, + "signal_type": { + "type": "string", + "enum": [ + "ip_address", + "device", + "face", + "document_number", + "phone", + "email", + "address" + ] + }, + "confidence": { + "type": "number", + "format": "float", + "description": "1.0 for an exact match." + }, + "observation_count": { + "type": "integer" + }, + "first_seen_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "last_seen_at": { + "type": "string", + "format": "date-time", + "nullable": true } - ], - "responses": { - "201": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseReviewTeam" - } - } + } + }, + "NetworkGraph": { + "type": "object", + "properties": { + "network": { + "$ref": "#/components/schemas/NetworkDetail" + }, + "depth": { + "type": "integer", + "nullable": true, + "description": "Effective depth applied, or null when unbounded (depth=all)." + }, + "truncated": { + "type": "boolean", + "description": "True if the neighborhood was cut off by depth." + }, + "nodes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkGraphNode" } }, - "400": { - "description": "Invalid input." + "edges": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkGraphEdge" + } }, - "403": { - "description": "Insufficient permission." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseReviewTeamCreate" + "metrics": { + "type": "object", + "properties": { + "node_count": { + "type": "integer" + }, + "edge_count": { + "type": "integer" + }, + "hub_signal_count": { + "type": "integer" } } } } - } - }, - "/v3/fraud/checks/{check_id}/": { - "get": { - "operationId": "root_retrieve", - "description": "Fetch a previously created check by its `check_id`.", - "summary": "Retrieve a Fraud Check", - "parameters": [ - { - "in": "path", - "name": "check_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FraudCheckResponse" - } - } - }, - "description": "" + }, + "NetworkMember": { + "type": "object", + "properties": { + "subject": { + "type": "object", + "description": "The vendor_user, vendor_business, or transaction_party this member represents." }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FraudClientCredentialsErrorMessage" - } - } - }, - "description": "" + "outcome_status": { + "type": "string", + "description": "The subject's verification/decision outcome." }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FraudClientCredentialsForbiddenErrorMessage" + "node_status": { + "type": "string", + "enum": [ + "approved", + "in_review", + "declined", + "not_completed", + "signal_active", + "signal_guarded" + ], + "description": "For a member row this is always one of the subject-outcome values: approved, in_review, declined, or not_completed." + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkNodeTag" + } + }, + "risk": { + "$ref": "#/components/schemas/NetworkRiskDisplay" + }, + "signal_coverage": { + "type": "object" + }, + "shared_infrastructure": { + "type": "object" + }, + "joined_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "last_seen_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "provenance": { + "type": "object", + "properties": { + "session_ids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "business_session_ids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "transaction_ids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" } } - }, - "description": "" - } - }, - "security": [ - { - "ApiKeyAuth": [] - }, - { - "BearerAuth": [] + } } - ], - "tags": [ - "Fraud Check" - ] - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/": { - "get": { - "operationId": "organization_application_cases_requests_list", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + } + }, + "NetworkSignal": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" }, - { - "in": "path", - "name": "case_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "signal_type": { + "type": "string", + "enum": [ + "ip_address", + "device", + "face", + "document_number", + "phone", + "email", + "address" + ] }, - { - "name": "ordering", - "required": false, - "in": "query", - "description": "Which field to use when ordering the results.", - "schema": { - "type": "string" - } + "subject_count": { + "type": "integer" }, - { - "name": "limit", - "required": false, - "in": "query", - "description": "Number of results to return per page.", - "schema": { - "type": "integer" - } + "coverage": { + "type": "number", + "format": "float" }, - { - "name": "offset", - "required": false, - "in": "query", - "description": "The initial index from which to return the results.", - "schema": { - "type": "integer" - } - } - ], - "tags": [ - "Case Management" - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustomerMessagingPaginatedCaseRequestList" - } - } - }, - "description": "" + "is_hub": { + "type": "boolean", + "description": "True when this signal links an unusually large number of subjects." }, - "404": { - "description": "The request or case is unavailable for this caller." + "is_datacenter_ip": { + "type": "boolean" }, - "403": { - "description": "The caller lacks the required cases permission." - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - }, - "post": { - "operationId": "organization_application_cases_requests_create", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "is_high_collision_device": { + "type": "boolean" }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "node_status": { + "type": "string", + "enum": [ + "signal_active", + "signal_guarded" + ] }, - { - "in": "path", - "name": "case_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - } - ], - "tags": [ - "Case Management" - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CustomerMessagingCaseRequestConfirmRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustomerMessagingCaseRequestConfirmRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/CustomerMessagingCaseRequestConfirmRequest" - } + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkNodeTag" } }, - "required": true - }, - "responses": { - "404": { - "description": "The request or case is unavailable for this caller." + "risk_evidence": { + "type": "object" }, - "403": { - "description": "The caller lacks the required cases permission." + "first_seen_at": { + "type": "string", + "format": "date-time", + "nullable": true }, - "400": { - "description": "Invalid inputs, unsupported purpose, unavailable verified contact or an existing immutable response." + "last_seen_at": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, + "NetworkTimelineEvent": { + "type": "object", + "properties": { + "event_type": { + "type": "string", + "description": "e.g. network_detected, signal_observed, network_in_review, network_resolved, network_dismissed." }, - "409": { - "description": "The preview changed or expired. Preview again before confirming." + "occurred_at": { + "type": "string", + "format": "date-time", + "nullable": true }, - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustomerMessagingCaseRequest" - } - } - }, - "description": "" - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Confirm the exact signed preview within 15 minutes. Only fixed templates and an OTP-verified contact belonging to the case subject are accepted. The request ID makes retries idempotent. Delivery is queued; SENT, SIMULATED and UNKNOWN are distinct outcomes. An actionable request sets the case to Awaiting user. A review update leaves its status unchanged." - } - }, - "/v3/fraud/checks/": { - "post": { - "operationId": "create_fraud_check", - "description": "Submit one or more typed identifiers and receive normalization, fresh enrichment, authoritative validation where the country and identifier type are covered, independent risk dimensions and a recommendation in one response.\n\nThese profiles support identifier checks. Selfie-based identity binding (`biometric_identity`) requires profile version `2026-09-14.2` and a Brazilian CPF; it runs only with the user's consent and when your organization is entitled to it. Earlier versions report it as `not_available_in_profile_version`. Modules that cannot run are reported as skipped with a deterministic reason; a source that could not answer is never reported as an identity mismatch.", - "summary": "Run a Fraud Check", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/FraudCheckRequestRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/FraudCheckRequestRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/FraudCheckRequestRequest" - } - } + "label": { + "type": "string" }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FraudCheckResponse" - } - } - }, - "description": "" + "metadata": { + "type": "object" + } + } + }, + "NetworkMap": { + "type": "object", + "properties": { + "network": { + "$ref": "#/components/schemas/NetworkDetail" }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FraudClientCredentialsErrorMessage" - } - } - }, - "description": "" + "convergence_count": { + "type": "integer", + "description": "Number of distinct address/location points members converge on." }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FraudClientCredentialsForbiddenErrorMessage" - } - } - }, - "description": "" + "countries": { + "type": "array", + "items": { + "type": "string" + } }, - "409": { - "description": "Conflict, returned without a charge: the `client_reference` was already used with a different request body; a check with the same `client_reference` is still running (retry after `Retry-After` seconds with the same reference); or the verification this check records into was deleted before the check completed.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - }, - "required": [ - "error" - ] + "points": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" }, - "examples": { - "different_body": { - "summary": "Reference reused with a different body", - "value": { - "error": "client_reference has already been used with a different request body." - } - }, - "still_running": { - "summary": "A check with this reference is still running", - "value": { - "error": "A check with this client_reference is already running. Retry with the same reference later." - } - }, - "verification_deleted": { - "summary": "The verification was deleted before the check completed", - "value": { - "error": "This verification was deleted before the check completed." - } - } - } - } - }, - "headers": { - "Retry-After": { - "description": "Seconds to wait before retrying, sent when a check with the same `client_reference` is still running.", - "schema": { - "type": "integer" + "country": { + "type": "string", + "nullable": true + }, + "country_code": { + "type": "string", + "nullable": true + }, + "city": { + "type": "string", + "nullable": true + }, + "latitude": { + "type": "number", + "format": "float", + "nullable": true + }, + "longitude": { + "type": "number", + "format": "float", + "nullable": true + }, + "observed_at": { + "type": "string", + "format": "date-time", + "nullable": true } } } } - }, - "security": [ - { - "ApiKeyAuth": [] + } + }, + "EntityNetworkMembership": { + "type": "object", + "properties": { + "network": { + "$ref": "#/components/schemas/NetworkListItem" }, - { - "BearerAuth": [] - } - ], - "tags": [ - "Fraud Check" - ] - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/cancel/": { - "post": { - "operationId": "organization_application_cases_requests_cancel_create", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "subject": { + "type": "object" }, - { - "in": "path", - "name": "application_id", - "schema": { + "signal_types": { + "type": "array", + "items": { "type": "string", - "format": "uuid" - }, - "required": true + "enum": [ + "ip_address", + "device", + "face", + "document_number", + "phone", + "email", + "address" + ] + } }, - { - "in": "path", - "name": "case_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "signal_count": { + "type": "integer" }, - { - "in": "path", - "name": "request_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "joined_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "last_seen_at": { + "type": "string", + "format": "date-time", + "nullable": true } - ], - "tags": [ - "Case Management" - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustomerMessagingCaseRequest" - } - } - }, - "description": "" + } + }, + "RiskSignals": { + "type": "object", + "description": "Optional device and network context observed by your application. All fields are optional. Passing them feeds cross-session match detection in Networks.", + "properties": { + "ip": { + "type": "string", + "format": "ip", + "description": "IPv4 or IPv6 address observed for the end user.", + "example": "192.0.2.1" }, - "404": { - "description": "The request or case is unavailable for this caller." + "device_id": { + "type": "string", + "maxLength": 255, + "description": "Stable device identifier collected by your application (Android `ANDROID_ID`, iOS `identifierForVendor`)." }, - "403": { - "description": "The caller lacks the required cases permission." + "device_platform": { + "type": "string", + "enum": [ + "android", + "ios", + "ipados", + "tvos", + "web" + ], + "description": "Platform of the end user's device." }, - "400": { - "description": "Invalid inputs, unsupported purpose, unavailable verified contact or an existing immutable response." + "device_model": { + "type": "string", + "maxLength": 255, + "description": "Device model reported by your application." }, - "409": { - "description": "The preview changed or expired. Preview again before confirming." - } - }, - "security": [ - { - "ApiKeyAuth": [] + "os_version": { + "type": "string", + "maxLength": 64, + "description": "Operating-system version." + }, + "app_version": { + "type": "string", + "maxLength": 64, + "description": "Version of your application." + }, + "user_agent": { + "type": "string", + "maxLength": 512, + "description": "Browser or app user agent." } - ] - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/options/": { - "get": { - "operationId": "organization_application_cases_requests_options_retrieve", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + } + }, + "PhoneRiskRequest": { + "type": "object", + "required": [ + "phone_number" + ], + "properties": { + "phone_number": { + "type": "string", + "maxLength": 20, + "description": "Phone number to score. It is validated and normalized to E.164.", + "example": "+14155552671" + }, + "country_code": { + "type": "string", + "maxLength": 3, + "description": "Optional ISO 3166-1 alpha-2 or alpha-3 country hint for intelligence coverage.", + "example": "US" }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "signals": { + "$ref": "#/components/schemas/RiskSignals" }, - { - "in": "path", - "name": "case_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "vendor_data": { + "type": "string", + "description": "Your stable reference for this request. Stored on the API session and echoed in the response." + }, + "metadata": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Your non-sensitive JSON metadata. Stored on the API session and echoed in the response." + }, + "enable_social": { + "type": "boolean", + "default": false, + "description": "When true, also look up the social footprint of the number - the messaging, social, e-commerce and professional platforms it is registered on. Billed as one `phone_social` unit per request in addition to this endpoint's own price.", + "example": true } + } + }, + "EmailRiskRequest": { + "type": "object", + "required": [ + "email" ], - "tags": [ - "Case Management" - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustomerMessagingCaseRequestOptions" - } - } - }, - "description": "" + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Email address to score.", + "example": "alice@example.com" }, - "404": { - "description": "The request or case is unavailable for this caller." + "country_code": { + "type": "string", + "maxLength": 3, + "description": "Optional ISO 3166-1 alpha-2 or alpha-3 country hint for intelligence coverage.", + "example": "US" }, - "403": { - "description": "The caller lacks the required cases permission." - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/attachment/": { - "get": { - "operationId": "organization_application_cases_requests_attachment_retrieve", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "signals": { + "$ref": "#/components/schemas/RiskSignals" }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "vendor_data": { + "type": "string", + "description": "Your stable reference for this request. Stored on the API session and echoed in the response." }, - { - "in": "path", - "name": "case_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "metadata": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Your non-sensitive JSON metadata. Stored on the API session and echoed in the response." }, - { - "in": "path", - "name": "request_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "enable_social": { + "type": "boolean", + "default": false, + "description": "When true, also look up the social footprint of the address - the email, social, professional, e-commerce and entertainment platforms it is registered on. Billed as one `email_social` unit per request in addition to this endpoint's own price.", + "example": true } + } + }, + "IPRiskRequest": { + "type": "object", + "required": [ + "ip_address" ], - "tags": [ - "Case Management" - ], - "responses": { - "200": { - "content": { - "image/jpeg": { - "schema": { - "type": "string", - "format": "binary" - } - } - }, - "description": "" + "properties": { + "ip_address": { + "type": "string", + "format": "ip", + "description": "IPv4 or IPv6 address to score.", + "example": "8.8.8.8" }, - "404": { - "description": "The request or case is unavailable for this caller." + "claimed_country": { + "type": "string", + "maxLength": 3, + "description": "Optional ISO 3166-1 alpha-2 or alpha-3 country claimed by the user or session. Used to calculate `country_mismatch`.", + "example": "ESP" }, - "403": { - "description": "The caller lacks the required cases permission." - } - }, - "security": [ - { - "ApiKeyAuth": [] + "user_agent": { + "type": "string", + "maxLength": 512, + "description": "User agent observed by your backend. Stored as request context; it does not provide device fingerprinting." + }, + "vendor_data": { + "type": "string", + "description": "Your stable reference for this request. Stored on the API session and echoed in the response." + }, + "metadata": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Your non-sensitive JSON metadata. Stored on the API session and echoed in the response." } - ] - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/history/": { - "get": { - "operationId": "list_case_routing_rule_history", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + } + }, + "RiskWarning": { + "type": "object", + "required": [ + "feature", + "risk", + "log_type", + "short_description", + "long_description" + ], + "properties": { + "feature": { + "type": "string", + "description": "Feature that produced the warning." }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "risk": { + "type": "string", + "description": "Machine-readable risk code. New values can be added over time." }, - { - "in": "path", - "name": "record_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "additional_data": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Risk-specific context, or `null`." }, - { - "name": "ordering", - "required": false, - "in": "query", - "description": "Which field to use when ordering the results.", - "schema": { - "type": "string" - } + "log_type": { + "type": "string", + "enum": [ + "error", + "warning", + "information" + ], + "description": "Severity used by the decision rules." }, - { - "name": "limit", - "required": false, - "in": "query", - "description": "Number of results to return per page.", - "schema": { - "type": "integer" - } + "short_description": { + "type": "string", + "description": "Human-readable one-line summary." }, - { - "name": "offset", - "required": false, - "in": "query", - "description": "The initial index from which to return the results.", - "schema": { - "type": "integer" - } + "long_description": { + "type": "string", + "description": "Human-readable explanation." } + } + }, + "RiskLifecycleEvent": { + "type": "object", + "required": [ + "type", + "timestamp", + "details", + "fee" ], - "tags": [ - "history" - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoutingPublicationPaginatedPolicyRevisionList" - } - } - }, - "description": "" + "properties": { + "type": { + "type": "string", + "description": "Machine-readable event type. New values can be added over time." }, - "403": { - "description": "Case access is required." - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - } - }, - "/v3/session/{sessionId}/resubmission-preview/": { - "post": { - "summary": "Preview a resubmission email", - "description": "Render the email that would accompany a KYC or KYB resubmission without changing the session, resetting checks, extending its expiry, running checks or sending mail. Requires write:sessions and an application-scoped session in Approved, Declined, In Review, Kyc Expired or Abandoned status. The server validates the selected workflow steps and renders its fixed translated template with the applicable verified white-label sender and branding. The request cannot set a subject, message body or action URL.\n\nThe response includes the recipient, sender, subject, HTML, resolved steps and a signed preview_token. requires_user_action describes whether the selected steps include an interactive check. Email delivery remains conditional: if automatic checks finalize the verification during the subsequent resubmission, no action-request email is sent.\n\nThis preview does not reserve session state or authorize delivery. After reviewing it, call Update Session Status with new_status=Resubmitted, the same selection, recipient and language, and email_preview_token set to the returned preview_token. The receipt expires after 15 minutes. When a receipt is supplied, changed inputs, message content, sender or session state return 409 before any mutation or send. Existing integrations may omit the receipt. Regenerate the preview after a conflict. Treat the response as sensitive verification content; responses use Cache-Control: no-store. Render HTML in a sandboxed frame, never directly in your application DOM.", - "security": [ - { - "ApiKeyAuth": [] - } - ], - "parameters": [ - { - "in": "path", - "name": "sessionId", - "required": true, - "description": "UUID of the verification session to update. Accepts both user (KYC) and business (KYB) session IDs — the service resolves the ID across both session types.", - "schema": { - "type": "string", - "format": "uuid", - "example": "11111111-2222-3333-4444-555555555555" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "email_address" - ], - "properties": { - "nodes_to_resubmit": { - "type": "array", - "description": "Steps to preview, using the same validation, selection and workflow ordering as a resubmission. Omit to auto-select recorded attempts needing resubmission.", - "items": { - "type": "object", - "required": [ - "node_id", - "feature" - ], - "properties": { - "node_id": { - "type": "string", - "description": "Node identifier as it appears in the session's workflow definition (for example `feature_ocr`, `feature_liveness`).", - "example": "feature_ocr" - }, - "feature": { - "type": "string", - "description": "Feature type of the node. The aliases `ID_VERIFICATION`, `POA`, `PHONE`, and `EMAIL` are normalized server-side to `OCR`, `PROOF_OF_ADDRESS`, `PHONE_VERIFICATION`, and `EMAIL_VERIFICATION`. `KYB_REGISTRY`, `KYB_KEY_PEOPLE`, and `KYB` pass schema validation but are always rejected with the business-rule `400` shown in the examples.", - "enum": [ - "OCR", - "OCR_BACK", - "NFC", - "AML", - "FACE", - "LIVENESS", - "FACE_MATCH", - "IP_ANALYSIS", - "AGE_ESTIMATION", - "PROOF_OF_ADDRESS", - "PHONE_VERIFICATION", - "EMAIL_VERIFICATION", - "FACE_SEARCH", - "DATABASE_VALIDATION", - "QUESTIONNAIRE", - "DOCUMENT_AI", - "KYB_DOCUMENTS", - "ID_VERIFICATION", - "POA", - "PHONE", - "EMAIL", - "KYB_REGISTRY", - "KYB_KEY_PEOPLE", - "KYB" - ], - "example": "OCR" - } - } - } - }, - "email_address": { - "type": "string", - "format": "email", - "description": "Recipient shown in the preview. No email is sent.", - "example": "user@example.com" - }, - "email_language": { - "type": "string", - "description": "Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`).", - "default": "en", - "example": "en" - } - } - } - } + "timestamp": { + "type": "string", + "format": "date-time", + "description": "When the event happened." + }, + "details": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Event-specific payload, or `null`." + }, + "fee": { + "type": "number", + "description": "USD amount billed for this event." } - }, - "responses": { - "200": { - "description": "Preview only. The session is unchanged and no email is sent.", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "recipient", - "sender", - "subject", - "html", - "nodes_to_resubmit", - "requires_user_action", - "preview_token" - ], - "properties": { - "recipient": { - "type": "string", - "format": "email" - }, - "sender": { - "type": "string" - }, - "subject": { - "type": "string" - }, - "html": { - "type": "string", - "description": "Rendered email. Inline image attachments use data URLs for preview." - }, - "nodes_to_resubmit": { - "type": "array", - "items": { - "type": "object", - "required": [ - "node_id", - "feature" - ], - "properties": { - "node_id": { - "type": "string", - "description": "Node identifier as it appears in the session's workflow definition (for example `feature_ocr`, `feature_liveness`).", - "example": "feature_ocr" - }, - "feature": { - "type": "string", - "description": "Feature type of the node. The aliases `ID_VERIFICATION`, `POA`, `PHONE`, and `EMAIL` are normalized server-side to `OCR`, `PROOF_OF_ADDRESS`, `PHONE_VERIFICATION`, and `EMAIL_VERIFICATION`. `KYB_REGISTRY`, `KYB_KEY_PEOPLE`, and `KYB` pass schema validation but are always rejected with the business-rule `400` shown in the examples.", - "enum": [ - "OCR", - "OCR_BACK", - "NFC", - "AML", - "FACE", - "LIVENESS", - "FACE_MATCH", - "IP_ANALYSIS", - "AGE_ESTIMATION", - "PROOF_OF_ADDRESS", - "PHONE_VERIFICATION", - "EMAIL_VERIFICATION", - "FACE_SEARCH", - "DATABASE_VALIDATION", - "QUESTIONNAIRE", - "DOCUMENT_AI", - "KYB_DOCUMENTS", - "ID_VERIFICATION", - "POA", - "PHONE", - "EMAIL", - "KYB_REGISTRY", - "KYB_KEY_PEOPLE", - "KYB" - ], - "example": "OCR" - } - } - } - }, - "requires_user_action": { - "type": "boolean", - "description": "Whether at least one selected check requires user interaction. This does not predict the final workflow outcome." - }, - "preview_token": { - "type": "string", - "description": "Signed confirmation receipt, valid for 15 minutes. Pass it as email_preview_token when confirming the same message." - } - } - } - } - } + } + }, + "PhoneMatchItem": { + "type": "object", + "description": "Another session of your application that used the same phone number with a different `vendor_data`.", + "properties": { + "session_id": { + "type": "string", + "format": "uuid", + "description": "Session id of the other verification that used this phone number." }, - "400": { - "description": "Invalid email, state or workflow selection." + "session_number": { + "type": "integer", + "description": "Human-friendly sequential number of that session." }, - "403": { - "description": "Authentication or write:sessions permission is missing." + "vendor_data": { + "type": "string", + "nullable": true, + "description": "`vendor_data` of that session." }, - "404": { - "description": "No session exists in the requested application." + "verification_date": { + "type": "string", + "description": "When that session was created (`YYYY-MM-DDTHH:MM:SSZ`)." + }, + "phone_number": { + "type": "string", + "description": "The matched phone number." + }, + "status": { + "type": "string", + "description": "Status of that session (e.g., `Approved`, `Declined`, `In Progress`)." + }, + "is_blocklisted": { + "type": "boolean", + "description": "Whether the phone number is on your application's blocklist." + }, + "api_service": { + "type": "string", + "nullable": true, + "description": "For API-type sessions, the standalone API that created it; `null` for regular verification sessions." + }, + "source": { + "type": "string", + "description": "Where the match was found. Usually `session`; a blocklist hit via an organization list entry uses `list_entry`." } } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/": { - "get": { - "summary": "Retrieve routing rule", - "description": "Requires `read:cases` for the requested application.", - "operationId": "retrieve_case_routing_rules", - "tags": [ - "Case Assignment" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." + }, + "EmailMatchItem": { + "type": "object", + "description": "Another session of your application that used the same email address with a different `vendor_data`.", + "properties": { + "session_id": { + "type": "string", + "format": "uuid", + "description": "Session id of the other verification that used this email address." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "session_number": { + "type": "integer", + "description": "Human-friendly sequential number of that session." }, - { - "name": "record_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseRoutingRule" - } - } - } + "vendor_data": { + "type": "string", + "nullable": true, + "description": "`vendor_data` of that session." }, - "400": { - "description": "Invalid input." + "verification_date": { + "type": "string", + "description": "When that session was created (`YYYY-MM-DDTHH:MM:SSZ`)." }, - "403": { - "description": "Insufficient permission." + "email": { + "type": "string", + "description": "The matched email address." }, - "404": { - "description": "Record not found in this application." + "status": { + "type": "string", + "description": "Status of that session (e.g., `Approved`, `Declined`, `In Progress`)." + }, + "is_blocklisted": { + "type": "boolean", + "description": "Whether the email address is on your application's blocklist." + }, + "api_service": { + "type": "string", + "nullable": true, + "description": "For API-type sessions, the standalone API that created it; `null` for regular verification sessions." + }, + "source": { + "type": "string", + "description": "Where the match was found. Usually `session`; a blocklist hit via an organization list entry uses `list_entry`." } } }, - "patch": { - "summary": "Update routing rule", - "description": "Requires `write:cases` for the requested application.", - "operationId": "update_case_routing_rules", - "tags": [ - "Case Assignment" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." + "PhoneIntelligence": { + "type": "object", + "description": "Normalized phone-intelligence block. Present only when the intelligence add-on ran for this lookup; when present it is also exposed under the `enrichment` alias with identical values.", + "properties": { + "score": { + "type": "number", + "nullable": true, + "description": "Normalized 0-100 risk score. Higher is riskier." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "phone_type_risk": { + "type": "string", + "nullable": true, + "description": "`Low`, `Medium` or `High`, derived from the score." }, - { - "name": "record_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseRoutingRule" - } - } - } + "phone_trust_index": { + "type": "number", + "nullable": true, + "description": "Inverse of the score, for dashboards that prefer higher is better." }, - "400": { - "description": "Invalid input." + "network_stability_index": { + "type": "number", + "nullable": true, + "description": "Stability of the line's network." }, - "403": { - "description": "Insufficient permission." + "is_disposable": { + "type": "boolean", + "nullable": true, + "description": "`true` when the number looks disposable or virtual." }, - "404": { - "description": "Record not found in this application." + "is_ported": { + "type": "boolean", + "nullable": true, + "description": "`true` when the number has moved carrier." }, - "409": { - "description": "The routing rule changed since expected_version. Review the latest rule before retrying." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseRoutingRuleUpdate" - } - } + "is_recently_ported": { + "type": "boolean", + "nullable": true, + "description": "`true` when the port happened recently." + }, + "carrier_type": { + "type": "string", + "nullable": true, + "description": "Line classification reported by the lookup (e.g., `mobile`)." + }, + "reason_codes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Machine-readable codes explaining the score. Values can grow over time, so do not reject unknown codes." } } - } - }, - "/v3/case-requests/{request_uuid}/": { - "get": { - "operationId": "case_requests_retrieve", - "parameters": [ - { - "in": "path", - "name": "request_uuid", - "schema": { - "type": "string", - "format": "uuid" + }, + "EmailIntelligence": { + "type": "object", + "description": "Normalized email-intelligence block. Present only when the intelligence add-on ran for this email; when present it is also exposed as the `enrichment` alias with identical values.", + "properties": { + "score": { + "type": "number", + "nullable": true, + "description": "Normalized 0-100 risk score. Higher is riskier." + }, + "deliverable": { + "type": "string", + "nullable": true, + "description": "Deliverability band (e.g., `high`)." + }, + "breach_risk_level": { + "type": "string", + "nullable": true, + "description": "`Low`, `Medium` or `High`." + }, + "breach_risk_index": { + "type": "number", + "nullable": true, + "description": "Numeric breach-risk index." + }, + "is_recent_breach": { + "type": "boolean", + "nullable": true, + "description": "`true` when the address appears in a recent breach." + }, + "is_frequent_breached": { + "type": "boolean", + "nullable": true, + "description": "`true` when the address appears in many breaches." + }, + "is_disposable": { + "type": "boolean", + "nullable": true, + "description": "`true` when the domain is a throwaway provider." + }, + "is_free_domain": { + "type": "boolean", + "nullable": true, + "description": "`true` when the domain is a free mail provider." + }, + "is_breached": { + "type": "boolean", + "nullable": true, + "description": "`true` when the address appears in known breaches." + }, + "no_of_breaches": { + "type": "number", + "nullable": true, + "description": "Number of known breaches containing the address." + }, + "reason_codes": { + "type": "array", + "items": { + "type": "string" }, - "required": true + "description": "Machine-readable codes explaining the score. Values can grow over time, so do not reject unknown codes." } + } + }, + "IPRiskResult": { + "type": "object", + "description": "IP risk result. Individual enrichment fields can be `null` when the lookup returned no usable data.", + "required": [ + "status", + "threat" ], - "tags": [ - "Case Management" - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustomerMessagingCaseRequestPublic" - } - } - }, - "description": "" + "properties": { + "status": { + "type": "string", + "enum": [ + "Approved", + "In Review" + ], + "description": "`In Review` when the resolved country conflicts with `claimed_country`, or when the address is associated with VPN, Tor, or data-center infrastructure. Otherwise `Approved`." }, - "404": { - "description": "The request or case is unavailable for this caller." - } - }, - "security": [ - { - "CaseRequestToken": [] + "ip_address": { + "type": "string", + "nullable": true, + "description": "The scored address. `null` when no enrichment source returned usable data." + }, + "ip_country": { + "type": "string", + "nullable": true, + "description": "Resolved country name." + }, + "ip_country_code": { + "type": "string", + "nullable": true, + "description": "Resolved country code. Compare countries through `country_mismatch` instead of comparing raw country strings yourself." + }, + "ip_state": { + "type": "string", + "nullable": true, + "description": "Resolved state or region." + }, + "ip_city": { + "type": "string", + "nullable": true, + "description": "Resolved city." + }, + "latitude": { + "type": "number", + "nullable": true, + "description": "Approximate network geolocation, not a precise device location." + }, + "longitude": { + "type": "number", + "nullable": true, + "description": "Approximate network geolocation, not a precise device location." + }, + "isp": { + "type": "string", + "nullable": true, + "description": "Internet service provider serving the address." + }, + "organization": { + "type": "string", + "nullable": true, + "description": "Organization owning the address block." + }, + "is_vpn_or_tor": { + "type": "boolean", + "nullable": true, + "description": "`true` when the address is associated with VPN or Tor infrastructure." + }, + "is_data_center": { + "type": "boolean", + "nullable": true, + "description": "`true` when the address belongs to a hosting or data-center network." + }, + "time_zone": { + "type": "string", + "nullable": true, + "description": "Time zone of the resolved location." + }, + "time_zone_offset": { + "type": "string", + "nullable": true, + "description": "UTC offset of the resolved location." + }, + "asn_number": { + "type": "number", + "nullable": true, + "description": "Autonomous system number." + }, + "asn_organization": { + "type": "string", + "nullable": true, + "description": "Organization operating the autonomous system." + }, + "connection_type": { + "type": "string", + "nullable": true, + "description": "Connection classification (e.g., `cellular`, `hosting`)." + }, + "carrier": { + "type": "string", + "nullable": true, + "description": "Mobile carrier when the address is on a mobile network." + }, + "proxy_type": { + "type": "string", + "nullable": true, + "description": "Proxy classification when the address is behind a proxy." + }, + "claimed_country": { + "type": "string", + "nullable": true, + "description": "Echo of the `claimed_country` you sent." + }, + "country_mismatch": { + "type": "boolean", + "nullable": true, + "description": "`true` when the resolved country differs from `claimed_country`. `null` when no enrichment data or no claimed country is available." + }, + "threat": { + "type": "object", + "additionalProperties": true, + "description": "Free-form network-threat evidence from the enrichment source (e.g., `is_proxy`, `is_tor`). Empty object when no enrichment data is available. Treat as diagnostic detail, not a stable contract." } - ], - "description": "Access one customer request using its request-specific token. No private case findings, original identity data or recipient address are returned. Actionable requests expire after seven days. Cancellation or case resolution invalidates the link. Responses are immutable and remain evidence for reviewer assessment; they never approve or update a source verification." + } }, - "post": { - "operationId": "case_requests_create", - "parameters": [ - { - "in": "path", - "name": "request_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - } - ], - "tags": [ - "Case Management" + "PhoneRiskResponse": { + "type": "object", + "required": [ + "request_id", + "status", + "created_at" ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustomerMessagingCaseCustomerResponseRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/CustomerMessagingCaseCustomerResponseRequest" - } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustomerMessagingCaseCustomerResponseResult" - } - } - }, - "description": "" + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Id of the API session created for this request. Store it with `vendor_data` if you need to correlate the result with later webhooks or console activity." + }, + "status": { + "type": "string", + "enum": [ + "Approved", + "In Review", + "Declined" + ], + "description": "Final decision for this API session. Normally `Approved`; the risk evaluation can produce `In Review` or `Declined`." }, - "404": { - "description": "The request or case is unavailable for this caller." + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Echo of the `vendor_data` you sent, `null` when you did not send one." }, - "400": { - "description": "Invalid inputs, unsupported purpose, unavailable verified contact or an existing immutable response." + "metadata": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Echo of the `metadata` you sent, `null` when you did not send one." }, - "409": { - "description": "The preview changed or expired. Preview again before confirming." - } - }, - "security": [ - { - "CaseRequestToken": [] - } - ], - "description": "Access one customer request using its request-specific token. No private case findings, original identity data or recipient address are returned. Actionable requests expire after seven days. Cancellation or case resolution invalidates the link. Responses are immutable and remain evidence for reviewer assessment; they never approve or update a source verification." - } - }, - "/v3/fraud/checks/plan/": { - "post": { - "operationId": "plan_create", - "description": "Validate selected checks and estimate their price without executing, creating a session or billing.", - "summary": "Plan a Fraud Check", - "tags": [ - "Fraud Check" - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/FraudCheckRequestRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/FraudCheckRequestRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/FraudCheckRequestRequest" - } - } + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of this response." }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FraudCheckPlan" + "phone": { + "type": "object", + "description": "Normalized phone report, the same block used by Phone Verification responses.", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "Approved", + "In Review", + "Declined" + ], + "description": "Decision for this phone number. Mirrors the top-level `status`." + }, + "phone_number_prefix": { + "type": "string", + "description": "Country calling code with leading `+` (e.g., `+1`).", + "example": "+1" + }, + "phone_number": { + "type": "string", + "description": "National number without the country calling code.", + "example": "4155552671" + }, + "full_number": { + "type": "string", + "description": "Complete number in E.164 format.", + "example": "+14155552671" + }, + "country_code": { + "type": "string", + "nullable": true, + "description": "ISO 3166-1 alpha-2 country of the number.", + "example": "US" + }, + "country_name": { + "type": "string", + "description": "Full country name derived from `country_code`.", + "example": "United States" + }, + "carrier": { + "type": "object", + "nullable": true, + "description": "Current network serving the number. `null` when the lookup could not resolve a carrier.", + "properties": { + "name": { + "type": "string", + "description": "Carrier name. Empty string when the lookup could not identify it." + }, + "type": { + "type": "string", + "description": "Line type reported by the lookup: `mobile`, `fixed_line`, `voip`, `isp`, `vpn`, `toll_free`, `premium_rate`, or similar." + } } - } - }, - "description": "" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FraudBadRequestErrorMessage" + }, + "is_disposable": { + "type": "boolean", + "description": "`true` when the number belongs to a temporary or burner number service." + }, + "is_virtual": { + "type": "boolean", + "description": "`true` when the line type is virtual (`voip`, `isp`, or `vpn`)." + }, + "verification_method": { + "type": "string", + "nullable": true, + "description": "OTP channel of the underlying verification. `null` for risk lookups, which never send a code." + }, + "verification_attempts": { + "type": "integer", + "description": "OTP sends for this number. `0` for risk lookups." + }, + "verified_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the number was last verified by an OTP. `null` for risk lookups." + }, + "warnings": { + "type": "array", + "description": "Risk warnings recorded for this request. Empty array when there are none.", + "items": { + "$ref": "#/components/schemas/RiskWarning" + } + }, + "lifecycle": { + "type": "array", + "description": "Chronological audit trail of the request. Empty array for risk lookups without send events.", + "items": { + "$ref": "#/components/schemas/RiskLifecycleEvent" } + }, + "matches": { + "type": "array", + "description": "Other sessions of your application where the same phone number was used by a different user (different `vendor_data`). Up to 5 entries. Empty array when there are no matches.", + "items": { + "$ref": "#/components/schemas/PhoneMatchItem" + } + }, + "phone_intelligence": { + "$ref": "#/components/schemas/PhoneIntelligence" + }, + "enrichment": { + "$ref": "#/components/schemas/PhoneIntelligence" + }, + "phone_social": { + "$ref": "#/components/schemas/PhoneSocialFootprint" } - }, - "description": "" - } - }, - "security": [ - { - "ApiKeyAuth": [] - }, - { - "BearerAuth": [] + } } - ] - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/": { - "get": { - "summary": "List routing rules", - "description": "Requires `list:cases` for the requested application.", - "operationId": "list_case_routing_rules", - "tags": [ - "Case Assignment" + } + }, + "EmailRiskResponse": { + "type": "object", + "required": [ + "request_id", + "status", + "created_at" ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Id of the API session created for this request. Store it with `vendor_data` if you need to correlate the result with later webhooks or console activity." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "status": { + "type": "string", + "enum": [ + "Approved", + "In Review", + "Declined" + ], + "description": "Final decision for this API session. Normally `Approved`; the risk evaluation can produce `In Review` or `Declined`." }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - } + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Echo of the `vendor_data` you sent, `null` when you did not send one." }, - { - "name": "offset", - "in": "query", - "schema": { - "type": "integer", - "minimum": 0, - "maximum": 1000000, - "default": 0 - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { + "metadata": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Echo of the `metadata` you sent, `null` when you did not send one." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of this response." + }, + "email": { + "type": "object", + "description": "Normalized email report, the same block used by Email Verification responses.", + "required": [ + "status", + "email", + "is_breached", + "breaches", + "is_disposable", + "is_undeliverable" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "Approved", + "In Review", + "Declined" + ], + "description": "Decision for this email address. Mirrors the top-level `status`." + }, + "email": { + "type": "string", + "description": "The scored address.", + "example": "alice@example.com" + }, + "is_breached": { + "type": "boolean", + "description": "Whether the address appears in a known credential breach." + }, + "breaches": { + "type": "array", + "description": "Up to 5 most recent known breaches containing this address. Empty array when `is_breached` is `false`.", + "items": { "type": "object", "properties": { - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaseRoutingRule" - } + "name": { + "type": "string", + "description": "Breach identifier." }, - "count": { + "domain": { + "type": "string", + "description": "Domain of the breached service." + }, + "breach_date": { + "type": "string", + "format": "date", + "description": "Date the breach occurred." + }, + "breach_emails_count": { "type": "integer", - "description": "Matching count, capped at 100. Follow next to continue beyond the cap." + "description": "Total number of accounts exposed in the breach." }, - "next": { + "description": { "type": "string", - "nullable": true + "description": "Human-readable description of the breach (may contain HTML markup)." }, - "previous": { + "logo_path": { "type": "string", - "nullable": true + "description": "URL of the breached service's logo." + }, + "data_classes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Categories of data exposed, in snake_case (e.g., `email_addresses`, `passwords`)." + }, + "is_verified": { + "type": "boolean", + "description": "Whether the breach has been verified as legitimate." } } } - } - } - }, - "400": { - "description": "Invalid input." - }, - "403": { - "description": "Insufficient permission." - } - } - }, - "post": { - "summary": "Create routing rule", - "description": "Requires `write:cases` for the requested application. Set publish=false to create an unpublished draft; Publish creates its first live version. Discarding a never-published draft deletes it.", - "operationId": "create_case_routing_rules", - "tags": [ - "Case Assignment" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." - }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - } - ], - "responses": { - "201": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseRoutingRule" + }, + "is_disposable": { + "type": "boolean", + "description": "Whether the domain belongs to a throwaway provider." + }, + "is_undeliverable": { + "type": "boolean", + "description": "Whether the address was found undeliverable." + }, + "verification_attempts": { + "type": "integer", + "description": "OTP sends for this address. `0` for risk lookups." + }, + "verified_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the address was last verified by an OTP. `null` for risk lookups." + }, + "warnings": { + "type": "array", + "description": "Risk warnings recorded for this request. Empty array when there are none.", + "items": { + "$ref": "#/components/schemas/RiskWarning" } + }, + "lifecycle": { + "type": "array", + "description": "Chronological audit trail of the request. Empty array for risk lookups without sends.", + "items": { + "$ref": "#/components/schemas/RiskLifecycleEvent" + } + }, + "matches": { + "type": "array", + "description": "Other sessions of your application where the same email address was used by a different user (different `vendor_data`). Up to 5 entries. Empty array when there are no matches.", + "items": { + "$ref": "#/components/schemas/EmailMatchItem" + } + }, + "email_intelligence": { + "$ref": "#/components/schemas/EmailIntelligence" + }, + "enrichment": { + "$ref": "#/components/schemas/EmailIntelligence" + }, + "email_social": { + "$ref": "#/components/schemas/EmailSocialFootprint" } } + } + } + }, + "IPRiskResponse": { + "type": "object", + "required": [ + "request_id", + "status", + "created_at" + ], + "properties": { + "request_id": { + "type": "string", + "format": "uuid", + "description": "Id of the API session created for this request. Store it with `vendor_data` if you need to correlate the result with later webhooks or console activity." }, - "400": { - "description": "Invalid input." - }, - "403": { - "description": "Insufficient permission." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseRoutingRuleCreate" - } - } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/publish/": { - "post": { - "operationId": "publish_case_routing_rule", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - }, - { - "in": "path", - "name": "record_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - } - ], - "tags": [ - "publish" - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/RoutingPublicationPolicyDraftPreconditionRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoutingPublicationPolicyDraftPreconditionRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/RoutingPublicationPolicyDraftPreconditionRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoutingPublicationCaseRoutingRule" - } - } - }, - "description": "" + "status": { + "type": "string", + "enum": [ + "Approved", + "In Review" + ], + "description": "Decision for this API session: `In Review` on network or country-mismatch risk, otherwise `Approved`." }, - "403": { - "description": "Case access is required." + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Echo of the `vendor_data` you sent, `null` when you did not send one." }, - "409": { - "description": "The published version or draft revision has changed. Reload before retrying." - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/publish/": { - "post": { - "operationId": "publish_case_blueprint", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "metadata": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Echo of the `metadata` you sent, `null` when you did not send one." }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of this response." }, - { - "in": "path", - "name": "blueprint_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - } - ], - "tags": [ - "Case Management" - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CasePublicationPolicyDraftPreconditionRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CasePublicationPolicyDraftPreconditionRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/CasePublicationPolicyDraftPreconditionRequest" + "ip": { + "type": "object", + "description": "IP risk result. Individual enrichment fields can be `null` when the lookup returned no usable data.", + "required": [ + "status", + "threat" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "Approved", + "In Review" + ], + "description": "`In Review` when the resolved country conflicts with `claimed_country`, or when the address is associated with VPN, Tor, or data-center infrastructure. Otherwise `Approved`." + }, + "ip_address": { + "type": "string", + "nullable": true, + "description": "The scored address. `null` when no enrichment source returned usable data." + }, + "ip_country": { + "type": "string", + "nullable": true, + "description": "Resolved country name." + }, + "ip_country_code": { + "type": "string", + "nullable": true, + "description": "Resolved country code. Compare countries through `country_mismatch` instead of comparing raw country strings yourself." + }, + "ip_state": { + "type": "string", + "nullable": true, + "description": "Resolved state or region." + }, + "ip_city": { + "type": "string", + "nullable": true, + "description": "Resolved city." + }, + "latitude": { + "type": "number", + "nullable": true, + "description": "Approximate network geolocation, not a precise device location." + }, + "longitude": { + "type": "number", + "nullable": true, + "description": "Approximate network geolocation, not a precise device location." + }, + "isp": { + "type": "string", + "nullable": true, + "description": "Internet service provider serving the address." + }, + "organization": { + "type": "string", + "nullable": true, + "description": "Organization owning the address block." + }, + "is_vpn_or_tor": { + "type": "boolean", + "nullable": true, + "description": "`true` when the address is associated with VPN or Tor infrastructure." + }, + "is_data_center": { + "type": "boolean", + "nullable": true, + "description": "`true` when the address belongs to a hosting or data-center network." + }, + "time_zone": { + "type": "string", + "nullable": true, + "description": "Time zone of the resolved location." + }, + "time_zone_offset": { + "type": "string", + "nullable": true, + "description": "UTC offset of the resolved location." + }, + "asn_number": { + "type": "number", + "nullable": true, + "description": "Autonomous system number." + }, + "asn_organization": { + "type": "string", + "nullable": true, + "description": "Organization operating the autonomous system." + }, + "connection_type": { + "type": "string", + "nullable": true, + "description": "Connection classification (e.g., `cellular`, `hosting`)." + }, + "carrier": { + "type": "string", + "nullable": true, + "description": "Mobile carrier when the address is on a mobile network." + }, + "proxy_type": { + "type": "string", + "nullable": true, + "description": "Proxy classification when the address is behind a proxy." + }, + "claimed_country": { + "type": "string", + "nullable": true, + "description": "Echo of the `claimed_country` you sent." + }, + "country_mismatch": { + "type": "boolean", + "nullable": true, + "description": "`true` when the resolved country differs from `claimed_country`. `null` when no enrichment data or no claimed country is available." + }, + "threat": { + "type": "object", + "additionalProperties": true, + "description": "Free-form network-threat evidence from the enrichment source (e.g., `is_proxy`, `is_tor`). Empty object when no enrichment data is available. Treat as diagnostic detail, not a stable contract." } } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CasePublicationCaseBlueprint" - } - } - }, - "description": "" - }, - "409": { - "description": "Published version or saved draft revision changed. Reload before retrying." - }, - "403": { - "description": "Required case permission is missing." - } - }, - "security": [ - { - "ApiKeyAuth": [] } - ] - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/preview/": { - "post": { - "operationId": "organization_application_cases_requests_preview_create", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + } + }, + "SocialFootprintPlatform": { + "type": "object", + "properties": { + "platform": { + "type": "string", + "description": "Platform identifier.", + "example": "whatsapp" }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "category": { + "type": "string", + "description": "Platform category, e.g. MESSAGING, SOCIAL_MEDIA, ECOMMERCE, PROFESSIONAL, EMAIL_PROVIDER.", + "example": "MESSAGING" }, - { - "in": "path", - "name": "case_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "scope": { + "type": "string", + "enum": [ + "global", + "local" + ], + "description": "`global` for worldwide platforms, `local` for region-specific ones.", + "example": "global" } - ], - "tags": [ - "Case Management" - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CustomerMessagingCaseRequestPreviewRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustomerMessagingCaseRequestPreviewRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/CustomerMessagingCaseRequestPreviewRequest" - } - } + } + }, + "SocialFootprintCategorySummary": { + "type": "object", + "properties": { + "registered": { + "type": "integer", + "nullable": true, + "description": "Platforms in this category where registration is confirmed.", + "example": 2 }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustomerMessagingCaseRequestPreviewResponse" - } - } - }, - "description": "" + "ratio": { + "type": "number", + "format": "float", + "nullable": true, + "description": "Share of the category's checked platforms with a registration, 0.0-1.0.", + "example": 0.6667 + } + } + }, + "SocialFootprintProfile": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "description": "Profile name, when the platform exposes one." }, - "404": { - "description": "The request or case is unavailable for this caller." + "photo": { + "type": "string", + "nullable": true, + "description": "Profile photo URL, when the platform exposes one." }, - "403": { - "description": "The caller lacks the required cases permission." + "about": { + "type": "string", + "nullable": true, + "description": "Profile about/bio text, when the platform exposes one." }, - "400": { - "description": "Invalid inputs, unsupported purpose, unavailable verified contact or an existing immutable response." + "privacy_status": { + "type": "string", + "nullable": true, + "enum": [ + "PUBLIC", + "PRIVATE", + null + ], + "description": "Profile visibility, when the platform exposes it." }, - "409": { - "description": "The preview changed or expired. Preview again before confirming." - } - }, - "security": [ - { - "ApiKeyAuth": [] + "business_account": { + "type": "boolean", + "nullable": true, + "description": "Whether the profile is a business account, when the platform exposes it." } - ], - "description": "Render the production email without sending or changing the case. Returns the exact recipient, sender, subject, body and a signed preview_token. Send the same contact_id, purpose, language, request_id, expires_at and preview_token to confirm within 15 minutes." - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/action-receipts/": { - "get": { - "operationId": "action_receipts_retrieve", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - }, - { - "in": "path", - "name": "case_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + } + }, + "PhoneSocialFootprint": { + "type": "object", + "nullable": true, + "description": "Social footprint of the phone number: the online platforms it is registered on. Present only when `enable_social` was set and the check completed. Every count may be null when no data was returned for it. `profiles_registered: 0` means the platforms were checked and the phone number was found on none of them - a valid, billable answer and a common synthetic-identity signal.", + "properties": { + "profiles_registered": { + "type": "integer", + "nullable": true, + "description": "Platforms where the phone number is confirmed registered.", + "example": 5 }, - { - "in": "query", - "name": "offset", - "schema": { - "type": "integer", - "maximum": 1000000, - "minimum": 0, - "default": 0 - } - } - ], - "tags": [ - "Case Management" - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseActionCaseActionReceiptPage" - } - } - }, - "description": "" - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "List subject-action receipts, newest first, with at most 20 results per page. Use next_offset for the next page. Input note text is not exposed in the history." - } - }, - "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/{note_uuid}/": { - "delete": { - "operationId": "delete_network_note", - "tags": [ - "Networks" - ], - "summary": "Delete your network note", - "description": "Requires write:networks and the authenticated author of the note. Deletion is irreversible. Connected account and verification data are unaffected.", - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." + "profiles_checked": { + "type": "integer", + "nullable": true, + "description": "Platforms evaluated for registration.", + "example": 14 }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "names_found": { + "type": "integer", + "nullable": true, + "description": "Platforms that returned an associated name.", + "example": 0 }, - { - "name": "network_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Network UUID." + "photos_found": { + "type": "integer", + "nullable": true, + "description": "Platforms that returned a profile photo.", + "example": 0 }, - { - "name": "note_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } + "communication_breadth": { + "type": "integer", + "nullable": true, + "description": "Communication platforms with detected presence.", + "example": 2 }, - { - "name": "Authorization", - "in": "header", - "required": true, - "schema": { - "type": "string" - }, - "description": "Console user access token in the form Bearer ." - } - ], - "responses": { - "204": { - "description": "Note deleted." + "digital_footprint_diversity": { + "type": "integer", + "nullable": true, + "description": "Distinct platform categories with detected presence.", + "example": 5 }, - "401": { - "description": "Authentication is required." + "digital_sophistication_index": { + "type": "integer", + "nullable": true, + "description": "Composite digital-maturity indicator, 0-10.", + "example": 5 }, - "403": { - "description": "Missing permission or Networks access." + "engagement_depth_index": { + "type": "integer", + "nullable": true, + "description": "Profile-richness indicator, 0-10.", + "example": 3 }, - "404": { - "description": "Network or note is not visible to this application." - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/action-receipts/{receipt_uuid}/retry/": { - "post": { - "operationId": "action_receipts_retry_create", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "subscription_affordability_index": { + "type": "integer", + "nullable": true, + "description": "Free vs paid platform mix, 0-10.", + "example": 2 }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "free_platforms": { + "type": "integer", + "nullable": true, + "description": "Registrations on free platforms.", + "example": 4 }, - { - "in": "path", - "name": "case_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "full_paid_platforms": { + "type": "integer", + "nullable": true, + "description": "Registrations on fully paid platforms.", + "example": 0 }, - { - "in": "path", - "name": "receipt_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - } - ], - "tags": [ - "Case Management" - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseActionCaseActionReceipt" - } - } - }, - "description": "" - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Retry only this failed subject action using its saved inputs. Current write permission is checked. Applied actions return their receipt without executing again. Case closure is independent." - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/": { - "get": { - "operationId": "get_case_blueprint_draft", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "partial_paid_platforms": { + "type": "integer", + "nullable": true, + "description": "Registrations on freemium platforms.", + "example": 1 }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "whatsapp_profile_completeness": { + "type": "string", + "nullable": true, + "enum": [ + "Basic", + "Partial", + "Complete", + "Not Present", + null + ], + "description": "Completeness of the WhatsApp profile. Phone only; null for email.", + "example": "Partial" }, - { - "in": "path", - "name": "blueprint_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - } - ], - "tags": [ - "Case Management" - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CasePublicationPolicyDraftResponse" - } - } - }, - "description": "" + "registered_platforms": { + "type": "array", + "description": "Platforms where registration is confirmed.", + "items": { + "$ref": "#/components/schemas/SocialFootprintPlatform" + } }, - "403": { - "description": "Required case permission is missing." - } - }, - "security": [ - { - "ApiKeyAuth": [] + "not_registered_platforms": { + "type": "array", + "description": "Platforms checked with a negative result.", + "items": { + "$ref": "#/components/schemas/SocialFootprintPlatform" + } + }, + "categories": { + "type": "object", + "description": "Registered count and ratio per platform category.", + "additionalProperties": { + "$ref": "#/components/schemas/SocialFootprintCategorySummary" + } + }, + "profiles": { + "type": "object", + "description": "Per-platform profile details, keyed by platform, when the platform exposes them.", + "additionalProperties": { + "$ref": "#/components/schemas/SocialFootprintProfile" + } } - ] + } }, - "put": { - "operationId": "save_case_blueprint_draft", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "EmailSocialFootprint": { + "type": "object", + "nullable": true, + "description": "Social footprint of the email address: the online platforms it is registered on. Present only when `enable_social` was set and the check completed. Every count may be null when no data was returned for it. `profiles_registered: 0` means the platforms were checked and the email address was found on none of them - a valid, billable answer and a common synthetic-identity signal.", + "properties": { + "profiles_registered": { + "type": "integer", + "nullable": true, + "description": "Platforms where the email address is confirmed registered.", + "example": 5 }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "profiles_checked": { + "type": "integer", + "nullable": true, + "description": "Platforms evaluated for registration.", + "example": 14 }, - { - "in": "path", - "name": "blueprint_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - } - ], - "tags": [ - "Case Management" - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CasePublicationPolicyDraftWriteRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CasePublicationPolicyDraftWriteRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/CasePublicationPolicyDraftWriteRequest" - } - } + "names_found": { + "type": "integer", + "nullable": true, + "description": "Platforms that returned an associated name.", + "example": 0 }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CasePublicationPolicyDraft" - } - } - }, - "description": "" + "photos_found": { + "type": "integer", + "nullable": true, + "description": "Platforms that returned a profile photo.", + "example": 0 }, - "409": { - "description": "Published version or saved draft revision changed. Reload before retrying." + "communication_breadth": { + "type": "integer", + "nullable": true, + "description": "Communication platforms with detected presence.", + "example": 2 }, - "403": { - "description": "Required case permission is missing." - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ] - }, - "delete": { - "operationId": "discard_case_blueprint_draft", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "digital_footprint_diversity": { + "type": "integer", + "nullable": true, + "description": "Distinct platform categories with detected presence.", + "example": 5 }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "digital_sophistication_index": { + "type": "integer", + "nullable": true, + "description": "Composite digital-maturity indicator, 0-10.", + "example": 5 }, - { - "in": "path", - "name": "blueprint_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - } - ], - "tags": [ - "Case Management" - ], - "responses": { - "204": { - "description": "No response body" + "engagement_depth_index": { + "type": "integer", + "nullable": true, + "description": "Profile-richness indicator, 0-10.", + "example": 3 }, - "409": { - "description": "Published version or saved draft revision changed. Reload before retrying." + "subscription_affordability_index": { + "type": "integer", + "nullable": true, + "description": "Free vs paid platform mix, 0-10.", + "example": 2 }, - "403": { - "description": "Required case permission is missing." - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CasePublicationPolicyDraftPreconditionRequest" - } - } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/teams/{record_uuid}/": { - "get": { - "summary": "Retrieve review team", - "description": "Requires `read:cases` for the requested application.", - "operationId": "retrieve_case_teams", - "tags": [ - "Case Assignment" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." + "free_platforms": { + "type": "integer", + "nullable": true, + "description": "Registrations on free platforms.", + "example": 4 }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "full_paid_platforms": { + "type": "integer", + "nullable": true, + "description": "Registrations on fully paid platforms.", + "example": 0 }, - { - "name": "record_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseReviewTeam" - } - } + "partial_paid_platforms": { + "type": "integer", + "nullable": true, + "description": "Registrations on freemium platforms.", + "example": 1 + }, + "whatsapp_profile_completeness": { + "type": "string", + "nullable": true, + "enum": [ + "Basic", + "Partial", + "Complete", + "Not Present", + null + ], + "description": "Completeness of the WhatsApp profile. Phone only; null for email.", + "example": "Partial" + }, + "registered_platforms": { + "type": "array", + "description": "Platforms where registration is confirmed.", + "items": { + "$ref": "#/components/schemas/SocialFootprintPlatform" } }, - "400": { - "description": "Invalid input." + "not_registered_platforms": { + "type": "array", + "description": "Platforms checked with a negative result.", + "items": { + "$ref": "#/components/schemas/SocialFootprintPlatform" + } }, - "403": { - "description": "Insufficient permission." + "categories": { + "type": "object", + "description": "Registered count and ratio per platform category.", + "additionalProperties": { + "$ref": "#/components/schemas/SocialFootprintCategorySummary" + } }, - "404": { - "description": "Record not found in this application." + "profiles": { + "type": "object", + "description": "Per-platform profile details, keyed by platform, when the platform exposes them.", + "additionalProperties": { + "$ref": "#/components/schemas/SocialFootprintProfile" + } } } }, - "patch": { - "summary": "Update review team", - "description": "Requires `write:cases` for the requested application.", - "operationId": "update_case_teams", - "tags": [ - "Case Assignment" - ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." + "KYBRegistryAddress": { + "type": "object", + "description": "An address the registry states, in the same shape for every country and registry source. Every key is always present; a part the registry does not publish is null.", + "properties": { + "type": { + "type": "string", + "nullable": true, + "description": "Address type in lower case: `registered` for the registered office; other types as the registry names them (for example `postal`, `office`). `null` when the registry does not say what the address is." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "address": { + "type": "string", + "description": "The full address as one line. Always present on every address." }, - { - "name": "record_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseReviewTeam" - } - } - } + "street": { + "type": "string", + "nullable": true, + "description": "Street and number." }, - "400": { - "description": "Invalid input." + "city": { + "type": "string", + "nullable": true, + "description": "City or locality." }, - "403": { - "description": "Insufficient permission." + "postal_code": { + "type": "string", + "nullable": true, + "description": "Postal code." }, - "404": { - "description": "Record not found in this application." + "region": { + "type": "string", + "nullable": true, + "description": "Region, state or province." + }, + "country_code": { + "type": "string", + "nullable": true, + "description": "ISO 3166-1 alpha-2 country code." + }, + "description": { + "type": "string", + "nullable": true, + "description": "The registry's own label for the address, when it gives one." } }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseReviewTeamUpdate" - } - } + "required": [ + "type", + "address", + "street", + "city", + "postal_code", + "region", + "country_code", + "description" + ] + }, + "KYBRegistryIndustry": { + "type": "object", + "description": "An activity the registry classifies the company under, in the same shape for every country and registry source.", + "properties": { + "code": { + "type": "string", + "nullable": true, + "description": "Activity code as the registry prints it (for example `73110`)." + }, + "description": { + "type": "string", + "nullable": true, + "description": "Activity label, when the registry publishes one." + }, + "scheme": { + "type": "string", + "nullable": true, + "description": "Classification system, one name per system whichever source reported it: `UK SIC 2007`, `US SIC`, `NACE Rev. 2`, `NACE Rev. 2.1`, `NAF Rev. 2`, `CNAE`, `SBI`, `NOGA 2008`, `CAE Rev. 4`, `Canada SIC 1977`; other national systems keep the registry's own name. Null when the registry does not name the system." + }, + "is_primary": { + "type": "boolean", + "description": "True for the company's main activity. Exactly one entry is primary when the list is not empty." } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/report/": { - "get": { - "operationId": "download_network_report", - "tags": [ - "Networks" - ], - "summary": "Download a network evidence report", - "description": "Download a PDF of the current organization network, its observed account connections, signal references and review status. Requires read:networks. Global intelligence and private analyst notes are not included. Each account and signal list is capped at 500 entries and the report states when it is incomplete. The request does not change network status. Errors are returned as JSON.", - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." + }, + "required": [ + "code", + "description", + "scheme", + "is_primary" + ] + }, + "KYBRegistryShareholding": { + "type": "object", + "nullable": true, + "description": "What an owner holds, in the same shape for every country and registry source. Null when the registry states no holding. A band (for example a UK PSC's 25-50%) is not a stake: `percentage` is null and the band stays in `ownership_min_shares` / `ownership_max_shares`.", + "properties": { + "share_class": { + "type": "string", + "nullable": true, + "description": "Share class or type as the registry names it." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "number_of_shares": { + "type": "number", + "nullable": true, + "description": "Number of shares held." }, - { - "name": "network_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Network UUID." + "currency": { + "type": "string", + "nullable": true, + "description": "ISO 4217 currency of the values below." }, - { - "name": "Authorization", - "in": "header", - "required": true, - "schema": { - "type": "string" - }, - "description": "Console user access token in the form Bearer ." + "nominal_value": { + "type": "number", + "nullable": true, + "description": "Nominal value per share." + }, + "total_value": { + "type": "number", + "nullable": true, + "description": "Total value of the holding." + }, + "percentage": { + "type": "number", + "nullable": true, + "description": "Exact stake in percent, to four decimal places." } - ], - "responses": { - "200": { - "description": "Network evidence report. Cache-Control: no-store.", - "content": { - "application/pdf": { - "schema": { + }, + "required": [ + "share_class", + "number_of_shares", + "currency", + "nominal_value", + "total_value", + "percentage" + ] + }, + "KYBRegistryOwnership": { + "type": "object", + "description": "The outcome of the ownership (Shareholders / UBO) tier, in the same shape for every country and registry source.", + "properties": { + "status": { + "type": "string", + "nullable": true, + "enum": [ + "not_requested", + "pending", + "delivered", + "unavailable", + null + ], + "description": "`delivered` when the registry published ownership; `unavailable` when it did not (see `unavailable_reason`); `pending` while the registry is still fetching it; `not_requested` for the Basic tier." + }, + "tier_requested": { + "type": "string", + "nullable": true, + "enum": [ + "basic", + "shareholders", + "ubo", + null + ], + "description": "The tier the workflow ordered." + }, + "tier_delivered": { + "type": "string", + "nullable": true, + "enum": [ + "basic", + "shareholders", + "ubo", + null + ], + "description": "The tier the registry delivered (and that is billed). A UBO request answered with direct shareholders only is `shareholders`." + }, + "unavailable_reason": { + "type": "string", + "nullable": true, + "enum": [ + "no_owners_on_record", + "no_natural_person_owner", + "ownership_section_unavailable", + "company_not_found", + "ambiguous_company_match", + "missing_registration_number", + "entity_type_not_covered", + "provider_unavailable", + "other", + null + ], + "description": "Why a requested ownership tier was not delivered. Null unless `status` is `unavailable` or `pending`." + }, + "direct_ownership_complete": { + "type": "boolean", + "nullable": true, + "description": "True when the direct shareholders' stated stakes add up to 100% (within one percentage point of rounding); false when they do not; null when ownership was not delivered or a stake is not stated." + }, + "unresolved": { + "type": "array", + "description": "Parts of the ownership chain the registry could not resolve, for example no group structure on record above the direct owners.", + "items": { + "type": "object", + "properties": { + "reason": { + "type": "string" + }, + "section": { "type": "string", - "format": "binary" + "nullable": true } } } + } + }, + "required": [ + "status", + "tier_requested", + "tier_delivered", + "unavailable_reason", + "direct_ownership_complete", + "unresolved" + ] + }, + "DigitalIdWalletApiWalletVerificationError": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Stable machine-readable code, e.g. wallet_unavailable." }, - "401": { - "description": "Authentication is required." - }, - "403": { - "description": "Missing permission or Networks access." + "detail": { + "type": "string" + } + }, + "required": [ + "detail", + "error" + ] + }, + "DigitalIdWalletApiWalletVerificationStartForbidden": { + "oneOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiClientCredentialsForbiddenErrorMessage" }, - "404": { - "description": "Network or note is not visible to this application." + { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + } + ] + }, + "DigitalIdWalletApiClientCredentialsForbiddenErrorMessage": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "default": "Forbidden - User doesn't have permission" } } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/": { - "get": { - "operationId": "list_network_notes", - "tags": [ - "Networks" - ], - "summary": "List private network notes", - "description": "Console users with read:networks can read notes on a network visible to the application. Notes stay within the organization and are not contributed to shared intelligence. Newest notes appear first.", - "parameters": [ + }, + "DigitalIdWalletApiWalletVerificationStartBadRequest": { + "oneOf": [ { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" }, { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationFieldErrors" + } + ] + }, + "DigitalIdWalletApiWalletVerificationFieldErrors": { + "type": "object", + "description": "Request validation errors: each rejected field with its messages.", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "DigitalIdWalletApiWalletVerificationResponse": { + "type": "object", + "description": "One standalone wallet verification, as every wallet endpoint answers it.", + "properties": { + "request_id": { + "type": "string", + "format": "uuid" }, - { - "name": "network_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Network UUID." + "wallet_verification": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResult" }, - { - "name": "Authorization", - "in": "header", - "required": true, - "schema": { - "type": "string" - }, - "description": "Console user access token in the form Bearer ." + "vendor_data": { + "type": "string", + "nullable": true }, - { - "name": "limit", - "in": "query", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 30 - } + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "The metadata object sent at start, or null." }, - { - "name": "offset", - "in": "query", - "schema": { - "type": "integer", - "minimum": 0, - "maximum": 1000000, - "default": 0 - } + "created_at": { + "type": "string", + "format": "date-time" } - ], - "responses": { - "200": { - "description": "A page of notes. Count is capped at 100; use next to continue.", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "count": { - "type": "integer" - }, - "next": { - "type": "string", - "nullable": true - }, - "previous": { - "type": "string", - "nullable": true - }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NetworkNote" - } - } - } - } + }, + "required": [ + "created_at", + "metadata", + "request_id", + "vendor_data", + "wallet_verification" + ] + }, + "DigitalIdWalletApiWalletVerificationResult": { + "type": "object", + "properties": { + "wallet": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiWallet" } - } - }, - "401": { - "description": "Authentication is required." + ], + "readOnly": true }, - "403": { - "description": "Missing permission or Networks access." + "country": { + "type": "string" }, - "404": { - "description": "Network or note is not visible to this application." - } - } - }, - "post": { - "operationId": "create_network_note", - "tags": [ - "Networks" - ], - "summary": "Add a private network note", - "description": "Requires a console user with write:networks. The author is assigned from the authenticated user. Supply a new random UUID client_reference for each note; retry the same body and reference after an uncertain response to avoid duplicates. Reusing a reference for another body, author or network returns 409. Notes follow the surviving investigation when networks merge.", - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." + "status": { + "type": "string", + "description": "Not Finished while pending, then Approved (verified) or Declined." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." + "outcome": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResultOutcomeEnum" }, - { - "name": "network_uuid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Network UUID." + "error": { + "type": "string", + "nullable": true, + "readOnly": true }, - { - "name": "Authorization", - "in": "header", - "required": true, - "schema": { - "type": "string" - }, - "description": "Console user access token in the form Bearer ." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "comment", - "client_reference" - ], - "properties": { - "comment": { - "type": "string", - "minLength": 1, - "maxLength": 10000 - }, - "client_reference": { - "type": "string", - "format": "uuid" - } - } + "interaction": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiInteractionEnum" } - } - } - }, - "responses": { - "201": { - "description": "Saved private note.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NetworkNote" - } + ], + "readOnly": true + }, + "simulated": { + "type": "boolean", + "readOnly": true + }, + "expires_at": { + "type": "string", + "format": "date-time" + }, + "next_action": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletNextAction" } - } + ], + "nullable": true, + "readOnly": true }, - "200": { - "description": "Saved private note.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NetworkNote" - } + "identity": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletIdentity" } - } + ], + "nullable": true, + "readOnly": true }, - "400": { - "description": "Invalid or blank note." + "attributes": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "readOnly": true }, - "409": { - "description": "The reference was already used for another note." + "level_of_assurance": { + "type": "string", + "nullable": true }, - "401": { - "description": "Authentication is required." + "verified_at": { + "type": "string", + "format": "date-time", + "nullable": true }, - "403": { - "description": "Missing permission or Networks access." + "signature_valid": { + "type": "boolean", + "nullable": true, + "description": "True once the credential's signature was verified; null for a simulated one." }, - "404": { - "description": "Network or note is not visible to this application." + "credential_type": { + "type": "string", + "nullable": true } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/": { - "get": { - "operationId": "get_case_routing_rule_draft", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + }, + "required": [ + "attributes", + "country", + "credential_type", + "error", + "expires_at", + "identity", + "interaction", + "level_of_assurance", + "next_action", + "outcome", + "signature_valid", + "simulated", + "status", + "verified_at", + "wallet" + ] + }, + "DigitalIdWalletApiWalletIdentity": { + "type": "object", + "properties": { + "full_name": { + "type": "string", + "nullable": true }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "first_name": { + "type": "string", + "nullable": true }, - { - "in": "path", - "name": "record_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "last_name": { + "type": "string", + "nullable": true + }, + "date_of_birth": { + "type": "string", + "format": "date", + "nullable": true + }, + "nationality": { + "type": "string", + "nullable": true, + "description": "ISO3 nationality, only when the wallet asserts one (never inferred)." + }, + "issuing_state": { + "type": "string", + "description": "ISO3 country whose scheme issued the identity." } - ], - "tags": [ - "draft" - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoutingPublicationPolicyDraftResponse" - } + }, + "required": [ + "date_of_birth", + "first_name", + "full_name", + "issuing_state", + "last_name", + "nationality" + ] + }, + "DigitalIdWalletApiWalletNextAction": { + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletNextActionTypeEnum" } - }, - "description": "" + ], + "description": "redirect: send the user's browser to url. confirm_in_app: show verification_code to the user, who checks it matches the code in their wallet app and confirms there; then poll.\n\n* `redirect` - redirect\n* `confirm_in_app` - confirm_in_app" }, - "403": { - "description": "Case access is required." + "url": { + "type": "string", + "format": "uri", + "description": "redirect only: where the user signs in with the wallet." + }, + "verification_code": { + "type": "string", + "description": "confirm_in_app only: the 4-digit comparison code the wallet app shows too." + }, + "poll_interval_seconds": { + "type": "integer", + "description": "confirm_in_app only: how often to call POST .../verifications/{request_id}/poll/ for prompt feedback. Didit also polls pending sign-ins in the background, so the webhook reports the outcome even when you do not poll." } }, - "security": [ - { - "ApiKeyAuth": [] + "required": [ + "type" + ] + }, + "DigitalIdWalletApiWalletNextActionTypeEnum": { + "enum": [ + "redirect", + "confirm_in_app" + ], + "type": "string", + "description": "* `redirect` - redirect\n* `confirm_in_app` - confirm_in_app" + }, + "DigitalIdWalletApiInteractionEnum": { + "enum": [ + "redirect", + "in_app" + ], + "type": "string", + "description": "* `redirect` - redirect\n* `in_app` - in_app" + }, + "DigitalIdWalletApiWalletVerificationResultOutcomeEnum": { + "enum": [ + "pending", + "verified", + "cancelled", + "timeout", + "failed" + ], + "type": "string", + "description": "* `pending` - Pending\n* `verified` - Verified\n* `cancelled` - Cancelled by the user\n* `timeout` - Timed out\n* `failed` - Failed" + }, + "DigitalIdWalletApiWallet": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Catalog id of the wallet, e.g. bankid_se." + }, + "name": { + "type": "string" + }, + "issuing_authority": { + "type": "string", + "nullable": true } + }, + "required": [ + "id", + "issuing_authority", + "name" ] }, - "put": { - "operationId": "save_case_routing_rule_draft", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "DigitalIdWalletApiEudiWalletVerificationStartRequest": { + "type": "object", + "description": "Start a EUDI Wallet verification (AUT, BEL, BGR, HRV, CYP, CZE, DNK, EST, FIN, FRA, DEU, GRC, HUN, IRL, ITA, LVA, LTU, LUX, MLT, NLD, POL, PRT, ROU, SVK, SVN, ESP, SWE, ISL, LIE, NOR).", + "properties": { + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - { - "in": "path", - "name": "record_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + }, + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiEudiWalletVerificationStartCountryEnum" + } + ], + "description": "ISO 3166-1 alpha-3 country of the EUDI Wallet identity.\n\n* `AUT` - AUT\n* `BEL` - BEL\n* `BGR` - BGR\n* `HRV` - HRV\n* `CYP` - CYP\n* `CZE` - CZE\n* `DNK` - DNK\n* `EST` - EST\n* `FIN` - FIN\n* `FRA` - FRA\n* `DEU` - DEU\n* `GRC` - GRC\n* `HUN` - HUN\n* `IRL` - IRL\n* `ITA` - ITA\n* `LVA` - LVA\n* `LTU` - LTU\n* `LUX` - LUX\n* `MLT` - MLT\n* `NLD` - NLD\n* `POL` - POL\n* `PRT` - PRT\n* `ROU` - ROU\n* `SVK` - SVK\n* `SVN` - SVN\n* `ESP` - ESP\n* `SWE` - SWE\n* `ISL` - ISL\n* `LIE` - LIE\n* `NOR` - NOR" + }, + "return_url": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 + }, + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" } + }, + "required": [ + "country", + "return_url" + ] + }, + "DigitalIdWalletApiDeviceEnum": { + "enum": [ + "same_device", + "cross_device" ], - "tags": [ - "draft" + "type": "string", + "description": "* `same_device` - same_device\n* `cross_device` - cross_device" + }, + "DigitalIdWalletApiEudiWalletVerificationStartCountryEnum": { + "enum": [ + "AUT", + "BEL", + "BGR", + "HRV", + "CYP", + "CZE", + "DNK", + "EST", + "FIN", + "FRA", + "DEU", + "GRC", + "HUN", + "IRL", + "ITA", + "LVA", + "LTU", + "LUX", + "MLT", + "NLD", + "POL", + "PRT", + "ROU", + "SVK", + "SVN", + "ESP", + "SWE", + "ISL", + "LIE", + "NOR" ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/RoutingPublicationPolicyDraftWriteRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoutingPublicationPolicyDraftWriteRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/RoutingPublicationPolicyDraftWriteRequest" - } - } + "type": "string", + "description": "* `AUT` - AUT\n* `BEL` - BEL\n* `BGR` - BGR\n* `HRV` - HRV\n* `CYP` - CYP\n* `CZE` - CZE\n* `DNK` - DNK\n* `EST` - EST\n* `FIN` - FIN\n* `FRA` - FRA\n* `DEU` - DEU\n* `GRC` - GRC\n* `HUN` - HUN\n* `IRL` - IRL\n* `ITA` - ITA\n* `LVA` - LVA\n* `LTU` - LTU\n* `LUX` - LUX\n* `MLT` - MLT\n* `NLD` - NLD\n* `POL` - POL\n* `PRT` - PRT\n* `ROU` - ROU\n* `SVK` - SVK\n* `SVN` - SVN\n* `ESP` - ESP\n* `SWE` - SWE\n* `ISL` - ISL\n* `LIE` - LIE\n* `NOR` - NOR" + }, + "DigitalIdWalletApiConnectidWalletVerificationStartRequest": { + "type": "object", + "description": "Start a ConnectID verification (AUS).", + "properties": { + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoutingPublicationPolicyDraft" - } + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." + }, + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + }, + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiConnectidWalletVerificationStartCountryEnum" } - }, - "description": "" + ], + "default": "AUS", + "description": "ISO 3166-1 alpha-3 country of the ConnectID identity. Defaults to AUS, the only country it serves.\n\n* `AUS` - AUS" }, - "403": { - "description": "Case access is required." + "authorisation_server_id": { + "type": "string", + "minLength": 1, + "description": "Authorisation server ID selected with ConnectID's bank selector. Sandbox defaults to Mock Bank A.", + "maxLength": 36 }, - "409": { - "description": "The published version or draft revision has changed. Reload before retrying." + "return_url": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 + }, + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" } }, - "security": [ - { - "ApiKeyAuth": [] - } + "required": [ + "return_url" ] }, - "delete": { - "operationId": "discard_case_routing_rule_draft", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - }, - { - "in": "path", - "name": "record_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - } - ], - "tags": [ - "draft" + "DigitalIdWalletApiConnectidWalletVerificationStartCountryEnum": { + "enum": [ + "AUS" ], - "responses": { - "204": { - "description": "No response body" + "type": "string", + "description": "* `AUS` - AUS" + }, + "DigitalIdWalletApiAudkenniWalletVerificationStartRequest": { + "type": "object", + "description": "Start a Auðkenni verification (ISL).", + "properties": { + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "403": { - "description": "Case access is required." + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "409": { - "description": "The published version or draft revision has changed. Reload before retrying." - } - }, - "security": [ - { - "ApiKeyAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoutingPublicationPolicyDraftPreconditionRequest" - } - } - } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/duplicates/": { - "post": { - "operationId": "duplicates_create", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true - } - ], - "tags": [ - "Case Management" - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/CaseDuplicateCaseDuplicateQueryRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseDuplicateCaseDuplicateQueryRequest" - } - }, - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/CaseDuplicateCaseDuplicateQueryRequest" + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiAudkenniWalletVerificationStartCountryEnum" } - } - } - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseDuplicateCaseDuplicatePage" - } + ], + "default": "ISL", + "description": "ISO 3166-1 alpha-3 country of the Auðkenni identity. Defaults to ISL, the only country it serves.\n\n* `ISL` - ISL" + }, + "return_url": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 + }, + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" } - }, - "description": "" + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" } }, - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Review existing cases for a subject or owned source record before manual case creation. Both open and resolved investigations are returned. Each page contains at most 20 cases; use next_offset to request another page." - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/preview/": { - "post": { - "summary": "Test case routing", - "description": "Requires `read:cases` for the requested application. Evaluates current rules or a supplied draft against current reviewer access, availability, skills and capacity. It does not save the draft or reserve reviewer capacity.", - "operationId": "preview_case_routing", - "tags": [ - "Case Assignment" + "required": [ + "return_url" + ] + }, + "DigitalIdWalletApiAudkenniWalletVerificationStartCountryEnum": { + "enum": [ + "ISL" ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." + "type": "string", + "description": "* `ISL` - ISL" + }, + "DigitalIdWalletApiFranceconnectWalletVerificationStartRequest": { + "type": "object", + "description": "Start a FranceConnect verification (FRA).", + "properties": { + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseRoutingPreview" - } + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." + }, + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + }, + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiFranceconnectWalletVerificationStartCountryEnum" } - } + ], + "default": "FRA", + "description": "ISO 3166-1 alpha-3 country of the FranceConnect identity. Defaults to FRA, the only country it serves.\n\n* `FRA` - FRA" }, - "400": { - "description": "Invalid input." + "return_url": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 }, - "403": { - "description": "Insufficient permission." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CaseRoutingPreviewRequest" + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" } - } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/reviewers/": { - "get": { - "summary": "List eligible case reviewers", - "description": "Requires `list:cases` for the requested application.", - "operationId": "list_case_reviewers", - "tags": [ - "Case Assignment" + }, + "required": [ + "return_url" + ] + }, + "DigitalIdWalletApiFranceconnectWalletVerificationStartCountryEnum": { + "enum": [ + "FRA" ], - "parameters": [ - { - "name": "organization_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Organization UUID." + "type": "string", + "description": "* `FRA` - FRA" + }, + "DigitalIdWalletApiDiiaWalletVerificationStartRequest": { + "type": "object", + "description": "Start a Diia verification (UKR).", + "properties": { + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - { - "name": "application_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - }, - "description": "Application UUID." - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "results": { - "type": "array", - "items": { - "type": "object", - "properties": { - "user_id": { - "type": "string", - "format": "uuid" - }, - "email": { - "type": "string", - "format": "email" - }, - "name": { - "type": "string" - } - } - } - } - } - } - } - } + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "400": { - "description": "Invalid input." + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - "403": { - "description": "Insufficient permission." + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDiiaWalletVerificationStartCountryEnum" + } + ], + "default": "UKR", + "description": "ISO 3166-1 alpha-3 country of the Diia identity. Defaults to UKR, the only country it serves.\n\n* `UKR` - UKR" }, - "503": { - "description": "Reviewer access could not be verified. Retry later." + "return_url": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 + }, + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" } - } - } - }, - "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/history/": { - "get": { - "operationId": "list_case_blueprint_history", - "parameters": [ - { - "in": "path", - "name": "organization_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + }, + "required": [ + "return_url" + ] + }, + "DigitalIdWalletApiDiiaWalletVerificationStartCountryEnum": { + "enum": [ + "UKR" + ], + "type": "string", + "description": "* `UKR` - UKR" + }, + "DigitalIdWalletApiMojeidWalletVerificationStartRequest": { + "type": "object", + "description": "Start a MojeID verification (CZE).", + "properties": { + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - { - "in": "path", - "name": "application_id", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - { - "in": "path", - "name": "blueprint_uuid", - "schema": { - "type": "string", - "format": "uuid" - }, - "required": true + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - { - "name": "ordering", - "required": false, - "in": "query", - "description": "Which field to use when ordering the results.", - "schema": { - "type": "string" - } + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiCountryE2dEnum" + } + ], + "default": "CZE", + "description": "ISO 3166-1 alpha-3 country of the MojeID identity. Defaults to CZE, the only country it serves.\n\n* `CZE` - CZE" }, - { - "name": "limit", - "required": false, - "in": "query", - "description": "Number of results to return per page.", - "schema": { - "type": "integer" - } + "return_url": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 }, - { - "name": "offset", - "required": false, - "in": "query", - "description": "The initial index from which to return the results.", - "schema": { - "type": "integer" - } + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" } + }, + "required": [ + "return_url" + ] + }, + "DigitalIdWalletApiCountryE2dEnum": { + "enum": [ + "CZE" ], - "tags": [ - "Case Management" - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CasePublicationPaginatedPolicyRevisionList" - } + "type": "string", + "description": "* `CZE` - CZE" + }, + "DigitalIdWalletApiBankidCzWalletVerificationStartRequest": { + "type": "object", + "description": "Start a Bank iD verification (CZE).", + "properties": { + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + }, + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." + }, + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + }, + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiCountryE2dEnum" } - }, - "description": "" + ], + "default": "CZE", + "description": "ISO 3166-1 alpha-3 country of the Bank iD identity. Defaults to CZE, the only country it serves.\n\n* `CZE` - CZE" }, - "403": { - "description": "Required case permission is missing." + "return_url": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 + }, + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" } }, - "security": [ - { - "ApiKeyAuth": [] - } + "required": [ + "return_url" ] - } - } - }, - "components": { - "schemas": { - "DocumentAIDetectedCode": { + }, + "DigitalIdWalletApiEparakstsMobileWalletVerificationStartRequest": { "type": "object", - "required": [ - "type", - "page_number", - "position", - "x", - "y", - "width", - "height", - "page_width", - "page_height", - "raw_payload", - "parsed_payload" - ], + "description": "Start a eParaksts mobile verification (LVA).", "properties": { - "type": { + "vendor_data": { "type": "string", - "description": "Detected symbology, for example `QR_CODE`, `CODE_128`, `EAN_13`, `DATA_MATRIX`, or `PDF_417`." - }, - "page_number": { - "type": "integer", - "minimum": 1, - "description": "1-based page number." + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "position": { - "type": "array", + "metadata": { + "type": "object", + "additionalProperties": {}, "nullable": true, - "description": "Detected polygon in rendered-page pixel coordinates. `null` when no reliable location is available.", - "items": { - "type": "array", - "minItems": 2, - "maxItems": 2, - "items": { - "type": "integer" + "description": "Optional JSON object stored with the verification and returned in results and webhooks." + }, + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + }, + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiEparakstsMobileWalletVerificationStartCountryEnum" } - } + ], + "default": "LVA", + "description": "ISO 3166-1 alpha-3 country of the eParaksts mobile identity. Defaults to LVA, the only country it serves.\n\n* `LVA` - LVA" }, - "x": { - "type": "integer", + "return_url": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 + }, + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + } + }, + "required": [ + "return_url" + ] + }, + "DigitalIdWalletApiEparakstsMobileWalletVerificationStartCountryEnum": { + "enum": [ + "LVA" + ], + "type": "string", + "description": "* `LVA` - LVA" + }, + "DigitalIdWalletApiIdcardEeWalletVerificationStartRequest": { + "type": "object", + "description": "Start a Estonian ID-card verification (EST).", + "properties": { + "vendor_data": { + "type": "string", "nullable": true, - "description": "Left edge of the bounding box in rendered-page pixels." + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "y": { - "type": "integer", + "metadata": { + "type": "object", + "additionalProperties": {}, "nullable": true, - "description": "Top edge of the bounding box in rendered-page pixels." + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "width": { - "type": "integer", + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + }, + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiIdcardEeWalletVerificationStartCountryEnum" + } + ], + "default": "EST", + "description": "ISO 3166-1 alpha-3 country of the Estonian ID-card identity. Defaults to EST, the only country it serves.\n\n* `EST` - EST" + }, + "return_url": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 + }, + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + } + }, + "required": [ + "return_url" + ] + }, + "DigitalIdWalletApiIdcardEeWalletVerificationStartCountryEnum": { + "enum": [ + "EST" + ], + "type": "string", + "description": "* `EST` - EST" + }, + "DigitalIdWalletApiMobileIdWalletVerificationStartRequest": { + "type": "object", + "description": "Start a Mobile-ID verification (EST, LTU).", + "properties": { + "vendor_data": { + "type": "string", "nullable": true, - "description": "Bounding-box width in rendered-page pixels." + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "height": { - "type": "integer", + "metadata": { + "type": "object", + "additionalProperties": {}, "nullable": true, - "description": "Bounding-box height in rendered-page pixels." + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "page_width": { - "type": "integer", - "description": "Rendered page width in pixels." + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - "page_height": { - "type": "integer", - "description": "Rendered page height in pixels." + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiMobileIdWalletVerificationStartCountryEnum" + } + ], + "description": "ISO 3166-1 alpha-3 country of the Mobile-ID identity.\n\n* `EST` - EST\n* `LTU` - LTU" }, - "raw_payload": { + "personal_code": { "type": "string", - "description": "Raw decoder payload as a base64 string when available; otherwise an empty string." + "minLength": 1, + "description": "The user's national personal identification code. It addresses the user's Mobile-ID app; the user confirms the comparison code and enters their PIN in the app. Never collect the PIN.", + "pattern": "^[0-9-]{6,20}$" }, - "parsed_payload": { - "nullable": true, - "description": "Decoded string or structured value. `null` when the code was located but could not be decoded." + "phone_number": { + "type": "string", + "minLength": 1, + "description": "The Mobile-ID phone number in E.164 format (+372... or +370...).", + "maxLength": 16 } - } - }, - "SessionImportJob": { - "type": "object", + }, "required": [ - "uuid", - "status", - "import_type", - "source_format", - "source_filename", - "total_rows", - "processed_rows", - "created_count", - "updated_count", - "skipped_count", - "failed_count" + "country", + "personal_code", + "phone_number" + ] + }, + "DigitalIdWalletApiMobileIdWalletVerificationStartCountryEnum": { + "enum": [ + "EST", + "LTU" ], + "type": "string", + "description": "* `EST` - EST\n* `LTU` - LTU" + }, + "DigitalIdWalletApiSmartIdWalletVerificationStartRequest": { + "type": "object", + "description": "Start a Smart-ID verification (EST, LVA, LTU, BEL).", "properties": { - "uuid": { + "vendor_data": { "type": "string", - "format": "uuid", - "description": "Import job id. Use this value to poll progress and fetch row errors." + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "status": { - "type": "string", - "enum": [ - "PENDING", - "PROCESSING", - "COMPLETED", - "FAILED", - "PAUSED", - "CANCELLED" - ] + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "import_type": { + "sandbox_scenario": { "type": "string", - "enum": [ - "user_verification", - "business_verification", - "status_rules", - "transactions" - ] + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - "source_format": { + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiSmartIdWalletVerificationStartCountryEnum" + } + ], + "description": "ISO 3166-1 alpha-3 country of the Smart-ID identity.\n\n* `EST` - EST\n* `LVA` - LVA\n* `LTU` - LTU\n* `BEL` - BEL" + }, + "personal_code": { "type": "string", - "enum": [ - "csv", - "ndjson" - ] + "minLength": 1, + "description": "The user's national personal identification code. It addresses the user's Smart-ID app; the user confirms the comparison code and enters their PIN in the app. Never collect the PIN.", + "pattern": "^[0-9-]{6,20}$" + } + }, + "required": [ + "country", + "personal_code" + ] + }, + "DigitalIdWalletApiSmartIdWalletVerificationStartCountryEnum": { + "enum": [ + "EST", + "LVA", + "LTU", + "BEL" + ], + "type": "string", + "description": "* `EST` - EST\n* `LVA` - LVA\n* `LTU` - LTU\n* `BEL` - BEL" + }, + "DigitalIdWalletApiGovukWalletWalletVerificationStartRequest": { + "type": "object", + "description": "Start a GOV.UK Wallet verification (GBR).", + "properties": { + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "source_filename": { - "type": "string" + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "total_rows": { - "type": "integer", - "format": "int64" + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - "processed_rows": { - "type": "integer", - "format": "int64" + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiCountry520Enum" + } + ], + "default": "GBR", + "description": "ISO 3166-1 alpha-3 country of the GOV.UK Wallet identity. Defaults to GBR, the only country it serves.\n\n* `GBR` - GBR" }, - "checkpoint_row": { - "type": "integer", - "format": "int64" + "return_url": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 }, - "created_count": { - "type": "integer", - "format": "int64" + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + } + }, + "required": [ + "return_url" + ] + }, + "DigitalIdWalletApiCountry520Enum": { + "enum": [ + "GBR" + ], + "type": "string", + "description": "* `GBR` - GBR" + }, + "DigitalIdWalletApiOneidWalletVerificationStartRequest": { + "type": "object", + "description": "Start a OneID verification (GBR).", + "properties": { + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "updated_count": { - "type": "integer", - "format": "int64" + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "skipped_count": { - "type": "integer", - "format": "int64" + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - "failed_count": { - "type": "integer", - "format": "int64" + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiCountry520Enum" + } + ], + "default": "GBR", + "description": "ISO 3166-1 alpha-3 country of the OneID identity. Defaults to GBR, the only country it serves.\n\n* `GBR` - GBR" }, - "total_media_bytes": { - "type": "integer", - "format": "int64" + "return_url": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 }, - "last_error": { + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + } + }, + "required": [ + "return_url" + ] + }, + "DigitalIdWalletApiGovbrWalletVerificationStartRequest": { + "type": "object", + "description": "Start a gov.br verification (BRA).", + "properties": { + "vendor_data": { "type": "string", - "nullable": true + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "created_at": { - "type": "string", - "format": "date-time" + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "updated_at": { + "sandbox_scenario": { "type": "string", - "format": "date-time" + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - "started_at": { - "type": "string", - "format": "date-time", - "nullable": true + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiGovbrWalletVerificationStartCountryEnum" + } + ], + "default": "BRA", + "description": "ISO 3166-1 alpha-3 country of the gov.br identity. Defaults to BRA, the only country it serves.\n\n* `BRA` - BRA" }, - "completed_at": { + "return_url": { "type": "string", - "format": "date-time", - "nullable": true + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 + }, + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" } - } - }, - "SessionImportError": { - "type": "object", + }, "required": [ - "uuid", - "row_number", - "error", - "raw_row" + "return_url" + ] + }, + "DigitalIdWalletApiGovbrWalletVerificationStartCountryEnum": { + "enum": [ + "BRA" ], + "type": "string", + "description": "* `BRA` - BRA" + }, + "DigitalIdWalletApiUaePassWalletVerificationStartRequest": { + "type": "object", + "description": "Start a UAE PASS verification (ARE).", "properties": { - "uuid": { + "vendor_data": { "type": "string", - "format": "uuid" + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "row_number": { - "type": "integer", - "format": "int64" + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "external_id": { + "sandbox_scenario": { "type": "string", - "nullable": true - }, - "error": { - "type": "string" + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - "raw_row": { - "type": "object", - "additionalProperties": true + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiUaePassWalletVerificationStartCountryEnum" + } + ], + "default": "ARE", + "description": "ISO 3166-1 alpha-3 country of the UAE PASS identity. Defaults to ARE, the only country it serves.\n\n* `ARE` - ARE" }, - "created_at": { + "return_url": { "type": "string", - "format": "date-time" + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 + }, + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" } - } + }, + "required": [ + "return_url" + ] }, - "BrandingCustomizationUpdate": { + "DigitalIdWalletApiUaePassWalletVerificationStartCountryEnum": { + "enum": [ + "ARE" + ], + "type": "string", + "description": "* `ARE` - ARE" + }, + "DigitalIdWalletApiFrejaidWalletVerificationStartRequest": { "type": "object", - "description": "Partial branding update body. Provide only the fields you want to change. Hex colors accept `#RRGGBB` or `#RRGGBBAA`. Custom domain and white-label email are not accepted here.", + "description": "Start a Freja eID verification (SWE).", "properties": { - "color_primary": { + "vendor_data": { "type": "string", - "description": "Headers / primary text color." + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "color_secondary": { - "type": "string", - "description": "Paragraph / secondary text color." + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "color_background": { + "sandbox_scenario": { "type": "string", - "description": "Page background color." + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - "color_panel": { - "type": "string", - "description": "Panel / accent color." + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiCountryEe9Enum" + } + ], + "default": "SWE", + "description": "ISO 3166-1 alpha-3 country of the Freja eID identity. Defaults to SWE, the only country it serves.\n\n* `SWE` - SWE" }, - "color_panel_10": { + "return_url": { "type": "string", - "description": "Panel color at 10% opacity." + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 }, - "color_on_panel_1": { + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + } + }, + "required": [ + "return_url" + ] + }, + "DigitalIdWalletApiCountryEe9Enum": { + "enum": [ + "SWE" + ], + "type": "string", + "description": "* `SWE` - SWE" + }, + "DigitalIdWalletApiPersonalausweisWalletVerificationStartRequest": { + "type": "object", + "description": "Start a Personalausweis verification (DEU).", + "properties": { + "vendor_data": { "type": "string", - "description": "Support text color on panels." + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "color_on_panel_2": { - "type": "string", - "description": "Pill / badge color on panels." + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "color_on_background": { + "sandbox_scenario": { "type": "string", - "description": "Text color on background." + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - "color_button_1": { - "type": "string", - "description": "Primary button color." + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiPersonalausweisWalletVerificationStartCountryEnum" + } + ], + "default": "DEU", + "description": "ISO 3166-1 alpha-3 country of the Personalausweis identity. Defaults to DEU, the only country it serves.\n\n* `DEU` - DEU" }, - "color_button_2": { + "return_url": { "type": "string", - "description": "Secondary button color." + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 }, - "color_button_text_1": { + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + } + }, + "required": [ + "return_url" + ] + }, + "DigitalIdWalletApiPersonalausweisWalletVerificationStartCountryEnum": { + "enum": [ + "DEU" + ], + "type": "string", + "description": "* `DEU` - DEU" + }, + "DigitalIdWalletApiFtnWalletVerificationStartRequest": { + "type": "object", + "description": "Start a Finnish Trust Network verification (FIN).", + "properties": { + "vendor_data": { "type": "string", - "description": "Primary button text color." + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "color_button_text_2": { - "type": "string", - "description": "Secondary button text color." + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "color_pill_text": { + "sandbox_scenario": { "type": "string", - "description": "Text color for pills, inputs and selectors." - }, - "border_radius_panel": { - "type": "integer", - "minimum": 0, - "description": "Panel corner radius in pixels." - }, - "border_radius_buttons": { - "type": "integer", - "minimum": 0, - "description": "Button corner radius in pixels." + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - "font_family": { - "type": "string", - "description": "Google Font family name (e.g. `Inter`, `AR One Sans`)." + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiFtnWalletVerificationStartCountryEnum" + } + ], + "default": "FIN", + "description": "ISO 3166-1 alpha-3 country of the Finnish Trust Network identity. Defaults to FIN, the only country it serves.\n\n* `FIN` - FIN" }, - "font_weight": { + "return_url": { "type": "string", - "description": "Font weight available for the selected family (e.g. `regular`, `500`, `700`)." + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 }, - "app_public_name": { + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + } + }, + "required": [ + "return_url" + ] + }, + "DigitalIdWalletApiFtnWalletVerificationStartCountryEnum": { + "enum": [ + "FIN" + ], + "type": "string", + "description": "* `FIN` - FIN" + }, + "DigitalIdWalletApiIdinWalletVerificationStartRequest": { + "type": "object", + "description": "Start a iDIN verification (NLD).", + "properties": { + "vendor_data": { "type": "string", "nullable": true, - "description": "Public name shown in the verification flow." + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "privacy_policy_url": { - "type": "string", + "metadata": { + "type": "object", + "additionalProperties": {}, "nullable": true, - "description": "Privacy policy URL shown to the end user." - }, - "skip_welcome_screen": { - "type": "boolean", - "description": "Skip the initial welcome screen." - }, - "disable_login_with_didit": { - "type": "boolean", - "description": "Continue as guest instead of showing the login-with-Didit screen. Cannot be set back to `false` while a custom domain is configured (400)." - }, - "hide_progress_bar": { - "type": "boolean", - "description": "Hide the progress bar during the flow." + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "callback_seconds": { - "type": "integer", - "minimum": 0, - "description": "Delay (seconds) before redirecting to the callback URL." + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - "delete_logo_square": { - "type": "boolean", - "description": "When `true`, removes the current square logo." + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiIdinWalletVerificationStartCountryEnum" + } + ], + "default": "NLD", + "description": "ISO 3166-1 alpha-3 country of the iDIN identity. Defaults to NLD, the only country it serves.\n\n* `NLD` - NLD" }, - "delete_logo_rectangular": { - "type": "boolean", - "description": "When `true`, removes the current rectangular logo." + "return_url": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 }, - "delete_favicon": { - "type": "boolean", - "description": "When `true`, removes the current favicon." + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" } - } + }, + "required": [ + "return_url" + ] }, - "BrandingCustomization": { + "DigitalIdWalletApiIdinWalletVerificationStartCountryEnum": { + "enum": [ + "NLD" + ], + "type": "string", + "description": "* `NLD` - NLD" + }, + "DigitalIdWalletApiItsmeWalletVerificationStartRequest": { "type": "object", - "description": "White-label branding for an application: colors, fonts, radii, logo URLs and UI/UX options. Logo URLs are read-only (set them by uploading images). Custom domain and white-label email are not included.", + "description": "Start a itsme verification (BEL, LUX, NLD).", "properties": { - "color_primary": { - "type": "string" - }, - "color_secondary": { - "type": "string" - }, - "color_background": { - "type": "string" - }, - "color_panel": { - "type": "string" - }, - "color_panel_10": { - "type": "string" - }, - "color_on_panel_1": { - "type": "string" - }, - "color_on_panel_2": { - "type": "string" - }, - "color_on_background": { - "type": "string" - }, - "color_button_1": { - "type": "string" + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "color_button_2": { - "type": "string" + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "color_button_text_1": { - "type": "string" + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - "color_button_text_2": { - "type": "string" + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiItsmeWalletVerificationStartCountryEnum" + } + ], + "description": "ISO 3166-1 alpha-3 country of the itsme identity.\n\n* `BEL` - BEL\n* `LUX` - LUX\n* `NLD` - NLD" }, - "color_pill_text": { - "type": "string" + "return_url": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 }, - "border_radius_panel": { - "type": "integer" + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + } + }, + "required": [ + "country", + "return_url" + ] + }, + "DigitalIdWalletApiItsmeWalletVerificationStartCountryEnum": { + "enum": [ + "BEL", + "LUX", + "NLD" + ], + "type": "string", + "description": "* `BEL` - BEL\n* `LUX` - LUX\n* `NLD` - NLD" + }, + "DigitalIdWalletApiBuypassWalletVerificationStartRequest": { + "type": "object", + "description": "Start a Buypass ID verification (NOR).", + "properties": { + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "border_radius_buttons": { - "type": "integer" + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "font_family": { - "type": "string" + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - "font_weight": { - "type": "string" + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiCountryAfcEnum" + } + ], + "default": "NOR", + "description": "ISO 3166-1 alpha-3 country of the Buypass ID identity. Defaults to NOR, the only country it serves.\n\n* `NOR` - NOR" }, - "font_url": { + "return_url": { "type": "string", - "nullable": true, - "description": "Read-only. Resolved Google Fonts stylesheet URL for the selected family." + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 }, - "logo_square": { + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + } + }, + "required": [ + "return_url" + ] + }, + "DigitalIdWalletApiCountryAfcEnum": { + "enum": [ + "NOR" + ], + "type": "string", + "description": "* `NOR` - NOR" + }, + "DigitalIdWalletApiVippsWalletVerificationStartRequest": { + "type": "object", + "description": "Start a Vipps verification (NOR).", + "properties": { + "vendor_data": { "type": "string", "nullable": true, - "description": "Read-only URL of the square logo. Upload via `image_square`." + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "logo_rectangular": { - "type": "string", + "metadata": { + "type": "object", + "additionalProperties": {}, "nullable": true, - "description": "Read-only URL of the rectangular logo. Upload via `image_rectangular`." + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "favicon": { + "sandbox_scenario": { "type": "string", - "nullable": true, - "description": "Read-only URL of the favicon. Upload via `image_favicon`." + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - "app_public_name": { + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiCountryAfcEnum" + } + ], + "default": "NOR", + "description": "ISO 3166-1 alpha-3 country of the Vipps identity. Defaults to NOR, the only country it serves.\n\n* `NOR` - NOR" + }, + "return_url": { "type": "string", - "nullable": true + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 }, - "privacy_policy_url": { + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + } + }, + "required": [ + "return_url" + ] + }, + "DigitalIdWalletApiBankidNoWalletVerificationStartRequest": { + "type": "object", + "description": "Start a BankID verification (NOR).", + "properties": { + "vendor_data": { "type": "string", - "nullable": true + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "skip_welcome_screen": { - "type": "boolean" + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." }, - "disable_login_with_didit": { - "type": "boolean" + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." }, - "hide_progress_bar": { - "type": "boolean" + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiCountryAfcEnum" + } + ], + "default": "NOR", + "description": "ISO 3166-1 alpha-3 country of the BankID identity. Defaults to NOR, the only country it serves.\n\n* `NOR` - NOR" }, - "callback_seconds": { - "type": "integer" + "return_url": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 + }, + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" } - } - }, - "WebhookEvent": { - "type": "string", - "description": "Supported webhook event name. Use these exact strings in `subscribed_events`; unsupported values are rejected.", - "enum": [ - "status.updated", - "data.updated", - "user.status.updated", - "user.data.updated", - "business.status.updated", - "business.data.updated", - "activity.created", - "transaction.created", - "transaction.status.updated", - "travel_rule.status.updated" - ] - }, - "WebhookSubscribedEvents": { - "type": "array", - "description": "Event filter for this webhook destination. Didit delivers only webhooks whose event type exactly matches one of these values — there is no wildcard subscription. When sent, the list must contain at least one valid event (an explicit `[]` is rejected with 400); a destination whose list is empty (field omitted at create) receives nothing. On update the list is replaced wholesale, never merged.", - "minItems": 1, - "uniqueItems": true, - "items": { - "$ref": "#/components/schemas/WebhookEvent" }, - "example": [ - "status.updated", - "data.updated" + "required": [ + "return_url" ] }, - "PhoneVerificationSendResponse": { + "DigitalIdWalletApiBankidSeWalletVerificationStartRequest": { "type": "object", + "description": "Start a BankID verification (SWE).", "properties": { - "request_id": { + "vendor_data": { "type": "string", - "format": "uuid", - "description": "Session id of the verification. A `Retry` send returns the same `request_id` as the original send. This id appears in the Business Console, is returned again by a finalized `POST /v3/phone/check/`, and can be passed to `GET /v3/session/{sessionId}/decision/`." + "nullable": true, + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, - "status": { + "metadata": { + "type": "object", + "additionalProperties": {}, + "nullable": true, + "description": "Optional JSON object stored with the verification and returned in results and webhooks." + }, + "sandbox_scenario": { "type": "string", - "enum": [ - "Success", - "Retry", - "Blocked" + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + }, + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiCountryEe9Enum" + } ], - "description": "`Success` — OTP sent to a new verification. `Retry` — OTP re-sent for the pending verification created by a previous send. `Blocked` — the anti-fraud layer refused to send; the verification is immediately finalized as `Declined` and nothing is billed." + "default": "SWE", + "description": "ISO 3166-1 alpha-3 country of the BankID identity. Defaults to SWE, the only country it serves.\n\n* `SWE` - SWE" }, - "reason": { + "return_url": { "type": "string", - "nullable": true, - "description": "Why a send was `Blocked` (e.g., `repeated_attempts`, `suspicious`, `spam`). `null` on `Success` and `Retry`." + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 }, + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + } + }, + "required": [ + "return_url" + ] + }, + "DigitalIdWalletApiMitidWalletVerificationStartRequest": { + "type": "object", + "description": "Start a MitID verification (DNK).", + "properties": { "vendor_data": { "type": "string", "nullable": true, - "description": "Echo of the `vendor_data` stored on the session (from the first send)." + "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." }, "metadata": { "type": "object", + "additionalProperties": {}, "nullable": true, - "description": "Echo of the `metadata` stored on the session (from the first send)." + "description": "Optional JSON object stored with the verification and returned in results and webhooks." + }, + "sandbox_scenario": { + "type": "string", + "minLength": 1, + "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + }, + "country": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiMitidWalletVerificationStartCountryEnum" + } + ], + "default": "DNK", + "description": "ISO 3166-1 alpha-3 country of the MitID identity. Defaults to DNK, the only country it serves.\n\n* `DNK` - DNK" + }, + "return_url": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", + "maxLength": 2048 + }, + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + } + ], + "default": "same_device", + "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" } - } + }, + "required": [ + "return_url" + ] }, - "PhoneVerificationCheckResponse": { + "DigitalIdWalletApiMitidWalletVerificationStartCountryEnum": { + "enum": [ + "DNK" + ], + "type": "string", + "description": "* `DNK` - DNK" + }, + "DigitalIdWalletApiWalletCatalogResponse": { "type": "object", "properties": { - "request_id": { - "type": "string", - "format": "uuid", - "description": "On `Approved`/`Declined`: the session id of the matched verification — identical to the `request_id` returned by `POST /v3/phone/send/`. On `Failed` and `Expired or Not Found`: a random one-off UUID that cannot be looked up later." + "wallets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DigitalIdWalletApiWalletCatalogEntry" + } + } + }, + "required": [ + "wallets" + ] + }, + "DigitalIdWalletApiWalletCatalogEntry": { + "type": "object", + "properties": { + "id": { + "type": "string" }, - "status": { + "name": { + "type": "string" + }, + "path": { "type": "string", - "enum": [ - "Approved", - "Declined", - "Failed", - "Expired or Not Found" - ], - "description": "`Approved` — correct code, no declining risk. `Declined` — terminal: a declining risk matched or the attempt budget (3) was exhausted. `Failed` — wrong code, attempts remaining. `Expired or Not Found` — no pending verification for this number in the last 5 minutes." + "description": "The wallet's own start endpoint." }, - "message": { + "countries": { + "type": "array", + "items": { + "type": "string" + } + }, + "issuing_authority": { + "type": "string" + }, + "level_of_assurance": { "type": "string", - "description": "Human-readable explanation of the outcome, including the number of attempts remaining after a wrong code." + "nullable": true }, - "phone": { - "type": "object", - "nullable": true, - "description": "Full phone report. Present (non-null) only on finalized outcomes (`Approved`/`Declined`); `null` on `Failed` and absent on `Expired or Not Found`.", - "properties": { - "status": { - "type": "string", - "enum": [ - "Approved", - "Declined", - "In Review", - "Not Finished", - "Expired" - ], - "description": "Final status of the phone verification. Matches the top-level `status` for finalized checks." - }, - "phone_number_prefix": { - "type": "string", - "description": "Country calling code with leading `+` (e.g., `+1`).", - "example": "+1" - }, - "phone_number": { - "type": "string", - "description": "National number without the country calling code.", - "example": "4155552671" - }, - "full_number": { - "type": "string", - "description": "Complete number in E.164 format.", - "example": "+14155552671" - }, - "country_code": { - "type": "string", - "nullable": true, - "description": "ISO 3166-1 alpha-2 country of the number.", - "example": "US" - }, - "country_name": { - "type": "string", - "description": "Full country name derived from `country_code`.", - "example": "United States" - }, - "carrier": { - "type": "object", - "description": "Current network serving the number, from the carrier lookup run at finalization.", - "properties": { - "name": { - "type": "string", - "description": "Carrier name. Empty string when the lookup could not identify it." - }, - "type": { - "type": "string", - "description": "Line type reported by the lookup: `mobile`, `fixed_line`, `voip`, `isp`, `vpn`, `toll_free`, `premium_rate`, `pager`, `payphone`, `satellite`, `service`, `shared_cost`, `short_codes_commercial`, `calling_cards`, `local_rate`, `universal_access`, `voice_mail`, `other`, or `unknown`." - } - } - }, - "is_disposable": { - "type": "boolean", - "description": "`true` when the number belongs to a temporary/burner number service." - }, - "is_virtual": { - "type": "boolean", - "description": "`true` when the line type is virtual (`voip`, `isp`, or `vpn`)." - }, - "verification_method": { - "type": "string", - "description": "Channel that actually delivered the OTP once delivery is confirmed (e.g., `whatsapp`, `sms`); until then, the requested channel." - }, - "verification_attempts": { - "type": "integer", - "description": "Number of OTP sends for this verification (1, or 2 after a retry)." - }, - "verified_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the correct code was entered. `null` if the verification was never approved." - }, - "warnings": { - "type": "array", - "description": "Risk warnings recorded during the verification (duplicate, disposable, VoIP, attempts exceeded, blocklist, etc.). Empty array when there are none.", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "description": "Feature that produced the warning. Always `PHONE` here." - }, - "risk": { - "type": "string", - "description": "Machine-readable risk code." - }, - "additional_data": { - "type": "object", - "nullable": true, - "description": "Risk-specific context (e.g., the duplicated session id for duplicate risks). `null` when the risk carries no extra data." - }, - "log_type": { - "type": "string", - "enum": [ - "error", - "warning", - "information" - ], - "description": "Severity. `error` risks decline the verification, `warning` flags it for review, `information` is recorded without affecting the status." - }, - "short_description": { - "type": "string", - "description": "Human-readable one-line summary of the risk." - }, - "long_description": { - "type": "string", - "description": "Human-readable explanation of the risk." - } - } - } - }, - "lifecycle": { - "type": "array", - "description": "Chronological audit trail of the verification. Event `type` is one of `PHONE_VERIFICATION_MESSAGE_SENT`, `PHONE_VERIFICATION_RETRY_MESSAGE_SENT`, `PHONE_VERIFICATION_BLOCKED`, `PHONE_DELIVERY_DELIVERED`, `PHONE_DELIVERY_UNDELIVERABLE`, `VALID_CODE_ENTERED`, `INVALID_CODE_ENTERED`, `PHONE_VERIFICATION_APPROVED`, `PHONE_VERIFICATION_DECLINED`, `PHONE_VERIFICATION_IN_REVIEW`, `PHONE_VERIFICATION_EXPIRED`.", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "Event type." - }, - "timestamp": { - "type": "string", - "format": "date-time", - "description": "When the event happened." - }, - "details": { - "type": "object", - "nullable": true, - "description": "Event-specific payload. Send events: `{status, reason, channel, actual_channel}` (`actual_channel` is filled once delivery is confirmed). Delivery events: `{channel, status}`. Code-entry events: `{code_tried, status}`. Final-status events: `null`, or `{reason}` when declined or in review." - }, - "fee": { - "type": "number", - "description": "USD amount billed for this event. Non-zero only on send events (the delivery price); 0 for code entries, delivery confirmations, and status events." - } - } - } - }, - "matches": { - "type": "array", - "description": "Other sessions of your application (KYC, KYB, or API) where the same phone number was used by a different user (different `vendor_data`). Up to 5 entries, a synthetic list-entry blocklist match first when present (session-derived matches keep query order). Empty array when there are no matches.", - "items": { - "type": "object", - "properties": { - "session_id": { - "type": "string", - "format": "uuid", - "description": "Session id of the other verification that used this phone number." - }, - "session_number": { - "type": "integer", - "description": "Human-friendly sequential number of that session." - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "`vendor_data` of that session." - }, - "verification_date": { - "type": "string", - "description": "When that session was created (`YYYY-MM-DDTHH:MM:SSZ`)." - }, - "phone_number": { - "type": "string", - "description": "The matched phone number." - }, - "status": { - "type": "string", - "description": "Status of that session (e.g., `Approved`, `Declined`, `In Progress`)." - }, - "is_blocklisted": { - "type": "boolean", - "description": "Whether the phone number is on your application's blocklist." - }, - "api_service": { - "type": "string", - "nullable": true, - "description": "For API-type sessions, the standalone API that created it (e.g., `PHONE_VERIFICATION`); `null` for regular verification sessions." - }, - "source": { - "type": "string", - "description": "Where the match was found. Usually `session`; a blocklist hit via an organization list entry prepends a synthetic match with `source: \"list_entry\"` and null `session_id`/`session_number`." - } - } - } - }, - "phone_social": { - "$ref": "#/components/schemas/PhoneSocialFootprint" - } + "interaction": { + "$ref": "#/components/schemas/DigitalIdWalletApiInteractionEnum" + }, + "required_fields": { + "type": "array", + "items": { + "type": "string" } }, - "vendor_data": { + "available": { + "type": "boolean", + "description": "Whether this application can start it in this environment now. Listed does not mean live." + } + }, + "required": [ + "available", + "countries", + "id", + "interaction", + "issuing_authority", + "level_of_assurance", + "name", + "path", + "required_fields" + ] + }, + "CaseDuplicateCaseDuplicate": { + "type": "object", + "properties": { + "uuid": { "type": "string", - "nullable": true, - "description": "`vendor_data` of the matched verification's session. `null` on `Expired or Not Found`." + "format": "uuid", + "readOnly": true }, - "metadata": { - "type": "object", + "case_number": { + "type": "string", "nullable": true, - "description": "`metadata` of the matched verification's session. `null` on `Expired or Not Found`." + "maxLength": 32 + }, + "title": { + "type": "string", + "maxLength": 255 + }, + "status": { + "$ref": "#/components/schemas/CaseDuplicateStatusEnum" + }, + "priority": { + "$ref": "#/components/schemas/CaseDuplicatePriorityEnum" }, "created_at": { "type": "string", "format": "date-time", - "description": "Timestamp of this check response." + "readOnly": true } - } + }, + "required": [ + "created_at", + "title", + "uuid" + ] }, - "EmailVerificationSendResponse": { + "CaseDuplicateCaseDuplicatePage": { "type": "object", "properties": { - "request_id": { + "count": { + "type": "integer" + }, + "next_offset": { + "type": "integer", + "nullable": true + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseDuplicateCaseDuplicate" + } + } + }, + "required": [ + "count", + "next_offset", + "results" + ] + }, + "RoutingPublicationPolicyDraftResponse": { + "type": "object", + "properties": { + "draft": { + "allOf": [ + { + "$ref": "#/components/schemas/RoutingPublicationPolicyDraft" + } + ], + "nullable": true + } + }, + "required": [ + "draft" + ] + }, + "RoutingPublicationPolicyDraftWriteRequest": { + "type": "object", + "properties": { + "expected_version": { + "type": "integer", + "minimum": 0 + }, + "expected_revision": { "type": "string", "format": "uuid", - "description": "Session id of the verification. A `Retry` send returns the same `request_id` as the original send. This id appears in the Business Console, is returned again by a finalized `POST /v3/email/check/`, and can be passed to `GET /v3/session/{sessionId}/decision/`." + "nullable": true }, - "status": { + "changes": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "changes", + "expected_revision", + "expected_version" + ] + }, + "CasePublicationPolicyDraft": { + "type": "object", + "properties": { + "revision": { "type": "string", - "enum": [ - "Success", - "Retry", - "Undeliverable" - ], - "description": "`Success` — OTP emailed to a new verification (billed). `Retry` — fresh OTP emailed for the pending verification created by a previous send (free). `Undeliverable` — the address failed deliverability validation or the message could not be sent; the verification is immediately finalized as `Declined` and nothing is billed." + "format": "uuid" }, - "reason": { + "base_version": { + "type": "integer", + "maximum": 2147483647, + "minimum": 0 + }, + "changes": {}, + "actor_email": { "type": "string", + "format": "email", "nullable": true, - "enum": [ - "email_can_not_be_delivered", - null - ], - "description": "`email_can_not_be_delivered` when `status` is `Undeliverable`; `null` otherwise." + "maxLength": 254 }, - "vendor_data": { + "updated_at": { "type": "string", - "nullable": true, - "description": "Echo of the `vendor_data` stored on the session (from the first send)." + "format": "date-time", + "readOnly": true + } + }, + "required": [ + "base_version", + "updated_at" + ] + }, + "CasePublicationPolicyDraftPreconditionRequest": { + "type": "object", + "properties": { + "expected_version": { + "type": "integer", + "minimum": 0 }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Echo of the `metadata` stored on the session (from the first send)." + "expected_revision": { + "type": "string", + "format": "uuid", + "nullable": true } - } + }, + "required": [ + "expected_revision", + "expected_version" + ] }, - "EmailVerificationCheckResponse": { + "CaseCollaborationCaseNote": { "type": "object", "properties": { - "request_id": { + "uuid": { "type": "string", "format": "uuid", - "description": "On `Approved`/`Declined`: the session id of the matched verification — identical to the `request_id` returned by `POST /v3/email/send/`. On `Failed` and `Expired or Not Found`: a random one-off UUID that cannot be looked up later." + "readOnly": true }, - "status": { - "type": "string", - "enum": [ - "Approved", - "Declined", - "Failed", - "Expired or Not Found" - ], - "description": "`Approved` — correct code, no declining risk. `Declined` — terminal: a declining risk matched or the attempt budget (3) was exhausted. `Failed` — wrong code, attempts remaining. `Expired or Not Found` — no pending verification for this address in the last 5 minutes." + "comment": { + "type": "string" }, - "message": { + "actor_email": { "type": "string", - "description": "Human-readable explanation of the outcome, including the number of attempts remaining after a wrong code." - }, - "email": { - "type": "object", + "format": "email", "nullable": true, - "description": "Full email report. Present (non-null) only on finalized outcomes (`Approved`/`Declined`); `null` on `Failed` and absent on `Expired or Not Found`.", - "properties": { - "status": { - "type": "string", - "enum": [ - "Approved", - "Declined", - "In Review", - "Not Finished", - "Expired" - ], - "description": "Final status of the email verification. Matches the top-level `status` for finalized checks." - }, - "email": { - "type": "string", - "format": "email", - "description": "The verified email address.", - "example": "alice@example.com" - }, - "is_breached": { - "type": "boolean", - "description": "`true` when the address appears in known data breaches (breach intelligence lookup run at finalization)." - }, - "breaches": { - "type": "array", - "description": "Up to 5 most recent known breaches containing this address. Empty array (or omitted) when `is_breached` is `false`.", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Breach identifier." - }, - "domain": { - "type": "string", - "description": "Domain of the breached service." - }, - "breach_date": { - "type": "string", - "format": "date", - "description": "Date the breach occurred." - }, - "breach_emails_count": { - "type": "integer", - "description": "Total number of accounts exposed in the breach." - }, - "description": { - "type": "string", - "description": "Human-readable description of the breach (may contain HTML markup)." - }, - "logo_path": { - "type": "string", - "description": "URL of the breached service's logo." - }, - "data_classes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Categories of data exposed, in snake_case (e.g., `email_addresses`, `passwords`)." - }, - "is_verified": { - "type": "boolean", - "description": "Whether the breach has been verified as legitimate." - } - } - } - }, - "is_disposable": { - "type": "boolean", - "description": "`true` when the domain belongs to a known disposable/temporary-mail provider." - }, - "is_undeliverable": { - "type": "boolean", - "description": "`true` when the address cannot receive mail (failed syntax or DNS/MX validation, or the message bounced)." - }, - "verification_attempts": { - "type": "integer", - "description": "Number of OTP sends for this verification (1, or 2 after a retry)." - }, - "verified_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the correct code was entered. `null` if the verification was never approved." - }, - "warnings": { - "type": "array", - "description": "Risk warnings recorded during the verification (duplicate, breached, disposable, undeliverable, attempts exceeded, blocklist, etc.). Empty array when there are none.", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "description": "Feature that produced the warning. Always `EMAIL` here." - }, - "risk": { - "type": "string", - "description": "Machine-readable risk code." - }, - "additional_data": { - "type": "object", - "nullable": true, - "description": "Risk-specific context (e.g., the duplicated session id for duplicate risks). `null` when the risk carries no extra data." - }, - "log_type": { - "type": "string", - "enum": [ - "error", - "warning", - "information" - ], - "description": "Severity. `error` risks decline the verification, `warning` flags it for review, `information` is recorded without affecting the status." - }, - "short_description": { - "type": "string", - "description": "Human-readable one-line summary of the risk." - }, - "long_description": { - "type": "string", - "description": "Human-readable explanation of the risk." - } - } - } - }, - "lifecycle": { - "type": "array", - "description": "Chronological audit trail of the verification. Event `type` is one of `EMAIL_VERIFICATION_MESSAGE_SENT`, `EMAIL_VERIFICATION_RETRY_MESSAGE_SENT`, `VALID_CODE_ENTERED`, `INVALID_CODE_ENTERED`, `EMAIL_VERIFICATION_APPROVED`, `EMAIL_VERIFICATION_DECLINED`, `EMAIL_VERIFICATION_IN_REVIEW`, `EMAIL_VERIFICATION_EXPIRED`.", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "Event type." - }, - "timestamp": { - "type": "string", - "format": "date-time", - "description": "When the event happened." - }, - "details": { - "type": "object", - "nullable": true, - "description": "Event-specific payload. Send events: `{status, reason}`. Code-entry events: `{code_tried, status}`. Final-status events: `null`, or `{reason}` when declined or in review." - }, - "fee": { - "type": "number", - "description": "USD amount billed for this event. Non-zero only on the first successful send; 0 for retries, code entries, and status events." - } - } - } - }, - "matches": { - "type": "array", - "description": "Other sessions of your application (KYC, KYB, or API) where the same email address was used by a different user (different `vendor_data`). Up to 5 entries; a synthetic list-entry blocklist match is prepended when present (and only when no session-derived blocklisted email exists) — session-derived matches keep created_at order. Empty array when there are no matches.", - "items": { - "type": "object", - "properties": { - "session_id": { - "type": "string", - "format": "uuid", - "description": "Session id of the other verification that used this email address." - }, - "session_number": { - "type": "integer", - "description": "Human-friendly sequential number of that session." - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "`vendor_data` of that session." - }, - "verification_date": { - "type": "string", - "description": "When that session was created (`YYYY-MM-DDTHH:MM:SSZ`)." - }, - "email": { - "type": "string", - "description": "The matched email address." - }, - "status": { - "type": "string", - "description": "Status of that session (e.g., `Approved`, `Declined`, `In Progress`)." - }, - "is_blocklisted": { - "type": "boolean", - "description": "Whether the email address is on your application's blocklist." - }, - "api_service": { - "type": "string", - "nullable": true, - "description": "For API-type sessions, the standalone API that created it (e.g., `PHONE_VERIFICATION`); `null` for regular verification sessions." - }, - "source": { - "type": "string", - "description": "Where the match was found. Usually `session`; a blocklist hit via an organization list entry prepends a synthetic match with `source: \"list_entry\"` and null `session_id`/`session_number`." - } - } - } - }, - "email_social": { - "$ref": "#/components/schemas/EmailSocialFootprint" - } - } + "maxLength": 255 }, - "vendor_data": { + "actor_name": { "type": "string", "nullable": true, - "description": "`vendor_data` of the matched verification's session. `null` on `Expired or Not Found`." + "maxLength": 255 }, - "metadata": { - "type": "object", - "nullable": true, - "description": "`metadata` of the matched verification's session. `null` on `Expired or Not Found`." + "mentioned_emails": {}, + "is_internal": { + "type": "boolean" }, + "attachments": {}, + "tags": {}, "created_at": { "type": "string", "format": "date-time", - "description": "Timestamp of this check response." + "readOnly": true + }, + "reply_to": { + "allOf": [ + { + "$ref": "#/components/schemas/CaseCollaborationCaseNoteReply" + } + ], + "nullable": true, + "readOnly": true + }, + "is_pinned": { + "type": "boolean" + }, + "client_reference": { + "type": "string", + "format": "uuid", + "nullable": true } - } + }, + "required": [ + "comment", + "created_at", + "reply_to", + "uuid" + ] }, - "WorkflowListItem": { + "CustomerMessagingPaginatedCaseRequestList": { "type": "object", - "description": "One workflow version in the list view.", + "required": [ + "count", + "results" + ], "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Per-version UUID. Use as `{settings_uuid}` for get/update/delete, and as `workflow_id` when creating sessions." + "count": { + "type": "integer", + "example": 123 }, - "workflow_id": { + "next": { "type": "string", - "format": "uuid", - "description": "Stable group ID shared by every version of the same workflow. Equals `uuid` for version 1." + "nullable": true, + "format": "uri", + "example": "http://api.example.org/accounts/?offset=400&limit=100" }, - "workflow_label": { + "previous": { "type": "string", - "description": "Display name for the workflow." + "nullable": true, + "format": "uri", + "example": "http://api.example.org/accounts/?offset=200&limit=100" }, - "workflow_type": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerMessagingCaseRequest" + } + } + } + }, + "FraudCheckResponse": { + "type": "object", + "properties": { + "check_id": { "type": "string", - "nullable": true, - "description": "Base type for simple workflows: `kyc`, `kyb`, `adaptive_age_verification`, `biometric_authentication`, etc. `null` for graph-based workflows without a KYB feature." + "format": "uuid" }, - "is_default": { - "type": "boolean", - "description": "Whether this is the default workflow for new sessions." + "request_id": { + "type": "string", + "format": "uuid", + "description": "Native verification session created for this check." }, - "is_archived": { - "type": "boolean" + "client_reference": { + "type": "string", + "nullable": true }, - "is_white_label_enabled": { - "type": "boolean", - "description": "Whether white-label customization applies to sessions created with this workflow." + "processing_status": { + "$ref": "#/components/schemas/FraudProcessingStatusEnum" }, - "total_price": { - "type": "number", - "description": "Fixed price per verification in USD (JSON number, excludes variable-priced features such as DATABASE_VALIDATION and PHONE_VERIFICATION)." + "profile_id": { + "type": "string" }, - "min_price": { - "type": "number", - "description": "Cheapest fixed price across workflow-graph paths, in USD." + "profile_version": { + "type": "string" }, - "max_price": { - "type": "number", - "description": "Most expensive fixed price across workflow-graph paths, in USD." + "created_at": { + "type": "string", + "format": "date-time" }, - "features": { + "completed_at": { "type": "string", - "description": "Enabled features as a single `\" + \"`-joined string, e.g. `\"OCR + LIVENESS + FACE_MATCH\"`. Split on `\" + \"` to get a list." + "format": "date-time", + "nullable": true }, - "is_simple_workflow": { - "type": "boolean", - "description": "True when the workflow has no custom workflow graph." + "inputs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FraudCheckInput" + } }, - "is_editable": { - "type": "boolean", - "description": "True when this version is a `draft` (drafts are editable in place; published versions are immutable in the Console)." + "modules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FraudCheckModuleReport" + } }, - "workflow_url": { - "type": "string", - "format": "uri", - "description": "Public shareable verification URL for this workflow (`https://verify.didit.me/u/`)." + "attribute_evidence": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": {} + } }, - "max_retry_attempts": { - "type": "integer", - "description": "Maximum retry attempts allowed after a declined verification (0-10)." + "compound_evidence": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": {} + } }, - "retry_window_days": { - "type": "integer", - "nullable": true, - "description": "Rolling window in days used to count retries (1-365). `null` = all-time limit." + "risk": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FraudCheckRisk" + } }, - "session_expiration_time": { - "type": "integer", - "description": "Seconds before an unfinished session expires (3600-2419200)." + "network": { + "type": "object", + "additionalProperties": {} }, - "version": { - "type": "integer", - "description": "Version number within the `workflow_id` group, starting at 1." + "enrichment": { + "type": "object", + "additionalProperties": {} }, - "status": { - "type": "string", - "enum": [ - "draft", - "published" + "identity_relationship": { + "allOf": [ + { + "$ref": "#/components/schemas/FraudCheckIdentityRelationship" + } ], - "description": "Version status. New sessions use the latest `published` version of a group." + "nullable": true }, - "has_draft": { - "type": "boolean", - "description": "True when any version in this `workflow_id` group is currently a draft." + "recommendation": { + "$ref": "#/components/schemas/FraudRecommendationEnum" }, - "created_by": { + "next_action": { + "$ref": "#/components/schemas/FraudNextActionEnum" + }, + "decision_reasons": { + "type": "array", + "items": { + "type": "string" + } + }, + "coverage": { "type": "object", - "nullable": true, - "description": "Who created this version: `{name, email, type}` where `type` is `user` (Console user, `email` set) or `api_key` (`name` is the client id, `email` is null). `null` when no creator was recorded.", - "properties": { - "name": { - "type": "string" - }, - "email": { - "type": "string", - "nullable": true - }, - "type": { - "type": "string", - "enum": [ - "user", - "api_key" - ] - } + "additionalProperties": {} + }, + "usage": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": {} } }, - "updated_at": { + "vendor_data": { "type": "string", - "format": "date-time" + "nullable": true + }, + "metadata": { + "nullable": true } - } + }, + "required": [ + "attribute_evidence", + "check_id", + "client_reference", + "completed_at", + "compound_evidence", + "coverage", + "created_at", + "decision_reasons", + "inputs", + "modules", + "network", + "next_action", + "processing_status", + "profile_id", + "profile_version", + "recommendation", + "risk", + "usage" + ] }, - "SimpleWorkflowRequest": { + "CaseReviewTeam": { "type": "object", - "description": "Create a simple linear workflow. The API converts this feature list into a node-based workflow graph internally. STRICT field whitelist: any key not listed here (including `workflow_type`) is rejected with a 400.", - "required": [ - "features" - ], "properties": { - "workflow_label": { + "name": { "type": "string", - "maxLength": 50, - "default": "ID Verification", - "description": "Display name for the workflow (max 50 characters). Defaults to `\"ID Verification\"` when omitted — always set it so workflows stay distinguishable.", - "example": "Standard KYC" - }, - "is_default": { - "type": "boolean", - "description": "Set this workflow as the default for new sessions." + "maxLength": 100 }, - "status": { + "description": { "type": "string", - "enum": [ - "draft", - "published" - ], - "description": "Omit this field to create a published workflow ready for sessions. Use `draft` only when you want to save without publishing." + "maxLength": 500 }, - "features": { + "members": { "type": "array", - "minItems": 1, - "description": "Verification features in execution order. The API links each item to the next one and adds one final status node automatically.", "items": { - "$ref": "#/components/schemas/SimpleWorkflowFeature" - } - }, - "is_white_label_enabled": { - "type": "boolean", - "description": "Enable white-label customization for sessions created with this workflow.", - "default": false - }, - "is_desktop_allowed": { - "type": "boolean", - "description": "Allow the verification flow to run on desktop browsers.", - "default": false - }, - "max_retry_attempts": { - "type": "integer", - "minimum": 0, - "maximum": 10, - "default": 3, - "description": "Maximum retry attempts allowed after a declined verification. `0` blocks the user after the first decline." + "$ref": "#/components/schemas/CaseReviewTeamMember" + }, + "maxItems": 250 }, - "retry_window_days": { - "type": "integer", - "minimum": 1, - "maximum": 365, - "nullable": true, - "default": 7, - "description": "Rolling window in days used to count retries. `null` enforces an all-time limit." + "is_active": { + "type": "boolean" }, - "face_liveness_max_attempts": { - "type": "integer", - "minimum": 1, - "maximum": 3, - "default": 3, - "description": "Maximum liveness submissions per session. Default 3: one initial attempt plus two retries. This value is also copied into generated liveness and age-estimation workflow nodes unless the feature-level config overrides it." + "uuid": { + "type": "string", + "format": "uuid", + "readOnly": true }, - "face_match_max_attempts": { - "type": "integer", - "minimum": 1, - "maximum": 3, - "default": 3, - "description": "Maximum face-match submissions per session. Default 3: one initial attempt plus two retries. This value is also copied into generated face-match workflow nodes unless the feature-level config overrides it." + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true }, - "session_expiration_time": { - "type": "integer", - "minimum": 3600, - "maximum": 2419200, - "default": 604800, - "description": "Seconds before an unfinished session expires. Minimum 1 hour (3600), maximum 4 weeks (2419200)." + "updated_at": { + "type": "string", + "format": "date-time", + "readOnly": true } } }, - "SimpleWorkflowUpdateRequest": { + "CaseActionCaseEntityActionRequest": { "type": "object", - "description": "Update workflow metadata or replace the full linear feature list. STRICT field whitelist: any key not listed here is rejected with a 400.", "properties": { - "workflow_label": { + "client_reference": { "type": "string", - "maxLength": 50, - "description": "Display name for the workflow (max 50 characters)." + "format": "uuid" }, - "is_default": { + "retry": { "type": "boolean", - "description": "Set this workflow as the default for new sessions." + "default": false + }, + "action": { + "$ref": "#/components/schemas/CaseActionActionEnum" }, "status": { "type": "string", - "enum": [ - "draft", - "published" - ], - "description": "Controls versioning. Omit to update the current version in place (drafts auto-publish). `published` on an already-published version creates and returns a NEW version. `draft` is only valid while the version is still a draft." + "nullable": true }, - "features": { + "tags": { "type": "array", - "minItems": 1, - "description": "If provided, this replaces the full feature order and configuration.", "items": { - "$ref": "#/components/schemas/SimpleWorkflowFeature" + "type": "string", + "minLength": 1 } }, - "is_white_label_enabled": { - "type": "boolean", - "description": "Enable white-label customization for sessions created with this workflow." - }, - "is_desktop_allowed": { - "type": "boolean", - "description": "Allow the verification flow to run on desktop browsers." - }, - "max_retry_attempts": { - "type": "integer", - "minimum": 0, - "maximum": 10, - "description": "Maximum retry attempts allowed after a declined verification. `0` blocks the user after the first decline." - }, - "retry_window_days": { - "type": "integer", - "minimum": 1, - "maximum": 365, - "nullable": true, - "description": "Rolling window in days used to count retries. `null` enforces an all-time limit." - }, - "face_liveness_max_attempts": { - "type": "integer", - "minimum": 1, - "maximum": 3, - "description": "Maximum liveness submissions per session. This value is also copied into generated liveness and age-estimation workflow nodes unless the feature-level config overrides it." - }, - "face_match_max_attempts": { - "type": "integer", - "minimum": 1, - "maximum": 3, - "description": "Maximum face-match submissions per session. This value is also copied into generated face-match workflow nodes unless the feature-level config overrides it." - }, - "session_expiration_time": { - "type": "integer", - "minimum": 3600, - "maximum": 2419200, - "description": "Seconds before an unfinished session expires. Minimum 1 hour (3600), maximum 4 weeks (2419200)." + "note": { + "type": "string", + "nullable": true } - } + }, + "required": [ + "action" + ] }, - "SimpleWorkflowFeature": { + "RoutingPublicationPaginatedPolicyRevisionList": { "type": "object", "required": [ - "feature" + "count", + "results" ], "properties": { - "feature": { - "type": "string", - "description": "Feature to run. Use the uppercase values shown here.", - "enum": [ - "OCR", - "NFC", - "LIVENESS", - "FACE_MATCH", - "PROOF_OF_ADDRESS", - "QUESTIONNAIRE", - "DOCUMENT_AI", - "PHONE_VERIFICATION", - "EMAIL_VERIFICATION", - "DATABASE_VALIDATION", - "AML", - "IP_ANALYSIS", - "AGE_ESTIMATION", - "KYB_REGISTRY", - "KYB_DOCUMENTS", - "KYB_KEY_PEOPLE" - ] + "count": { + "type": "integer", + "example": 123 }, - "config": { - "type": "object", - "description": "Feature-specific settings. For `QUESTIONNAIRE`, include `questionnaire_uuid` with the `questionnaire_id` returned by the questionnaire create endpoint. For `OCR`, `methods` selects which ID Verification methods each country may use (document capture, non-doc lookup against a government or other authoritative source, and digital identity wallets); omit it and every country is document capture only.", - "additionalProperties": true + "next": { + "type": "string", + "nullable": true, + "format": "uri", + "example": "http://api.example.org/accounts/?offset=400&limit=100" }, - "label": { + "previous": { "type": "string", - "description": "Optional internal label for this workflow node." + "nullable": true, + "format": "uri", + "example": "http://api.example.org/accounts/?offset=200&limit=100" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutingPublicationPolicyRevision" + } } } }, - "ReviewItem": { + "FraudRecommendationEnum": { + "enum": [ + "approve", + "review", + "reject" + ], + "type": "string", + "description": "* `approve` - Approve\n* `review` - Review\n* `reject` - Reject" + }, + "CustomerMessagingCaseRequestPreviewResponse": { "type": "object", - "description": "One entry in a session's review / activity feed. Status changes carry `previous_status`, `new_status`, `previous_value`, `new_value`, and `changed_fields`; comments carry `comment` and (for Console comments) `mentioned_emails`; the remaining fields are `null` or empty when they do not apply to the activity type.", "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Unique identifier of this feed entry.", - "example": "de0561bc-5583-4409-9171-1a31a4c8a5f4" - }, - "activity_type": { - "type": "string", - "enum": [ - "COMMENT", - "STATUS_UPDATED", - "KYC_DATA_UPDATED", - "POA_DATA_UPDATED", - "FILE_ADDED", - "FILE_REMOVED", - "AML_ONGOING_MATCH", - "AML_STATUS_UPDATED", - "AML_HIT_STATUS_UPDATED", - "KYB_DATA_UPDATED", - "KYB_DOCUMENT_UPLOADED", - "TAG_ADDED", - "TAG_REMOVED" - ], - "description": "What kind of activity this entry records. `STATUS_UPDATED` rows come from manual decisions (`PATCH /v3/session/{session_id}/update-status/`) and from entries added via `POST …/reviews/` (which default to this type even when no status is recorded). `COMMENT` rows come from Console reviewers; Console detail-page views are also stored as `COMMENT` rows flagged with `metadata.system_event = \"DETAIL_VIEWED\"`. The remaining types track data edits, file changes, tags, and AML monitoring activity. STATUS_UPDATED rows are also written by system actors (e.g. AML ongoing monitoring with actor_type SYSTEM, and automatic feature-status updates), not only by the update-status endpoint and manual review posts.", - "example": "STATUS_UPDATED" - }, - "actor_display": { - "type": "string", - "description": "Human-readable author label, resolved in order: actor's full name → Console account identifier → local part of `actor_email` → `\"API Client\"` (for `API_KEY` actors) → `\"System\"` (for `SYSTEM` actors) → `\"Unknown\"`. Entries created via `POST …/reviews/` always display `\"Unknown\"` because that endpoint records no actor attribution.", - "example": "API Client" - }, - "actor_email": { - "type": "string", - "nullable": true, - "description": "Email of the Console user who performed the action. `null` for API-key and system actors.", - "example": null - }, - "actor_type": { + "request_id": { "type": "string", - "enum": [ - "API_KEY", - "CONSOLE_USER", - "SYSTEM" - ], - "description": "Who performed the action: an API key, a Console user, or Didit itself (`SYSTEM`, e.g. AML ongoing monitoring). Entries created via `POST …/reviews/` carry the default `CONSOLE_USER` regardless of the caller.", - "example": "API_KEY" + "format": "uuid" }, - "new_status": { + "expires_at": { "type": "string", - "nullable": true, - "enum": [ - "Not Started", - "In Progress", - "Approved", - "Declined", - "In Review", - "Expired", - "Abandoned", - "Kyc Expired", - "Resubmitted", - "Awaiting User" - ], - "description": "Session status recorded by this entry (for status updates). `null` for comments and data-change entries.", - "example": "Approved" + "format": "date-time" }, - "previous_status": { + "recipient": { "type": "string", - "nullable": true, - "enum": [ - "Not Started", - "In Progress", - "Approved", - "Declined", - "In Review", - "Expired", - "Abandoned", - "Kyc Expired", - "Resubmitted", - "Awaiting User" - ], - "description": "Status the session had before the change. Populated on `STATUS_UPDATED` entries written by the update-status endpoint; `null` on entries added via `POST …/reviews/`. Also populated on system-written STATUS_UPDATED rows.", - "example": "In Review" + "format": "email" }, - "comment": { - "type": "string", - "nullable": true, - "description": "Free-text note attached to the entry, or `null`.", - "example": "Document re-checked manually; address matches." + "sender": { + "type": "string" }, - "mentioned_emails": { - "type": "array", - "items": { - "type": "string", - "format": "email" - }, - "description": "Console users `@email`-mentioned in the comment. Always empty for entries created via this API — mention processing is a Console-only feature.", - "example": [] + "subject": { + "type": "string" }, - "previous_value": { - "type": "object", - "nullable": true, - "description": "Snapshot of the changed data before the action, e.g. `{\"status\": \"In Review\"}` on a status change. `null` when nothing was changed.", - "example": { - "status": "In Review" - } + "html": { + "type": "string" }, - "new_value": { - "type": "object", - "nullable": true, - "description": "Snapshot of the changed data after the action, e.g. `{\"status\": \"Approved\"}`. `null` when nothing was changed.", - "example": { - "status": "Approved" - } + "requires_user_action": { + "type": "boolean" }, - "changed_fields": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Names of the fields modified by this activity (e.g. `[\"status\"]`). Empty for comments and for entries added via `POST …/reviews/`.", - "example": [ - "status" - ] + "preview_token": { + "type": "string" + } + }, + "required": [ + "expires_at", + "html", + "preview_token", + "recipient", + "request_id", + "requires_user_action", + "sender", + "subject" + ] + }, + "CustomerMessagingCaseCustomerResponseRequest": { + "type": "object", + "properties": { + "full_name": { + "type": "string", + "minLength": 1, + "maxLength": 512 }, - "metadata": { - "type": "object", - "nullable": true, - "description": "Extra context for system-generated entries. Console detail-page views carry `{\"system_event\": \"DETAIL_VIEWED\", \"detail_type\": \"verification\"}` so they can be filtered out of notification logic. `null` for ordinary entries.", - "example": null + "date_of_birth": { + "type": "string", + "format": "date" }, - "created_at": { + "file": { "type": "string", - "format": "date-time", - "description": "When the entry was written, ISO 8601 UTC.", - "example": "2026-06-12T00:19:31.918735Z" + "format": "binary", + "writeOnly": true } } }, - "QuestionnaireListItem": { + "CaseCollaborationCaseNoteReply": { "type": "object", - "description": "One questionnaire version in the list view.", "properties": { "uuid": { "type": "string", - "format": "uuid", - "description": "Per-version UUID. Use as `{questionnaire_uuid}` for get/update/delete and as `questionnaire_uuid` in a workflow's QUESTIONNAIRE feature config." + "format": "uuid" }, - "title": { - "type": "string" + "actor_name": { + "type": "string", + "nullable": true }, - "description": { + "actor_email": { "type": "string", + "format": "email", "nullable": true }, - "languages": { - "type": "array", - "items": { - "type": "string" - } + "comment": { + "type": "string", + "maxLength": 300 + } + }, + "required": [ + "actor_email", + "actor_name", + "comment", + "uuid" + ] + }, + "FraudCheckModuleReport": { + "type": "object", + "properties": { + "module": { + "$ref": "#/components/schemas/FraudExcludeEnum" }, - "default_language": { - "type": "string" + "status": { + "$ref": "#/components/schemas/FraudStatusEnum" }, - "is_active": { - "type": "boolean" + "reason": { + "type": "string", + "nullable": true }, - "is_simple_questionnaire": { - "type": "boolean", - "description": "True for linear questionnaires created through this API; false for graph-based questionnaires built in the Console." + "latency_ms": { + "type": "integer" }, - "created_at": { + "freshness": { "type": "string", - "format": "date-time" + "nullable": true }, - "updated_at": { + "billable_units": { + "type": "integer" + } + }, + "required": [ + "billable_units", + "freshness", + "latency_ms", + "module", + "reason", + "status" + ] + }, + "CaseDuplicateEntityTypeEnum": { + "enum": [ + "session", + "business_session", + "transaction" + ], + "type": "string", + "description": "* `session` - Session\n* `business_session` - Business Session\n* `transaction` - Transaction" + }, + "FraudClientCredentialsErrorMessage": { + "type": "object", + "properties": { + "detail": { "type": "string", - "format": "date-time" + "default": "Authentication failed: Invalid client access token." + } + } + }, + "FraudAvailabilityEnum": { + "enum": [ + "available", + "not_requested", + "no_reference", + "insufficient_history", + "unsupported_region", + "source_unavailable" + ], + "type": "string", + "description": "* `available` - Available\n* `not_requested` - Not Requested\n* `no_reference` - No Reference\n* `insufficient_history` - Insufficient History\n* `unsupported_region` - Unsupported Region\n* `source_unavailable` - Source Unavailable" + }, + "CasePublicationPolicyDraftResponse": { + "type": "object", + "properties": { + "draft": { + "allOf": [ + { + "$ref": "#/components/schemas/CasePublicationPolicyDraft" + } + ], + "nullable": true + } + }, + "required": [ + "draft" + ] + }, + "FraudCheckAttributesRequest": { + "type": "object", + "description": "Corroborating attributes. All optional; they widen module coverage.", + "properties": { + "first_name": { + "type": "string", + "maxLength": 128 }, - "question_types": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Uppercase element types of the answerable questions, in creation order (e.g. `SHORT_TEXT`, `MULTIPLE_CHOICE`, `FILE_UPLOAD`). Structural `SECTION_HEADER` and `SEPARATOR` elements are excluded. One entry per question, so values can repeat." + "last_name": { + "type": "string", + "maxLength": 128 }, - "workflow_names": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Names of workflows using this questionnaire. Currently always `[]` on this endpoint (populated only in the Console listing)." + "full_name": { + "type": "string", + "maxLength": 256 }, - "questionnaire_group_id": { + "date_of_birth": { "type": "string", - "format": "uuid", - "description": "Stable identifier that groups all versions of the same questionnaire." + "format": "date" }, - "version": { - "type": "integer", - "description": "Version number within the group, starting at 1." + "nationality": { + "type": "string", + "maxLength": 3 }, - "status": { + "gender": { "type": "string", - "enum": [ - "draft", - "published" - ] + "maxLength": 16 }, - "published_at": { + "address": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "When this version was published. `null` for drafts." + "maxLength": 512 }, - "is_editable": { - "type": "boolean", - "description": "True when this version is a `draft`." + "city": { + "type": "string", + "maxLength": 128 + }, + "state": { + "type": "string", + "maxLength": 128 + }, + "postal_code": { + "type": "string", + "maxLength": 32 } } }, - "SimpleQuestionnaireRequest": { + "NetworkNote": { "type": "object", - "description": "Create a simple linear questionnaire. The API converts `form_elements` into the internal questionnaire graph.", "required": [ - "title", - "languages", - "form_elements" + "uuid", + "actor_email", + "comment", + "client_reference", + "created_at" ], "properties": { - "title": { + "uuid": { "type": "string", - "description": "Internal name for the questionnaire." + "format": "uuid", + "readOnly": true }, - "description": { + "actor_email": { "type": "string", - "nullable": true - }, - "languages": { - "type": "array", - "description": "Languages included in every label. Must include `en`.", - "items": { - "type": "string" - }, - "example": [ - "en" - ] + "format": "email", + "readOnly": true, + "description": "Authenticated author, assigned by the server." }, - "default_language": { + "comment": { "type": "string", - "description": "Default language. Must be included in `languages`.", - "example": "en" - }, - "is_active": { - "type": "boolean", - "default": true + "minLength": 1, + "maxLength": 10000 }, - "status": { + "client_reference": { "type": "string", - "enum": [ - "draft", - "published" - ], - "description": "Omit this field to create a published questionnaire ready for workflows." + "format": "uuid" }, - "form_elements": { - "type": "array", - "minItems": 1, - "description": "Questions in the exact order users should see them. Branching and conditional fields are not supported.", - "items": { - "$ref": "#/components/schemas/SimpleQuestionnaireElement" - } + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true } } }, - "SimpleQuestionnaireUpdateRequest": { + "CaseActionEntityTypeEnum": { + "enum": [ + "user", + "business" + ], + "type": "string", + "description": "* `user` - user\n* `business` - business" + }, + "CaseRoutingCondition": { "type": "object", - "description": "Update questionnaire metadata or replace the full linear `form_elements` list.", "properties": { - "title": { - "type": "string" - }, - "description": { + "field": { "type": "string", - "nullable": true - }, - "languages": { - "type": "array", - "items": { - "type": "string" - } - }, - "default_language": { - "type": "string" - }, - "is_active": { - "type": "boolean" + "enum": [ + "source", + "priority", + "tags" + ] }, - "status": { + "operator": { "type": "string", "enum": [ - "draft", - "published" + "equals", + "in", + "contains" ] }, - "form_elements": { - "type": "array", - "minItems": 1, - "description": "If provided, this replaces the full linear question list.", - "items": { - "$ref": "#/components/schemas/SimpleQuestionnaireElement" - } + "value": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] } - } - }, - "SimpleQuestionnaireElement": { - "type": "object", + }, "required": [ - "element_type", - "label" + "field", + "operator", + "value" ], + "description": "Source and priority support equals or in with supported values. Tags support contains with one non-empty tag." + }, + "RoutingPublicationPolicyRevision": { + "type": "object", "properties": { - "id": { - "type": "string", - "description": "Optional stable question id. If omitted, the API generates `q1`, `q2`, etc. Use lowercase letters, numbers, and underscores for readability.", - "example": "source_of_funds" + "version": { + "type": "integer", + "maximum": 2147483647, + "minimum": 0 }, - "element_type": { + "snapshot": {}, + "actor_email": { "type": "string", - "description": "Question type. The API accepts these lowercase values and their uppercase equivalents.", - "enum": [ - "short_text", - "long_text", - "dropdown", - "single_choice", - "multiple_choice", - "number", - "image", - "file_upload", - "time", - "email", - "address", - "phone", - "country", - "date_picker", - "consent", - "section_header", - "separator", - "heading", - "paragraph" - ] + "format": "email", + "nullable": true, + "maxLength": 254 }, - "label": { - "type": "object", - "description": "Question label by language code, for example `{ \"en\": \"Source of funds\" }`.", - "additionalProperties": { - "type": "string" - } + "published_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "published_at", + "snapshot", + "version" + ] + }, + "CasePublicationPolicyRevision": { + "type": "object", + "properties": { + "version": { + "type": "integer", + "maximum": 2147483647, + "minimum": 0 }, - "description": { - "type": "object", + "snapshot": {}, + "actor_email": { + "type": "string", + "format": "email", "nullable": true, - "additionalProperties": { - "type": "string" - } + "maxLength": 254 }, - "placeholder": { - "type": "object", - "nullable": true, - "additionalProperties": { - "type": "string" - } + "published_at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "published_at", + "snapshot", + "version" + ] + }, + "FraudCheckIdentityRelationship": { + "type": "object", + "properties": { + "result": { + "$ref": "#/components/schemas/FraudResultEnum" }, - "is_required": { - "type": "boolean", - "default": false + "reference_type": { + "type": "string" }, - "options": { + "country": { + "type": "string" + }, + "availability": { + "$ref": "#/components/schemas/FraudAvailabilityEnum" + }, + "reason_codes": { "type": "array", - "description": "Use for `dropdown`, `single_choice`, and `multiple_choice`. If `value` is omitted, the API generates one from the option label.", "items": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "value": { - "type": "string" - }, - "label": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } + "type": "string" } - }, - "max_files": { - "type": "integer", - "minimum": 1, - "maximum": 5, - "description": "Use with `file_upload` or `image`." - }, - "validation": { - "type": "object", - "nullable": true, - "description": "Optional format validation for `short_text`, `long_text`, or `number`. Enforced while the user fills the field (they cannot proceed until it passes) and re-checked by the API on submission.", - "properties": { - "pattern": { - "type": "string", - "description": "Regular expression (ECMA-262 subset) the value must fully match. For `short_text` and `long_text`. Do not add ^/$ anchors; the whole value is matched." - }, - "min_length": { - "type": "integer", - "minimum": 0, - "description": "Minimum number of characters. For `short_text` and `long_text`." - }, - "max_length": { - "type": "integer", - "minimum": 0, - "description": "Maximum number of characters. For `short_text` and `long_text`." - }, - "min": { - "type": "number", - "description": "Minimum numeric value. For `number`." - }, - "max": { - "type": "number", - "description": "Maximum numeric value. For `number`." - }, - "error_message": { - "type": "object", - "description": "Translated message shown when validation fails, keyed by language code.", - "additionalProperties": { - "type": "string" - } - } + } + }, + "required": [ + "availability", + "country", + "reason_codes", + "reference_type", + "result" + ] + }, + "CasePublicationPaginatedPolicyRevisionList": { + "type": "object", + "required": [ + "count", + "results" + ], + "properties": { + "count": { + "type": "integer", + "example": 123 + }, + "next": { + "type": "string", + "nullable": true, + "format": "uri", + "example": "http://api.example.org/accounts/?offset=400&limit=100" + }, + "previous": { + "type": "string", + "nullable": true, + "format": "uri", + "example": "http://api.example.org/accounts/?offset=200&limit=100" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CasePublicationPolicyRevision" } } } }, - "QuestionnaireDetail": { + "CaseDuplicateBlankEnum": { + "enum": [ + "" + ] + }, + "FraudCheckPlan": { "type": "object", - "description": "Full questionnaire detail returned from GET, POST (create) and PATCH (update) endpoints.", "properties": { - "questionnaire_id": { - "type": "string", - "format": "uuid", - "description": "Unique identifier of the questionnaire. Use this id when referencing the questionnaire from a workflow or in subsequent update/delete requests." - }, - "title": { + "profile_id": { "type": "string" }, - "description": { - "type": "string", - "nullable": true + "profile_version": { + "type": "string" }, - "languages": { + "modules": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/FraudCheckPlannedModule" } }, - "default_language": { - "type": "string" + "estimated_price": { + "type": "string", + "nullable": true }, - "is_active": { + "currency": { + "type": "string", + "nullable": true + }, + "is_sandbox": { "type": "boolean" }, - "is_simple_questionnaire": { + "can_afford": { "type": "boolean" + } + }, + "required": [ + "can_afford", + "currency", + "estimated_price", + "is_sandbox", + "modules", + "profile_id", + "profile_version" + ] + }, + "CaseReviewTeamUpdate": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 }, - "graph": { - "type": "object", - "description": "Graph structure with start_node and nodes map." + "description": { + "type": "string", + "maxLength": 500 }, - "sections": { + "members": { "type": "array", - "description": "Questionnaire content grouped into sections (derived from the graph).", "items": { - "type": "object" - } + "$ref": "#/components/schemas/CaseReviewTeamMember" + }, + "maxItems": 250 }, - "questionnaire_group_id": { + "is_active": { + "type": "boolean" + } + } + }, + "RoutingPublicationPolicyDraft": { + "type": "object", + "properties": { + "revision": { "type": "string", - "format": "uuid", - "description": "Stable identifier that groups all versions of the same questionnaire." + "format": "uuid" }, - "version": { - "type": "integer" + "base_version": { + "type": "integer", + "maximum": 2147483647, + "minimum": 0 }, - "status": { + "changes": {}, + "actor_email": { "type": "string", - "enum": [ - "draft", - "published" - ] + "format": "email", + "nullable": true, + "maxLength": 254 }, - "published_at": { + "updated_at": { "type": "string", "format": "date-time", - "nullable": true - }, - "is_editable": { - "type": "boolean", - "description": "True when the questionnaire can still be edited in place (draft versions)." + "readOnly": true } - } + }, + "required": [ + "base_version", + "updated_at" + ] }, - "UserListItem": { + "FraudCheckRequestRequest": { "type": "object", - "description": "A verified user.", "properties": { - "didit_internal_id": { + "client_reference": { "type": "string", - "format": "uuid", - "description": "Didit's stable internal UUID for this user." + "minLength": 1, + "description": "Your own reference for this check. Doubles as the idempotency key: reusing it with the same body returns the original check without running or billing the modules again; reusing it with a different body returns 409.", + "maxLength": 255 }, - "vendor_data": { + "profile": { "type": "string", - "nullable": true, - "description": "Your unique identifier for this user (passed when creating sessions). This can be null when no vendor identifier was supplied." + "minLength": 1, + "default": "standard", + "description": "Orchestration profile id. Defaults to the Didit `standard` profile.", + "maxLength": 64 }, - "display_name": { + "profile_version": { "type": "string", - "nullable": true, - "description": "Custom display name set by you" + "minLength": 1, + "description": "Pin a frozen profile version. Defaults to the current version of the profile.", + "maxLength": 32 }, - "full_name": { - "type": "string", - "nullable": true, - "description": "Full name extracted from verified documents" + "subject": { + "$ref": "#/components/schemas/FraudCheckSubjectRequest" }, - "date_of_birth": { + "context": { + "$ref": "#/components/schemas/FraudCheckContextRequest" + }, + "checks": { + "$ref": "#/components/schemas/FraudCheckModuleSelectionRequest" + }, + "vendor_data": { "type": "string", - "format": "date", - "nullable": true + "maxLength": 1000 }, - "effective_name": { + "metadata": {} + }, + "required": [ + "subject" + ] + }, + "CaseDuplicateCaseSubjectInputRequest": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/CaseDuplicateTypeEnum" + }, + "uuid": { "type": "string", - "nullable": true, - "description": "Best available name: display_name if set, otherwise full_name" + "format": "uuid" + } + }, + "required": [ + "type", + "uuid" + ] + }, + "CaseReviewTeamMember": { + "type": "object", + "properties": { + "user_id": { + "type": "string", + "format": "uuid" }, - "status": { + "availability": { "type": "string", "enum": [ - "ACTIVE", - "FLAGGED", - "BLOCKED" + "available", + "unavailable" ], - "description": "Lifecycle status of the user record (NOT a session status). `ACTIVE` is the default, `FLAGGED` marks the user for manual attention, `BLOCKED` prevents new sessions for this `vendor_data`." - }, - "portrait_image_url": { - "type": "string", - "nullable": true, - "description": "Presigned URL of the user's portrait photo (expires after a few hours)" - }, - "session_count": { - "type": "integer", - "description": "Total number of verification sessions for this user" - }, - "approved_count": { - "type": "integer", - "description": "Number of approved sessions" - }, - "declined_count": { - "type": "integer", - "description": "Number of declined sessions" + "default": "available" }, - "in_review_count": { + "capacity": { "type": "integer", - "description": "Number of sessions in review" + "minimum": 1, + "maximum": 1000, + "default": 10 }, - "issuing_states": { + "skills": { "type": "array", "items": { - "type": "string" + "type": "string", + "maxLength": 64 }, - "description": "ISO 3166-1 alpha-3 codes of issuing countries seen on this user's approved ID documents, e.g. `[\"USA\", \"ESP\"]`. Empty array when none." + "maxItems": 25 }, - "approved_emails": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Verified email addresses collected from this user's approved sessions, e.g. `[\"john@example.com\"]`." + "available_from": { + "type": "string", + "format": "date-time", + "nullable": true }, - "approved_phones": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Verified phone numbers collected from this user's approved sessions, e.g. `[\"+14155551234\"]`." + "available_until": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + "required": [ + "user_id" + ] + }, + "CaseRoutingPreviewRequest": { + "type": "object", + "properties": { + "source": { + "type": "string", + "enum": [ + "MANUAL", + "AML_SCREENING", + "TRANSACTION_RULE", + "WORKFLOW_BUILDER" + ] }, - "features": { - "type": "object", - "description": "Aggregated per-feature status across all of this user's sessions. Possible keys: `ID_VERIFICATION`, `NFC`, `LIVENESS`, `FACE_MATCH`, `POA`, `QUESTIONNAIRE`, `EMAIL_VERIFICATION`, `PHONE`, `AML`, `IP_ANALYSIS`, `AGE_ESTIMATION`, `DATABASE_VALIDATION`. Possible values: `Approved`, `Declined`, `In Review`, `Not Finished`, `Resub Requested`." + "priority": { + "type": "string", + "enum": [ + "LOW", + "MEDIUM", + "HIGH" + ], + "default": "MEDIUM" }, - "features_list": { + "tags": { "type": "array", "items": { - "type": "object", - "properties": { - "feature": { - "type": "string" - }, - "status": { - "type": "string" - } - } + "type": "string", + "maxLength": 64 }, - "description": "Same data as `features`, as an ordered array of `{feature, status}` objects." + "maxItems": 50 }, - "last_session_at": { + "rule_uuid": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "Timestamp of the most recent session" + "format": "uuid" }, - "first_session_at": { + "draft": { + "$ref": "#/components/schemas/CaseRoutingRuleCreate" + } + }, + "required": [ + "source" + ] + }, + "FraudExcludeEnum": { + "enum": [ + "normalization", + "email_risk", + "email_social", + "phone_social", + "phone_risk", + "ip_risk", + "database_validation", + "networks_same_org", + "networks_cross_org", + "biometric_identity" + ], + "type": "string", + "description": "* `normalization` - Normalization\n* `email_risk` - Email Risk\n* `email_social` - Email Social\n* `phone_social` - Phone Social\n* `phone_risk` - Phone Risk\n* `ip_risk` - Ip Risk\n* `database_validation` - Database Validation\n* `networks_same_org` - Networks Same Org\n* `networks_cross_org` - Networks Cross Org\n* `biometric_identity` - Biometric Identity" + }, + "CaseDuplicateCaseLinkCreateRequest": { + "type": "object", + "properties": { + "entity_type": { + "$ref": "#/components/schemas/CaseDuplicateEntityTypeEnum" + }, + "entity_uuid": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "Timestamp of the first session" + "format": "uuid" + } + }, + "required": [ + "entity_type", + "entity_uuid" + ] + }, + "CaseDuplicateStatusEnum": { + "enum": [ + "OPEN", + "AWAITING_USER", + "RESOLVED" + ], + "type": "string", + "description": "* `OPEN` - Open\n* `AWAITING_USER` - Awaiting user\n* `RESOLVED` - Resolved" + }, + "FraudBandEnum": { + "enum": [ + "low", + "medium", + "high", + "unknown" + ], + "type": "string", + "description": "* `low` - Low\n* `medium` - Medium\n* `high` - High\n* `unknown` - Unknown" + }, + "CaseDuplicatePriorityEnum": { + "enum": [ + "LOW", + "MEDIUM", + "HIGH" + ], + "type": "string", + "description": "* `LOW` - Low\n* `MEDIUM` - Medium\n* `HIGH` - High" + }, + "FraudStatusEnum": { + "enum": [ + "planned", + "completed", + "skipped", + "failed" + ], + "type": "string", + "description": "* `planned` - Planned\n* `completed` - Completed\n* `skipped` - Skipped\n* `failed` - Failed" + }, + "FraudCheckInput": { + "type": "object", + "properties": { + "reference": { + "type": "string", + "description": "Redacted handle for the submitted identifier." }, - "last_activity_at": { + "type": { + "$ref": "#/components/schemas/FraudTypeEnum" + }, + "namespace": { + "type": "object", + "additionalProperties": {} + }, + "quality": { + "type": "string" + }, + "normalization": { + "type": "object", + "additionalProperties": {} + }, + "availability": { + "$ref": "#/components/schemas/FraudAvailabilityEnum" + } + }, + "required": [ + "availability", + "namespace", + "normalization", + "quality", + "reference", + "type" + ] + }, + "CustomerMessagingPurposesEnum": { + "enum": [ + "identity_document", + "proof_of_address", + "business_document", + "confirm_details", + "review_in_progress" + ], + "type": "string", + "description": "* `identity_document` - identity_document\n* `proof_of_address` - proof_of_address\n* `business_document` - business_document\n* `confirm_details` - confirm_details\n* `review_in_progress` - review_in_progress" + }, + "FraudTypeEnum": { + "enum": [ + "email", + "phone", + "national_id", + "tax_number", + "passport_number", + "driving_licence_number", + "residence_permit_number", + "voter_id", + "external_id" + ], + "type": "string", + "description": "* `email` - Email\n* `phone` - Phone\n* `national_id` - National Id\n* `tax_number` - Tax Number\n* `passport_number` - Passport Number\n* `driving_licence_number` - Driving Licence Number\n* `residence_permit_number` - Residence Permit Number\n* `voter_id` - Voter Id\n* `external_id` - External Id" + }, + "CaseDuplicateCaseSubjectInput": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/CaseDuplicateTypeEnum" + }, + "uuid": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "Timestamp of the most recent activity on this user (session, transaction, status change, data edit, etc.)." + "format": "uuid" + } + }, + "required": [ + "type", + "uuid" + ] + }, + "CaseReviewTeamCreate": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 }, - "tags": { + "description": { + "type": "string", + "maxLength": 500 + }, + "members": { "type": "array", "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string" - }, - "color": { - "type": "string" - } - } + "$ref": "#/components/schemas/CaseReviewTeamMember" }, - "description": "Tags assigned to this user" + "maxItems": 250 + }, + "is_active": { + "type": "boolean" + } + }, + "required": [ + "name", + "members" + ] + }, + "CaseDuplicateCaseDuplicateQueryRequest": { + "type": "object", + "properties": { + "subject": { + "$ref": "#/components/schemas/CaseDuplicateCaseSubjectInputRequest" }, - "created_at": { - "type": "string", - "format": "date-time" + "links": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseDuplicateCaseLinkCreateRequest" + } + }, + "offset": { + "type": "integer", + "maximum": 1000000, + "minimum": 0, + "default": 0 } } }, - "BlocklistItem": { + "CaseDuplicateResolutionEnum": { + "enum": [ + "FALSE_POSITIVE", + "VALID_THREAT" + ], + "type": "string", + "description": "* `FALSE_POSITIVE` - False positive\n* `VALID_THREAT` - Valid threat" + }, + "CasePublicationPolicyDraftWriteRequest": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": [ - "face", - "document", - "phone", - "email" - ] - }, - "session_id": { - "type": "string", - "format": "uuid" - }, - "session_number": { - "type": "integer" - }, - "blocklisted_at": { - "type": "string", - "format": "date-time" - }, - "face_image": { - "type": "string", - "nullable": true + "expected_version": { + "type": "integer", + "minimum": 0 }, - "document_type": { + "expected_revision": { "type": "string", + "format": "uuid", "nullable": true }, - "document_number": { + "changes": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "changes", + "expected_revision", + "expected_version" + ] + }, + "FraudCheckSubjectRequest": { + "type": "object", + "properties": { + "selfie": { "type": "string", - "nullable": true + "minLength": 1, + "description": "Base64 JPEG, PNG or WebP selfie, up to 6 MB. No reference image or URL.", + "maxLength": 8388672 }, - "phone_number": { - "type": "string", - "nullable": true + "identifiers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FraudCheckIdentifierRequest" + }, + "description": "One or more typed identifiers. One is enough.", + "maxItems": 10, + "minItems": 1 }, - "email": { - "type": "string", - "nullable": true + "attributes": { + "$ref": "#/components/schemas/FraudCheckAttributesRequest" } - } + }, + "required": [ + "identifiers" + ] }, - "UserDetailItem": { + "RoutingPublicationRoutingCondition": { "type": "object", - "description": "Full user detail. Extends UserListItem with metadata, comments, and updated_at.", - "allOf": [ - { - "$ref": "#/components/schemas/UserListItem" + "properties": { + "field": { + "$ref": "#/components/schemas/RoutingPublicationFieldEnum" }, - { - "type": "object", - "properties": { - "metadata": { - "type": "object", - "description": "Custom metadata JSON you attached to this user. Defaults to `{}`." - }, - "tags": { - "type": "array", - "description": "Tag assignments. NOTE: on detail responses each entry is a tag *link* object (`{uuid, tag: {...}, added_by_email, added_by_name, created_at}`), unlike the flat `{uuid, name, color}` shape used on list responses.", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "UUID of the tag assignment (not the tag itself)." - }, - "tag": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string" - }, - "color": { - "type": "string" - }, - "description": { - "type": "string", - "nullable": true - }, - "source": { - "type": "string", - "enum": [ - "didit", - "custom" - ] - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "updated_at": { - "type": "string", - "format": "date-time" - } - } - }, - "added_by_email": { - "type": "string", - "nullable": true - }, - "added_by_name": { - "type": "string", - "nullable": true - }, - "created_at": { - "type": "string", - "format": "date-time" - } - } - } - }, - "comments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "comment_type": { - "type": "string", - "enum": [ - "COMMENT", - "STATUS_CHANGED", - "METADATA_UPDATED", - "TAG_ADDED", - "TAG_REMOVED", - "PROFILE_UPDATED" - ] - }, - "comment": { - "type": "string", - "nullable": true - }, - "actor_email": { - "type": "string", - "nullable": true - }, - "actor_name": { - "type": "string", - "nullable": true - }, - "previous_status": { - "type": "string", - "nullable": true, - "enum": [ - "ACTIVE", - "FLAGGED", - "BLOCKED", - null - ] - }, - "new_status": { - "type": "string", - "nullable": true, - "enum": [ - "ACTIVE", - "FLAGGED", - "BLOCKED", - null - ] - }, - "previous_value": { - "type": "object", - "nullable": true, - "description": "Previous field values for PROFILE_UPDATED entries." - }, - "new_value": { - "type": "object", - "nullable": true, - "description": "New field values for PROFILE_UPDATED entries." - }, - "changed_fields": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Names of the fields changed by a PROFILE_UPDATED entry." - }, - "metadata": { - "type": "object", - "nullable": true - }, - "mentioned_emails": { - "type": "array", - "items": { - "type": "string" - } - }, - "created_at": { - "type": "string", - "format": "date-time" - } - } - }, - "description": "Activity log and comments for this user (status changes, profile edits, manual notes)." - }, - "updated_at": { - "type": "string", - "format": "date-time" - } - } - } + "operator": { + "$ref": "#/components/schemas/RoutingPublicationOperatorEnum" + }, + "value": {} + }, + "required": [ + "field", + "operator", + "value" ] }, - "BusinessListItem": { + "CaseActionCaseActionReceipt": { "type": "object", - "description": "A verified business.", "properties": { - "didit_internal_id": { + "uuid": { "type": "string", "format": "uuid", - "description": "Didit's stable internal UUID for this business." - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your unique identifier for this business (passed when creating sessions). This can be null when no vendor identifier was supplied." + "readOnly": true }, - "display_name": { + "client_reference": { "type": "string", - "nullable": true, - "description": "Custom display name set by you" + "format": "uuid" }, - "legal_name": { + "action": { "type": "string", - "nullable": true, - "description": "Official legal name from registry or manual entry" + "maxLength": 32 }, - "registration_number": { - "type": "string", - "nullable": true, - "description": "Company registration or incorporation number" + "status": { + "$ref": "#/components/schemas/CaseActionStatusEnum" }, - "country_code": { + "result": {}, + "error": { "type": "string", - "nullable": true, - "description": "Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `US`)." + "maxLength": 255 }, - "effective_name": { + "actor_email": { "type": "string", + "format": "email", "nullable": true, - "description": "Best available name: display_name if set, otherwise legal_name" - }, - "status": { - "type": "string", - "enum": [ - "ACTIVE", - "FLAGGED", - "BLOCKED" - ], - "description": "Lifecycle status of the business record (NOT a session status). `ACTIVE` is the default, `FLAGGED` marks it for manual attention, `BLOCKED` prevents new sessions for this `vendor_data`." - }, - "session_count": { - "type": "integer", - "description": "Total number of verification sessions for this business" - }, - "approved_count": { - "type": "integer", - "description": "Number of approved sessions" + "maxLength": 254 }, - "declined_count": { + "attempts": { "type": "integer", - "description": "Number of declined sessions" + "maximum": 2147483647, + "minimum": 0 }, - "in_review_count": { - "type": "integer", - "description": "Number of sessions in review" + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true }, - "features": { - "type": "object", - "description": "Aggregated per-feature status across all of this business's KYB sessions. Currently the only key is `KYB` (the registry company check status). Possible values: `Approved`, `Declined`, `In Review`, `Not Finished`, `Resub Requested`." + "updated_at": { + "type": "string", + "format": "date-time", + "readOnly": true + } + }, + "required": [ + "action", + "client_reference", + "created_at", + "status", + "updated_at", + "uuid" + ] + }, + "FraudCheckContextRequest": { + "type": "object", + "properties": { + "ip_address": { + "type": "string", + "minLength": 1 }, - "features_list": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, - "description": "Same data as `features`, as an ordered array of `{feature, status}` objects." + "user_agent": { + "type": "string", + "maxLength": 512 }, - "last_session_at": { + "country": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "Timestamp of the most recent session" + "description": "Country the subject claims to be in. Contextual only - never proof of identity ownership.", + "maxLength": 3 }, - "first_session_at": { + "purpose": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "Timestamp of the first session" + "maxLength": 128 }, - "last_activity_at": { + "consent_obtained": { + "type": "boolean" + } + } + }, + "CustomerMessagingCaseCustomerResponseResult": { + "type": "object", + "properties": { + "responded_at": { "type": "string", - "format": "date-time", - "nullable": true, - "description": "Timestamp of the most recent activity (status change, session update, etc.)" + "format": "date-time" + } + }, + "required": [ + "responded_at" + ] + }, + "CustomerMessagingCaseRequestContact": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" }, - "tags": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string" - }, - "color": { - "type": "string" - } - } - }, - "description": "Tags assigned to this business" + "email": { + "type": "string", + "format": "email" }, - "created_at": { + "verified_at": { "type": "string", "format": "date-time" } + }, + "required": [ + "email", + "uuid", + "verified_at" + ] + }, + "FraudClientCredentialsForbiddenErrorMessage": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "default": "Forbidden - User doesn't have permission" + } } }, - "BusinessDetailItem": { + "CasePublicationAssignmentMethodEnum": { + "enum": [ + "MANUAL", + "AUTOMATIC" + ], + "type": "string", + "description": "* `MANUAL` - Manual\n* `AUTOMATIC` - Automatic" + }, + "CaseDuplicateNullEnum": { + "enum": [ + null + ] + }, + "CustomerMessagingLanguageEnum": { + "enum": [ + "en", + "es", + "pt" + ], + "type": "string", + "description": "* `en` - en\n* `es` - es\n* `pt` - pt" + }, + "CaseDuplicateCaseFraudSourceContextRequest": { "type": "object", - "description": "Full business detail. Extends BusinessListItem with metadata and updated_at.", - "allOf": [ - { - "$ref": "#/components/schemas/BusinessListItem" + "properties": { + "check_id": { + "type": "string", + "format": "uuid" }, - { - "type": "object", - "properties": { - "metadata": { - "type": "object", - "description": "Custom metadata JSON you attached to this business. Defaults to `{}`." - }, - "updated_at": { - "type": "string", - "format": "date-time" - } - } + "request_id": { + "type": "string", + "format": "uuid" + }, + "source_section": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Z][A-Z0-9_:-]{0,63}$" } + }, + "required": [ + "check_id", + "request_id", + "source_section" ] }, - "TransactionDetail": { + "FraudCheckPlannedModule": { "type": "object", - "description": "Full monitoring record for one transaction, as returned by `GET /v3/transactions/{transaction_id}/` and `POST /v3/transactions/`.", "properties": { - "uuid": { - "type": "string", - "format": "uuid", - "description": "Didit-stable transaction identifier. Use as `{transaction_id}` for follow-up calls." + "module": { + "$ref": "#/components/schemas/FraudExcludeEnum" }, - "environment": { + "available": { + "type": "boolean" + }, + "availability_reason": { "type": "string", - "enum": [ - "live", - "sandbox" - ], - "description": "Environment of the application that owns the transaction. A `sandbox` transaction is stored but never evaluated, screened, notified or billed, so its empty `rule_runs` means \"not evaluated\"." + "nullable": true }, - "transaction_number": { - "type": "integer", - "description": "Application-scoped sequential transaction number shown in Console (e.g. `4123`)." + "will_run": { + "type": "boolean" }, - "txn_id": { + "selection_reason": { "type": "string", - "description": "The `transaction_id` you supplied at create time (max 128 chars). Unique per application." + "nullable": true }, - "txn_date": { + "estimated_price": { "type": "string", - "format": "date-time", - "description": "When the transaction occurred (from `transaction_at`; defaults to submission time)." + "nullable": true + } + }, + "required": [ + "availability_reason", + "available", + "estimated_price", + "module", + "selection_reason", + "will_run" + ] + }, + "CustomerMessagingCaseRequestOptions": { + "type": "object", + "properties": { + "contacts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerMessagingCaseRequestContact" + } }, - "zone_id": { - "type": "string", - "nullable": true, - "description": "IANA time zone identifier provided at create time." + "purposes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerMessagingPurposesEnum" + } }, - "transaction_type": { - "type": "string", - "description": "Top-level category as stored: `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet`, `gamblingLimitChange`, `gamblingBonusChange`, `auditTrailEvent`. Note multi-word values are echoed back in camelCase even when submitted in snake_case." + "languages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerMessagingLanguagesEnum" + } + } + }, + "required": [ + "contacts", + "languages", + "purposes" + ] + }, + "CaseCollaborationPaginatedCaseNoteList": { + "type": "object", + "required": [ + "count", + "results" + ], + "properties": { + "count": { + "type": "integer", + "example": 123 }, - "action_type": { + "next": { "type": "string", - "description": "Sub-type within the category (e.g. `deposit`, `withdrawal`, `transfer`). Defaults to the category when not supplied." + "nullable": true, + "format": "uri", + "example": "http://api.example.org/accounts/?offset=400&limit=100" }, - "direction": { + "previous": { "type": "string", - "enum": [ - "INBOUND", - "OUTBOUND" - ], - "description": "Direction relative to the subject. Stored uppercase regardless of the casing submitted (`in`/`out`/`inbound`/`outbound` are accepted on input)." + "nullable": true, + "format": "uri", + "example": "http://api.example.org/accounts/?offset=200&limit=100" }, - "status": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseCollaborationCaseNote" + } + } + } + }, + "FraudProcessingStatusEnum": { + "enum": [ + "pending", + "running", + "completed", + "failed" + ], + "type": "string", + "description": "* `pending` - Pending\n* `running` - Running\n* `completed` - Completed\n* `failed` - Failed" + }, + "CasePublicationCaseBlueprint": { + "type": "object", + "properties": { + "uuid": { "type": "string", - "enum": [ - "APPROVED", - "IN_REVIEW", - "DECLINED", - "AWAITING_USER" - ], - "description": "Current monitoring verdict. Transactions are created `APPROVED`; rules may flip them to `IN_REVIEW`/`DECLINED` synchronously." + "format": "uuid", + "readOnly": true }, - "source": { + "name": { "type": "string", - "enum": [ - "LIVE", - "IMPORTED" - ], - "description": "How the record originated: `LIVE` for transactions submitted through the API or SDK, `IMPORTED` for transactions loaded through the console import." + "maxLength": 24 }, - "amount": { + "description": { "type": "string", - "description": "Transaction amount as a decimal string with trailing zeros stripped (e.g. `\"1500\"`, `\"0.5\"`, `\"0.123456789012345678\"`). Up to 18 decimal places." + "nullable": true }, - "currency": { - "type": "string", - "description": "Currency code of `amount`. Any ISO 4217 alphabetic code is accepted (e.g. `EUR`, `USD`, `NGN`), as is a crypto asset symbol (e.g. `BTC`). Prefer a currently-circulating code. Not every accepted code has an exchange rate: when none is available for the code (for example the ISO accounting unit `XSU`, or a crypto symbol Didit does not price), automatic conversion fails and `preferred_currency_amount` is returned null unless the request supplied a pre-converted `amount_in_default_currency`." + "assignment_method": { + "$ref": "#/components/schemas/CasePublicationAssignmentMethodEnum" }, - "currency_type": { - "type": "string", - "nullable": true, - "description": "`fiat` or `crypto`, as submitted in `currency_kind`." + "assignee_pool": {}, + "max_active_cases": { + "type": "integer", + "maximum": 2147483647, + "minimum": 0, + "nullable": true }, - "amount_in_default_currency": { - "type": "string", - "nullable": true, - "description": "Pre-converted amount you supplied, as a decimal string with trailing zeros stripped." + "escalation_enabled": { + "type": "boolean" }, - "default_currency_code": { + "escalation_blueprint": { "type": "string", + "format": "uuid", "nullable": true }, - "preferred_currency_amount": { + "escalation_blueprint_name": { "type": "string", - "nullable": true, - "description": "`amount` converted to the application's preferred currency, when available." + "readOnly": true }, - "preferred_currency_code": { + "transfer_enabled": { + "type": "boolean" + }, + "four_eyes_checker_blueprint": { "type": "string", + "format": "uuid", "nullable": true }, - "payment_details": { + "four_eyes_checker_blueprint_name": { "type": "string", - "nullable": true, - "description": "Free-text payment reference or memo from the submission." + "readOnly": true }, - "payment_txn_id": { - "type": "string", - "nullable": true, - "description": "External payment system reference (e.g. blockchain transaction hash) from `payment_reference_id`." + "require_resolution_note": { + "type": "boolean" }, - "score": { + "content_config": {}, + "version": { "type": "integer", - "description": "Risk score accumulated by rules (typically 0–100). Higher = riskier." - }, - "severity": { - "type": "string", - "nullable": true, - "enum": [ - "UNKNOWN", - "LOW", - "MEDIUM", - "HIGH", - "CRITICAL" - ], - "description": "Categorical risk severity set by rules/providers (`UNKNOWN`, `LOW`, `MEDIUM`, `HIGH`, `CRITICAL`). `null` until something sets it." + "maximum": 2147483647, + "minimum": 0 }, - "decision_reason_code": { - "type": "string", - "nullable": true, - "description": "Machine-readable reason for the current `status`." + "default_deadline_hours": { + "type": "integer", + "maximum": 2147483647, + "minimum": 0, + "nullable": true }, - "decision_reason_label": { - "type": "string", - "nullable": true, - "description": "Human-readable label for `decision_reason_code`." + "is_active": { + "type": "boolean" }, - "vendor_data": { + "preset_key": { "type": "string", "nullable": true, - "description": "Convenience copy of the applicant's `vendor_data`." - }, - "metadata": { - "type": "object", - "description": "Snapshot of the `subject` and `counterparty` payloads as submitted at create time." - }, - "props": { - "type": "object", - "description": "The `custom_properties` you supplied at create time. Each key is addressable in rule conditions as `custom_values.`." - }, - "tags": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string" - }, - "color": { - "type": "string" - }, - "description": { - "type": "string", - "nullable": true - }, - "source": { - "type": "string", - "enum": [ - "didit", - "custom" - ], - "description": "Whether the tag is a Didit preset or a custom tag." - } - } - }, - "description": "Tags attached to the transaction (manually or by rules)." - }, - "parties": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "role": { - "type": "string", - "enum": [ - "APPLICANT", - "REMITTER", - "BENEFICIARY", - "COUNTERPARTY" - ], - "description": "Party role. The create endpoint assigns `APPLICANT` (the subject) and `COUNTERPARTY`." - }, - "entity_type": { - "type": "string", - "enum": [ - "individual", - "company", - "external", - "unhostedWallet" - ] - }, - "kind": { - "type": "string", - "enum": [ - "USER", - "BUSINESS", - "EXTERNAL" - ], - "description": "`USER`/`BUSINESS` parties link back to a Didit-owned entity via `vendor_data`. `EXTERNAL` parties have no Didit entity behind them." - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your internal user/business identifier." - }, - "full_name": { - "type": "string", - "nullable": true - }, - "first_name": { - "type": "string", - "nullable": true - }, - "last_name": { - "type": "string", - "nullable": true - }, - "country_code": { - "type": "string", - "nullable": true, - "description": "Country code from the submitted `address.country` (typically ISO 3166-1 alpha-3)." - }, - "dob": { - "type": "string", - "format": "date", - "nullable": true, - "description": "Date of birth from the submitted `date_of_birth`." - }, - "address": { - "type": "object", - "description": "Address object as submitted. `{}` when omitted." - }, - "institution_info": { - "type": "object", - "description": "Institution details as submitted. `{}` when omitted." - }, - "device": { - "type": "object", - "description": "Normalized device context, including server-side IP enrichment under network_context (geolocation, VPN/data-center flags) when an IP was provided.", - "properties": { - "fingerprint": { - "type": "string", - "description": "Unique device fingerprint." - }, - "persistent_device_id": { - "type": "string", - "description": "Stable cross-session device identifier (v2 fingerprint schema)." - }, - "user_agent": { - "type": "string", - "description": "Browser or app user agent string." - }, - "browser_family": { - "type": "string", - "description": "Browser family (e.g. Chrome, Firefox, Safari)." - }, - "browser_version": { - "type": "string", - "description": "Browser version string." - }, - "platform": { - "type": "string", - "description": "Platform identifier (web, ios, android, macos, windows, linux)." - }, - "os_family": { - "type": "string", - "description": "Operating system family (e.g. iOS, Android, Windows)." - }, - "os_version": { - "type": "string", - "description": "Operating system version string." - }, - "accept_language": { - "type": "string", - "description": "Accept-Language header value." - }, - "session_id": { - "type": "string", - "description": "Session identifier (stored in metadata)." - }, - "session_age_ms": { - "type": "number", - "description": "Session age in milliseconds." - }, - "network_context": { - "type": "object", - "description": "Network context with IP address and geo-location.", - "properties": { - "ip_address": { - "type": "string", - "description": "Client IP address." - }, - "ip_country_code": { - "type": "string", - "description": "Geo-resolved country of the IP (ISO 3166-1 alpha-3)." - }, - "latitude": { - "type": "number", - "description": "Geographic latitude of the IP." - }, - "longitude": { - "type": "number", - "description": "Geographic longitude of the IP." - } - } - } - } - }, - "external_party_snapshot": { - "type": "object", - "nullable": true, - "description": "Frozen copy of the party data for `EXTERNAL` parties. `null` while the party is linked to a Didit user or business." - } - } - }, - "description": "Transaction parties (applicant, counterparty)." - }, - "payment_methods": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "payment_method_type": { - "type": "string", - "enum": [ - "bank_card", - "bank_account", - "e_wallet", - "crypto_wallet", - "unhosted_wallet", - "other" - ] - }, - "label": { - "type": "string", - "nullable": true - }, - "fingerprint": { - "type": "string", - "nullable": true, - "description": "SHA-256 over `type:account_id:issuing_country` — stable identifier to correlate reuse of the same instrument." - }, - "account_id": { - "type": "string", - "nullable": true, - "description": "Account identifier (IBAN, wallet address, card token, …)." - }, - "issuing_country": { - "type": "string", - "nullable": true - }, - "owner_kind": { - "type": "string", - "enum": [ - "USER", - "BUSINESS", - "EXTERNAL" - ] - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your identifier for the owning user/business." - }, - "owner_name": { - "type": "string", - "nullable": true - }, - "institution_info": { - "type": "object" - }, - "details": { - "type": "object", - "description": "Raw `payment_method` object as submitted." - }, - "external_owner_snapshot": { - "type": "object", - "nullable": true - }, - "role": { - "type": "string", - "enum": [ - "SOURCE", - "DESTINATION", - "FUNDING", - "BENEFICIARY" - ], - "description": "How this method was used in the transaction. The create endpoint assigns `SOURCE`/`DESTINATION` from the direction (outbound: subject = SOURCE; inbound: subject = DESTINATION)." - } - } - }, - "description": "Payment methods linked to the transaction." + "maxLength": 64 }, - "documents": { + "creation_sources": { "type": "array", - "description": "Files attached to the transaction from the console. `[]` when none.", "items": { "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "label": { - "type": "string", - "nullable": true - }, - "note": { - "type": "string", - "nullable": true - }, - "uploaded_by": { - "type": "string", - "nullable": true - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "files": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "file_url": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "Short-lived signed download URL; fetch it when needed rather than storing it." - }, - "original_filename": { - "type": "string", - "nullable": true - }, - "file_size": { - "type": "integer", - "nullable": true - } - } - } - } - } - } + "additionalProperties": {} + }, + "readOnly": true }, - "activities": { + "receives_escalation_from": { "type": "array", "items": { "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "activity_type": { - "type": "string", - "description": "E.g. `TRANSACTION_CREATED`, `TRANSACTION_NOTE_ADDED`, `TRANSACTION_STATUS_CHANGED`." - }, - "source": { - "type": "string" - }, - "status": { - "type": "string", - "nullable": true - }, - "title": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "metadata": { - "type": "object" - }, - "occurred_at": { - "type": "string", - "format": "date-time" - } - } + "additionalProperties": {} }, - "description": "Activity log entries (creation, notes, manual reviews, status changes)." + "readOnly": true }, - "alerts": { + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "updated_at": { + "type": "string", + "format": "date-time", + "readOnly": true + } + }, + "required": [ + "created_at", + "creation_sources", + "escalation_blueprint_name", + "four_eyes_checker_blueprint_name", + "name", + "receives_escalation_from", + "updated_at", + "uuid" + ] + }, + "CaseRoutingRuleUpdate": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "description": { + "type": "string", + "maxLength": 500 + }, + "priority": { + "type": "integer", + "minimum": 0 + }, + "conditions": { "type": "array", "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string", - "nullable": true - }, - "severity": { - "type": "string", - "enum": [ - "UNKNOWN", - "LOW", - "MEDIUM", - "HIGH", - "CRITICAL" - ] - }, - "status": { - "type": "string", - "enum": [ - "OPEN", - "INVESTIGATING", - "AWAITING_USER", - "PENDING_SAR", - "SAR_FILED", - "RESOLVED", - "DISMISSED" - ] - }, - "source": { - "type": "string", - "enum": [ - "RULE", - "PROVIDER", - "MANUAL" - ] - }, - "metadata": { - "type": "object" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "due_at": { - "type": "string", - "format": "date-time", - "nullable": true - } - } + "$ref": "#/components/schemas/CaseRoutingCondition" }, - "description": "Alerts raised by rules and providers." + "maxItems": 20 }, - "rule_runs": { + "required_skills": { "type": "array", "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "rule": { - "type": "string", - "format": "uuid", - "description": "UUID of the rule that ran." - }, - "rule_title": { - "type": "string" - }, - "rule_description": { - "type": "string", - "nullable": true - }, - "rule_category": { - "type": "string" - }, - "matched": { - "type": "boolean", - "description": "Whether the rule's conditions matched this transaction. A run is recorded only for matched rules, so live evaluations always carry `true`." - }, - "is_test": { - "type": "boolean", - "description": "True when the rule ran in TEST mode (no effect on status/score)." - }, - "mode": { - "type": "string", - "enum": [ - "ACTIVE", - "DISABLED", - "TEST" - ] - }, - "severity": { - "type": "string", - "nullable": true - }, - "score_delta": { - "type": "integer", - "description": "Score added by this rule's `add_score` actions." - }, - "status_target": { - "type": "string", - "nullable": true, - "description": "Status set by a `change_status` action, if any." - }, - "action_summary": { - "type": "string", - "nullable": true, - "description": "Summary of the actions the rule executed, joined with `; ` (e.g. `score +30; change_status:IN_REVIEW; add_tag:high_risk_country`). Parts: `score +N`, `change_status:`, `add_tag:`, `add_note`, `add_to_list`, `open_case`. `null` when the rule executed no action." - }, - "metadata": { - "type": "object", - "description": "Evaluation detail: `scope_match`, `conditions[]` and `aggregations[]`. Each condition entry carries `field`, `operator`, `expected_value`, the `current_value` read from the transaction, `matched`, `group_index` and `group_logic`. Each aggregation entry (velocity / count / sum) carries `metric`, `field`, `operator`, `expected_value`, the `computed_value` the aggregate evaluated to, `window`, `filters`, `matched` and `sample_references`; deferred async aggregations have `computed_value: null` and `deferred: true`." - }, - "created_at": { - "type": "string", - "format": "date-time" - } - } + "type": "string", + "maxLength": 64 }, - "description": "Per-rule execution results — which rule fired, with what score impact." + "maxItems": 25 }, - "provider_results": { + "blueprint": { + "type": "string", + "format": "uuid" + }, + "team": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "is_active": { + "type": "boolean" + }, + "expected_version": { + "type": "integer", + "minimum": 1, + "writeOnly": true, + "description": "Version opened by the editor. When supplied, stale writes return 409 without changing the rule." + } + } + }, + "CustomerMessagingCaseRequestPreviewRequest": { + "type": "object", + "properties": { + "contact_id": { + "type": "string", + "format": "uuid" + }, + "purpose": { + "$ref": "#/components/schemas/CustomerMessagingPurposeEnum" + }, + "language": { + "allOf": [ + { + "$ref": "#/components/schemas/CustomerMessagingLanguageEnum" + } + ], + "default": "en" + } + }, + "required": [ + "contact_id", + "purpose" + ] + }, + "CaseDuplicateCaseFraudSourceContext": { + "type": "object", + "properties": { + "check_id": { + "type": "string", + "format": "uuid" + }, + "request_id": { + "type": "string", + "format": "uuid" + }, + "source_section": { + "type": "string", + "pattern": "^[A-Z][A-Z0-9_:-]{0,63}$" + } + }, + "required": [ + "check_id", + "request_id", + "source_section" + ] + }, + "RoutingPublicationOperatorEnum": { + "enum": [ + "equals", + "in", + "contains" + ], + "type": "string", + "description": "* `equals` - equals\n* `in` - in\n* `contains` - contains" + }, + "CaseRoutingPreview": { + "type": "object", + "properties": { + "rule_uuid": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "rule_name": { + "type": "string", + "nullable": true + }, + "rule_version": { + "type": "integer", + "nullable": true + }, + "blueprint_uuid": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "team_uuid": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "assignee_user_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "reason": { + "type": "string" + }, + "candidates": { "type": "array", "items": { "type": "object", "properties": { - "uuid": { + "user_id": { "type": "string", "format": "uuid" }, - "provider": { - "type": "string" - }, - "result_type": { - "type": "string", - "enum": [ - "FIAT_MONITORING", - "WALLET_SCREENING", - "TRANSACTION_SCREENING", - "TRAVEL_RULE", - "NETWORK_GRAPH", - "CUSTOM" - ] - }, - "status": { - "type": "string" - }, - "severity": { - "type": "string", - "nullable": true - }, - "score": { - "type": "integer" - }, - "summary": { - "type": "string", - "nullable": true - }, - "payload": { - "type": "object", - "description": "Raw provider response payload." - }, - "created_at": { - "type": "string", - "format": "date-time" - } - } - }, - "description": "Raw provider payloads (AML screening, blockchain analytics, etc.)." - }, - "travel_rule": { - "type": "object", - "nullable": true, - "description": "Travel Rule transfer or check. Populated when `travel_rule_details` was submitted, and also when Travel Rule is enabled and the transaction was created with `transaction_category: travelRule` (Didit initiates the exchange itself, even without `travel_rule_details`). `null` when no transfer or check exists.", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string" - }, - "protocol": { - "type": "string", - "nullable": true - }, - "required": { - "type": "boolean" - }, - "obligations_count": { - "type": "integer" - }, - "originator_data": { - "type": "object" - }, - "beneficiary_data": { - "type": "object" - }, - "metadata": { - "type": "object" - } - } - }, - "network_snapshot": { - "type": "object", - "nullable": true, - "description": "Graph snapshot of related transactions/parties used by the rules engine. Either the `network_snapshot` submitted with the transaction or the graph Didit builds from a successful AML wallet screening (which replaces a submitted one). `null` when neither exists.", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "nodes": { - "type": "array", - "items": { - "type": "object" - } - }, - "edges": { - "type": "array", - "items": { - "type": "object" - } - }, - "metrics": { - "type": "object" - } - } - }, - "remediation": { - "type": "object", - "nullable": true, - "description": "Remediation session offered to the user when re-verification is required. Superseded by `action_required`, which is the canonical block; kept for backward compatibility.", - "properties": { - "session_id": { - "type": "string" - }, - "session_token": { - "type": "string" - }, - "url": { - "type": "string", - "format": "uri" - }, - "status": { - "type": "string" - } - } - }, - "action_required": { - "type": "object", - "nullable": true, - "description": "Pending end-user action required to complete this transaction, or null when there is none. The Didit SDKs auto-launch the action by default and refresh the transaction when it finishes. Two variants, discriminated by `type`: `verification_session` (a hosted verification session created by a rule action) and `wallet_ownership` (a wallet-ownership widget session auto-minted for a Travel Rule transfer that needs proof of wallet control, gated by the `auto_wallet_verification` Travel Rule setting). When both could apply, `wallet_ownership` takes precedence because it blocks the Travel Rule exchange.", - "oneOf": [ - { - "title": "Verification session", - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "verification_session" - ] - }, - "url": { - "type": "string", - "format": "uri", - "description": "Hosted verification URL to open for the end user." - }, - "session_id": { - "type": "string", - "format": "uuid", - "description": "Verification session id." - }, - "session_token": { - "type": "string", - "description": "Session token, used by the mobile SDKs to launch the flow natively." - }, - "status": { - "type": "string", - "description": "Verification session status." - } + "eligible": { + "type": "boolean" }, - "required": [ - "type", - "url" - ] - }, - { - "title": "Wallet ownership", - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "wallet_ownership" - ] - }, - "url": { - "type": "string", - "format": "uri", - "description": "Hosted wallet-ownership widget URL. Open it in a real browser surface - on mobile use SFSafariViewController or a Chrome Custom Tab, never an embedded webview." - }, - "widget_session_id": { - "type": "string", - "format": "uuid", - "description": "Wallet-ownership widget session id." - }, - "expires_at": { - "type": "string", - "format": "date-time", - "description": "When the widget session expires." - } + "active_cases": { + "type": "integer", + "nullable": true }, - "required": [ - "type", - "url" - ] + "reason": { + "type": "string", + "nullable": true + } } - ] + } }, - "cost_breakdown": { - "type": "object", - "nullable": true, - "description": "Cost of this transaction: `total_price` and `items`, one per charge, each with `usage_type`, `price`, `details` and `created_at`. A re-screen of the same transaction is a new item." + "matched_rules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "priority": { + "type": "integer" + } + } + } } } }, - "TravelRuleSettingsDetail": { + "FraudDimensionEnum": { + "enum": [ + "identity", + "contact", + "device_network", + "behavioral", + "fraud_network" + ], + "type": "string", + "description": "* `identity` - Identity\n* `contact` - Contact\n* `device_network` - Device Network\n* `behavioral` - Behavioral\n* `fraud_network` - Fraud Network" + }, + "FraudBadRequestErrorMessage": { "type": "object", "properties": { - "is_enabled": { - "type": "boolean", - "description": "Whether the managed Travel Rule negotiation flow is active for outbound `travelRule` transactions on this application." - }, - "jurisdiction": { + "detail": { "type": "string", - "maxLength": 8, - "description": "Your VASP's operating jurisdiction code. Default `\"EU\"`." + "default": "Bad Request - Invalid request data" + } + } + }, + "FraudNextActionEnum": { + "enum": [ + "none", + "manual_review", + "step_up", + "resubmit_capture" + ], + "type": "string", + "description": "* `none` - None\n* `manual_review` - Manual Review\n* `step_up` - Step Up\n* `resubmit_capture` - Resubmit Capture" + }, + "RoutingPublicationPolicyDraftPreconditionRequest": { + "type": "object", + "properties": { + "expected_version": { + "type": "integer", + "minimum": 0 }, - "name_matching_strictness": { + "expected_revision": { "type": "string", - "enum": [ - "NONE", - "STRICT", - "DEFAULT", - "FUZZY" - ], - "description": "How strictly beneficiary names must match before a transfer can reach COMPLETED." + "format": "uuid", + "nullable": true + } + }, + "required": [ + "expected_revision", + "expected_version" + ] + }, + "FraudCheckRisk": { + "type": "object", + "properties": { + "dimension": { + "$ref": "#/components/schemas/FraudDimensionEnum" }, - "confirmation_timeout_hours": { + "score": { "type": "integer", - "minimum": 1, - "description": "Hours an AWAITING_COUNTERPARTY transfer may remain unconfirmed before the periodic sweeper expires it. Default 48." + "nullable": true }, - "timeout_outcome": { - "type": "string", - "enum": [ - "HOLD", - "REJECT", - "PROCEED" - ], - "description": "What happens to the underlying transaction when a transfer expires. Default HOLD." + "band": { + "$ref": "#/components/schemas/FraudBandEnum" }, - "threshold_amount": { - "type": "string", - "description": "Decimal string. Minimum transfer amount that triggers a managed exchange; transfers below it resolve straight to NOT_APPLICABLE. Defaults to \"0.00\" (EU TFR applies no minimum amount)." + "availability": { + "$ref": "#/components/schemas/FraudAvailabilityEnum" }, - "legal_name": { + "requested_but_unavailable": { + "type": "boolean", + "description": "True when the request supplied everything this dimension needed and the platform still could not answer it. Such a check is never recommended for approval on the strength of the dimensions that did answer." + }, + "reason_codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "evidence_indexes": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "required": [ + "availability", + "band", + "dimension", + "evidence_indexes", + "reason_codes", + "requested_but_unavailable", + "score" + ] + }, + "CaseDuplicateSourceEnum": { + "enum": [ + "MANUAL", + "AML_SCREENING", + "TRANSACTION_RULE", + "WORKFLOW_BUILDER" + ], + "type": "string", + "description": "* `MANUAL` - Manual\n* `AML_SCREENING` - AML screening\n* `TRANSACTION_RULE` - Transaction rule\n* `WORKFLOW_BUILDER` - Workflow builder" + }, + "CaseCollaborationPatchedCaseNoteUpdateRequest": { + "type": "object", + "properties": { + "is_pinned": { + "type": "boolean" + } + } + }, + "CustomerMessagingPurposeEnum": { + "enum": [ + "identity_document", + "proof_of_address", + "business_document", + "confirm_details", + "review_in_progress" + ], + "type": "string", + "description": "* `identity_document` - identity_document\n* `proof_of_address` - proof_of_address\n* `business_document` - business_document\n* `confirm_details` - confirm_details\n* `review_in_progress` - review_in_progress" + }, + "CaseDuplicateTypeEnum": { + "enum": [ + "user", + "business" + ], + "type": "string", + "description": "* `user` - user\n* `business` - business" + }, + "CaseCollaborationCaseNoteCreateRequest": { + "type": "object", + "properties": { + "comment": { "type": "string", - "description": "Your VASP's legal name. Required (non-blank) to set is_enabled to true." + "minLength": 1, + "maxLength": 10000 }, - "lei": { + "reply_to": { "type": "string", - "description": "Your Legal Entity Identifier, surfaced in the VASP directory." + "format": "uuid", + "nullable": true }, - "compliance_email": { + "client_reference": { "type": "string", - "format": "email", - "description": "Compliance contact email for your VASP profile." + "format": "uuid", + "nullable": true }, - "is_discoverable": { - "type": "boolean", - "description": "Whether your VASP profile appears in GET /v3/travel-rule/vasps/ search results and can be reached on the INTERNAL rail. Default true." + "mentioned_emails": { + "type": "array", + "items": { + "type": "string", + "format": "email", + "minLength": 1 + } }, - "inbound_auto_accept": { + "is_internal": { "type": "boolean", - "description": "When true, inbound transfers whose beneficiary wallet is a verified address-book entry with a matching name are accepted automatically, without a manual review step. Default false." + "default": false }, - "allow_self_declaration": { - "type": "boolean", - "description": "When true, the wallet-ownership widget offers self-declaration as a fallback proof method. Default false." + "attachments": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": {} + } }, - "allow_screenshot_proof": { - "type": "boolean", - "description": "When true, the wallet-ownership widget offers screenshot upload as an ownership evidence method. Default true." + "tags": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "required": [ + "comment" + ] + }, + "CustomerMessagingLanguagesEnum": { + "enum": [ + "en", + "es", + "pt" + ], + "type": "string", + "description": "* `en` - en\n* `es` - es\n* `pt` - pt" + }, + "CustomerMessagingCaseRequestConfirmRequest": { + "type": "object", + "properties": { + "contact_id": { + "type": "string", + "format": "uuid" }, - "auto_wallet_verification": { - "type": "boolean", - "description": "When true, submitting an outbound Travel Rule transfer that needs end-user proof of wallet control auto-mints a wallet-ownership widget session bound to the transaction and exposes it in the transaction's action_required block. Default true." + "purpose": { + "$ref": "#/components/schemas/CustomerMessagingPurposeEnum" }, - "vasp_attribution_enabled": { - "type": "boolean", - "description": "When true, an outbound Travel Rule transfer that no rail can route is attributed through blockchain analytics: the destination wallet is resolved to its owning VASP, the entity is recorded on the transfer and in the VASP directory, and the rails are re-run once with the attributed name. Default true." + "language": { + "allOf": [ + { + "$ref": "#/components/schemas/CustomerMessagingLanguageEnum" + } + ], + "default": "en" }, - "travel_address": { + "request_id": { + "type": "string", + "format": "uuid" + }, + "expires_at": { "type": "string", - "description": "Read-only. Your VASP's own travel address - the encoded inbound TRP inquiry endpoint other VASPs use to reach you. Populated once your VASP profile is complete." + "format": "date-time" }, - "disabled_networks": { + "preview_token": { + "type": "string", + "minLength": 1, + "maxLength": 512 + } + }, + "required": [ + "contact_id", + "expires_at", + "preview_token", + "purpose", + "request_id" + ] + }, + "CaseRoutingRule": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "description": { + "type": "string", + "maxLength": 500 + }, + "priority": { + "type": "integer", + "minimum": 0 + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseRoutingCondition" + }, + "maxItems": 20 + }, + "required_skills": { "type": "array", "items": { "type": "string", - "enum": [ - "GTR", - "TRUST", - "VERIFYVASP", - "SYGNA" - ] + "maxLength": 64 }, - "description": "Networks opted out of platform membership. Does not disable your connected own memberships or provision unavailable networks.", - "default": [] + "maxItems": 25 }, - "networks": { - "type": "object", - "readOnly": true, - "description": "Effective membership configuration for each network. No network, including GTR, is included by default. This is not a live health or delivery check.", - "properties": { - "gtr": { - "type": "object", - "properties": { - "rail": { - "type": "string", - "enum": [ - "GTR" - ] - }, - "mode": { - "type": "string", - "enum": [ - "own", - "didit", - "off", - "unavailable" - ], - "description": "own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available." - }, - "platform_available": { - "type": "boolean", - "description": "Whether platform membership is configured; not a live health check." - }, - "connected": { - "type": "boolean", - "description": "Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange." - } - } - }, - "trust": { - "type": "object", - "properties": { - "rail": { - "type": "string", - "enum": [ - "TRUST" - ] - }, - "mode": { - "type": "string", - "enum": [ - "own", - "didit", - "off", - "unavailable" - ], - "description": "own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available." - }, - "platform_available": { - "type": "boolean", - "description": "Whether platform membership is configured; not a live health check." - }, - "connected": { - "type": "boolean", - "description": "Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange." - } - } - }, - "verifyvasp": { - "type": "object", - "properties": { - "rail": { - "type": "string", - "enum": [ - "VERIFYVASP" - ] - }, - "mode": { - "type": "string", - "enum": [ - "own", - "didit", - "off", - "unavailable" - ], - "description": "own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available." - }, - "platform_available": { - "type": "boolean", - "description": "Whether platform membership is configured; not a live health check." - }, - "connected": { - "type": "boolean", - "description": "Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange." - } - } - }, - "sygna": { - "type": "object", - "properties": { - "rail": { - "type": "string", - "enum": [ - "SYGNA" - ] - }, - "mode": { - "type": "string", - "enum": [ - "own", - "didit", - "off", - "unavailable" - ], - "description": "own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available." - }, - "platform_available": { - "type": "boolean", - "description": "Whether platform membership is configured; not a live health check." - }, - "connected": { - "type": "boolean", - "description": "Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange." - } - } - } - } + "blueprint": { + "type": "string", + "format": "uuid" + }, + "team": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "is_active": { + "type": "boolean" + }, + "uuid": { + "type": "string", + "format": "uuid", + "readOnly": true + }, + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "updated_at": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "version": { + "type": "integer", + "readOnly": true + }, + "blueprint_name": { + "type": "string", + "readOnly": true + }, + "team_name": { + "type": "string", + "nullable": true, + "readOnly": true } } }, - "TravelRuleSettingsUpdate": { + "FraudResultEnum": { + "enum": [ + "match", + "mismatch", + "inconclusive", + "no_reference" + ], + "type": "string", + "description": "* `match` - Match\n* `mismatch` - Mismatch\n* `inconclusive` - Inconclusive\n* `no_reference` - No Reference" + }, + "CaseRoutingRuleCreate": { "type": "object", "properties": { - "is_enabled": { - "type": "boolean" + "name": { + "type": "string", + "maxLength": 100 }, - "legal_name": { - "type": "string" + "description": { + "type": "string", + "maxLength": 500 }, - "lei": { - "type": "string" + "priority": { + "type": "integer", + "minimum": 0 }, - "jurisdiction": { + "conditions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseRoutingCondition" + }, + "maxItems": 20 + }, + "required_skills": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64 + }, + "maxItems": 25 + }, + "blueprint": { "type": "string", - "maxLength": 8 + "format": "uuid" }, - "name_matching_strictness": { + "team": { "type": "string", - "enum": [ - "NONE", - "STRICT", - "DEFAULT", - "FUZZY" - ] + "format": "uuid", + "nullable": true }, - "confirmation_timeout_hours": { - "type": "integer", - "minimum": 1 + "is_active": { + "type": "boolean" }, - "timeout_outcome": { + "publish": { + "type": "boolean", + "writeOnly": true, + "description": "Set false to save a never-published draft. The record remains inactive at version 0 until the draft is explicitly published. Omitting this field preserves immediate creation for existing integrations." + } + }, + "required": [ + "name", + "conditions", + "blueprint" + ] + }, + "FraudCheckIdentifierRequest": { + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/FraudTypeEnum" + } + ], + "description": "Identifier type. Determines which namespace fields are required.\n\n* `email` - Email\n* `phone` - Phone\n* `national_id` - National Id\n* `tax_number` - Tax Number\n* `passport_number` - Passport Number\n* `driving_licence_number` - Driving Licence Number\n* `residence_permit_number` - Residence Permit Number\n* `voter_id` - Voter Id\n* `external_id` - External Id" + }, + "value": { "type": "string", - "enum": [ - "HOLD", - "REJECT", - "PROCEED" - ] + "minLength": 1, + "description": "The identifier value, in any customary format.", + "maxLength": 255 }, - "compliance_email": { + "country": { "type": "string", - "format": "email" + "description": "ISO-3166 alpha-2 or alpha-3 issuing country. Required for government identifier types: the same digits mean different people in different countries.", + "maxLength": 3 }, - "is_discoverable": { - "type": "boolean" + "issuer": { + "type": "string", + "description": "Optional issuing authority or sub-national issuer, when the country alone is not unique.", + "maxLength": 128 + } + }, + "required": [ + "type", + "value" + ] + }, + "CaseActionActionEnum": { + "enum": [ + "set_status", + "add_tags", + "add_note", + "add_to_blocklist" + ], + "type": "string", + "description": "* `set_status` - set_status\n* `add_tags` - add_tags\n* `add_note` - add_note\n* `add_to_blocklist` - add_to_blocklist" + }, + "CaseActionCaseActionReceiptPage": { + "type": "object", + "properties": { + "count": { + "type": "integer" }, - "threshold_amount": { + "next_offset": { + "type": "integer", + "nullable": true + }, + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CaseActionCaseActionReceipt" + } + } + }, + "required": [ + "count", + "next_offset", + "results" + ] + }, + "CaseActionCaseEntityActionResult": { + "type": "object", + "properties": { + "receipt": { + "$ref": "#/components/schemas/CaseActionCaseActionReceipt" + }, + "entity_type": { + "$ref": "#/components/schemas/CaseActionEntityTypeEnum" + }, + "entity_uuid": { "type": "string", - "description": "Decimal string. Minimum transfer amount that triggers a managed exchange." + "format": "uuid" }, - "inbound_auto_accept": { - "type": "boolean" + "action": { + "type": "string" }, - "allow_self_declaration": { - "type": "boolean" + "previous_status": { + "type": "string" }, - "allow_screenshot_proof": { - "type": "boolean", - "description": "When true, the wallet-ownership widget offers screenshot upload as an ownership evidence method. Default true." + "new_status": { + "type": "string" }, - "auto_wallet_verification": { - "type": "boolean" + "tags_added": { + "type": "array", + "items": { + "type": "string" + } }, - "vasp_attribution_enabled": { - "type": "boolean" + "comment_uuid": { + "type": "string", + "format": "uuid" }, - "disabled_networks": { + "blocklisted": { "type": "array", "items": { - "type": "string", - "enum": [ - "GTR", - "TRUST", - "VERIFYVASP", - "SYGNA" - ] - }, - "description": "Networks opted out of platform membership. Does not disable your connected own memberships or provision unavailable networks.", - "default": [] + "type": "string" + } } - }, - "description": "All fields optional; PUT applies a partial update." + } }, - "WalletAddressEntry": { + "CustomerMessagingCaseRequest": { "type": "object", "properties": { "uuid": { "type": "string", - "format": "uuid" + "format": "uuid", + "readOnly": true }, - "address": { + "purpose": { "type": "string", - "description": "The wallet address. Unique per application and chain among non-deleted entries." + "readOnly": true }, - "chain": { + "language": { "type": "string", - "nullable": true, - "description": "Free-text chain identifier, e.g. \"ethereum\", \"bitcoin\"." + "readOnly": true }, - "holder_vendor_data": { + "recipient": { "type": "string", - "nullable": true, - "description": "Your internal identifier for the holder." + "format": "email", + "readOnly": true }, - "holder_name": { + "expires_at": { "type": "string", - "nullable": true, - "description": "Name of the wallet holder, used for the beneficiary name match." + "format": "date-time", + "readOnly": true }, - "entity_type": { + "actor_email": { "type": "string", - "description": "Free text, defaults to \"individual\"." + "format": "email", + "readOnly": true, + "nullable": true }, - "is_ownership_verified": { - "type": "boolean", - "description": "Read-only. Whether ownership of this address has been proven." + "delivery_status": { + "type": "string", + "readOnly": true }, - "created_at": { + "delivered_at": { "type": "string", - "format": "date-time" + "format": "date-time", + "readOnly": true, + "nullable": true }, - "self_declared": { + "cancelled_at": { + "type": "string", + "format": "date-time", + "readOnly": true, + "nullable": true + }, + "responded_at": { + "type": "string", + "format": "date-time", + "readOnly": true, + "nullable": true + }, + "response": { + "readOnly": true, + "nullable": true + }, + "has_attachment": { "type": "boolean", - "description": "Whether the entry was created as a self-declared ownership proof." + "readOnly": true }, - "travel_address": { + "created_at": { "type": "string", - "nullable": true, - "description": "The counterparty VASP's travel address for this wallet, if known. Lets Didit route this beneficiary's transfers over the TRP rail." + "format": "date-time", + "readOnly": true } - } + }, + "required": [ + "actor_email", + "cancelled_at", + "created_at", + "delivered_at", + "delivery_status", + "expires_at", + "has_attachment", + "language", + "purpose", + "recipient", + "responded_at", + "response", + "uuid" + ] }, - "WalletAddressEntryCreate": { + "CustomerMessagingCaseRequestPublic": { "type": "object", - "required": [ - "address" - ], "properties": { - "address": { - "type": "string" - }, - "chain": { - "type": "string" + "request_id": { + "type": "string", + "format": "uuid" }, - "holder_vendor_data": { - "type": "string" + "purpose": { + "$ref": "#/components/schemas/CustomerMessagingPurposeEnum" }, - "holder_name": { - "type": "string" + "language": { + "$ref": "#/components/schemas/CustomerMessagingLanguageEnum" }, - "entity_type": { + "expires_at": { "type": "string", - "default": "individual" - }, - "self_declared": { - "type": "boolean", - "default": false, - "description": "Write-only. When true, the entry is created already ownership-verified via a SELF_DECLARATION proof." + "format": "date-time" }, - "travel_address": { + "responded_at": { "type": "string", - "description": "Optional. The counterparty VASP's travel address for this wallet, enabling TRP routing." - } - } - }, - "WalletAddressEntryUpdate": { - "type": "object", - "properties": { - "holder_name": { - "type": "string" + "format": "date-time", + "nullable": true }, - "holder_vendor_data": { - "type": "string" + "copy": { + "type": "object", + "additionalProperties": {} }, - "entity_type": { + "organization_name": { "type": "string" }, - "travel_address": { - "type": "string" + "is_white_label": { + "type": "boolean" + }, + "customization": { + "type": "object", + "additionalProperties": {}, + "nullable": true } }, - "description": "PATCH is a partial update; the response echoes only holder_name, holder_vendor_data, entity_type, and travel_address." + "required": [ + "copy", + "customization", + "expires_at", + "is_white_label", + "language", + "organization_name", + "purpose", + "request_id", + "responded_at" + ] }, - "TravelRuleTransferDetail": { + "RoutingPublicationFieldEnum": { + "enum": [ + "source", + "priority", + "tags" + ], + "type": "string", + "description": "* `source` - source\n* `priority` - priority\n* `tags` - tags" + }, + "RoutingPublicationCaseRoutingRule": { "type": "object", - "description": "Full transfer object returned by the ownership-confirmation and finish/cancel endpoints. originator_data/beneficiary_data are omitted while PII is masked (see description).", "properties": { "uuid": { "type": "string", - "format": "uuid" + "format": "uuid", + "readOnly": true }, - "status": { + "name": { "type": "string", - "enum": [ - "AWAITING_COUNTERPARTY", - "ON_HOLD", - "COMPLETED", - "FINISHED", - "CANCELLED", - "EXPIRED", - "COUNTERPARTY_MISMATCHED_DATA", - "COUNTERPARTY_VASP_NOT_FOUND", - "COUNTERPARTY_VASP_NOT_REACHABLE", - "UNCONFIRMED_OWNERSHIP", - "COUNTERPARTY_VASP_GENERAL_DECLINE", - "NOT_ENOUGH_COUNTERPARTY_DATA", - "NOT_APPLICABLE" - ] + "maxLength": 100 }, - "direction": { + "description": { "type": "string", - "enum": [ - "INBOUND", - "OUTBOUND" - ] + "maxLength": 500 }, - "rail": { + "priority": { + "type": "integer", + "maximum": 2147483647, + "minimum": 0 + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoutingPublicationRoutingCondition" + } + }, + "required_skills": { + "type": "array", + "items": { + "type": "string", + "maxLength": 64 + }, + "maxItems": 25 + }, + "blueprint": { "type": "string", - "nullable": true, - "enum": [ - "INTERNAL", - "TRP", - "GTR", - "EMAIL", - null - ] + "format": "uuid" }, - "protocol": { + "blueprint_name": { + "type": "string", + "readOnly": true + }, + "team": { "type": "string", + "format": "uuid", "nullable": true }, - "ivms_version": { + "team_name": { "type": "string", - "example": "ivms101.2023" + "readOnly": true }, - "required": { + "is_active": { "type": "boolean" }, - "ownership_confirmed": { - "type": "boolean", - "nullable": true + "version": { + "type": "integer", + "readOnly": true }, - "deadline_at": { + "created_at": { "type": "string", "format": "date-time", - "nullable": true - }, - "timeline": { - "type": "array", - "items": { - "type": "object", - "properties": { - "status": { - "type": "string" - }, - "at": { - "type": "string", - "format": "date-time" - }, - "detail": { - "type": "string" - } - } - } - }, - "counterparty_vasp": { - "type": "object", - "nullable": true, - "properties": { - "name": { - "type": "string" - }, - "lei": { - "type": "string" - }, - "dd_score": { - "type": "integer" - } - } - }, - "originator_data": { - "type": "object" - }, - "beneficiary_data": { - "type": "object" + "readOnly": true }, - "created_at": { + "updated_at": { "type": "string", - "format": "date-time" + "format": "date-time", + "readOnly": true } - } - }, - "TravelRuleOwnershipConfirmRequest": { - "type": "object", + }, "required": [ - "confirmed" + "blueprint", + "blueprint_name", + "conditions", + "created_at", + "name", + "team_name", + "updated_at", + "uuid", + "version" + ] + }, + "CaseActionStatusEnum": { + "enum": [ + "APPLIED", + "FAILED" ], + "type": "string", + "description": "* `APPLIED` - Applied\n* `FAILED` - Failed" + }, + "FraudCheckModuleSelectionRequest": { + "type": "object", "properties": { - "confirmed": { - "type": "boolean" + "include": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FraudExcludeEnum" + } + }, + "exclude": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FraudExcludeEnum" + } } } }, - "TravelRuleTransferPatchRequest": { + "SignatureField": { "type": "object", - "description": "Provide exactly one of: payment_txn_id (finish), action=cancel, or action=resend.", + "description": "A field to place on a session document: which signer fills it, how, and where. Give either a position (`page`, `x`, `y`, `width` and `height`) or an `anchor`. Positions are fractions of the page as it's displayed, from its top-left corner, rounded to 4 decimals, and the field must fit inside the page. A document takes up to 200 fields and one signer up to 500, and every role with fields on a document needs a `signature` or `initials` field there. See [Fields on the document](/core-technology/electronic-signature/overview#fields-on-the-document).", + "required": [ + "key", + "type" + ], "properties": { - "payment_txn_id": { + "key": { "type": "string", - "description": "The on-chain transaction hash. Requires the transfer to be an outbound transfer in COMPLETED status." + "pattern": "^[a-z][a-z0-9_]{0,63}$", + "description": "Names the field in the results and in `sign.field_values`. Unique within the document." }, - "action": { + "type": { "type": "string", "enum": [ - "cancel", - "resend" + "signature", + "initials", + "full_name", + "date_signed", + "text", + "checkbox", + "email", + "data" ], - "description": "\"cancel\" cancels a non-terminal transfer. \"resend\" re-runs routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA." - } - } - }, - "VaspDirectoryEntry": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "lei": { - "type": "string" - }, - "jurisdiction": { - "type": "string" + "description": "What the field holds. The signer applies their adopted `signature` and `initials`, types `text`, ticks a `checkbox`, and types an `email` when the session has no verified email. Didit fills `full_name` (the verified name), `date_signed` (the date of signing, in the signer's time zone), `data` (from `prefill`) and a verified `email`, and shows them read-only. There is no date picker type: use `text`." }, - "dd_status": { + "role": { "type": "string", "enum": [ - "NOT_STARTED", - "IN_PROGRESS", - "COMPLETED", - "REJECTED" - ] + "verified_person", + "legal_representative", + "key_people" + ], + "description": "Who fills the field, with the `signature_signer` values. A step fills and stamps only the fields of its own signer. Defaults to the step's signer." }, - "dd_score": { - "type": "integer" + "page": { + "type": "integer", + "minimum": 1, + "description": "Page number, from 1 to the document's page count. For a Word document, a page of the converted PDF." }, - "reachable_rails": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "INTERNAL", - "TRP", - "GTR", - "EMAIL" - ] - } + "x": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Left edge, as a fraction of the page width." }, - "is_didit_customer": { + "y": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Top edge, as a fraction of the page height." + }, + "width": { + "type": "number", + "minimum": 0.02, + "maximum": 1, + "description": "Width, as a fraction of the page width. A signature is at least `0.12` wide." + }, + "height": { + "type": "number", + "minimum": 0.012, + "maximum": 1, + "description": "Height, as a fraction of the page height. A signature is at least `0.035` high." + }, + "anchor": { + "$ref": "#/components/schemas/SignatureFieldAnchor" + }, + "required": { "type": "boolean", - "description": "true for discoverable Didit applications (dd_status is always COMPLETED, dd_score always 0 for these); false for internal counterparty-registry rows, which carry a real due-diligence status and score." + "description": "`signature` and `initials` are required unless you set `false`. `full_name`, `date_signed` and `data` are always required. The signer can't finish a document until its required fields have a value, and a required checkbox must be ticked." }, - "dd_score_breakdown": { - "type": "object", - "additionalProperties": { - "type": "integer" - }, - "description": "Per-component contributions to dd_score (licensing/registry, sanctions/PEP, jurisdiction risk, on-chain analytics risk), all derived from checks Didit runs in-house. Empty for Didit-customer rows." + "label": { + "type": "string", + "minLength": 1, + "maxLength": 80, + "description": "Shown to the signer and in the results. Defaults from the type." }, - "dd_assessed_at": { + "placeholder": { "type": "string", - "format": "date-time", "nullable": true, - "description": "When the due-diligence assessment last ran. Null for Didit-customer rows." - } - } - }, - "TravelRulePickupInfo": { - "type": "object", - "properties": { - "originator_vasp_name": { - "type": "string" + "maxLength": 80, + "description": "`text` and `email` only." }, - "amount": { - "type": "string" + "validation": { + "$ref": "#/components/schemas/SignatureFieldValidation" }, - "currency": { - "type": "string" + "prefill": { + "type": "string", + "nullable": true, + "description": "Where the value comes from: `identity.full_name`, `identity.first_name`, `identity.last_name`, `identity.date_of_birth` (`DD/MM/YYYY`), `identity.nationality`, `identity.document_number`, `identity.document_type`, `identity.issuing_country`, `identity.address`, `contact.email`, `contact.phone`, `company.name`, `company.registration_number`, `company.address`, `company.country` (KYB), `session.vendor_data`, `metadata.` (a key of the session's `metadata`) or `api.` (a key of `sign.field_values`). Required for `data`. `full_name` always uses `identity.full_name` and `email` uses `contact.email`. The values of `data`, `full_name` and verified `email` fields are fixed when the signer starts signing." }, - "beneficiary_wallet": { - "type": "string" + "editable_when_prefilled": { + "type": "boolean", + "default": false, + "description": "`text` and `email` only: the signer can change a pre-filled value." }, - "status": { - "type": "string" + "checked_by_default": { + "type": "boolean", + "default": false, + "description": "`checkbox` only: the box starts ticked." + }, + "date_format": { + "type": "string", + "enum": [ + "DD/MM/YYYY", + "MM/DD/YYYY", + "YYYY-MM-DD", + "D MMMM YYYY" + ], + "default": "DD/MM/YYYY", + "description": "`date_signed` only." } } }, - "TravelRulePickupRespondRequest": { + "SessionSignDocument": { "type": "object", "required": [ - "decision" + "file_name", + "content_base64" ], "properties": { - "decision": { + "title": { "type": "string", - "enum": [ - "accept", - "decline" - ] + "minLength": 1, + "maxLength": 255, + "description": "Title shown to the signer. Defaults to `file_name`." }, - "beneficiary_name": { + "file_name": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "File name, ending in `.pdf` or `.docx`." + }, + "content_base64": { "type": "string", - "description": "Required in practice for decision=accept; run through the originator's name_matching_strictness policy." + "format": "byte", + "description": "The file, base64-encoded. A PDF can have up to 20 MB and 100 pages, without a password, scripts or attached files. A Word file (`.docx`) can have up to 20 MB and is converted to PDF while the request runs, in at most 25 seconds; the PDF then has the same limits. Up to 5 Word files per session. A Word file with macros, tracked changes, links to external content or embedded objects is refused with `signature_document_not_supported`, and one that can't be converted returns `signature_document_conversion_failed`, each with a `reason`." + }, + "fields": { + "type": "array", + "description": "Optional fields to place on this document, up to 200. Pages and positions refer to the PDF, or to the converted PDF for a Word file. Without fields, the signer reads the document and agrees to it.", + "items": { + "$ref": "#/components/schemas/SignatureField" + }, + "maxItems": 200 } } }, - "TravelRuleInboundRequest": { + "SignatureResult": { "type": "object", - "required": [ - "chain", - "txid", - "wallet_address", - "amount", - "currency" - ], + "description": "One signer's result for one Electronic Signature step. See the [Electronic Signature report](/core-technology/electronic-signature/report-electronic-signature).", "properties": { - "chain": { + "id": { "type": "string", - "description": "Chain the deposit settled on, e.g. \"ethereum\"." + "format": "uuid", + "description": "Pass it as `signature_id` to download this signer's evidence or validate their signature." }, - "txid": { + "node_id": { "type": "string", - "description": "On-chain transaction hash. Deduped together with wallet_address." + "description": "The workflow step that produced it." }, - "wallet_address": { + "status": { "type": "string", - "description": "The destination wallet you received the deposit on." + "enum": [ + "intro", + "review", + "code", + "signing", + "signed", + "declined" + ], + "description": "Where the signer is in the step. Only `signed` means the documents are signed." }, - "amount": { + "verdict": { "type": "string", - "description": "Deposit amount as a decimal string." + "enum": [ + "Not Finished", + "Approved", + "In Review", + "Declined" + ], + "description": "The step's outcome. A warning sets it through its rule; custom status rules apply after." }, - "currency": { + "level": { "type": "string", - "description": "Deposit asset, e.g. \"USDC\"." + "enum": [ + "aes", + "qes" + ], + "description": "`aes`. `qes` (Qualified Electronic Signature, QES) is coming soon." }, - "originator_data": { + "format": { + "type": "string", + "enum": [ + "pades" + ] + }, + "profile": { + "type": "string", + "nullable": true, + "description": "`PAdES-B-LT` once signed." + }, + "signer": { "type": "object", - "description": "IVMS-101 originator payload from the sending VASP, if known.", - "additionalProperties": true + "properties": { + "name": { + "type": "string", + "nullable": true, + "description": "Verified name." + }, + "capacity": { + "type": "string", + "enum": [ + "self", + "company" + ] + }, + "company": { + "type": "string", + "nullable": true + }, + "role": { + "type": "string", + "nullable": true, + "description": "The key person's designation, as declared." + }, + "roles": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The key person's structured roles, as declared for the company." + }, + "identity_session_id": { + "type": "string", + "format": "uuid", + "description": "The KYC session that verified the signer." + } + } }, - "beneficiary_data": { + "identity_used": { "type": "object", - "description": "IVMS-101 beneficiary payload (your customer).", - "additionalProperties": true + "properties": { + "session_id": { + "type": "string", + "format": "uuid" + }, + "kyc_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "verified": { + "type": "boolean" + }, + "expected_name": { + "type": "string", + "nullable": true, + "description": "The name the signer had to match." + } + } }, - "originating_vasp": { + "certificate": { "type": "object", - "description": "Optional description of the sending VASP.", + "nullable": true, + "description": "The certificate the documents were signed with. `null` until signed.", "properties": { - "name": { + "issuer": { "type": "string" }, - "lei": { + "issued_to": { "type": "string" }, - "travel_address": { + "serial": { + "type": "string", + "description": "Serial number, hexadecimal." + }, + "valid_from": { "type": "string", - "description": "Travel address that lets Didit reach the sender on the TRP rail." + "format": "date-time" + }, + "valid_until": { + "type": "string", + "format": "date-time" } } - } - } - }, - "TravelRuleInboundResponse": { - "type": "object", - "properties": { - "transaction_id": { - "type": "string", - "format": "uuid", - "description": "Didit transaction UUID for the inbound deposit." - }, - "txn_id": { - "type": "string", - "description": "Didit-generated txn_id for the sunrise transaction." - }, - "created": { - "type": "boolean", - "description": "true when a new transaction was minted (HTTP 201); false when an existing unclaimed deposit was reused (HTTP 200)." - }, - "travel_rule": { - "$ref": "#/components/schemas/TravelRuleTransferDetail" - } - } - }, - "TravelRuleWidgetSessionCreate": { - "type": "object", - "required": [ - "wallet_address", - "chain" - ], - "properties": { - "wallet_address": { - "type": "string", - "description": "The wallet address to verify." - }, - "chain": { - "type": "string", - "description": "Chain identifier for the address, e.g. \"ethereum\", \"bitcoin\", \"solana\"." - }, - "holder_name": { - "type": "string", - "description": "Name of the wallet holder, used for the beneficiary name match." - }, - "vendor_data": { - "type": "string", - "description": "Your internal identifier for the holder, carried onto the created address-book entry." - }, - "transaction_id": { - "type": "string", - "format": "uuid", - "description": "Optional. Didit transaction UUID of a Travel Rule transfer to advance when the proof verifies." - }, - "satoshi_deposit_address": { - "type": "string", - "description": "Optional. A deposit address you control; supplying it enables the SATOSHI_TEST proof method." - }, - "callback_url": { - "type": "string", - "description": "Optional. Where the widget returns the customer after completion." - }, - "expires_in_minutes": { - "type": "integer", - "description": "Optional. Lifetime of the minted link, in minutes. Automatically extended to cover the Satoshi test window when SATOSHI_TEST is offered." - } - } - }, - "TravelRuleWidgetSessionResponse": { - "type": "object", - "properties": { - "widget_session_id": { - "type": "string", - "format": "uuid" - }, - "token": { - "type": "string", - "description": "Opaque session token embedded in the widget URL." - }, - "url": { - "type": "string", - "description": "The hosted wallet-ownership widget URL. Open it verbatim in the system browser (never inside a mobile SDK webview)." - }, - "expires_at": { - "type": "string", - "format": "date-time" - } - } - }, - "TravelRuleWidgetContext": { - "type": "object", - "properties": { - "vasp_name": { - "type": "string", - "description": "Legal name from your VASP profile, shown to the customer in the widget. Empty when no profile exists." - }, - "wallet_address": { - "type": "string" }, - "chain": { - "type": "string" + "confirmation": { + "type": "object", + "properties": { + "channel": { + "type": "string", + "enum": [ + "email", + "sms" + ] + }, + "destination": { + "type": "string", + "nullable": true, + "description": "The masked contact the code went to." + }, + "confirmed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "attempts": { + "type": "integer", + "description": "Codes entered, the right one included." + } + } }, - "holder_name": { - "type": "string" + "timestamp": { + "type": "object", + "nullable": true, + "description": "The trusted timestamp in the signature. `null` until signed.", + "properties": { + "time": { + "type": "string", + "format": "date-time" + }, + "qualified": { + "type": "boolean", + "description": "Always `false` for AES." + }, + "valid": { + "type": "boolean" + } + } }, - "allowed_methods": { + "documents": { "type": "array", "items": { - "type": "string", - "enum": [ - "MESSAGE_SIGNING", - "SCREENSHOT", - "SATOSHI_TEST", - "SELF_DECLARATION" - ] - }, - "description": "Proof methods available for this session. MESSAGE_SIGNING requires a supported chain family, SATOSHI_TEST requires a satoshi_deposit_address on the session, SCREENSHOT requires the allow_screenshot_proof setting, and SELF_DECLARATION requires the allow_self_declaration setting." + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "The document on this signer's copy. Use it to download the signed file." + }, + "uuid": { + "type": "string", + "format": "uuid", + "description": "Same as `id`." + }, + "title": { + "type": "string" + }, + "file_name": { + "type": "string" + }, + "pages": { + "type": "integer", + "description": "Pages of the document signed. For Word, of the converted PDF." + }, + "size_bytes": { + "type": "integer" + }, + "sha256_original": { + "type": "string", + "description": "SHA-256 of the document as the signer was shown it." + }, + "sha256_signed": { + "type": "string", + "nullable": true, + "description": "SHA-256 of the signed PDF, once signed." + }, + "status": { + "type": "string", + "enum": [ + "signed", + "declined", + "not_signed", + "not_reached" + ], + "description": "`not_signed`: opened but not signed. `not_reached`: never opened." + }, + "opened_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "read_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the signer reached the end of a document without fields, when the step requires it." + }, + "read_to_end": { + "type": "boolean" + }, + "consented_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the signer agreed to a document without fields, or finished the fields of a document with fields." + }, + "consent": { + "type": "boolean" + }, + "signed_file": { + "type": "string", + "nullable": true, + "description": "Path of the signed PDF, once signed. See [Download a signed document](/sessions-api/electronic-signature/download-signed-document)." + }, + "mode": { + "type": "string", + "enum": [ + "fields", + "read_and_agree" + ], + "description": "`fields` for a document with fields, `read_and_agree` for a document without." + }, + "source_format": { + "type": "string", + "enum": [ + "pdf", + "docx" + ], + "description": "`pdf`, or `docx` for a Word document Didit converted to PDF." + }, + "conversion": { + "type": "object", + "nullable": true, + "description": "`null` for a PDF. For a Word document, the original file and the conversion.", + "properties": { + "from": { + "type": "string", + "enum": [ + "docx" + ] + }, + "file_name": { + "type": "string", + "description": "Your original Word file name." + }, + "sha256": { + "type": "string", + "description": "SHA-256 of your original Word file." + }, + "size_bytes": { + "type": "integer", + "description": "Size of your original Word file." + }, + "converter": { + "type": "string", + "description": "Version of the converter that produced the PDF." + } + } + }, + "fields": { + "type": "array", + "description": "Fields on the document, with their values. Empty for a document without fields.", + "items": { + "$ref": "#/components/schemas/SignatureFieldResult" + } + } + } + } }, - "transfer_status": { - "type": "string", + "adopted_signature": { + "type": "object", "nullable": true, - "description": "Status of the linked Travel Rule transfer. Null when the session was minted without a transaction_id." - }, - "callback_url": { - "type": "string", - "description": "Where the widget returns the customer after completion. Empty when not set." + "description": "The signature and initials the signer adopted for signature and initials fields. `null` until the signer adopts one.", + "properties": { + "method": { + "type": "string", + "enum": [ + "typed", + "drawn" + ], + "description": "`typed`: the verified name in a handwriting style. `drawn`: drawn with a finger or a mouse." + }, + "font": { + "type": "string", + "nullable": true, + "description": "The handwriting style of a typed signature. `null` when drawn." + }, + "adopted_at": { + "type": "string", + "format": "date-time" + } + } }, - "expires_at": { + "electronic_signature_consent_at": { "type": "string", - "format": "date-time" + "format": "date-time", + "nullable": true, + "description": "When the signer agreed, on the review screen, to sign the documents electronically. Set when any document has fields." }, - "completed_at": { + "evidence_file": { "type": "string", - "format": "date-time", "nullable": true, - "description": "Non-null once a proof has been verified." + "description": "Path of the evidence PDF, once signed. See [Download the signature evidence](/sessions-api/electronic-signature/download-evidence)." }, - "proofs": { + "warnings": { "type": "array", + "description": "Warnings raised for this signer. See [Electronic Signature warnings](/core-technology/electronic-signature/warnings-electronic-signature).", "items": { - "$ref": "#/components/schemas/TravelRuleWidgetProofSummary" + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "SIGNATURE_DECLINED", + "SIGNATURE_OTP_FAILED", + "SIGNATURE_NAME_MISMATCH", + "SIGNATURE_IDENTITY_NOT_VERIFIED", + "SIGNATURE_DOCUMENT_CHANGED", + "SIGNATURE_FIELD_DATA_MISSING" + ] + }, + "node_id": { + "type": "string" + } + } + } + }, + "validation": { + "type": "object", + "nullable": true, + "description": "The last cryptographic check of the signed documents. `null` until the documents are signed. If Didit can't confirm the outcome of signing, it's `{\"valid\": null, \"reason\": \"signature_provider_outcome_unknown\", \"checked_at\": ...}` and `status` stays `signing`: treat it as not signed.", + "properties": { + "valid": { + "type": "boolean", + "nullable": true, + "description": "`true` when every document is valid. `null` when the outcome of signing couldn't be confirmed." + }, + "reason": { + "type": "string", + "description": "Set when `valid` is `null`: `signature_provider_outcome_unknown`." + }, + "checked_at": { + "type": "string", + "format": "date-time" + }, + "documents": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "checked_at": { + "type": "string", + "format": "date-time" + }, + "valid": { + "type": "boolean", + "description": "`true` when the signature and the timestamp are both valid and trusted." + }, + "integrity": { + "type": "boolean", + "description": "`true` when the document is unchanged since signing." + }, + "trusted": { + "type": "boolean", + "description": "`true` when the signing certificate chains to a trusted root." + }, + "revoked": { + "type": "boolean" + }, + "trusted_list": { + "type": "object", + "description": "EU Trusted List check. For AES: `{\"status\": \"not_applicable\", \"reason\": \"aes_not_qualified\"}`.", + "properties": { + "status": { + "type": "string" + }, + "reason": { + "type": "string" + } + } + }, + "certificate": { + "type": "object", + "nullable": true, + "description": "The certificate the documents were signed with. `null` until signed.", + "properties": { + "issuer": { + "type": "string" + }, + "issued_to": { + "type": "string" + }, + "serial": { + "type": "string", + "description": "Serial number, hexadecimal." + }, + "valid_from": { + "type": "string", + "format": "date-time" + }, + "valid_until": { + "type": "string", + "format": "date-time" + } + } + }, + "timestamp": { + "type": "object", + "nullable": true, + "description": "The trusted timestamp in the signature. `null` until signed.", + "properties": { + "time": { + "type": "string", + "format": "date-time" + }, + "qualified": { + "type": "boolean", + "description": "Always `false` for AES." + }, + "valid": { + "type": "boolean" + } + } + } + } + } + } } }, - "customization": { - "type": "object", - "nullable": true, - "description": "The application's whitelabel customization (colors, logo, font family) when white label is enabled - the widget renders with the same branding as the verification flow. Null otherwise." - } - } - }, - "TravelRuleWidgetProofSummary": { - "type": "object", - "properties": { - "proof_id": { + "declined_at": { "type": "string", - "format": "uuid" + "format": "date-time", + "nullable": true }, - "method": { + "declined_action": { "type": "string", "enum": [ - "MESSAGE_SIGNING", - "SATOSHI_TEST", - "SCREENSHOT", - "SELF_DECLARATION" + "DECLINE", + "REVIEW", + "NO_ACTION" ] }, - "status": { + "signed_at": { "type": "string", - "enum": [ - "PENDING", - "VERIFIED", - "REJECTED", - "EXPIRED" - ], - "description": "PENDING on a SATOSHI_TEST proof means the deposit has been seen on-chain but has not yet reached enough confirmations; a background job automatically rechecks it and moves it to VERIFIED once it confirms, with no resubmission needed. REJECTED means a genuinely wrong deposit was submitted (absent, wrong sender, or wrong amount). EXPIRED means the proof's own test window closed before a valid deposit was seen." - }, - "wallet_address": { - "type": "string" + "format": "date-time", + "nullable": true }, - "chain": { - "type": "string" + "expires_at": { + "type": "string", + "format": "date-time", + "description": "When the signing link stops working." }, - "deposit_address": { + "email_sent_at": { "type": "string", - "nullable": true, - "description": "Deposit address the customer must send the Satoshi test amount to. Present only on a SATOSHI_TEST proof." + "format": "date-time", + "nullable": true }, - "expected_amount": { + "email_unavailable_reason": { "type": "string", - "nullable": true, - "description": "Exact on-chain amount, in the deposit's native currency, the Satoshi test deposit must match. Present only on a SATOSHI_TEST proof." + "nullable": true }, - "expires_at": { + "ip_address": { "type": "string", - "format": "date-time", "nullable": true, - "description": "Deadline for this proof's own Satoshi test window. Present only on a SATOSHI_TEST proof; the widget session's own expires_at is extended to cover it when SATOSHI_TEST is offered." + "description": "The signer's IP address when they asked for their code." }, - "challenge": { + "place": { "type": "string", "nullable": true, - "description": "Didit-issued message the customer signs with their wallet. Present only on a MESSAGE_SIGNING proof." + "description": "Not collected: always `null`." }, - "created_at": { + "place_unavailable_reason": { "type": "string", - "format": "date-time" + "nullable": true, + "enum": [ + "not_collected" + ] } } }, - "CaseSubject": { + "SignatureFieldValidation": { "type": "object", "nullable": true, - "description": "The single user or business the case is anchored to.", + "description": "Rules for a `text` field's value. The console preview, the signing flow and the API apply the same rules.", "properties": { - "type": { + "kind": { "type": "string", "enum": [ - "user", - "business" - ] + "any", + "letters", + "number", + "email", + "pattern" + ], + "description": "`letters`: letters in any script, spaces, hyphens and apostrophes. `number`: digits with an optional decimal separator. `pattern`: the value must match `pattern`." }, - "uuid": { - "type": "string", - "format": "uuid" + "min_length": { + "type": "integer", + "minimum": 0, + "maximum": 500, + "description": "From `0` up to `max_length`." }, - "name": { - "type": "string" + "max_length": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 120 }, - "external_id": { + "pattern": { "type": "string", "nullable": true, - "description": "The subject's vendor_data." - } - } - }, - "CaseAssignee": { - "type": "object", - "nullable": true, - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "full_name": { - "type": "string" + "minLength": 1, + "maxLength": 200, + "description": "Required with `kind: \"pattern\"`. A regular expression the whole value must match. Lookarounds and backreferences aren't supported." }, - "email": { + "message": { "type": "string", - "format": "email" + "nullable": true, + "maxLength": 120, + "description": "Shown to the signer instead of the default error." } } }, - "CaseBlueprintSummary": { + "SignatureFieldAnchor": { "type": "object", - "nullable": true, + "description": "Places a field by a piece of text in the document instead of a position. Didit doesn't remove the text from the PDF, so write it in the same color as the page background. When the text isn't found, the call returns `400` with `field_anchor_not_found`. See [Place fields by anchor text](/sessions-api/create-session#place-fields-by-anchor-text).", + "required": [ + "text" + ], "properties": { - "uuid": { + "text": { "type": "string", - "format": "uuid" + "minLength": 1, + "maxLength": 64, + "description": "Text searched in the document. For a Word file, in the converted PDF." }, - "name": { + "occurrence": { "type": "string", - "maxLength": 24 - } - } - }, - "CaseContentConfig": { - "type": "object", - "description": "The six togglable sections that drive which tabs and fields appear on the case page. Unknown sections or keys are rejected.", - "properties": { - "checklist": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "optional": { - "type": "boolean", - "description": "When true, a case can be resolved with incomplete checklist items." - }, - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "label": { - "type": "string", - "maxLength": 255 - }, - "order": { - "type": "integer" - } - } - } - } - } - }, - "aml_control": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "financial": { - "type": "boolean" - }, - "identity": { - "type": "boolean" - } - } + "enum": [ + "first", + "all" + ], + "default": "first", + "description": "`all` places one field per match; the keys after the first get `_2`, `_3` and so on." }, - "identity": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "personal_information": { - "type": "boolean" - }, - "applicant_tags": { - "type": "boolean" - }, - "contact_information": { - "type": "boolean" - }, - "risk_overview": { - "type": "boolean" - }, - "poi_documents": { - "type": "boolean" - }, - "poa_documents": { - "type": "boolean" - } - } + "offset_x": { + "type": "number", + "description": "Added to the left edge of the text, as a fraction of the page width." }, - "verifications": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "summary": { - "type": "boolean" - } - } + "offset_y": { + "type": "number", + "description": "Added to the top edge of the text, as a fraction of the page height." }, - "reporting": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "fiu_reports": { - "type": "boolean" - } - } + "width": { + "type": "number", + "minimum": 0.02, + "maximum": 1, + "description": "Width of the field, as a fraction of the page width." }, - "financial": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "payment_methods": { - "type": "boolean" - }, - "transactions": { - "type": "boolean" - } - } + "height": { + "type": "number", + "minimum": 0.012, + "maximum": 1, + "description": "Height of the field, as a fraction of the page height." } } }, - "CaseNote": { + "SignatureFieldResult": { "type": "object", + "description": "A field of a signed document, with its value on this signer's copy. See [Fields](/core-technology/electronic-signature/report-electronic-signature#fields).", "properties": { - "uuid": { + "key": { "type": "string", - "format": "uuid" + "description": "The field's key, as used in `sign.field_values`." }, - "comment": { - "type": "string" - }, - "actor_email": { + "type": { "type": "string", - "format": "email", - "nullable": true + "enum": [ + "signature", + "initials", + "full_name", + "date_signed", + "text", + "checkbox", + "email", + "data" + ] }, - "actor_name": { + "role": { "type": "string", - "nullable": true - }, - "mentioned_emails": { - "type": "array", - "items": { - "type": "string", - "format": "email" - } - }, - "is_internal": { - "type": "boolean" - }, - "attachments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "s3_key": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "file_size": { - "type": "integer" - }, - "url": { - "type": "string", - "format": "uri" - } - } - } + "enum": [ + "verified_person", + "legal_representative", + "key_people" + ] }, - "tags": { - "type": "array", - "items": { - "type": "string" - } + "label": { + "type": "string" }, - "created_at": { - "type": "string", - "format": "date-time" - } - } - }, - "CaseEvent": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" + "page": { + "type": "integer", + "description": "Page number, from 1. For a Word document, a page of the converted PDF." }, - "event_type": { - "type": "string", - "enum": [ - "CREATED", - "STATUS_CHANGED", - "ASSIGNED", - "UNASSIGNED", - "RESOLVED", - "REOPENED", - "LINK_ADDED", - "LINK_REMOVED", - "NOTE_ADDED", - "PRIORITY_CHANGED", - "DUE_DATE_CHANGED", - "TAG_ADDED", - "TAG_REMOVED", - "ESCALATED", - "TRANSFERRED", - "CHECKLIST_COMPLETED", - "REPORT_CREATED", - "REPORT_FINALIZED", - "SUBMITTED_FOR_APPROVAL", - "APPROVAL_GRANTED", - "APPROVAL_REJECTED", - "ENTITY_ACTION", - "APPROVAL_INVALIDATED" - ] + "x": { + "type": "number", + "description": "Left edge, as a fraction of the page width." }, - "actor_email": { - "type": "string", - "format": "email", - "nullable": true + "y": { + "type": "number", + "description": "Top edge, as a fraction of the page height." }, - "actor_name": { - "type": "string", - "nullable": true + "width": { + "type": "number", + "description": "Width, as a fraction of the page width." }, - "previous_status": { - "type": "string", - "enum": [ - "OPEN", - "AWAITING_USER", - "RESOLVED" - ], - "nullable": true + "height": { + "type": "number", + "description": "Height, as a fraction of the page height." }, - "new_status": { - "type": "string", - "enum": [ - "OPEN", - "AWAITING_USER", - "RESOLVED" - ], - "nullable": true + "required": { + "type": "boolean" }, - "metadata": { - "type": "object" + "value": { + "nullable": true, + "description": "A string for `text`, `email`, `full_name`, `date_signed` and `data`, a boolean for a `checkbox`, and `true` for an applied signature or initials. `null` when empty." }, - "created_at": { - "type": "string", - "format": "date-time" - } - } - }, - "CaseLink": { - "type": "object", - "properties": { - "uuid": { + "display_value": { "type": "string", - "format": "uuid" + "nullable": true, + "description": "The value as stamped on the page: dates in the field's `date_format`, `Yes` or `No` for a checkbox, `Signed` for a signature or initials." }, - "linked_entity_type": { + "value_source": { "type": "string", + "nullable": true, "enum": [ + "signer", + "identity", + "contact", + "company", "session", - "business_session", - "transaction" + "metadata", + "api", + "signing_time", + null ], - "nullable": true + "description": "Where the value came from: `signer` (typed, ticked or applied by the signer), `identity` (the verified ID), `contact` (the verified email or phone), `company` (the verified company), `session` (`vendor_data`), `metadata`, `api` (`sign.field_values`) or `signing_time` (the date signed)." }, - "linked_entity_uuid": { + "filled_at": { "type": "string", - "format": "uuid", + "format": "date-time", "nullable": true }, - "linked_entity_summary": { - "type": "object", + "image_url": { + "type": "string", "nullable": true, - "description": "session_id/session_number/status for session and business_session links; uuid/txn_id/status for transaction links." + "description": "Signature and initials only: the path of the applied image. See [Download a signature image](/sessions-api/electronic-signature/download-field-image)." }, - "created_at": { + "value_unavailable_reason": { "type": "string", - "format": "date-time" + "nullable": true, + "enum": [ + "source_empty", + null + ], + "description": "`source_empty` when a pre-filled field had no value in the session and `signature_field_data_missing_action` let the signer sign anyway." } } }, - "CaseChecklistItem": { + "VaspDirectoryWidgetEntry": { "type": "object", + "description": "What the end user's VASP picker shows: who the VASP is and whether it can be reached.", "properties": { - "uuid": { + "id": { "type": "string", - "format": "uuid" + "format": "uuid", + "description": "The entry's id, for GET /v3/travel-rule/vasps/{vasp_id}/." }, - "label": { + "kind": { "type": "string", - "maxLength": 255 + "enum": [ + "directory", + "didit" + ], + "description": "directory for an entry of the shared directory; didit for a member of the Didit network." }, - "is_completed": { - "type": "boolean" + "name": { + "type": "string" }, - "completed_by": { - "type": "string", - "format": "email", - "nullable": true + "lei": { + "type": "string" }, - "completed_at": { + "jurisdiction": { "type": "string", - "format": "date-time", - "nullable": true + "description": "ISO 3166 country code, as the source gives it." }, - "order": { - "type": "integer" + "domains": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The VASP's web domains. Always empty for Didit network members." + }, + "reachable": { + "type": "boolean", + "description": "Whether your application has an available route to this VASP, or to one of its entities for a brand. Always true for a Didit network member." } } }, - "CaseBlueprintListItem": { + "TravelRuleDataSharingFields": { "type": "object", + "description": "The fields Didit sends to the counterparty VASP about your own users, per entity type. The `name` and the `wallet_address` are always sent, whatever the list says. An entity type you never chose shares every field. An empty list is a choice too: it shares the name and the wallet only.", "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string", - "maxLength": 24 - }, - "description": { - "type": "string", - "nullable": true - }, - "assignment_method": { - "type": "string", - "enum": [ - "MANUAL", - "AUTOMATIC" - ] + "individual": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "name", + "wallet_address", + "address", + "national_id", + "customer_identification", + "date_of_birth", + "place_of_birth" + ] + }, + "description": "Fields shared about a natural person." }, - "assignee_pool": { + "company": { "type": "array", "items": { "type": "string", - "format": "uuid" + "enum": [ + "name", + "wallet_address", + "address", + "national_id", + "customer_identification" + ] + }, + "description": "Fields shared about a legal person." + } + } + }, + "TravelRuleMatchingRules": { + "type": "object", + "description": "How strictly each field of the counterparty's data is compared with what you hold, per entity type. A field you leave out keeps its default. `NONE` skips the comparison, `STRICT` needs exactly the same words, `CONTAINS` accepts one value whose words all appear in the other, `DEFAULT` is the standard subset match, and `FUZZY` accepts close spellings (and, for dates of birth, a swapped day and month).", + "properties": { + "individual": { + "type": "object", + "description": "Rules for a natural person.", + "properties": { + "name": { + "type": "string", + "enum": [ + "NONE", + "STRICT", + "CONTAINS", + "DEFAULT", + "FUZZY" + ], + "description": "Default: follows `name_matching_strictness`." + }, + "date_of_birth": { + "type": "string", + "enum": [ + "NONE", + "STRICT", + "CONTAINS", + "DEFAULT", + "FUZZY" + ], + "description": "Default `NONE`." + }, + "address": { + "type": "string", + "enum": [ + "NONE", + "STRICT", + "CONTAINS", + "DEFAULT", + "FUZZY" + ], + "description": "Default `NONE`." + }, + "account": { + "type": "string", + "enum": [ + "NONE", + "STRICT", + "CONTAINS", + "DEFAULT", + "FUZZY" + ], + "description": "The wallet the exchange was routed by. Default `STRICT`." + } } }, - "max_active_cases": { - "type": "integer", - "nullable": true - }, - "escalation_enabled": { - "type": "boolean" - }, - "transfer_enabled": { - "type": "boolean" - }, - "require_resolution_note": { - "type": "boolean" - }, - "is_active": { - "type": "boolean" + "company": { + "type": "object", + "description": "Rules for a legal person.", + "properties": { + "name": { + "type": "string", + "enum": [ + "NONE", + "STRICT", + "CONTAINS", + "DEFAULT", + "FUZZY" + ], + "description": "Default: follows `name_matching_strictness`." + }, + "address": { + "type": "string", + "enum": [ + "NONE", + "STRICT", + "CONTAINS", + "DEFAULT", + "FUZZY" + ], + "description": "Default `NONE`." + }, + "account": { + "type": "string", + "enum": [ + "NONE", + "STRICT", + "CONTAINS", + "DEFAULT", + "FUZZY" + ], + "description": "The wallet the exchange was routed by. Default `STRICT`." + } + } + } + } + }, + "TravelRuleDataMatch": { + "type": "object", + "nullable": true, + "description": "Field-by-field comparison of the transfer's party data with what the other side knows about the wallet holder. It names fields and results, never values, so it stays readable while the counterparty's data is masked. Null until a comparison ran.", + "properties": { + "matched": { + "type": "boolean", + "description": "True when no compared field differs." }, - "preset_key": { - "type": "string", - "nullable": true + "mismatched_fields": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "name", + "date_of_birth", + "address", + "account" + ] + }, + "description": "The fields that differ." }, - "created_at": { + "fields": { + "type": "object", + "description": "One entry per field compared for the party's entity type (`name`, `date_of_birth`, `address`, `account`; a company has no `date_of_birth`).", + "additionalProperties": { + "type": "object", + "properties": { + "result": { + "type": "string", + "enum": [ + "matched", + "mismatched", + "not_compared" + ], + "description": "`not_compared` when the rule is `NONE` or either side has no value for the field." + }, + "strictness": { + "type": "string", + "enum": [ + "NONE", + "STRICT", + "CONTAINS", + "DEFAULT", + "FUZZY" + ] + } + } + } + } + } + }, + "TravelRuleDataUnmasked": { + "type": "object", + "nullable": true, + "description": "Audit record of the moment the counterparty's data was unmasked. Null while the data is still masked, and for outbound transfers.", + "properties": { + "at": { "type": "string", "format": "date-time" }, - "version": { - "type": "integer", - "readOnly": true + "reason": { + "type": "string", + "enum": [ + "ownership_confirmed", + "wallet_proof", + "address_book_match", + "own_verified_wallet", + "inquiry_approved", + "known_payment_method", + "travel_address" + ], + "description": "What confirmed ownership: ownership_confirmed (your confirmation through the console or the API), wallet_proof (a wallet proof in the widget, or an INTERNAL inbound transfer, set when it is created), address_book_match (a verified address-book wallet), own_verified_wallet (your user's own verified wallet), inquiry_approved (your approval of an inbound TRP inquiry or a GTR or Sygna message), known_payment_method (a wallet your user already paid with) or travel_address (the per-wallet travel address named the wallet)." }, - "default_deadline_hours": { - "type": "integer", - "nullable": true, - "minimum": 1, - "maximum": 8760 + "actor": { + "type": "string", + "description": "Who confirmed it: the console user's email or the API key's label. Blank for an automatic decision." } } }, - "CaseBlueprintDetail": { + "TravelRuleMessageAttachment": { "type": "object", + "nullable": true, + "description": "The document attached to a message, stored like any other transaction document.", "properties": { "uuid": { "type": "string", "format": "uuid" }, - "name": { - "type": "string", - "maxLength": 24 - }, - "description": { + "label": { "type": "string", "nullable": true }, - "assignment_method": { + "note": { "type": "string", - "enum": [ - "MANUAL", - "AUTOMATIC" - ] - }, - "assignee_pool": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - } - }, - "max_active_cases": { - "type": "integer", "nullable": true }, - "escalation_enabled": { - "type": "boolean" - }, - "transfer_enabled": { - "type": "boolean" - }, - "require_resolution_note": { - "type": "boolean" - }, - "is_active": { - "type": "boolean" - }, - "preset_key": { + "uploaded_by": { "type": "string", - "nullable": true + "nullable": true, + "description": "Who attached it. In the pickup portal, a file you sent shows your VASP name, never the person or API key behind it." }, "created_at": { "type": "string", "format": "date-time" }, - "escalation_blueprint": { - "type": "string", - "format": "uuid", - "nullable": true - }, - "escalation_blueprint_name": { - "type": "string", - "nullable": true - }, - "four_eyes_checker_blueprint": { - "type": "string", - "format": "uuid", - "nullable": true - }, - "four_eyes_checker_blueprint_name": { - "type": "string", - "nullable": true - }, - "content_config": { - "type": "object", - "description": "The six togglable sections that drive which tabs and fields appear on the case page. Unknown sections or keys are rejected.", - "properties": { - "checklist": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "optional": { - "type": "boolean", - "description": "When true, a case can be resolved with incomplete checklist items." - }, - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "label": { - "type": "string", - "maxLength": 255 - }, - "order": { - "type": "integer" - } - } - } - } - } - }, - "aml_control": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "financial": { - "type": "boolean" - }, - "identity": { - "type": "boolean" - } - } - }, - "identity": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "personal_information": { - "type": "boolean" - }, - "applicant_tags": { - "type": "boolean" - }, - "contact_information": { - "type": "boolean" - }, - "risk_overview": { - "type": "boolean" - }, - "poi_documents": { - "type": "boolean" - }, - "poa_documents": { - "type": "boolean" - } - } - }, - "verifications": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "summary": { - "type": "boolean" - } - } - }, - "reporting": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "fiu_reports": { - "type": "boolean" - } - } - }, - "financial": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "payment_methods": { - "type": "boolean" - }, - "transactions": { - "type": "boolean" - } - } - } - } - }, - "creation_sources": { + "files": { "type": "array", - "description": "Connectors currently routing new cases into this blueprint.", "items": { "type": "object", "properties": { - "kind": { + "uuid": { "type": "string", - "enum": [ - "aml", - "rule", - "workflow" - ] + "format": "uuid" }, - "id": { - "type": "string" + "file_url": { + "type": "string", + "nullable": true, + "description": "Time-limited download URL." }, - "name": { - "type": "string" - } - } - } - }, - "receives_escalation_from": { - "type": "array", - "description": "Other blueprints whose Escalation target points at this one.", - "items": { - "type": "object", - "properties": { - "id": { + "original_filename": { "type": "string" }, - "name": { - "type": "string" + "file_size": { + "type": "integer" } } } - }, - "updated_at": { - "type": "string", - "format": "date-time" - }, - "version": { - "type": "integer", - "readOnly": true - }, - "default_deadline_hours": { - "type": "integer", - "nullable": true, - "minimum": 1, - "maximum": 8760 } } }, - "CaseListItem": { + "TravelRuleMessage": { "type": "object", "properties": { - "uuid": { + "id": { "type": "string", "format": "uuid" }, - "case_number": { - "type": "string", - "nullable": true, - "description": "Auto-assigned, e.g. CS-0001." - }, - "title": { - "type": "string" - }, - "status": { + "kind": { "type": "string", "enum": [ - "OPEN", - "AWAITING_USER", - "RESOLVED" + "MESSAGE", + "DOCUMENT_REQUEST" ] }, - "resolution": { + "direction": { "type": "string", "enum": [ - "FALSE_POSITIVE", - "VALID_THREAT" + "INBOUND", + "OUTBOUND" ], - "nullable": true + "description": "`OUTBOUND` is what the reader's own side sent. In the pickup portal the direction is from the counterparty's side." }, - "priority": { + "author": { "type": "string", - "enum": [ - "LOW", - "MEDIUM", - "HIGH" - ] + "description": "Who wrote it on its own side: a console user's email or an API key label, or the other VASP's name." }, - "source": { - "type": "string", - "enum": [ - "MANUAL", - "AML_SCREENING", - "TRANSACTION_RULE", - "WORKFLOW_BUILDER" - ] + "body": { + "type": "string" }, - "tags": { + "requested_documents": { "type": "array", "items": { "type": "string" - } - }, - "subject": { - "$ref": "#/components/schemas/CaseSubject" - }, - "assigned_to": { - "$ref": "#/components/schemas/CaseAssignee" + }, + "description": "What a DOCUMENT_REQUEST asks for, in the requester's words." }, - "assigned_at": { + "reply_to": { "type": "string", - "format": "date-time", + "format": "uuid", "nullable": true }, - "due_at": { + "fulfilled_at": { "type": "string", "format": "date-time", - "nullable": true + "nullable": true, + "description": "Set on a DOCUMENT_REQUEST once a reply to it carries a document." }, - "resolved_at": { - "type": "string", - "format": "date-time", - "nullable": true + "attachment": { + "$ref": "#/components/schemas/TravelRuleMessageAttachment" }, - "created_by_email": { + "created_at": { "type": "string", - "nullable": true - }, - "created_by_name": { + "format": "date-time" + } + } + }, + "TravelRuleMessageCreateRequest": { + "type": "object", + "description": "Send as JSON, or as multipart/form-data with the attachments in `files`.", + "properties": { + "kind": { "type": "string", - "nullable": true + "enum": [ + "MESSAGE", + "DOCUMENT_REQUEST" + ], + "default": "MESSAGE" }, - "blueprint": { - "$ref": "#/components/schemas/CaseBlueprintSummary" + "body": { + "type": "string", + "maxLength": 5000, + "description": "Required for a MESSAGE unless a file is attached." }, - "links_count": { - "type": "integer" + "requested_documents": { + "type": "array", + "items": { + "type": "string", + "maxLength": 255 + }, + "maxItems": 10, + "description": "Required (at least one) for a DOCUMENT_REQUEST, and only allowed there." }, - "notes_count": { - "type": "integer" + "reply_to": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "A message of the same transfer. A reply that carries a document fulfils the document request it answers." }, - "created_at": { + "files": { + "type": "array", + "items": { + "type": "string", + "format": "binary" + }, + "maxItems": 5, + "description": "Multipart only. Up to 5 files of 10 MB each." + } + } + }, + "SessionDeletionJob": { + "type": "object", + "description": "A bulk session deletion running in the background: progress and counts per outcome, never which sessions it deleted.", + "properties": { + "job_id": { "type": "string", - "format": "date-time" + "format": "uuid", + "description": "Poll `GET /v3/sessions/delete/{job_id}/` with this id." }, - "updated_at": { + "status": { "type": "string", - "format": "date-time" + "enum": [ + "PENDING", + "PROCESSING", + "COMPLETED", + "FAILED" + ], + "description": "`PENDING`: accepted, not started yet. `PROCESSING`: deleting; `processed` and `outcomes` are updating. `COMPLETED`: every selected session is accounted for. `FAILED`: the job stopped early; see `error`." }, - "application_id": { + "total": { + "type": "integer", + "description": "Sessions selected when the deletion was requested." + }, + "processed": { + "type": "integer", + "description": "Sessions the job has finished with so far." + }, + "outcomes": { + "type": "object", + "description": "How many sessions ended in each outcome. Every key is always present.", + "properties": { + "deleted": { + "type": "integer", + "description": "Sessions deleted together with their face embedding." + }, + "none": { + "type": "integer", + "description": "Sessions deleted that had no face embedding." + }, + "retained_with_user": { + "type": "integer", + "description": "Sessions deleted while one image-free biometric template was retained for their User, as the request or the application policy asked." + }, + "ineligible_no_vendor_user": { + "type": "integer", + "description": "Sessions deleted, with their face embedding, because retention was requested but they had no linked User." + }, + "failed_retryable": { + "type": "integer", + "description": "Sessions reported as not deleted. A job retries a group it could not delete by itself instead of counting it here." + }, + "already_deleted": { + "type": "integer", + "description": "Selected sessions that were already deleted, for example by another request, when the job reached them." + } + } + }, + "deleted_child_verifications": { + "type": "integer", + "description": "Verifications that the deleted business verifications created, deleted with them. Always `0` for a job started by `POST /v3/sessions/delete/`, which deletes KYC sessions only." + }, + "error": { "type": "string", - "format": "uuid" + "nullable": true, + "description": "`null` unless `status` is `FAILED`; then why the deletion stopped. The sessions the job had not reached are still in place: repeat the request to delete them." }, - "application_name": { - "type": "string" + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the deletion was requested." }, - "organization_id": { + "completed_at": { "type": "string", - "format": "uuid" + "format": "date-time", + "nullable": true, + "description": "When the job finished; `null` while it runs." + } + } + }, + "EntityStatusUpdateRequest": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "ACTIVE", + "FLAGGED", + "BLOCKED" + ], + "description": "New lifecycle status. `BLOCKED` also adds the `vendor_data` to the system blocklist." + } + } + }, + "AMLMonitoringToggleResult": { + "oneOf": [ + { + "type": "object", + "required": [ + "users_total", + "already_enabled", + "with_existing_aml", + "needing_screening", + "skipped_no_identity_data", + "skipped_blocked", + "estimated_screening_cost_usd", + "estimated_annual_monitoring_cost_usd", + "balance_sufficient", + "unmatched" + ], + "description": "Cost preview. Returned directly when estimate_only is true (nothing is written).", + "properties": { + "users_total": { + "type": "integer", + "description": "Profiles matched by the selection." + }, + "already_enabled": { + "type": "integer", + "description": "Profiles already monitored (nothing to do when enabling; these are what disabling turns off)." + }, + "with_existing_aml": { + "type": "integer", + "description": "Profiles whose existing AML screening will be switched to monitored (no new screening)." + }, + "needing_screening": { + "type": "integer", + "description": "Profiles without any AML screening that will be screened first (billed as a regular AML screening)." + }, + "skipped_no_identity_data": { + "type": "integer", + "description": "Profiles that cannot be screened because identity data is missing." + }, + "skipped_blocked": { + "type": "integer", + "description": "Blocked users that will be skipped (always 0 for businesses)." + }, + "estimated_screening_cost_usd": { + "type": "number", + "example": 0.2, + "description": "One-time cost of the new screenings." + }, + "estimated_annual_monitoring_cost_usd": { + "type": "number", + "example": 0.07, + "description": "Yearly monitoring cost of the profiles that will become monitored." + }, + "balance_sufficient": { + "type": "boolean", + "description": "Whether the organization balance covers the screening plus the first year of monitoring." + }, + "unmatched": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Identifiers of the request that matched no profile in this application, exactly as sent." + } + } }, - "organization_name": { - "type": "string" + { + "type": "object", + "required": [ + "job_id", + "status", + "is_enabled", + "target_type", + "total", + "processed", + "outcomes", + "results", + "error", + "created_at", + "completed_at", + "estimate", + "unmatched" + ], + "properties": { + "job_id": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string", + "enum": [ + "PENDING", + "PROCESSING", + "COMPLETED", + "FAILED" + ] + }, + "is_enabled": { + "type": "boolean", + "description": "true when the job enables monitoring, false when it disables it." + }, + "target_type": { + "type": "string", + "enum": [ + "user", + "business" + ], + "description": "Which profile family the job toggles." + }, + "total": { + "type": "integer", + "description": "Profiles the job processes." + }, + "processed": { + "type": "integer", + "description": "Profiles processed so far." + }, + "outcomes": { + "type": "object", + "description": "Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.", + "properties": { + "enabled": { + "type": "integer", + "description": "Monitoring enabled on an existing AML screening." + }, + "screened_and_enabled": { + "type": "integer", + "description": "A new AML screening was created (billed) and monitoring enabled on it." + }, + "already_enabled": { + "type": "integer", + "description": "Monitoring was already active; nothing changed and nothing was billed." + }, + "disabled": { + "type": "integer", + "description": "Monitoring turned off across every AML screening of the profile." + }, + "skipped_no_identity_data": { + "type": "integer", + "description": "Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen." + }, + "skipped_blocked": { + "type": "integer", + "description": "Skipped: the user is blocked." + }, + "skipped_sandbox": { + "type": "integer", + "description": "Skipped: sandbox application, no screening is created." + }, + "failed_provider": { + "type": "integer", + "description": "The screening could not be completed; retry later." + }, + "failed_insufficient_balance": { + "type": "integer", + "description": "Balance ran out while the job was running." + } + } + }, + "results": { + "type": "array", + "description": "One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API.", + "items": { + "type": "object", + "properties": { + "didit_internal_id": { + "type": "string", + "format": "uuid", + "description": "Didit internal id of the profile." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your own identifier for the profile." + }, + "outcome": { + "type": "string", + "enum": [ + "enabled", + "screened_and_enabled", + "already_enabled", + "disabled", + "skipped_no_identity_data", + "skipped_blocked", + "skipped_sandbox", + "failed_provider", + "failed_insufficient_balance" + ] + } + } + } + }, + "error": { + "type": "string", + "nullable": true, + "description": "Generic failure message when status is FAILED; the detailed cause is kept on the platform side." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "completed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "estimate": { + "type": "object", + "description": "Cost preview. Returned directly when estimate_only is true (nothing is written).", + "properties": { + "users_total": { + "type": "integer", + "description": "Profiles matched by the selection." + }, + "already_enabled": { + "type": "integer", + "description": "Profiles already monitored (nothing to do when enabling; these are what disabling turns off)." + }, + "with_existing_aml": { + "type": "integer", + "description": "Profiles whose existing AML screening will be switched to monitored (no new screening)." + }, + "needing_screening": { + "type": "integer", + "description": "Profiles without any AML screening that will be screened first (billed as a regular AML screening)." + }, + "skipped_no_identity_data": { + "type": "integer", + "description": "Profiles that cannot be screened because identity data is missing." + }, + "skipped_blocked": { + "type": "integer", + "description": "Blocked users that will be skipped (always 0 for businesses)." + }, + "estimated_screening_cost_usd": { + "type": "number", + "example": 0.2, + "description": "One-time cost of the new screenings." + }, + "estimated_annual_monitoring_cost_usd": { + "type": "number", + "example": 0.07, + "description": "Yearly monitoring cost of the profiles that will become monitored." + }, + "balance_sufficient": { + "type": "boolean", + "description": "Whether the organization balance covers the screening plus the first year of monitoring." + } + } + }, + "unmatched": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Identifiers of the request that matched no profile in this application, exactly as sent. They are not part of the job." + } + } } - } + ] }, - "CaseDetail": { + "AMLMonitoringJob": { "type": "object", "properties": { - "uuid": { + "job_id": { "type": "string", "format": "uuid" }, - "case_number": { - "type": "string", - "nullable": true - }, - "title": { - "type": "string" - }, "status": { "type": "string", "enum": [ - "OPEN", - "AWAITING_USER", - "RESOLVED" + "PENDING", + "PROCESSING", + "COMPLETED", + "FAILED" ] }, - "resolution": { + "is_enabled": { + "type": "boolean", + "description": "true when the job enables monitoring, false when it disables it." + }, + "target_type": { "type": "string", "enum": [ - "FALSE_POSITIVE", - "VALID_THREAT" + "user", + "business" ], - "nullable": true + "description": "Which profile family the job toggles." }, - "resolution_notes": { - "type": "string", - "nullable": true + "total": { + "type": "integer", + "description": "Profiles the job processes." }, - "priority": { - "type": "string", - "enum": [ - "LOW", - "MEDIUM", - "HIGH" - ] + "processed": { + "type": "integer", + "description": "Profiles processed so far." }, - "source": { - "type": "string", - "enum": [ - "MANUAL", - "AML_SCREENING", - "TRANSACTION_RULE", - "WORKFLOW_BUILDER" - ] + "outcomes": { + "type": "object", + "description": "Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.", + "properties": { + "enabled": { + "type": "integer", + "description": "Monitoring enabled on an existing AML screening." + }, + "screened_and_enabled": { + "type": "integer", + "description": "A new AML screening was created (billed) and monitoring enabled on it." + }, + "already_enabled": { + "type": "integer", + "description": "Monitoring was already active; nothing changed and nothing was billed." + }, + "disabled": { + "type": "integer", + "description": "Monitoring turned off across every AML screening of the profile." + }, + "skipped_no_identity_data": { + "type": "integer", + "description": "Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen." + }, + "skipped_blocked": { + "type": "integer", + "description": "Skipped: the user is blocked." + }, + "skipped_sandbox": { + "type": "integer", + "description": "Skipped: sandbox application, no screening is created." + }, + "failed_provider": { + "type": "integer", + "description": "The screening could not be completed; retry later." + }, + "failed_insufficient_balance": { + "type": "integer", + "description": "Balance ran out while the job was running." + } + } }, - "tags": { + "results": { "type": "array", + "description": "One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API.", "items": { - "type": "string" + "type": "object", + "properties": { + "didit_internal_id": { + "type": "string", + "format": "uuid", + "description": "Didit internal id of the profile." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your own identifier for the profile." + }, + "outcome": { + "type": "string", + "enum": [ + "enabled", + "screened_and_enabled", + "already_enabled", + "disabled", + "skipped_no_identity_data", + "skipped_blocked", + "skipped_sandbox", + "failed_provider", + "failed_insufficient_balance" + ] + } + } } }, - "subject": { - "$ref": "#/components/schemas/CaseSubject" - }, - "assigned_to": { - "$ref": "#/components/schemas/CaseAssignee" - }, - "assigned_at": { + "error": { "type": "string", - "format": "date-time", - "nullable": true + "nullable": true, + "description": "Generic failure message when status is FAILED; the detailed cause is kept on the platform side." }, - "due_at": { + "created_at": { "type": "string", - "format": "date-time", - "nullable": true + "format": "date-time" }, - "resolved_at": { + "completed_at": { "type": "string", "format": "date-time", "nullable": true }, - "resolved_by_email": { - "type": "string", - "nullable": true - }, - "pending_resolution": { + "estimate": { "type": "object", - "nullable": true, - "description": "Set only while a 4-eyes resolution is staged on the checker blueprint, awaiting approve or reject-approval.", + "description": "Cost preview. Returned directly when estimate_only is true (nothing is written).", "properties": { - "resolution": { - "type": "string", - "enum": [ - "FALSE_POSITIVE", - "VALID_THREAT" - ] + "users_total": { + "type": "integer", + "description": "Profiles matched by the selection." }, - "resolution_notes": { - "type": "string", - "nullable": true + "already_enabled": { + "type": "integer", + "description": "Profiles already monitored (nothing to do when enabling; these are what disabling turns off)." }, - "maker_email": { - "type": "string", - "format": "email" + "with_existing_aml": { + "type": "integer", + "description": "Profiles whose existing AML screening will be switched to monitored (no new screening)." }, - "maker_name": { - "type": "string", - "nullable": true + "needing_screening": { + "type": "integer", + "description": "Profiles without any AML screening that will be screened first (billed as a regular AML screening)." }, - "maker_blueprint_id": { - "type": "string", - "format": "uuid" + "skipped_no_identity_data": { + "type": "integer", + "description": "Profiles that cannot be screened because identity data is missing." }, - "staged_at": { - "type": "string", - "format": "date-time" + "skipped_blocked": { + "type": "integer", + "description": "Blocked users that will be skipped (always 0 for businesses)." + }, + "estimated_screening_cost_usd": { + "type": "number", + "example": 0.2, + "description": "One-time cost of the new screenings." + }, + "estimated_annual_monitoring_cost_usd": { + "type": "number", + "example": 0.07, + "description": "Yearly monitoring cost of the profiles that will become monitored." + }, + "balance_sufficient": { + "type": "boolean", + "description": "Whether the organization balance covers the screening plus the first year of monitoring." } } }, - "metadata": { - "type": "object" - }, - "blueprint": { - "$ref": "#/components/schemas/CaseBlueprintDetail" - }, - "notes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaseNote" - }, - "description": "Newest 50 notes." - }, - "events": { + "unmatched": { "type": "array", "items": { - "$ref": "#/components/schemas/CaseEvent" + "type": "string" }, - "description": "Newest 100 events." + "description": "Identifiers of the request that matched no profile in this application, exactly as sent. They are not part of the job." + } + } + }, + "OngoingMonitoringToggleRequest": { + "type": "object", + "required": [ + "is_enabled" + ], + "properties": { + "is_enabled": { + "type": "boolean", + "description": "true to enable monitoring, false to disable it." }, - "links": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaseLink" - } + "estimate_only": { + "type": "boolean", + "default": false, + "description": "When true, returns the cost estimate and performs no writes." }, - "links_count": { - "type": "integer" + "update_all": { + "type": "boolean", + "default": false, + "description": "Apply to every entity of the application (scoped by the filter_* params when present)." }, - "notes_count": { - "type": "integer" + "filter_search": { + "type": "string" }, - "checklist_items": { + "filter_status": { "type": "array", "items": { - "$ref": "#/components/schemas/CaseChecklistItem" + "type": "string" } }, - "created_at": { + "filter_country": { + "type": "string" + }, + "filter_date_from": { "type": "string", - "format": "date-time" + "format": "date" }, - "updated_at": { + "filter_date_to": { "type": "string", - "format": "date-time" + "format": "date" + }, + "didit_internal_id_list": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "description": "Entities to toggle. Required unless update_all is true." } } }, - "ReportTemplateDetail": { + "OngoingMonitoringJob": { "type": "object", "properties": { - "uuid": { + "job_id": { "type": "string", "format": "uuid" }, - "name": { - "type": "string" - }, - "jurisdiction": { - "type": "string", - "minLength": 2, - "maxLength": 2, - "description": "ISO 3166-1 alpha-2 country code." - }, - "report_type": { + "status": { "type": "string", "enum": [ - "SAR", - "STR", - "CTR", - "TTR", - "UTR", - "IFT", - "CBR", - "TFR" + "PENDING", + "PROCESSING", + "COMPLETED", + "FAILED" ] }, - "report_type_display": { - "type": "string" + "is_enabled": { + "type": "boolean" }, - "field_config": { + "total": { + "type": "integer" + }, + "processed": { + "type": "integer" + }, + "outcomes": { "type": "object", - "description": "Reusable goAML header defaults. Template-level obligatory fields: rentity_id, submission_code, and currency_code_local (3-letter). Optional: enabled_fields (which optional report blocks to include) plus reporting_person, location, reason, and action defaults. Per-filing header fields (action, submission_date, transactions, jurisdiction) are enforced when the report is validated or finalized, not on the template." + "description": "Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.", + "properties": { + "enabled": { + "type": "integer", + "description": "Monitoring enabled on an existing AML screening." + }, + "screened_and_enabled": { + "type": "integer", + "description": "A new AML screening was created (billed) and monitoring enabled." + }, + "already_enabled": { + "type": "integer", + "description": "Monitoring was already active (legacy duplicate rows are consolidated)." + }, + "disabled": { + "type": "integer", + "description": "Monitoring disabled on every AML row of the entity." + }, + "skipped_no_identity_data": { + "type": "integer", + "description": "Skipped: no full name + date of birth + country available to screen." + }, + "skipped_blocked": { + "type": "integer", + "description": "Skipped: the user is blocked." + }, + "skipped_sandbox": { + "type": "integer", + "description": "Skipped: sandbox application." + }, + "failed_provider": { + "type": "integer", + "description": "The screening provider call failed." + }, + "failed_insufficient_balance": { + "type": "integer", + "description": "Balance ran out mid-job." + } + } }, - "created_at": { + "error": { "type": "string", - "format": "date-time" + "nullable": true }, - "updated_at": { - "type": "string", - "format": "date-time" + "estimate": { + "type": "object", + "description": "Cost preview. Returned directly when estimate_only is true (no writes are performed).", + "properties": { + "users_total": { + "type": "integer", + "description": "Entities in the selection." + }, + "already_enabled": { + "type": "integer" + }, + "with_existing_aml": { + "type": "integer", + "description": "Entities whose existing AML screening will be switched to monitored." + }, + "needing_screening": { + "type": "integer", + "description": "Entities without any AML screening that will be screened first (billed)." + }, + "skipped_no_identity_data": { + "type": "integer" + }, + "skipped_blocked": { + "type": "integer" + }, + "estimated_screening_cost_usd": { + "type": "number", + "example": 0.2 + }, + "estimated_annual_monitoring_cost_usd": { + "type": "number", + "example": 0.07 + }, + "balance_sufficient": { + "type": "boolean" + } + } } } }, - "RegulatoryReportDetail": { + "OngoingMonitoringToggleResult": { + "oneOf": [ + { + "type": "object", + "description": "Cost preview. Returned directly when estimate_only is true (no writes are performed).", + "properties": { + "users_total": { + "type": "integer", + "description": "Entities in the selection." + }, + "already_enabled": { + "type": "integer" + }, + "with_existing_aml": { + "type": "integer", + "description": "Entities whose existing AML screening will be switched to monitored." + }, + "needing_screening": { + "type": "integer", + "description": "Entities without any AML screening that will be screened first (billed)." + }, + "skipped_no_identity_data": { + "type": "integer" + }, + "skipped_blocked": { + "type": "integer" + }, + "estimated_screening_cost_usd": { + "type": "number", + "example": 0.2 + }, + "estimated_annual_monitoring_cost_usd": { + "type": "number", + "example": 0.07 + }, + "balance_sufficient": { + "type": "boolean" + } + } + }, + { + "$ref": "#/components/schemas/OngoingMonitoringJob" + } + ] + }, + "CaseBlueprintPresetInstallResult": { "type": "object", "properties": { - "uuid": { - "type": "string", - "format": "uuid" + "created": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "CaseBulkUpdateResult": { + "type": "object", + "properties": { + "updated": { + "type": "integer" }, - "case_uuid": { - "type": "string", - "format": "uuid" + "failures": { + "type": "array", + "items": { + "type": "object", + "properties": { + "case_uuid": { + "type": "string", + "format": "uuid" + }, + "detail": { + "type": "string" + } + } + } + } + } + }, + "RegulatoryReportList": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "description": "Number of matching rows, capped at 100 for performance." }, - "case_number": { + "next": { "type": "string", "nullable": true }, - "report_type": { - "type": "string", - "enum": [ - "SAR", - "STR", - "CTR", - "TTR", - "UTR", - "IFT", - "CBR", - "TFR" - ] - }, - "report_type_display": { - "type": "string" - }, - "report_code": { - "type": "string", - "description": "goAML report code derived from report_type, e.g. STR, CTR, BCR." - }, - "report_number": { + "previous": { "type": "string", "nullable": true }, - "status": { - "type": "string", - "enum": [ - "DRAFT", - "VALIDATED", - "FINALIZED" - ] - }, - "status_display": { - "type": "string" - }, - "jurisdiction": { - "type": "string", - "minLength": 2, - "maxLength": 2 - }, - "rentity_id": { - "type": "string", - "nullable": true, - "description": "Reporting entity ID issued by the FIU." - }, - "rentity_branch": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RegulatoryReportDetail" + } + } + } + }, + "EntityNetworkMembershipList": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityNetworkMembership" + } + } + } + }, + "SessionDecision": { + "type": "object", + "properties": { + "session_id": { "type": "string", - "nullable": true + "format": "uuid", + "description": "The unique identifier of the session." }, - "submission_code": { + "session_kind": { "type": "string", "enum": [ - "E", - "M" - ] + "user", + "business" + ], + "description": "Discriminator indicating whether this is a User Verification (KYC) or Business Verification (KYB) session. When `user`, the KYC feature arrays (`id_verifications`, `nfc_verifications`, `liveness_checks`, `face_matches`, `poa_verifications`, `document_ai_documents`, `database_validations`, ...) apply and the business arrays are absent. When `business`, the payload instead carries `registry_checks`, `document_verifications`, `key_people_checks` plus the shared arrays (`aml_screenings`, `phone_verifications`, `email_verifications`, `ip_analyses`, `questionnaire_responses`). **Omitted when `include=events` is passed** — the events shape only exists for user sessions, so the discriminator is dropped from that variant." }, - "entity_reference": { + "shared_from_session": { "type": "string", - "nullable": true + "format": "uuid", + "nullable": true, + "description": "For a session cloned with `POST /v3/session/import-shared/`: the `session_id` of the original session in the source application. `null` for every other session. Omitted when `include=events` is passed." }, - "fiu_ref_number": { - "type": "string", + "session_number": { + "type": "integer", + "description": "The number of the session.", "nullable": true }, - "currency_code_local": { + "session_url": { "type": "string", - "minLength": 3, - "maxLength": 3, + "format": "uri", + "description": "The URL of the session.", "nullable": true }, - "submission_date": { + "status": { "type": "string", - "format": "date-time", - "nullable": true - }, - "reporting_person": { - "type": "object" - }, - "location": { - "type": "object" + "enum": [ + "Not Started", + "In Progress", + "Awaiting User", + "In Review", + "Approved", + "Declined", + "Resubmitted", + "Expired", + "Kyc Expired", + "Abandoned" + ], + "description": "Overall lifecycle status of the session — the rolled-up workflow result. Terminal values are `Approved`, `Declined`, `In Review`, `Expired`, `Kyc Expired`, and `Abandoned`; `Not Started`, `In Progress`, `Awaiting User`, and `Resubmitted` indicate the session is still moving through the workflow." }, - "reason": { + "workflow_id": { "type": "string", + "format": "uuid", + "description": "Stable identifier of the workflow group that produced this session. Workflows are versioned — this value identifies the workflow itself, not the specific version.", "nullable": true }, - "action": { + "features": { + "type": "array", + "items": { + "type": "string", + "description": "The feature of the session." + }, + "description": "List of verification features configured for this session, as display names: `ID_VERIFICATION`, `NFC`, `LIVENESS`, `FACE_MATCH`, `POA`, `PHONE`, `EMAIL_VERIFICATION`, `AML`, `IP_ANALYSIS`, `DATABASE_VALIDATION`, `QUESTIONNAIRE`, `AGE_ESTIMATION`, `FACE_SEARCH` (Business Verification sessions use the KYB feature set: `KYB_REGISTRY`, `KYB_DOCUMENTS`, `KYB_KEY_PEOPLE`, `AML`, `PHONE`, `EMAIL_VERIFICATION`, `IP_ANALYSIS`, `QUESTIONNAIRE`). With `include=events` each entry becomes an object `{feature, node_id}` instead of a plain string, and the email feature is renamed: it appears as `EMAIL` in events mode instead of the `EMAIL_VERIFICATION` used here." + }, + "vendor_data": { "type": "string", + "description": "The vendor data of the session.", "nullable": true }, - "activity": { - "type": "string", + "metadata": { + "type": "object", + "description": "The metadata of the session.", "nullable": true }, - "narrative": { + "expected_details": { "type": "object", - "description": "Five Ws narrative sections.", + "description": "Expected details supplied at session creation — used for cross-validation against extracted data (KYC) or to pre-fill the company registry search (KYB). Echoes back exactly the keys that were provided; `null` when none were supplied. KYC sessions carry the person fields; Business (KYB) sessions carry `company_name`, `registry_country` and `registration_number`.", "properties": { - "who": { - "type": "string" + "first_name": { + "type": "string", + "description": "User's first name. For example, `John`." }, - "what": { - "type": "string" + "last_name": { + "type": "string", + "description": "User's last name. For example, `Doe`." }, - "when": { - "type": "string" + "date_of_birth": { + "type": "string", + "format": "date", + "description": "User's date of birth with format: YYYY-MM-DD. For example, `1990-05-15`." }, - "where": { - "type": "string" + "gender": { + "type": "string", + "nullable": true, + "enum": [ + "M", + "F" + ], + "default": null, + "description": "User's gender. Must be either 'M', 'F', or null." }, - "why": { - "type": "string" + "nationality": { + "type": "string", + "description": "ISO 3166-1 alpha-3 country code representing the applicant's country of origin. For example, `USA`." }, - "how": { - "type": "string" + "country": { + "type": "string", + "description": "ISO 3166-1 alpha-3 country code representing the country of the Proof of Address document, or the country of the applicant's ID document, which may differ from nationality. For example, `GBR`." + }, + "address": { + "type": "string", + "description": "The address in a human readable format, including as much information as possible. For example, `123 Main St, San Francisco, CA 94105, USA`." + }, + "identification_number": { + "type": "string", + "description": "The user's document number, personal number, or tax number. For example, `123456789`." + }, + "ip_address": { + "type": "string", + "format": "ipv4", + "description": "Expected IP address for the session. If the actual IP address differs from this value, a warning will be logged. For example, `192.168.1.100`." + }, + "expected_document_types": { + "type": "array", + "description": "Document types the ID verification step was restricted to at session creation. Empty/unset means no restriction. Codes: `P` (passport), `ID` (national ID), `DL` (driver's license), `RP` (residence permit), `HIC` (health insurance card), `TC` (tax card), `SSC` (social security card).", + "items": { + "type": "string", + "enum": [ + "P", + "ID", + "DL", + "RP", + "HIC", + "TC", + "SSC" + ] + } + }, + "id_country": { + "type": "string", + "nullable": true, + "description": "Expected ID-document issuing country (ISO 3166-1 alpha-3), when restricted at creation time.", + "example": "ESP" + }, + "poa_country": { + "type": "string", + "nullable": true, + "description": "Expected Proof of Address document country (ISO 3166-1 alpha-3), when restricted at creation time.", + "example": "ESP" + }, + "company_name": { + "type": "string", + "description": "Expected legal name of the business. Business (KYB) sessions only." + }, + "registry_country": { + "type": "string", + "description": "Expected registry country as `XX` or `XX-YY` (ISO 3166-1 alpha-2, optionally with an ISO 3166-2 subdivision for state-level registries, e.g. `US-CA`). Business (KYB) sessions only." + }, + "registration_number": { + "type": "string", + "description": "Expected company registration number. Business (KYB) sessions only." } - } - }, - "report_indicators": { - "type": "array", - "items": { - "type": "string" - } + }, + "nullable": true }, - "included_fields": { - "type": "array", - "items": { - "type": "string" - } + "contact_details": { + "type": "object", + "description": "Contact details supplied when the session was created (used for notification emails, prefilled phone/email verification, and communication language). `null` when neither an email nor a phone number was provided.", + "properties": { + "email": { + "type": "string", + "description": "Email address of the user (e.g., \"john.doe@example.com\") that will be used during the Email Verification step. If not provided, the user must provide it during the verification flow." + }, + "email_lang": { + "type": "string", + "description": "Language code (ISO 639-1) for email notifications. Controls the language of all email communications (e.g., \"en\", \"es\", \"fr\")." + }, + "send_notification_emails": { + "type": "boolean", + "default": false, + "description": "If true and an email is provided, Didit sends the initial \"Verify your identity\" email asynchronously when the User Verification (KYC) or Business Verification (KYB) session is created. Didit also sends verification status notifications for sessions requiring manual review to the provided email address." + }, + "phone": { + "type": "string", + "nullable": true, + "description": "Phone number of the user in E.164 format if available (e.g., +14155552671). `null` when only an email was supplied at session creation. Note: This field returns the original phone number provided during session creation. If the number was invalid, it may not be in E.164 format." + } + }, + "nullable": true }, - "transactions": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - } + "callback": { + "type": "string", + "format": "uri", + "description": "The callback URL of the session.", + "nullable": true }, - "validation_errors": { + "id_verifications": { "type": "array", - "description": "Result of the most recent validation; each entry is a {field, message} object.", + "nullable": true, + "description": "**Array of ID verification (OCR) reports** produced by ID-document steps in the workflow graph. Always a JSON array — read `id_verifications[0]` and iterate if your workflow can run more than one ID check (for example a primary check plus a step-up). `null` until at least one ID step has produced data. Each item is the full report, including `node_id`, feature-level `status`, extracted document fields, image URLs, quality scores, MRZ, address parsing, cross-session matches, and `warnings[]`.", "items": { "type": "object", + "description": "One ID verification report - the full report for a single ID Verification step. `verification_method` says which of the three methods produced it (document capture, non-doc lookup or wallet sign-in) and the method-specific evidence block is populated accordingly.", "properties": { - "field": { - "type": "string" + "node_id": { + "type": "string", + "description": "Workflow graph node id of the OCR step that produced this report." + }, + "verification_method": { + "type": "string", + "enum": [ + "document", + "id_lookup", + "wallet" + ], + "description": "Which ID Verification method produced this report. `document` - the user photographed a physical ID (the default, and the value on every session created before the other methods existed). `id_lookup` - the user typed their national ID number and it was checked against a government register or another authoritative source for that country (`id_lookup.source` names which). `wallet` - the user signed in with a government or bank digital identity." + }, + "assurance": { + "type": "string", + "enum": [ + "documentary", + "data_match", + "cryptographic" + ], + "description": "Assurance tier derived from `verification_method`: `documentary` for a document that was captured and checked, `data_match` for details that matched a record held by the source queried, `cryptographic` for attributes an identity provider authenticated and signed." + }, + "wallet_provider": { + "type": "string", + "nullable": true, + "description": "Catalog wallet id when `verification_method` is `wallet` (`mitid`, `bankid_se`, `itsme`, ...). `null` otherwise." + }, + "fallback_from": { + "type": "object", + "nullable": true, + "description": "Why a non-document method ended the ID step. Set on the declined report when a lookup or a wallet sign-in failed and the country's fallback switch was `decline`. `null` otherwise.", + "properties": { + "method": { + "type": "string", + "enum": [ + "id_lookup", + "wallet" + ], + "description": "The method that failed." + }, + "reason": { + "type": "string", + "description": "The outcome that ended it, e.g. `no_match`, `provider_error`, `cancelled`, `timeout`." + }, + "action": { + "type": "string", + "description": "The configured action that was taken, e.g. `decline`." + } + } + }, + "id_lookup": { + "type": "object", + "nullable": true, + "description": "Registry evidence. Present only when `verification_method` is `id_lookup`; `null` otherwise.", + "properties": { + "source": { + "type": "string", + "description": "Customer-facing name of the source queried, e.g. `Department of Home Affairs` (a government register) or `UK credit bureau and financial services records` (not a government register). This is the only statement of provenance for the lookup; do not assume a government source." + }, + "checked_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the register answered." + }, + "attempts": { + "type": "integer", + "description": "1-based attempt number this report came from." + }, + "max_attempts": { + "type": "integer", + "description": "Attempt budget configured for the country (1 to 5, default 1)." + }, + "outcome": { + "type": "string", + "enum": [ + "match", + "partial_match", + "no_match", + "provider_error", + "format_rejected" + ], + "nullable": true, + "description": "What the register answered. Only `match`, `partial_match` and `no_match` count against the attempt budget and bill." + }, + "comparison": { + "type": "array", + "description": "One row per comparable field the source answered on, plus one row per source error. Comparison rows use the normalised field names below rather than the country's request field keys, because several request fields (first and last name, or any of the identifier aliases) collapse into a single comparison. A source that failed appends a row with `field: \"provider\"` and `result: \"error\"`, so read `result` before treating a row as a field verdict.", + "items": { + "type": "object", + "required": [ + "field", + "label", + "provided", + "record", + "result" + ], + "properties": { + "field": { + "type": "string", + "enum": [ + "identification_number", + "full_name", + "date_of_birth", + "address", + "provider" + ], + "description": "Normalised comparison field. `provider` is not a user field: it marks a source error row." + }, + "label": { + "type": "string", + "description": "Display label for the row, e.g. `ID number`, `Full name`, `Provider`." + }, + "provided": { + "type": "string", + "nullable": true, + "description": "What the user typed. `null` on a `provider` error row." + }, + "record": { + "type": "string", + "nullable": true, + "description": "What the source held. On a `provider` error row this carries the error message instead." + }, + "result": { + "type": "string", + "enum": [ + "match", + "partial", + "no_match", + "error" + ], + "description": "Per-field comparison result. `error` appears only on a `provider` row and means that source did not answer; treat it as evidence about the query, never as a field mismatch." + } + } + } + }, + "registry_portrait": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Short-lived presigned URL to the portrait the source returned. Only the four government registers that return one (South Africa, Nigeria, Argentina, Panama)." + }, + "face_match_score": { + "type": "number", + "nullable": true, + "description": "Selfie against the registry portrait, 0-100. Only where the register returns a portrait." + } + } + }, + "wallet_verification": { + "type": "object", + "nullable": true, + "description": "Credential evidence. Present only when `verification_method` is `wallet`; `null` otherwise.", + "properties": { + "provider": { + "type": "string", + "description": "Catalog wallet id, e.g. `mitid`." + }, + "provider_name": { + "type": "string", + "nullable": true, + "description": "Wallet name as the end user knows it, e.g. `MitID`." + }, + "issuing_authority": { + "type": "string", + "nullable": true, + "description": "The authority behind the wallet, e.g. `Danish Agency for Digital Government`." + }, + "issuing_country": { + "type": "string", + "description": "ISO 3166-1 alpha-3 country the wallet was offered in." + }, + "credential_type": { + "type": "string", + "nullable": true, + "description": "Credential the wallet presented, e.g. `person_identification`." + }, + "level_of_assurance": { + "type": "string", + "enum": [ + "low", + "substantial", + "high" + ], + "nullable": true, + "description": "The level of assurance the wallet asserted. A sign-in that asserts less than the level requested fails instead of downgrading." + }, + "verified_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "The wallet's authentication instant." + }, + "signature_valid": { + "type": "boolean", + "nullable": true, + "description": "Whether the signed assertion the wallet returned validated." + }, + "attributes": { + "type": "object", + "additionalProperties": true, + "description": "The attributes the wallet shared, keyed as the wallet's catalog entry lists them." + }, + "portrait": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Short-lived presigned URL to the portrait the wallet shared, where it shares one." + }, + "face_match_score": { + "type": "number", + "nullable": true, + "description": "Selfie against the wallet portrait, 0-100, where the wallet shares one." + } + } + }, + "status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Expired" + ], + "description": "Feature-level status of this ID verification step. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`." + }, + "document_type": { + "type": "string", + "description": "Type of identity document" + }, + "document_subtype": { + "type": "string", + "nullable": true, + "description": "Accepted document subtype code from the workflow document settings, including the document region when applicable. Examples: `ID_CARD_GENERIC`, `QUEENSLAND_DRIVER_LICENSE_GENERIC`." + }, + "document_number": { + "type": "string", + "description": "Document identification number" + }, + "personal_number": { + "type": "string", + "description": "Personal identification number" + }, + "portrait_image": { + "type": "string", + "format": "uri", + "description": "URL of the portrait image" + }, + "front_image": { + "type": "string", + "format": "uri", + "description": "URL of the front document image" + }, + "front_video": { + "type": "string", + "format": "uri", + "description": "URL of the front document video" + }, + "back_image": { + "type": "string", + "format": "uri", + "description": "URL of the back document image" + }, + "back_video": { + "type": "string", + "format": "uri", + "description": "URL of the back document video" + }, + "full_front_image": { + "type": "string", + "format": "uri", + "description": "URL of the full front document image" + }, + "full_back_image": { + "type": "string", + "format": "uri", + "description": "URL of the full back document image" + }, + "front_image_camera_front": { + "type": "string", + "format": "uri", + "description": "URL of the front document image captured from the front-facing camera" + }, + "back_image_camera_front": { + "type": "string", + "format": "uri", + "description": "URL of the back document image captured from the front-facing camera" + }, + "date_of_birth": { + "type": "string", + "format": "date", + "description": "Date of birth from document" + }, + "age": { + "type": "integer", + "description": "Calculated age from date of birth" + }, + "expiration_date": { + "type": "string", + "format": "date", + "description": "Document expiration date" + }, + "date_of_issue": { + "type": "string", + "format": "date", + "description": "Document issue date" + }, + "issuing_state": { + "type": "string", + "description": "ISO code of issuing state" + }, + "issuing_state_name": { + "type": "string", + "description": "Full name of issuing state" + }, + "first_name": { + "type": "string", + "description": "First name from document" + }, + "last_name": { + "type": "string", + "description": "Last name from document" + }, + "full_name": { + "type": "string", + "description": "Full name from document" + }, + "gender": { + "type": "string", + "description": "Gender from document" + }, + "address": { + "type": "string", + "description": "Raw address from document" + }, + "formatted_address": { + "type": "string", + "description": "Formatted full address" + }, + "place_of_birth": { + "type": "string", + "description": "Place of birth from document" + }, + "marital_status": { + "type": "string", + "description": "Marital status from document" + }, + "nationality": { + "type": "string", + "description": "ISO nationality code" + }, + "extra_fields": { + "type": "object", + "description": "Additional document fields. Includes document-specific attributes (e.g. `dl_categories`, `blood_group`, `sponsor`, `employer`, `dl_class_code_b_from`, `dl_class_code_b_to`). Driver license class validity dates are returned as `dl_class_code__from` and `dl_class_code__to`, and per-class restriction/information codes (e.g. column 12 of UK licences) as `dl_class_code__notes`. When the document carries names, address, or place of birth in multiple scripts, this object also includes alternate-script versions of those fields. The workflow setting `preferred_characters` (`latin` or `non_latin`) controls which script populates the top-level fields (`first_name`, `last_name`, `full_name`, `address`, `place_of_birth`); the opposite script lands here as `first_name_non_latin` / `last_name_non_latin` / `full_name_non_latin` / `middle_name_non_latin` / `address_non_latin` / `place_of_birth_non_latin` (when `preferred_characters` is `latin`) or with a `_latin` suffix (when `preferred_characters` is `non_latin`). Keys appear only when the OCR actually extracts a real alternate-script value — a field whose alternate value is in the same script as the preferred set (for example, a Cyrillic-labelled field whose OCR value came back in Latin homoglyphs) is skipped. When `preferred_characters` is `latin` but the document only carries non-Latin text, Didit transliterates the extracted values and exposes `first_name_latin` / `last_name_latin` / etc. as a fallback.", + "additionalProperties": true, + "properties": { + "dl_categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "blood_group": { + "type": "string", + "nullable": true + }, + "first_name_non_latin": { + "type": "string", + "description": "First name in the non-Latin script when `preferred_characters=latin` and a non-Latin version is present on the document (e.g. `Айна` for a Kyrgyz ID)." + }, + "last_name_non_latin": { + "type": "string", + "description": "Surname in the non-Latin script when `preferred_characters=latin` and a non-Latin version is present on the document." + }, + "middle_name_non_latin": { + "type": "string", + "description": "Middle name / patronymic in the non-Latin script when `preferred_characters=latin` and a non-Latin version is present on the document." + }, + "full_name_non_latin": { + "type": "string", + "description": "Composed full name in the non-Latin script. Reconstructed from the individual components when both first and last names are available." + }, + "address_non_latin": { + "type": "string", + "description": "Address in the non-Latin script when `preferred_characters=latin` and a non-Latin version is present on the document." + }, + "place_of_birth_non_latin": { + "type": "string", + "description": "Place of birth in the non-Latin script when `preferred_characters=latin` and a non-Latin version is present on the document." + }, + "first_name_latin": { + "type": "string", + "description": "First name in Latin script. Populated when `preferred_characters=non_latin` and a Latin version is present on the document, or — as a transliteration fallback — when `preferred_characters=latin` but the document only carries non-Latin text." + }, + "last_name_latin": { + "type": "string", + "description": "Surname in Latin script. Same rules as `first_name_latin`." + }, + "middle_name_latin": { + "type": "string", + "description": "Middle name / patronymic in Latin script. Same rules as `first_name_latin`." + }, + "full_name_latin": { + "type": "string", + "description": "Composed full name in Latin script. Always reconstructed from the individual components when both first and last names are available." + }, + "address_latin": { + "type": "string", + "description": "Address in Latin script. Same rules as `first_name_latin`." + }, + "place_of_birth_latin": { + "type": "string", + "description": "Place of birth in Latin script. Same rules as `first_name_latin`." + } + } + }, + "mrz": { + "type": "object", + "nullable": true, + "description": "Parsed Machine-Readable Zone (MRZ) fields extracted from the document. `null` if no MRZ was detected.", + "properties": { + "surname": { + "type": "string", + "example": "MARTINEZ for any key submitted in custom_properties on transaction create. Country-code fields accept ISO-2 or ISO-3; both sides are normalized to ISO-3 before comparison." - }, - "operator": { - "type": "string", - "enum": [ - "eq", - "ne", - "gt", - "gte", - "lt", - "lte", - "in", - "not_in", - "contains", - "not_contains", - "contains_any", - "regex", - "fuzzy_match", - "exists", - "is_not_empty", - "is_not_null", - "not_exists", - "is_empty", - "is_null" - ], - "description": "Comparison operator. in/not_in test membership (value may be a list or a single scalar). contains/not_contains are case-insensitive substring checks against a string value. contains_any takes a list of strings and matches when any is a case-insensitive substring of the field value. regex matches the value pattern against the field. fuzzy_match compares against the value string with the required `score` threshold (0-100). gt/gte/lt/lte never match when either side is missing. exists/is_not_empty/is_not_null (aliases) and not_exists/is_empty/is_null (aliases) are presence checks that ignore `value`." - }, - "value": { - "description": "Comparison value. Its shape depends on the operator and value_type; omit for the presence-check operators." - }, - "value_type": { - "type": "string", - "enum": [ - "list", - "field", - "relative_date" - ], - "description": "Optional value interpretation. `list`: value is a List UUID and the list's entries become the comparison value. `field`: value is another field path, compared field-to-field. `relative_date`: value is a relative-date object, e.g. {\"direction\": \"past\", \"unit\": \"days\", \"amount\": 30}." - }, - "score": { - "type": "number", - "minimum": 0, - "maximum": 100, - "description": "Required when operator is fuzzy_match: the 0-100 similarity threshold." - }, - "group_index": { - "type": "integer", - "description": "Optional grouped logic. When any condition carries group_index, conditions sharing an index form a group and grouped evaluation replaces evaluation_mode." - }, - "group_logic": { - "type": "string", - "enum": [ - "AND", - "OR" - ], - "default": "AND", - "description": "How conditions combine WITHIN a group." - }, - "groups_logic": { - "type": "string", - "enum": [ - "AND", - "OR" - ], - "default": "OR", - "description": "How group outcomes combine ACROSS groups. Read from the first condition." - } - }, - "description": "A condition reads only the transaction being evaluated - it never filters the aggregation window." - }, - "TransactionRuleAggregation": { - "type": "object", - "required": [ - "value" - ], - "properties": { - "metric": { - "type": "string", - "enum": [ - "count", - "sum", - "max", - "min", - "avg", - "distinct_count" - ], - "default": "count", - "description": "How the matching historical transactions are aggregated. unique_count is accepted as an alias of distinct_count." - }, - "field": { - "type": "string", - "default": "amount", - "description": "Field aggregated by sum/max/min/avg/distinct_count. Ignored for count." - }, - "operator": { - "type": "string", - "enum": [ - "eq", - "ne", - "gt", - "gte", - "lt", - "lte" - ], - "default": "eq", - "description": "Comparison applied to the computed metric." - }, - "value": { - "type": "number", - "description": "Threshold the computed metric is compared against." - }, - "window": { - "type": "string", - "pattern": "^\\d+(m|h|d)$", - "default": "1d", - "description": "Look-back window ending at the transaction's txn_date: m (MINUTES, not months), h (hours) or d (days), e.g. 30m, 24h, 7d, 30d." - }, - "filters": { - "type": "object", - "additionalProperties": true, - "description": "Narrows which historical transactions are aggregated. Keys are field paths from the condition field catalog; a value of \"__current__\" resolves to the same field's value on the transaction being evaluated (e.g. {\"subject_vendor_data\": \"__current__\"} scopes the window to the same subject), and a list value means membership. Filters compare by equality or membership ONLY - there is no lt/gte filter, so a range such as \"historical transactions under 10,000\" cannot be expressed here. Put the range on the metric instead (metric max with operator lt keeps every transaction in the window below the threshold)." - } - }, - "description": "Velocity check over the application's historical transactions inside the window. Every aggregation entry must match for the rule to match, regardless of evaluation_mode. An aggregation constrains only the window; the transaction being evaluated is constrained by scope and conditions, so repeat direction, currency or transaction_type on both halves when the rule promises them." - }, - "TransactionRuleScope": { - "type": "object", - "properties": { - "transaction_types": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Restrict to transaction types, e.g. finance, kyc, travel_rule, user_event. Empty or missing = no restriction." - }, - "directions": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "INBOUND", - "OUTBOUND" - ] - }, - "description": "Restrict to transaction directions (case-insensitive on write)." - }, - "action_types": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Restrict to your action_type values, e.g. withdrawal, deposit." - } - }, - "description": "Which transactions the rule applies to. Only these three keys are accepted." - }, - "TransactionRuleAction": { - "oneOf": [ - { - "type": "object", - "required": [ - "type", - "value" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "add_score" - ] - }, - "value": { - "type": "integer", - "description": "Points added to the transaction's risk score. May be negative." - } - }, - "description": "Add points to the transaction's cumulative risk score; the total decides the status via the application's review/decline thresholds." - }, - { - "type": "object", - "required": [ - "type", - "value" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "change_status" - ] - }, - "value": { - "type": "string", - "enum": [ - "APPROVED", - "IN_REVIEW", - "DECLINED", - "AWAITING_USER" - ], - "description": "Status to set immediately, regardless of score. Alias key: status." - }, - "workflow_id": { - "type": "string", - "format": "uuid", - "description": "REQUIRED when value is AWAITING_USER: the verification workflow the subject is asked to complete. Alias key: remediation_workflow_id. Didit creates a hosted remediation session for the subject when the rule fires." - } - }, - "description": "Directly set the transaction status. DECLINED always wins across matched rules." - }, - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "add_tags" - ] - }, - "tag_uuid": { - "type": "string", - "format": "uuid", - "description": "Existing application tag to attach." - }, - "tag_name": { - "type": "string", - "description": "Tag name to attach; created on first use (stored uppercased). Provide tag_uuid or tag_name." - }, - "tag_color": { - "type": "string", - "description": "Hex color used when tag_name creates a new tag, e.g. #007AFE." - } - }, - "description": "Attach an application tag to the matched transaction." - }, - { - "type": "object", - "required": [ - "type", - "note" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "add_note" - ] - }, - "note": { - "type": "string", - "description": "Note text attached to the transaction (author: Rule Engine). Alias key: value." - } - }, - "description": "Attach an automated analyst note to the matched transaction." - }, - { - "type": "object", - "required": [ - "type", - "list_id" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "add_to_list" - ] - }, - "list_id": { - "type": "string", - "format": "uuid", - "description": "List to add the subject's vendor_data to." - } - }, - "description": "Add the subject's vendor_data to a blocklist, allowlist or custom list." - }, - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "open_case" - ] - }, - "blueprint": { - "type": "string", - "format": "uuid", - "description": "Case blueprint UUID; defaults to the application's default blueprint." - }, - "grouping": { - "type": "string", - "enum": [ - "by_applicant", - "by_rule_and_applicant" - ], - "description": "Whether repeat matches join one case per applicant or one per rule+applicant." - }, - "attach_matched_transaction": { - "type": "boolean", - "description": "Attach the matched transaction to the case." + "extra_files": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Additional document verification files" + }, + "front_image_camera_front_face_match_score": { + "type": "number", + "nullable": true, + "description": "Face match score between the portrait extracted from the front document image and the front camera image" + }, + "back_image_camera_front_face_match_score": { + "type": "number", + "nullable": true, + "description": "Face match score between the portrait extracted from the back document image and the front camera image" + }, + "document_selfie_face_matches": { + "type": "array", + "description": "Previous sessions or face blocklist entries whose face matched the selfie the front camera took during document capture (Silent Capture Face Search). Same entry format as `liveness_checks[].matches`. `null` for sessions created before this field existed or when the search did not run; an empty array when it ran and found nothing. `match_image_url` is a presigned URL that expires.", + "items": { + "type": "object", + "properties": { + "session_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Session ID of the matching face. `null` when `source` is `imported`, `list_entry`, or `retained_template`." + }, + "session_number": { + "type": "integer", + "nullable": true + }, + "similarity_percentage": { + "type": "number", + "description": "Face similarity between the two images, clamped to 0–100." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "`vendor_data` of the matching session or linked User for imported and retained-template faces." + }, + "vendor_user_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Linked User id for `imported` and `retained_template` matches and for session matches linked to a User. `null` for list entries and sessions without a User." + }, + "biometric_template_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Id of the retained biometric template. Set only when `source` is `retained_template`; `null` otherwise." + }, + "verification_date": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the matching face was captured, or when a retained template was created. `null` for list entries." + }, + "user_details": { + "type": "object", + "nullable": true, + "description": "Identity data from the matching session or imported User profile. Always `null` for `retained_template` and `list_entry` matches.", + "properties": { + "full_name": { + "type": "string", + "nullable": true + }, + "document_type": { + "type": "string", + "nullable": true + }, + "document_number": { + "type": "string", + "nullable": true + } + } + }, + "match_image_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Reference image for image-backed matches. Always `null` for `retained_template` matches because retained templates contain no image." + }, + "status": { + "type": "string", + "nullable": true, + "description": "Lifecycle status of the matching session. `null` for non-session sources." + }, + "is_blocklisted": { + "type": "boolean", + "description": "Whether the matching face is on a blocklist." + }, + "is_allowlisted": { + "type": "boolean", + "description": "Whether the matching face is on an allowlist." + }, + "api_service": { + "type": "string", + "nullable": true, + "description": "Standalone API service of the matching session (e.g. `PASSIVE_LIVENESS`, `FACE_MATCH`); `null` for workflow sessions and non-session sources." + }, + "source": { + "type": "string", + "enum": [ + "session", + "imported", + "list_entry", + "retained_template" + ], + "description": "Origin of the matching face: `session`, `imported` (User face upload), `list_entry` (allow/block list), or `retained_template` (an image-free biometric template stored on a User, listed under `/v3/biometric-templates/`)." + } + } + }, + "nullable": true + }, + "front_image_quality_score": { + "type": "object", + "nullable": true, + "description": "Quality metrics for the front document image. Each sub-score ranges from 0 to 100.", + "properties": { + "focus_score": { + "type": "number", + "description": "Sharpness of the image measured via top-10% local Laplacian variance. Robust to documents with large uniform backgrounds. Higher values indicate a sharper, less blurry image." + }, + "brightness_score": { + "type": "number", + "description": "How well-lit the image is. Penalizes images that are too dark or overexposed, with ideal brightness around mid-range." + }, + "brightness_issue": { + "type": "string", + "enum": [ + "ok", + "too_dark", + "too_bright" + ], + "description": "Indicates the direction of the brightness problem when the brightness_score is low. 'ok' means brightness is acceptable, 'too_dark' means the image is underexposed, 'too_bright' means the image is overexposed." + }, + "is_document_fully_visible": { + "type": "boolean", + "nullable": true, + "description": "true if all four corners of the document are fully visible inside the image frame, false if any corner is cut off. Null when corner coordinates are unavailable." + }, + "resolution_score": { + "type": "number", + "description": "Effective resolution of the image based on total pixel count." + }, + "overall_score": { + "type": "number", + "description": "Weighted composite of sub-scores (focus 45%, brightness 30%, resolution 25%)." + } + } + }, + "back_image_quality_score": { + "type": "object", + "nullable": true, + "description": "Quality metrics for the back document image. Same structure as front_image_quality_score.", + "properties": { + "focus_score": { + "type": "number", + "description": "Sharpness of the image measured via top-10% local Laplacian variance. Robust to documents with large uniform backgrounds. Higher values indicate a sharper, less blurry image." + }, + "brightness_score": { + "type": "number", + "description": "How well-lit the image is. Penalizes images that are too dark or overexposed, with ideal brightness around mid-range." + }, + "brightness_issue": { + "type": "string", + "enum": [ + "ok", + "too_dark", + "too_bright" + ], + "description": "Indicates the direction of the brightness problem when the brightness_score is low. 'ok' means brightness is acceptable, 'too_dark' means the image is underexposed, 'too_bright' means the image is overexposed." + }, + "is_document_fully_visible": { + "type": "boolean", + "nullable": true, + "description": "true if all four corners of the document are fully visible inside the image frame, false if any corner is cut off. Null when corner coordinates are unavailable." + }, + "resolution_score": { + "type": "number", + "description": "Effective resolution of the image based on total pixel count." + }, + "overall_score": { + "type": "number", + "description": "Weighted composite of sub-scores (focus 45%, brightness 30%, resolution 25%)." + } + } + }, + "warnings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "enum": [ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE" + ], + "description": "The feature that generated this warning (e.g., `ID_VERIFICATION`, `NFC`, `LIVENESS`). The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module, but the enum above is authoritative across all feature warnings on the decision payload." + }, + "risk": { + "type": "string" + }, + "additional_data": { + "type": "object", + "nullable": true, + "description": "Additional contextual data about the warning" + }, + "log_type": { + "type": "string", + "description": "Type of log: information, warning, or error" + }, + "short_description": { + "type": "string" + }, + "long_description": { + "type": "string" + }, + "node_id": { + "type": "string", + "description": "The workflow graph node ID that generated this warning (for graph-based workflows)" + } + } + }, + "description": "Document verification warnings" + }, + "matches": { + "type": "array", + "description": "Matching documents found during duplicate/blocklist detection (up to 5 matches)", + "items": { + "type": "object", + "properties": { + "session_id": { + "type": "string", + "format": "uuid", + "description": "Session ID of the matching document" + }, + "session_number": { + "type": "integer", + "description": "Session number of the matching document" + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Vendor data associated with the matching session" + }, + "verification_date": { + "type": "string", + "format": "date-time", + "description": "Date when the matching session was verified" + }, + "user_details": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Full name from the matching document" + }, + "document_type": { + "type": "string", + "description": "Type of the matching document" + }, + "document_number": { + "type": "string", + "description": "Document number of the matching document" + } + } + }, + "status": { + "type": "string", + "description": "Status of the matching session" + }, + "is_blocklisted": { + "type": "boolean", + "description": "Whether the matching document is blocklisted" + }, + "api_service": { + "type": "string", + "nullable": true, + "description": "API service type of the matching session" + }, + "front_image_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL of the front document image from the matching document" + } + } + } + }, + "document_liveness": { + "type": "object", + "nullable": true, + "description": "Document-liveness scores per fraud type, reported whether or not a warning fired, so an approved document can still be audited. `null` when the check did not run.", + "properties": { + "portrait_replace": { + "type": "object", + "nullable": true, + "properties": { + "score": { + "type": "number", + "nullable": true, + "description": "Likelihood that the document is genuine for this fraud type, 0-100 (higher = more likely genuine). The lowest of the two sides - the value the decision used. `null` when neither side produced a usable score." + }, + "front": { + "type": "number", + "nullable": true, + "description": "Score for the front side; `null` when not captured or when the score was dropped." + }, + "back": { + "type": "number", + "nullable": true, + "description": "Score for the back side; `null` when not captured or when the score was dropped." + }, + "decline_threshold": { + "type": "number", + "nullable": true, + "description": "The decline threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable." + }, + "review_threshold": { + "type": "number", + "nullable": true, + "description": "The review threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable." + }, + "bucket": { + "type": "string", + "nullable": true, + "enum": [ + "approve", + "review", + "decline", + null + ], + "description": "The fraud-type bucket saved at evaluation time. Null when there is no usable score, the check was not applied, or historical evaluation evidence is unavailable. Later workflow edits do not change it." + } + } + }, + "printed_copy": { + "type": "object", + "nullable": true, + "properties": { + "score": { + "type": "number", + "nullable": true, + "description": "Likelihood that the document is genuine for this fraud type, 0-100 (higher = more likely genuine). The lowest of the two sides - the value the decision used. `null` when neither side produced a usable score." + }, + "front": { + "type": "number", + "nullable": true, + "description": "Score for the front side; `null` when not captured or when the score was dropped." + }, + "back": { + "type": "number", + "nullable": true, + "description": "Score for the back side; `null` when not captured or when the score was dropped." + }, + "decline_threshold": { + "type": "number", + "nullable": true, + "description": "The decline threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable." + }, + "review_threshold": { + "type": "number", + "nullable": true, + "description": "The review threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable." + }, + "bucket": { + "type": "string", + "nullable": true, + "enum": [ + "approve", + "review", + "decline", + null + ], + "description": "The fraud-type bucket saved at evaluation time. Null when there is no usable score, the check was not applied, or historical evaluation evidence is unavailable. Later workflow edits do not change it." + } + } + }, + "screen_replay": { + "type": "object", + "nullable": true, + "properties": { + "score": { + "type": "number", + "nullable": true, + "description": "Likelihood that the document is genuine for this fraud type, 0-100 (higher = more likely genuine). The lowest of the two sides - the value the decision used. `null` when neither side produced a usable score." + }, + "front": { + "type": "number", + "nullable": true, + "description": "Score for the front side; `null` when not captured or when the score was dropped." + }, + "back": { + "type": "number", + "nullable": true, + "description": "Score for the back side; `null` when not captured or when the score was dropped." + }, + "decline_threshold": { + "type": "number", + "nullable": true, + "description": "The decline threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable." + }, + "review_threshold": { + "type": "number", + "nullable": true, + "description": "The review threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable." + }, + "bucket": { + "type": "string", + "nullable": true, + "enum": [ + "approve", + "review", + "decline", + null + ], + "description": "The fraud-type bucket saved at evaluation time. Null when there is no usable score, the check was not applied, or historical evaluation evidence is unavailable. Later workflow edits do not change it." + } + } + } + } + } } - }, - "description": "Open (or add to) an investigation case. Only the first open_case action on a rule is applied." - } - ], - "description": "One rule action. A rule can carry several; all are applied together when it matches. Rules in TEST mode record what they would have done without applying any action." - }, - "TransactionRuleCreateRequest": { - "type": "object", - "required": [ - "title", - "category", - "mode" - ], - "properties": { - "title": { - "type": "string", - "description": "Rule name. Unique per application among non-deleted rules; duplicates return 400." - }, - "description": { - "type": "string", - "nullable": true - }, - "category": { - "type": "string", - "description": "Risk theme used for grouping and reporting, e.g. finance, aml_ctf, anomaly_detection, fatf, device_intelligence, crypto_monitoring, travel_rule, responsible_gaming, e_commerce." - }, - "mode": { - "type": "string", - "enum": [ - "ACTIVE", - "DISABLED", - "TEST" - ], - "description": "ACTIVE evaluates live. TEST evaluates and records rule runs but never touches score or status - the recommended starting mode. DISABLED is skipped." - }, - "evaluation_mode": { - "type": "string", - "enum": [ - "ALL", - "ANY" - ], - "default": "ALL", - "description": "Whether every condition must match (ALL) or at least one (ANY). Ignored when conditions use group_index." - }, - "scope": { - "$ref": "#/components/schemas/TransactionRuleScope" - }, - "conditions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TransactionRuleCondition" - } - }, - "aggregation": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TransactionRuleAggregation" - } - }, - "actions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TransactionRuleAction" - } - }, - "metadata": { - "type": "object", - "description": "Free-form metadata stored on the rule." - } - }, - "description": "Creates a CUSTOM rule. Rules in ACTIVE mode take effect immediately for new transactions - create in TEST mode and backtest first." - }, - "TransactionRuleUpdateRequest": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "Rule name. Unique per application among non-deleted rules; duplicates return 400." - }, - "description": { - "type": "string", - "nullable": true - }, - "category": { - "type": "string", - "description": "Risk theme used for grouping and reporting, e.g. finance, aml_ctf, anomaly_detection, fatf, device_intelligence, crypto_monitoring, travel_rule, responsible_gaming, e_commerce." - }, - "mode": { - "type": "string", - "enum": [ - "ACTIVE", - "DISABLED", - "TEST" - ], - "description": "ACTIVE evaluates live. TEST evaluates and records rule runs but never touches score or status - the recommended starting mode. DISABLED is skipped." - }, - "evaluation_mode": { - "type": "string", - "enum": [ - "ALL", - "ANY" - ], - "default": "ALL", - "description": "Whether every condition must match (ALL) or at least one (ANY). Ignored when conditions use group_index." - }, - "scope": { - "$ref": "#/components/schemas/TransactionRuleScope" - }, - "conditions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TransactionRuleCondition" - } - }, - "aggregation": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TransactionRuleAggregation" - } - }, - "actions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TransactionRuleAction" - } - }, - "metadata": { - "type": "object", - "description": "Free-form metadata stored on the rule." - } - }, - "description": "Partial update. Custom rules accept every field; library (PRESET) rules accept only mode - anything else returns 400." - }, - "TransactionRuleDetail": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "title": { - "type": "string", - "description": "Rule name. Unique per application among non-deleted rules; duplicates return 400." - }, - "description": { - "type": "string", - "nullable": true - }, - "category": { - "type": "string", - "description": "Risk theme used for grouping and reporting, e.g. finance, aml_ctf, anomaly_detection, fatf, device_intelligence, crypto_monitoring, travel_rule, responsible_gaming, e_commerce." - }, - "mode": { - "type": "string", - "enum": [ - "ACTIVE", - "DISABLED", - "TEST" - ], - "description": "ACTIVE evaluates live. TEST evaluates and records rule runs but never touches score or status - the recommended starting mode. DISABLED is skipped." - }, - "evaluation_mode": { - "type": "string", - "enum": [ - "ALL", - "ANY" - ], - "default": "ALL", - "description": "Whether every condition must match (ALL) or at least one (ANY). Ignored when conditions use group_index." - }, - "scope": { - "$ref": "#/components/schemas/TransactionRuleScope" - }, - "conditions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TransactionRuleCondition" - } - }, - "aggregation": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TransactionRuleAggregation" - } - }, - "actions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TransactionRuleAction" } }, - "metadata": { - "type": "object", - "description": "Free-form metadata stored on the rule." - }, - "severity": { + "nfc_skip_reason": { "type": "string", - "enum": [ - "UNKNOWN", - "LOW", - "MEDIUM", - "HIGH", - "CRITICAL" - ], "nullable": true, - "description": "Informational severity carried by library rules; not settable on custom rules." - }, - "source": { - "type": "string", "enum": [ - "PRESET", - "CUSTOM" + "USER_SKIPPED", + "DOCUMENT_WITHOUT_CHIP", + "CHIP_CERTIFICATE_UNAVAILABLE", + "DEVICE_WITHOUT_NFC", + "INTEGRATION_WITHOUT_NFC_ACCESS", + "MRZ_KEY_UNAVAILABLE", + "JPKI_UNAVAILABLE" ], - "description": "PRESET rules come from the library (install/uninstall, mode-only edits); CUSTOM rules are fully yours." + "description": "Why NFC chip verification did not run, populated whenever the workflow includes an NFC step. `null` while NFC is pending or when the chip was actually read. Values: `USER_SKIPPED` - the user chose to skip on an NFC-capable device (allowed by `allow_nfc_skip`); `DOCUMENT_WITHOUT_CHIP` - the document has no readable NFC chip (not an ICAO 9303 electronic document); `CHIP_CERTIFICATE_UNAVAILABLE` - the issuing country's signing certificate (CSCA) covering the document's issue period is unavailable, so the chip signature cannot be verified; `DEVICE_WITHOUT_NFC` - the device reported no NFC reader; `INTEGRATION_WITHOUT_NFC_ACCESS` - the verification ran without a native SDK (e.g. in a web browser), which has no access to the phone's NFC reader; `MRZ_KEY_UNAVAILABLE` - the chip access key could not be derived from the document's MRZ; `JPKI_UNAVAILABLE` - the document is a Japan My Number Card, whose chip is read through JPKI, and JPKI was not available for this verification: it is not enabled, or the app's native SDK cannot read My Number Cards." }, - "library_key": { - "type": "string", + "nfc_verifications": { + "type": "array", "nullable": true, - "description": "Stable library identifier; null for custom rules." - }, - "latest_triggered_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "run_count": { - "type": "integer", - "description": "How many transactions matched this rule." - }, - "approved_count": { - "type": "integer" - }, - "reviewed_count": { - "type": "integer" - }, - "declined_count": { - "type": "integer" - }, - "awaiting_user_count": { - "type": "integer" - }, - "approved_pct": { - "type": "number" - }, - "reviewed_pct": { - "type": "number" - }, - "declined_pct": { - "type": "number" - }, - "created_at": { - "type": "string", - "format": "date-time" + "description": "**Array of NFC / ePassport reports** produced by NFC steps in the workflow graph. **This is always a JSON array, never a singular `nfc` object** — read `nfc_verifications[0]` and iterate if your workflow can run NFC more than once. `null` until at least one NFC step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `chip_protocol`, `chip_data`, `authenticity` (SOD + DG integrity), `certificate_summary`, and `warnings[]`. A Japan My Number Card is read through JPKI instead of ICAO 9303: its item has `chip_protocol: \"jpki\"`, carries its evidence under `jpki`, and leaves the ICAO fields `null`. When the NFC step was bypassed before it could ever be offered (chipless document, missing certificate, no NFC capability reported, unreadable MRZ key, JPKI unavailable for a My Number Card), this stays `null` and `nfc_skip_reason` explains why; a skip at the step itself (user tapped skip, or the reader turned out unavailable at scan time) still produces a report with `is_nfc_skipped: true`.", + "items": { + "type": "object", + "description": "One NFC report: the full report for a single chip read (an ICAO 9303 ePassport or eID chip, or a Japan My Number Card read through JPKI).", + "properties": { + "node_id": { + "type": "string", + "description": "Workflow graph node id of the NFC step that produced this report. Use it to correlate the report with the workflow definition." + }, + "status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ], + "description": "Feature-level status of this NFC step. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`." + }, + "is_nfc_skipped": { + "type": "boolean", + "description": "`true` when this NFC step was explicitly skipped instead of read; `skip_reason` says why." + }, + "skip_reason": { + "type": "string", + "nullable": true, + "enum": [ + "USER_SKIPPED", + "DOCUMENT_WITHOUT_CHIP", + "CHIP_CERTIFICATE_UNAVAILABLE", + "DEVICE_WITHOUT_NFC", + "INTEGRATION_WITHOUT_NFC_ACCESS", + "MRZ_KEY_UNAVAILABLE", + "JPKI_UNAVAILABLE" + ], + "description": "Why this NFC step was skipped. Only set when `is_nfc_skipped` is `true`. Same taxonomy as the session-level `nfc_skip_reason`, which additionally covers the cases where NFC never produced a report at all." + }, + "chip_protocol": { + "type": "string", + "enum": [ + "icao", + "jpki" + ], + "description": "How the NFC step reads the chip. `icao`: ICAO 9303 ePassport or eID chip, opened with the access key from the MRZ. `jpki`: Japan My Number Card, where the card signs a one-time challenge after the holder enters the card PIN in a native SDK; the result is under `jpki`. Also set on skipped steps, so you can tell a skipped My Number Card read apart." + }, + "jpki": { + "type": "object", + "nullable": true, + "description": "What a Japan My Number Card read through JPKI proved. Present when `chip_protocol` is `jpki` and the card was read. `null` for ICAO chips, for skipped steps, and when the NFC step closed without any card read (that case raises `JPKI_SIGNATURE_INVALID`). Never carries the certificates, their serial numbers, the signatures or the 12-digit My Number.", + "properties": { + "certificate_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "user_authentication", + "signature" + ] + }, + "description": "The card certificates read. `user_authentication` (4-digit PIN) is always read; `signature` (signature PIN) is read when the NFC node's `jpki_request_attributes` setting is on." + }, + "chain_valid": { + "type": "boolean", + "description": "`true` when every certificate read chains to the JPKI certification authority, is inside its validity period and is a My Number Card certificate of its type." + }, + "signature_valid": { + "type": "boolean", + "description": "`true` when every signature the card made over the one-time challenge verifies. The card only signs after its PIN is entered, so this proves possession of the card and knowledge of the PIN." + }, + "revocation_status": { + "type": "string", + "enum": [ + "good", + "revoked", + "unchecked" + ], + "description": "`good` when the revocation check confirmed every certificate is valid, `revoked` when any certificate is revoked (for example a card reported lost), `unchecked` when no revocation answer was available. `unchecked` is never treated as `good`: the NFC node's `jpki_revocation_unchecked_action` decides the outcome (default `REVIEW`)." + }, + "ca_generation": { + "type": "string", + "nullable": true, + "description": "Generation of the JPKI certification authority that issued the certificates, for example `2023`. `null` when no certificate chains to one." + }, + "certificate_expires_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "The earliest expiry among the certificates read." + }, + "certificates": { + "type": "array", + "items": { + "type": "object", + "description": "What one certificate read from the card proved. Never the certificate itself or its serial number.", + "properties": { + "type": { + "type": "string", + "enum": [ + "user_authentication", + "signature" + ], + "description": "Which card certificate this entry describes." + }, + "chain_valid": { + "type": "boolean", + "description": "`true` when the certificate chains to the JPKI certification authority of its type, is inside its validity period and is a My Number Card certificate of that type." + }, + "signature_valid": { + "type": "boolean", + "description": "`true` when the card's signature over the one-time challenge verifies against this certificate." + }, + "ca_generation": { + "type": "string", + "nullable": true, + "description": "Generation of the JPKI certification authority that issued the certificate, for example `2023`. `null` when no trusted issuer was found." + }, + "not_valid_before": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Start of the certificate's validity period. `null` when the certificate could not be read." + }, + "not_valid_after": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "End of the certificate's validity period. `null` when the certificate could not be read." + }, + "failure": { + "type": "string", + "nullable": true, + "enum": [ + "unreadable_certificate", + "untrusted_issuer", + "expired", + "not_yet_valid", + "wrong_policy", + "key_usage", + null + ], + "description": "Why `chain_valid` is `false`: `unreadable_certificate`, `untrusted_issuer` (not issued by the JPKI certification authority of its type), `expired`, `not_yet_valid`, `wrong_policy` (not a My Number Card certificate of that type) or `key_usage` (the key may not sign). `null` when the chain is valid." + } + } + }, + "description": "Per-certificate results, one entry per certificate read." + }, + "attributes": { + "type": "object", + "nullable": true, + "description": "The holder's data from the signature certificate, once its chain is valid. `null` when the signature certificate was not requested or did not chain to the JPKI certification authority.", + "properties": { + "full_name": { + "type": "string", + "nullable": true, + "description": "Name as registered, usually in Japanese script." + }, + "date_of_birth": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Date of birth, `YYYY-MM-DD`. `null` when the registered date is incomplete." + }, + "gender": { + "type": "string", + "nullable": true, + "enum": [ + "M", + "F", + null + ], + "description": "`M`, `F`, or `null` when not registered." + }, + "address": { + "type": "string", + "nullable": true, + "description": "Registered address." + }, + "name_substitute_characters": { + "type": "string", + "nullable": true, + "description": "Position string marking the characters of `full_name` that were replaced by a substitute because the original character is outside the standard character set. `null` when none were replaced." + }, + "address_substitute_characters": { + "type": "string", + "nullable": true, + "description": "Position string marking substitute characters in `address`, as for `name_substitute_characters`." + } + } + }, + "attributes_match_ocr": { + "type": "boolean", + "nullable": true, + "description": "`true` when the attributes match the data read from the photo of the same card, `false` when any attribute differs (`JPKI_AND_OCR_DATA_NOT_SAME`), `null` when nothing could be compared, including whenever `attributes` is `null`." + }, + "mismatched_attributes": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "full_name", + "date_of_birth", + "gender", + "address" + ] + }, + "description": "Names of the attributes that differ from the photo data, never their values. Empty when everything compared matches." + } + } + }, + "portrait_image": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Presigned URL to the portrait read from the chip (ICAO DG2). `null` for a JPKI read (`chip_protocol: \"jpki\"`) and for skipped steps." + }, + "signature_image": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Presigned URL to the holder's signature image read from the chip (ICAO DG7), when the chip stores one. `null` for a JPKI read (`chip_protocol: \"jpki\"`) and for skipped steps." + }, + "chip_data": { + "type": "object", + "nullable": true, + "description": "Personal data parsed from the ICAO chip's Data Groups. `null` for a JPKI read (`chip_protocol: \"jpki\"`) and for skipped steps.", + "properties": { + "document_type": { + "type": "string" + }, + "issuing_country": { + "type": "string" + }, + "document_number": { + "type": "string" + }, + "expiration_date": { + "type": "string", + "format": "date" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "birth_date": { + "type": "string", + "format": "date" + }, + "gender": { + "type": "string" + }, + "nationality": { + "type": "string" + }, + "address": { + "type": "string" + }, + "place_of_birth": { + "type": "string" + } + } + }, + "authenticity": { + "type": "object", + "nullable": true, + "description": "ICAO passive authentication: SOD signature (`sod_integrity`) and Data Group hashes (`dg_integrity`). `null` for a JPKI read, which reports its proof under `jpki`.", + "properties": { + "sod_integrity": { + "type": "boolean" + }, + "dg_integrity": { + "type": "boolean" + } + } + }, + "certificate_summary": { + "type": "object", + "nullable": true, + "description": "Summary of the ICAO Document Signer Certificate. `null` for a JPKI read (`chip_protocol: \"jpki\"`) and for skipped steps.", + "properties": { + "issuer": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "serial_number": { + "type": "string" + }, + "not_valid_after": { + "type": "string", + "format": "date-time" + }, + "not_valid_before": { + "type": "string", + "format": "date-time" + } + } + }, + "warnings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "enum": [ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE" + ], + "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload." + }, + "risk": { + "type": "string" + }, + "additional_data": { + "type": "object", + "nullable": true + }, + "log_type": { + "type": "string" + }, + "short_description": { + "type": "string" + }, + "long_description": { + "type": "string" + }, + "node_id": { + "type": "string", + "description": "The workflow graph node ID that generated this warning" + } + } + } + } + } + } }, - "updated_at": { - "type": "string", - "format": "date-time" - } - } - }, - "TransactionRuleBacktestRequest": { - "type": "object", - "properties": { - "conditions": { + "liveness_checks": { "type": "array", + "nullable": true, + "description": "**Array of liveness reports** produced by liveness steps in the workflow graph. Always a JSON array — never a singular `liveness` object. `null` until at least one liveness step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `method`, `score`, `reference_image`, `video_url`, optional `age_estimation`, cross-session `matches[]`, and `warnings[]`.", "items": { - "$ref": "#/components/schemas/TransactionRuleCondition" - }, - "default": [] + "type": "object", + "description": "One liveness report — the full report for a single liveness pass.", + "properties": { + "node_id": { + "type": "string", + "description": "Workflow graph node id of the liveness step that produced this report." + }, + "status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ], + "description": "Feature-level status of this liveness step. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`." + }, + "method": { + "type": "string", + "description": "Method used for liveness detection" + }, + "score": { + "type": "number", + "description": "Confidence score of the liveness check" + }, + "reference_image": { + "type": "string", + "format": "uri", + "description": "URL of the reference image used" + }, + "video_url": { + "type": "string", + "format": "uri", + "description": "URL of the liveness check video" + }, + "age_estimation": { + "type": "number", + "description": "Estimated age of the person" + }, + "matches": { + "type": "array", + "description": "Similar faces found by the duplicate-face search (top matches, blocklisted/allowlisted entries first). Each entry's `source` tells you where the matching face came from: `session` (another verification session), `imported` (a face uploaded against a vendor user), `list_entry` (an org-managed allow/block list face), or `retained_template` (an image-free biometric template stored on a User, listed under `/v3/biometric-templates/`). `session_id`, `session_number`, `status` and `api_service` are `null` for non-session sources. `imported` matches still populate `vendor_data`, `verification_date` and — when the vendor user has a name on file — `user_details` (with `full_name` only) from the vendor-user record; `retained_template` matches populate `vendor_user_id`, `vendor_data`, `biometric_template_id` and `verification_date`, while their image and identity fields are `null`; `list_entry` matches return all of these as `null`.", + "items": { + "type": "object", + "properties": { + "session_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Session ID of the matching face. `null` when `source` is `imported`, `list_entry`, or `retained_template`." + }, + "session_number": { + "type": "integer", + "nullable": true + }, + "similarity_percentage": { + "type": "number", + "description": "Face similarity between the two images, clamped to 0–100." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "`vendor_data` of the matching session or linked User for imported and retained-template faces." + }, + "vendor_user_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Linked User id for `imported` and `retained_template` matches and for session matches linked to a User. `null` for list entries and sessions without a User." + }, + "biometric_template_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Id of the retained biometric template. Set only when `source` is `retained_template`; `null` otherwise." + }, + "verification_date": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the matching face was captured, or when a retained template was created. `null` for list entries." + }, + "user_details": { + "type": "object", + "nullable": true, + "description": "Identity data from the matching session or imported User profile. Always `null` for `retained_template` and `list_entry` matches.", + "properties": { + "full_name": { + "type": "string", + "nullable": true + }, + "document_type": { + "type": "string", + "nullable": true + }, + "document_number": { + "type": "string", + "nullable": true + } + } + }, + "match_image_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Reference image for image-backed matches. Always `null` for `retained_template` matches because retained templates contain no image." + }, + "status": { + "type": "string", + "nullable": true, + "description": "Lifecycle status of the matching session. `null` for non-session sources." + }, + "is_blocklisted": { + "type": "boolean", + "description": "Whether the matching face is on a blocklist." + }, + "is_allowlisted": { + "type": "boolean", + "description": "Whether the matching face is on an allowlist." + }, + "api_service": { + "type": "string", + "nullable": true, + "description": "Standalone API service of the matching session (e.g. `PASSIVE_LIVENESS`, `FACE_MATCH`); `null` for workflow sessions and non-session sources." + }, + "source": { + "type": "string", + "enum": [ + "session", + "imported", + "list_entry", + "retained_template" + ], + "description": "Origin of the matching face: `session`, `imported` (User face upload), `list_entry` (allow/block list), or `retained_template` (an image-free biometric template stored on a User, listed under `/v3/biometric-templates/`)." + } + } + } + }, + "warnings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "enum": [ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE" + ], + "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload." + }, + "risk": { + "type": "string" + }, + "additional_data": { + "type": "object", + "nullable": true + }, + "log_type": { + "type": "string" + }, + "short_description": { + "type": "string" + }, + "long_description": { + "type": "string" + }, + "node_id": { + "type": "string", + "description": "The workflow graph node ID that generated this warning" + } + } + } + }, + "face_quality": { + "type": "number", + "nullable": true, + "description": "Face image quality score (0-100%). Only available for passive liveness." + }, + "face_luminance": { + "type": "number", + "nullable": true, + "description": "Face luminance/brightness score (0-100%). Only available for passive liveness." + } + } + } }, - "aggregation": { + "face_matches": { "type": "array", + "nullable": true, + "description": "**Array of face-match reports** produced by face-match steps in the workflow graph. Always a JSON array — never a singular `face_match` object. `null` until at least one face-match step has produced data. Each item is the full report, including `node_id`, feature-level `status`, similarity `score`, `source_image`, `target_image`, and `warnings[]`.", "items": { - "$ref": "#/components/schemas/TransactionRuleAggregation" - }, - "default": [] - }, - "evaluation_mode": { - "type": "string", - "enum": [ - "ALL", - "ANY" - ], - "default": "ALL" - }, - "scope": { - "$ref": "#/components/schemas/TransactionRuleScope" - }, - "period_days": { - "type": "integer", - "minimum": 1, - "maximum": 365, - "default": 90 - } - }, - "description": "Hypothetical rule configuration to evaluate against recent transactions. Read-only and free: nothing is created or modified. Send the same conditions, aggregation, evaluation_mode AND scope as the rule you intend to activate. Omitted conditions, aggregation and scope add no constraints of their own; dropping scope can include directions or transaction types the saved rule skips. Omitted evaluation_mode defaults to ALL (which can reduce matches for an ANY rule), and omitted period_days defaults to 90 days, not unlimited history." - }, - "TransactionRuleBacktestResult": { - "type": "object", - "properties": { - "evaluated": { - "type": "integer", - "description": "Transactions evaluated (most recent first, capped)." - }, - "matched": { - "type": "integer", - "description": "Transactions the configuration would have matched." - }, - "affected_entities": { - "type": "integer", - "description": "Distinct subjects (by vendor_data) among the matches." - }, - "period_days": { - "type": "integer" - } - } - }, - "TransactionRuleLibraryEntry": { - "type": "object", - "properties": { - "library_key": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string", - "nullable": true - }, - "category": { - "type": "string" - }, - "evaluation_mode": { - "type": "string", - "enum": [ - "ALL", - "ANY" - ] - }, - "scope": { - "$ref": "#/components/schemas/TransactionRuleScope" + "type": "object", + "description": "One face-match report — the full report.", + "properties": { + "node_id": { + "type": "string", + "description": "Workflow graph node id of the face-match step that produced this report." + }, + "status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ], + "description": "Feature-level status of this face-match step. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`." + }, + "score": { + "type": "number", + "description": "Confidence score of the face match" + }, + "source_image_session_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Session id under which the source (reference) portrait is stored. In biometric-authentication flows this is the session's own id (the submitted portrait is stored under the new session). `null` when the source image is the same session's document portrait." + }, + "source_image": { + "type": "string", + "format": "uri", + "description": "URL of the source image used for comparison" + }, + "target_image": { + "type": "string", + "format": "uri", + "description": "URL of the target image used for comparison" + }, + "warnings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "enum": [ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE" + ], + "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload." + }, + "risk": { + "type": "string" + }, + "additional_data": { + "type": "object", + "nullable": true + }, + "log_type": { + "type": "string" + }, + "short_description": { + "type": "string" + }, + "long_description": { + "type": "string" + }, + "node_id": { + "type": "string", + "description": "The workflow graph node ID that generated this warning" + } + } + }, + "description": "Face match verification warnings" + } + } + } }, - "conditions": { + "phone_verifications": { "type": "array", + "nullable": true, + "description": "**Array of phone verification reports** — always a JSON array, never a singular `phone` object. `null` until at least one phone step has produced data. Each item is the full report, including `node_id`, feature-level `status`, the phone number and country metadata, `lifecycle[]`, `matches[]`, and `warnings[]`.", "items": { - "$ref": "#/components/schemas/TransactionRuleCondition" + "type": "object", + "properties": { + "node_id": { + "type": "string", + "description": "Unique identifier for this phone step in the workflow graph." + }, + "status": { + "type": "string", + "description": "Feature-level status of this phone verification step. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`.", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Expired" + ] + }, + "phone_number_prefix": { + "type": "string", + "description": "Country calling code prefix" + }, + "phone_number": { + "type": "string", + "description": "Phone number without country prefix" + }, + "full_number": { + "type": "string", + "description": "Complete phone number with country prefix" + }, + "country_code": { + "type": "string", + "description": "Two-letter country code (ISO 3166-1 alpha-2)" + }, + "country_name": { + "type": "string", + "description": "Full country name" + }, + "carrier": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the phone carrier" + }, + "type": { + "type": "string", + "description": "Type of phone line (mobile, landline, etc)" + } + } + }, + "is_disposable": { + "type": "boolean", + "description": "Whether the phone number is from a disposable service" + }, + "is_virtual": { + "type": "boolean", + "description": "Whether the phone number is virtual" + }, + "verification_method": { + "type": "string", + "description": "Method used to verify the phone number" + }, + "verification_attempts": { + "type": "integer", + "description": "Number of verification attempts made" + }, + "verified_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the phone was verified" + }, + "lifecycle": { + "type": "array", + "description": "Chronological list of events in the phone verification lifecycle.", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "details": { + "type": "object", + "nullable": true, + "description": "Event-specific payload. Send events carry `status`, `reason`, `channel`, `actual_channel`; delivery events carry `channel`, `status`; code-check events carry `code_tried`, `status`; final status events carry `reason` — or `null` when there is no reason to report (e.g. `PHONE_VERIFICATION_APPROVED`).", + "properties": { + "status": { + "type": "string" + }, + "reason": { + "type": "string", + "nullable": true + }, + "channel": { + "type": "string", + "nullable": true + }, + "actual_channel": { + "type": "string", + "nullable": true + }, + "code_tried": { + "type": "string" + } + } + }, + "fee": { + "type": "number" + } + } + } + }, + "warnings": { + "type": "array", + "description": "List of warnings related to phone verification", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "enum": [ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE" + ], + "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload." + }, + "risk": { + "type": "string" + }, + "additional_data": { + "type": "object", + "nullable": true + }, + "log_type": { + "type": "string" + }, + "short_description": { + "type": "string" + }, + "long_description": { + "type": "string" + }, + "node_id": { + "type": "string", + "description": "The workflow graph node ID that generated this warning" + } + } + } + }, + "matches": { + "type": "array", + "description": "Matches found for this phone number. Each match has a `source`: `session` (another verification session in your application matched on the same number, returned with `session_id`/`session_number`/`vendor_data`/`verification_date`) or `list_entry` (the number is on an org-managed allow/block list — see `/management-api/lists/overview`). Cross-session matches use different `vendor_data` and are capped at 5.", + "items": { + "type": "object", + "properties": { + "session_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "session_number": { + "type": "integer", + "nullable": true + }, + "vendor_data": { + "type": "string", + "nullable": true + }, + "verification_date": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "phone_number": { + "type": "string" + }, + "status": { + "type": "string", + "nullable": true + }, + "is_blocklisted": { + "type": "boolean" + }, + "api_service": { + "type": "string", + "nullable": true + }, + "source": { + "type": "string", + "enum": [ + "session", + "list_entry" + ], + "description": "Where this match came from. `session` means another verification session in your application matched on this value. `list_entry` means the value was hit against an org-managed list (allowlist/blocklist) — see the management API lists overview at `/management-api/lists/overview` for how to manage those lists." + } + } + } + } + } } }, - "aggregation": { + "email_verifications": { "type": "array", + "nullable": true, + "description": "**Array of email verification reports** — always a JSON array, never a singular `email` object. `null` until at least one email step has produced data. Each item is the full report, including `node_id`, feature-level `status`, the email address, breach data, `lifecycle[]`, `matches[]`, and `warnings[]`.", "items": { - "$ref": "#/components/schemas/TransactionRuleAggregation" + "type": "object", + "properties": { + "node_id": { + "type": "string", + "description": "Unique identifier for this email step in the workflow graph." + }, + "status": { + "type": "string", + "description": "Feature-level status of this email verification step. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`.", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Expired" + ] + }, + "email": { + "type": "string", + "description": "The email address being verified" + }, + "is_breached": { + "type": "boolean", + "description": "Indicates if the email was found in known data breaches" + }, + "breaches": { + "type": "array", + "description": "List of known breaches this email was found in", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the breach" + }, + "domain": { + "type": "string", + "description": "Domain affected by the breach" + }, + "logo_path": { + "type": "string", + "description": "URL to the logo of the breached service" + }, + "breach_date": { + "type": "string", + "format": "date", + "description": "Date when the breach occurred" + }, + "description": { + "type": "string", + "description": "Description of the breach" + }, + "is_verified": { + "type": "boolean", + "description": "Whether the breach is verified" + }, + "data_classes": { + "type": "array", + "description": "Types of data exposed in the breach", + "items": { + "type": "string" + } + }, + "breach_emails_count": { + "type": "integer", + "description": "Number of email addresses affected in the breach" + } + } + } + }, + "is_disposable": { + "type": "boolean", + "description": "Indicates if the email is from a disposable provider" + }, + "is_undeliverable": { + "type": "boolean", + "description": "Indicates if the email address is undeliverable" + }, + "verification_attempts": { + "type": "integer", + "description": "Number of verification attempts for this email" + }, + "verified_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when the email was verified" + }, + "warnings": { + "type": "array", + "description": "List of warnings related to email verification", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "enum": [ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE" + ], + "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload." + }, + "risk": { + "type": "string" + }, + "additional_data": { + "type": "object", + "nullable": true + }, + "log_type": { + "type": "string" + }, + "short_description": { + "type": "string" + }, + "long_description": { + "type": "string" + }, + "node_id": { + "type": "string", + "description": "The workflow graph node ID that generated this warning" + } + } + } + }, + "lifecycle": { + "type": "array", + "description": "Chronological list of events in the email verification lifecycle.", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "details": { + "type": "object", + "nullable": true, + "description": "Event-specific payload. Send events carry `status`, `reason`; code-check events carry `code_tried`, `status`; final status events carry `reason` — or `null` when there is no reason to report (e.g. `EMAIL_VERIFICATION_APPROVED`).", + "properties": { + "status": { + "type": "string" + }, + "reason": { + "type": "string", + "nullable": true + }, + "code_tried": { + "type": "string" + } + } + }, + "fee": { + "type": "number" + } + } + } + }, + "matches": { + "type": "array", + "description": "Matches found for this email address. Each match has a `source`: `session` (another verification session in your application matched on the same email, returned with `session_id`/`session_number`/`vendor_data`/`verification_date`) or `list_entry` (the email is on an org-managed allow/block list — see `/management-api/lists/overview`). Cross-session matches use different `vendor_data` and are capped at 5.", + "items": { + "type": "object", + "properties": { + "session_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "session_number": { + "type": "integer", + "nullable": true + }, + "vendor_data": { + "type": "string", + "nullable": true + }, + "verification_date": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "email": { + "type": "string", + "format": "email" + }, + "status": { + "type": "string", + "nullable": true + }, + "is_blocklisted": { + "type": "boolean" + }, + "api_service": { + "type": "string", + "nullable": true + }, + "source": { + "type": "string", + "enum": [ + "session", + "list_entry" + ], + "description": "Where this match came from. `session` means another verification session in your application matched on this value. `list_entry` means the value was hit against an org-managed list (allowlist/blocklist) — see the management API lists overview at `/management-api/lists/overview` for how to manage those lists." + } + } + } + } + } } }, - "actions": { + "poa_verifications": { "type": "array", - "items": { - "$ref": "#/components/schemas/TransactionRuleAction" - } - }, - "metadata": { - "type": "object" - }, - "bundle": { - "type": "string", "nullable": true, - "description": "Functional family, e.g. finance, aml_ctf, crypto_monitoring, travel_rule, responsible_gaming." - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "industries": { - "type": "array", + "description": "**Array of Proof of Address (POA) reports** — always a JSON array, never a singular `poa` object. `null` until at least one POA step has produced data. Each item is the full report, including `node_id`, feature-level `status`, the document file URL, issuer, dates, parsed address, and `warnings[]`.", "items": { - "type": "string" + "type": "object", + "properties": { + "node_id": { + "type": "string", + "description": "Unique identifier for this POA step in the workflow graph." + }, + "status": { + "type": "string", + "description": "Feature-level status of this Proof of Address step. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`.", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ] + }, + "document_file": { + "type": "string", + "format": "uri", + "description": "URL of the proof of address document" + }, + "issuing_state": { + "type": "string", + "description": "Two-letter country code of the issuing state" + }, + "document_type": { + "type": "string", + "description": "Type of proof of address document" + }, + "document_subtype": { + "type": "string", + "nullable": true, + "description": "More specific classification of the POA document within its `document_type`, when one was detected. `null` otherwise." + }, + "document_language": { + "type": "string", + "description": "Language code of the document" + }, + "document_metadata": { + "type": "object", + "properties": { + "file_size": { + "type": "integer", + "description": "Size of the document" + }, + "content_type": { + "type": "string", + "description": "Content type of the document" + }, + "creation_date": { + "type": "string", + "format": "date", + "description": "Date when the document was created" + }, + "modified_date": { + "type": "string", + "format": "date", + "description": "Date when the document was modified" + }, + "overlay_manipulation": { + "type": "object", + "nullable": true, + "description": "PDF forensic analysis for suspected overlay-text manipulation. Null when no overlay manipulation evidence was found or the document was not a PDF.", + "properties": { + "detected": { + "type": "boolean", + "description": "Whether overlay-text manipulation evidence was detected." + }, + "analyzed": { + "type": "boolean", + "description": "Whether the PDF could be analyzed." + }, + "signals": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Forensic signals that fired, such as duplicate_font_subset or glyph_fragmentation." + }, + "manipulated_regions": { + "type": "array", + "description": "Suspected manipulated areas in PDF page coordinates.", + "items": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "width": { + "type": "number" + }, + "height": { + "type": "number" + }, + "page_width": { + "type": "number" + }, + "page_height": { + "type": "number" + } + } + } + } + } + }, + "creator": { + "type": "string", + "nullable": true, + "description": "Creator application recorded in the document metadata." + }, + "producer": { + "type": "string", + "nullable": true, + "description": "Producer software recorded in the PDF metadata." + }, + "software": { + "type": "string", + "nullable": true, + "description": "Editing/processing software recorded in the file metadata." + }, + "encryption": { + "type": "string", + "nullable": true, + "description": "Encryption scheme of the document, when encrypted." + }, + "is_signed": { + "type": "boolean", + "nullable": true, + "description": "Whether the PDF carries a digital signature." + }, + "is_tampered": { + "type": "boolean", + "nullable": true, + "description": "Whether signature validation indicates the file was modified after signing." + }, + "signature_info": { + "type": "object", + "nullable": true, + "description": "Details of the digital signature(s), when present." + }, + "exif_original_date": { + "type": "string", + "nullable": true, + "description": "EXIF original capture date, for image uploads." + }, + "exif_digitized_date": { + "type": "string", + "nullable": true, + "description": "EXIF digitization date, for image uploads." + }, + "processed_by_known_editor": { + "type": "boolean", + "nullable": true, + "description": "Whether the file metadata indicates processing by a known document editor." + }, + "has_different_creation_mod_date": { + "type": "boolean", + "nullable": true, + "description": "Whether the creation and modification dates differ — a common manipulation signal." + } + }, + "description": "File-forensics metadata extracted from the uploaded POA document. Always contains the keys below (values are `null` when not applicable, e.g. EXIF dates for PDFs). `null` when no metadata could be extracted.", + "nullable": true + }, + "issuer": { + "type": "string", + "description": "Name of the document issuer" + }, + "issue_date": { + "type": "string", + "format": "date", + "description": "Date when the document was issued" + }, + "expiration_date": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Expiration date of the document, if available" + }, + "poa_address": { + "type": "string", + "description": "Raw address from the proof of address document" + }, + "poa_formatted_address": { + "type": "string", + "description": "Formatted address from the proof of address document" + }, + "poa_parsed_address": { + "type": "object", + "properties": { + "address_type": { + "type": "string", + "description": "Type of address (Street, Avenue, etc)" + }, + "street_1": { + "type": "string", + "description": "Primary street address" + }, + "street_2": { + "type": "string", + "nullable": true, + "description": "Secondary street address" + }, + "city": { + "type": "string", + "description": "City name" + }, + "region": { + "type": "string", + "description": "Region or state name" + }, + "country": { + "type": "string", + "description": "Two-letter country code" + }, + "postal_code": { + "type": "string", + "description": "Postal code" + }, + "document_location": { + "type": "object", + "properties": { + "latitude": { + "type": "number", + "description": "Latitude coordinate" + }, + "longitude": { + "type": "number", + "description": "Longitude coordinate" + } + } + } + } + }, + "name_on_document": { + "type": "string", + "nullable": true, + "description": "Account-holder / addressee name extracted from the POA document, used for name matching. `null` when no name could be extracted." + }, + "name_match_score_expected_details": { + "type": "number", + "nullable": true, + "description": "Similarity score (0-100) between `name_on_document` and the expected name supplied at session creation. `null` when either side is missing." + }, + "name_match_score_id_verification": { + "type": "number", + "nullable": true, + "description": "Similarity score (0-100) between `name_on_document` and the name extracted by the ID verification step. `null` when either side is missing." + }, + "expected_details_address": { + "type": "string", + "nullable": true, + "description": "Expected raw address for verification" + }, + "expected_details_formatted_address": { + "type": "string", + "nullable": true, + "description": "Expected formatted address for verification" + }, + "expected_details_parsed_address": { + "type": "object", + "description": "Expected parsed address details for verification" + }, + "extra_fields": { + "type": "object", + "description": "Additional banking and contact details extracted from the document.", + "properties": { + "bank_account_number": { + "type": "string", + "nullable": true + }, + "bank_iban": { + "type": "string", + "nullable": true + }, + "bank_sort_code": { + "type": "string", + "nullable": true + }, + "bank_routing_number": { + "type": "string", + "nullable": true + }, + "bank_swift_bic": { + "type": "string", + "nullable": true + }, + "bank_branch_name": { + "type": "string", + "nullable": true + }, + "bank_branch_address": { + "type": "string", + "nullable": true + }, + "document_phone_number": { + "type": "string", + "nullable": true + }, + "additional_names": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "extra_files": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Additional proof of address files" + }, + "warnings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "enum": [ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE" + ], + "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload." + }, + "risk": { + "type": "string" + }, + "additional_data": { + "type": "object", + "nullable": true + }, + "log_type": { + "type": "string" + }, + "short_description": { + "type": "string" + }, + "long_description": { + "type": "string" + }, + "node_id": { + "type": "string", + "description": "The workflow graph node ID that generated this warning" + } + } + } + }, + "detected_codes": { + "type": "array", + "description": "QR codes and barcodes detected across every processed page of the document. Empty when none are found. A located but undecodable QR code has `parsed_payload: null`.", + "items": { + "$ref": "#/components/schemas/DocumentAIDetectedCode" + } + } + } } }, - "source": { - "type": "string" - }, - "is_installed": { - "type": "boolean", - "description": "Whether this preset is currently installed on the application." - } - } - }, - "TransactionRuleInstallRequest": { - "type": "object", - "properties": { - "library_keys": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Specific presets to install or uninstall." - }, - "bundle": { - "type": "string", - "description": "Install or uninstall every preset in this bundle instead." - } - }, - "description": "Provide library_keys OR bundle." - }, - "TransactionRuleInstallResult": { - "type": "object", - "properties": { - "installed_library_keys": { + "document_ai_documents": { "type": "array", - "items": { - "type": "string" - }, - "description": "POST (install) only: presets installed by this call." - }, - "installed_count": { - "type": "integer", - "description": "POST (install) only." - }, - "uninstalled_count": { - "type": "integer", - "description": "DELETE (uninstall) only: presets removed by this call." - } - } - }, - "WalletScreeningResult": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Provider that performed the screening (e.g. `merklescience`, `crystal`)." - }, - "screening_type": { - "type": "string", - "enum": [ - "WALLET_SCREENING" - ], - "description": "Always `WALLET_SCREENING` for this endpoint." - }, - "risk_score": { - "type": "integer", - "description": "Normalised 0-100 risk score. Higher means greater exposure to risky entities. Merkle Science grades an address 0-5, which maps to 0, 15, 35, 55, 75 or 95." - }, - "severity": { - "type": "string", - "enum": [ - "UNKNOWN", - "LOW", - "MEDIUM", - "HIGH", - "CRITICAL" - ], - "description": "Risk bucket derived from `risk_score`: `0-9` UNKNOWN, `10-39` LOW, `40-69` MEDIUM, `70-89` HIGH, `90-100` CRITICAL. `UNKNOWN` is the lowest band, not a separate no-data state: `risk_score` 0 (the common clean-address case) means no adverse assessment, while a non-zero score in the 1-9 range is a real but sub-LOW signal - read the `risk_score`, not just the band. Never treat `UNKNOWN` as an affirmative low-risk or clear rating; do not display it as a pass." - }, - "status": { - "type": "string", - "enum": [ - "SCREENED", - "PENDING", - "ERROR" - ], - "description": "Screening outcome status." - }, - "summary": { - "type": "string", - "description": "Human-readable summary of the screening result." - }, - "wallet_address": { - "type": "string", - "description": "The screened address, echoed back." - }, - "transaction_hash": { - "type": "string", - "nullable": true, - "description": "Transaction screening field, shared with the result shape. Always `null` on a wallet screening result." - }, - "blockchain": { - "type": "string", - "description": "The blockchain that was screened." - }, - "block_timestamp": { - "type": "string", - "nullable": true, - "description": "Transaction screening field, shared with the result shape. Always `null` on a wallet screening result." - }, - "total_value_usd": { - "type": "number", "nullable": true, - "description": "Transaction screening field, shared with the result shape. Always `null` on a wallet screening result." - }, - "fee_usd": { - "type": "number", - "nullable": true, - "description": "Transaction screening field, shared with the result shape. Always `null` on a wallet screening result." - }, - "sender_rows": { - "type": "array", - "items": { - "type": "object" - }, - "description": "Transaction screening field, shared with the result shape. Always empty on a wallet screening result." - }, - "receiver_rows": { - "type": "array", + "description": "**Document AI reports grouped by workflow node** — always a JSON array, never a singular object. `null` until at least one Document AI step has produced data. Present only when `session_kind = user`. Each item is a node group with an aggregate `status`, the `node_id`, an `items[]` array of the per-document results (extracted data keyed by the configured field key), and `warnings`.", "items": { - "type": "object" - }, - "description": "Transaction screening field, shared with the result shape. Always empty on a wallet screening result." - }, - "transaction_flows": { - "type": "array", - "items": { - "type": "object" - }, - "description": "Transaction screening field, shared with the result shape. Always empty on a wallet screening result." - }, - "transaction_flows_status": { - "type": "string", - "nullable": true, - "description": "Transaction screening field, shared with the result shape. Always `null` on a wallet screening result." - }, - "transaction_flows_error": { - "type": "string", - "nullable": true, - "description": "Transaction screening field, shared with the result shape. Always `null` on a wallet screening result." + "type": "object", + "description": "Document AI results grouped by workflow node.", + "properties": { + "status": { + "type": "string", + "description": "Aggregate Document AI status for this node, rolled up from its `items[]`.", + "enum": [ + "Approved", + "Declined", + "In Review", + "Not Finished" + ] + }, + "node_id": { + "type": "string", + "nullable": true, + "description": "Unique identifier for this Document AI step in the workflow graph." + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "node_id": { + "type": "string", + "nullable": true, + "description": "Workflow graph node that produced this document." + }, + "document_key": { + "type": "string", + "description": "Key of the configured Document AI document this file corresponds to." + }, + "original_filename": { + "type": "string", + "nullable": true, + "description": "Filename as uploaded by the user." + }, + "file_size": { + "type": "integer", + "nullable": true, + "description": "File size in bytes." + }, + "file_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Short-lived presigned link to the uploaded file." + }, + "status": { + "type": "string", + "enum": [ + "Approved", + "Declined", + "In Review", + "Not Finished" + ], + "description": "Per-document Document AI status." + }, + "extracted_data": { + "type": "object", + "additionalProperties": true, + "nullable": true, + "description": "Values extracted by Document AI, keyed by the configured field `key`. Each value is a string, number, or `null`." + }, + "detected_codes": { + "type": "array", + "description": "QR codes and barcodes detected across every processed page. Empty when none are found. A located but undecodable QR code has `parsed_payload: null`.", + "items": { + "$ref": "#/components/schemas/DocumentAIDetectedCode" + } + }, + "fields": { + "type": "array", + "description": "The configured extraction fields for this document.", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Stable field key used as the key in `extracted_data`." + }, + "name": { + "type": "string", + "description": "Human-readable field name." + }, + "instruction": { + "type": "string", + "description": "Extraction instruction shown to the model." + }, + "type": { + "type": "string", + "enum": [ + "text", + "number", + "date" + ], + "description": "Expected value type of the extracted field." + }, + "required": { + "type": "boolean", + "description": "Whether this field is required." + } + } + } + }, + "document_metadata": { + "type": "object", + "nullable": true, + "description": "File-forensics metadata extracted from the uploaded document (e.g. `is_tampered`). `null` when nothing could be extracted." + }, + "cross_check_result": { + "type": "object", + "nullable": true, + "description": "Cross-reference of the extracted document fields against other session data. `null` when no cross-check was performed." + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } + } + }, + "warnings": { + "type": "array", + "nullable": true, + "items": { + "type": "object" + }, + "description": "Warning logs raised for this Document AI node." + } + } + } }, - "source_of_funds": { + "questionnaire_responses": { "type": "array", - "description": "Where the address received funds from, attributed by entity. Each entry is an exposure breakdown.", + "nullable": true, + "description": "**Array of questionnaire response reports** — always a JSON array, never a singular `questionnaire` object. `null` until at least one questionnaire step has produced data. Each item is the full report, including `node_id`, the questionnaire metadata, sections with form elements, and the user's answers.", "items": { "type": "object", "properties": { - "category": { + "node_id": { "type": "string", - "description": "Normalised risk category, e.g. `exchange_licensed`, `dex`, `defi`, `payment_processor`, `mixer`, `darknet_market`, `sanctioned`, `stolen_funds`, `high_risk_exchange`." + "description": "Unique identifier for this questionnaire step in the workflow graph." }, - "percentage": { - "type": "number", - "description": "Share of the direction's total exposure, 0-100." + "questionnaire_id": { + "type": "string", + "description": "Unique identifier for the questionnaire" }, - "amount_usd": { - "type": "number", - "nullable": true, - "description": "USD value attributed to this entity." + "title": { + "type": "string", + "description": "Title of the questionnaire" }, - "entity_name": { + "description": { "type": "string", - "nullable": true, - "description": "Name of the attributed entity, for example an exchange, a mixer or a sanctioned group." + "description": "Description of the questionnaire" }, - "entity_type": { + "languages": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Supported languages" + }, + "default_language": { "type": "string", - "nullable": true, - "description": "Provider entity type (e.g. `Exchange`, `DeFi`, `Sanctions`, `Theft`)." + "description": "Default language" }, - "entity_subtype": { + "is_active": { + "type": "boolean", + "description": "Whether the questionnaire is active" + }, + "is_simple_questionnaire": { + "type": "boolean", + "description": "Whether the questionnaire is a simple linear form (no branching graph)." + }, + "questionnaire_group_id": { "type": "string", + "format": "uuid", "nullable": true, - "description": "More granular subtype (e.g. `Mandatory KYC and AML`, `OFAC`, `Yield Aggregator`)." + "description": "Stable identifier of the questionnaire group across versions." }, - "hops": { + "version": { "type": "integer", "nullable": true, - "description": "Number of hops to the entity (1 for a direct counterparty), when the provider reports one. Always `null` for Merkle Science, which reports only whether exposure is direct or indirect: use `exposure_type`." + "description": "Version number of the questionnaire that was answered." }, - "description": { + "published_at": { "type": "string", + "format": "date-time", "nullable": true, - "description": "`entity_type / entity_subtype`, as the provider labels the entity." + "description": "Timestamp at which this questionnaire version was published." }, - "is_direct": { - "type": "boolean", - "description": "True when `exposure_type` is `direct`." + "sections": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string", + "nullable": true, + "description": "Section title" + }, + "description": { + "type": "string", + "nullable": true, + "description": "Section description" + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "description": "Unique identifier for the item" + }, + "value": { + "type": "string", + "nullable": true, + "description": "Stable node identifier of this form element within the questionnaire graph." + }, + "element_type": { + "type": "string", + "description": "Type of the questionnaire element" + }, + "is_required": { + "type": "boolean", + "description": "Whether the item is required" + }, + "title": { + "type": "string", + "description": "Title of the item" + }, + "description": { + "type": "string", + "nullable": true, + "description": "Description of the item" + }, + "placeholder": { + "type": "string", + "nullable": true, + "description": "Placeholder text" + }, + "choices": { + "type": "array", + "nullable": true, + "items": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + }, + "requires_text_input": { + "type": "boolean", + "description": "Whether this choice requires additional text input", + "nullable": true + } + }, + "required": [ + "label", + "value" + ] + }, + "description": "Choices for dropdown or single choice elements" + }, + "max_files": { + "type": "integer", + "description": "Maximum number of files allowed", + "nullable": true + }, + "answer": { + "type": "object", + "description": "Answer provided for the item", + "properties": { + "value": { + "type": "string", + "nullable": true + }, + "text": { + "type": "string", + "nullable": true + }, + "files": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + } + } + }, + "required_if": { + "type": "object", + "nullable": true, + "description": "Conditional-requirement rule, when the element is only required for certain answers." + }, + "repeatable_config": { + "type": "object", + "nullable": true, + "description": "Configuration for repeatable element groups, when applicable." + } + }, + "required": [ + "uuid", + "value", + "element_type", + "is_required", + "title" + ] + } + } + } + } }, - "exposure_direction": { + "status": { "type": "string", - "enum": [ - "incoming", - "outgoing", - "connected" - ], - "description": "Whether the funds came from (`incoming`) or went to (`outgoing`) this entity." + "description": "Status of the questionnaire" + } + } + } + }, + "signatures": { + "type": "array", + "description": "Electronic Signature results, one item per signer and signature step, for User and Business Verification. Present only when the workflow has an Electronic Signature step; an empty array until a signer reaches it. See the [Electronic Signature report](/core-technology/electronic-signature/report-electronic-signature).", + "items": { + "$ref": "#/components/schemas/SignatureResult" + } + }, + "aml_screenings": { + "type": "array", + "nullable": true, + "description": "**Array of AML screening reports** — always a JSON array, never a singular `aml` object. `null` until at least one AML step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `total_hits`, `entity_type`, `hits[]`, `score`, `screened_data`, `is_ongoing_monitoring_enabled`, and `warnings[]`.", + "items": { + "$ref": "#/components/schemas/AMLScreening" + } + }, + "ip_analyses": { + "type": "array", + "nullable": true, + "description": "**Array of Device & IP Analysis reports** — always a JSON array, never a singular `ip_analysis` object. `null` until at least one IP-analysis step has produced data. Each item is the full report, including `node_id`, feature-level `status`, IP and geolocation data, device fingerprint, and `warnings[]`. Entries with duplicate `(node_id, ip_address, device_fingerprint)` are deduplicated server-side.", + "items": { + "type": "object", + "properties": { + "node_id": { + "type": "string", + "description": "Unique identifier for this Device & IP Analysis step in the workflow graph." }, - "exposure_type": { + "status": { "type": "string", + "description": "Feature-level status of this Device & IP Analysis entry. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`.", "enum": [ - "direct", - "indirect" - ], - "description": "`direct` (1 hop) or `indirect` (multi-hop) exposure." + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ] }, - "provider_entity_id": { + "device_brand": { "type": "string", - "nullable": true, - "description": "The provider's own identifier for the entity." + "description": "Brand of the device used" }, - "provider_entity_type_id": { + "device_model": { "type": "string", - "nullable": true, - "description": "The provider's own identifier for `entity_type`." + "description": "Model of the device used" }, - "provider_entity_subtype_id": { + "browser_family": { "type": "string", - "nullable": true, - "description": "The provider's own identifier for `entity_subtype`." + "description": "Browser family used" }, - "country": { + "os_family": { "type": "string", - "nullable": true, - "description": "Primary ISO 3166-1 alpha-2 country of the entity. Empty or `null` when the provider does not know it." + "description": "Operating system family" }, - "raw_amount": { - "type": "number", - "nullable": true, - "description": "Amount in the asset's own unit, when the provider reports it." + "platform": { + "type": "string", + "description": "Platform type (mobile, desktop, etc)" }, - "raw_currency": { + "device_fingerprint": { "type": "string", "nullable": true, - "description": "Unit of `raw_amount`, when the provider reports it." - } - } - } - }, - "destination_of_funds": { - "type": "array", - "description": "Where the address sent funds to, attributed by entity. Same item shape as `source_of_funds` with `exposure_direction` = `outgoing`.", - "items": { - "type": "object", - "properties": { - "category": { + "description": "Unique device fingerprint identifier" + }, + "ip_country": { "type": "string", - "description": "Normalised risk category, e.g. `exchange_licensed`, `dex`, `defi`, `payment_processor`, `mixer`, `darknet_market`, `sanctioned`, `stolen_funds`, `high_risk_exchange`." + "description": "Country detected from IP" }, - "percentage": { + "ip_country_code": { + "type": "string", + "description": "Two-letter country code from IP" + }, + "ip_state": { + "type": "string", + "description": "State/region detected from IP" + }, + "ip_city": { + "type": "string", + "description": "City detected from IP" + }, + "latitude": { "type": "number", - "description": "Share of the direction's total exposure, 0-100." + "description": "Latitude coordinate of IP location" }, - "amount_usd": { + "longitude": { "type": "number", - "nullable": true, - "description": "USD value attributed to this entity." + "description": "Longitude coordinate of IP location" }, - "entity_name": { + "ip_address": { "type": "string", - "nullable": true, - "description": "Name of the attributed entity, for example an exchange, a mixer or a sanctioned group." + "description": "IP address" }, - "entity_type": { + "isp": { "type": "string", "nullable": true, - "description": "Provider entity type (e.g. `Exchange`, `DeFi`, `Sanctions`, `Theft`)." + "description": "Internet Service Provider" }, - "entity_subtype": { + "organization": { "type": "string", "nullable": true, - "description": "More granular subtype (e.g. `Mandatory KYC and AML`, `OFAC`, `Yield Aggregator`)." + "description": "Organization associated with IP" }, - "hops": { - "type": "integer", - "nullable": true, - "description": "Number of hops to the entity (1 for a direct counterparty), when the provider reports one. Always `null` for Merkle Science, which reports only whether exposure is direct or indirect: use `exposure_type`." + "is_vpn_or_tor": { + "type": "boolean", + "description": "Whether IP is from VPN or Tor network" }, - "description": { + "is_data_center": { + "type": "boolean", + "description": "Whether IP is from a data center" + }, + "time_zone": { "type": "string", - "nullable": true, - "description": "`entity_type / entity_subtype`, as the provider labels the entity." + "description": "Time zone detected from IP" }, - "is_direct": { - "type": "boolean", - "description": "True when `exposure_type` is `direct`." + "time_zone_offset": { + "type": "string", + "description": "Time zone offset" }, - "exposure_direction": { + "ip": { + "type": "object", + "description": "IP geolocation of this entry plus great-circle distances (km) to the ID document address and the POA document address. Distances are `null` when either side has no resolvable location.", + "properties": { + "location": { + "type": "object", + "nullable": true, + "properties": { + "latitude": { + "type": "number", + "example": 41.2706327 + }, + "longitude": { + "type": "number", + "example": 1.9770097 + } + }, + "description": "Coordinates, or `null` when unavailable." + }, + "distance_from_id_document": { + "type": "number", + "nullable": true, + "example": 23.4 + }, + "distance_from_poa_document": { + "type": "number", + "nullable": true, + "example": 12.3 + } + } + }, + "id_document": { + "type": "object", + "description": "Location extracted from the ID document address plus distances (km) to the IP geolocation and the POA document address. `location` is `null` when no document address could be geocoded.", + "properties": { + "location": { + "type": "object", + "nullable": true, + "properties": { + "latitude": { + "type": "number", + "example": 41.2706327 + }, + "longitude": { + "type": "number", + "example": 1.9770097 + } + }, + "description": "Coordinates, or `null` when unavailable." + }, + "distance_from_ip": { + "type": "number", + "nullable": true, + "example": 23.4 + }, + "distance_from_poa_document": { + "type": "number", + "nullable": true, + "example": 18.7 + } + } + }, + "poa_document": { + "type": "object", + "description": "Location extracted from the Proof of Address document plus distances (km) to the IP geolocation and the ID document address. `location` is `null` when the session has no POA step or the address could not be geocoded.", + "properties": { + "location": { + "type": "object", + "nullable": true, + "properties": { + "latitude": { + "type": "number", + "example": 41.2706327 + }, + "longitude": { + "type": "number", + "example": 1.9770097 + } + }, + "description": "Coordinates, or `null` when unavailable." + }, + "distance_from_ip": { + "type": "number", + "nullable": true, + "example": 12.3 + }, + "distance_from_id_document": { + "type": "number", + "nullable": true, + "example": 18.7 + } + } + }, + "warnings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "enum": [ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE" + ], + "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload." + }, + "risk": { + "type": "string" + }, + "additional_data": { + "type": "object", + "nullable": true + }, + "log_type": { + "type": "string" + }, + "short_description": { + "type": "string" + }, + "long_description": { + "type": "string" + }, + "node_id": { + "type": "string", + "description": "The workflow graph node ID that generated this warning" + } + } + } + }, + "matches": { + "type": "array", + "description": "Cross-session matches found for this entry's IP address or device identity. Every match is another verification session in your application (`source` is always `session`) that shares the same value, always belonging to a different vendor user / `vendor_data` than the current session, capped at 5 per collector (`device_fingerprint` sums two independently capped collectors, so up to 10 entries are possible). Every item carries `match_source`, `confidence`, `match_mode`, `device_info` and `location_info`; the keys `recovery_similarity`, `tls_ja4_corroborated` and `recovery_gate_reason` are present only when `match_source` is `recovered_high`.", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "session_id": { + "type": "string", + "format": "uuid", + "description": "Session ID of the matching session." + }, + "session_number": { + "type": "integer", + "description": "Sequential number of the matching session." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "`vendor_data` of the matching session — always different from the current session's vendor user." + }, + "verification_date": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Creation date of the matching session (`YYYY-MM-DDTHH:MM:SSZ`)." + }, + "match_type": { + "type": "string", + "enum": [ + "ip_address", + "device_fingerprint" + ], + "description": "Whether the match was on IP address or device fingerprint" + }, + "match_source": { + "type": "string", + "enum": [ + "ip_address", + "persistent_id", + "legacy_fp", + "recovered_high" + ], + "description": "How this match was reached: `ip_address` (shared IP address — network co-location, not a device-identity claim), `persistent_id` (exact stored persistent device identifier), `legacy_fp` (legacy `didit-fp-*` device-fingerprint hash), or `recovered_high` (high-confidence fuzzy fingerprint-recovery hit). Lower-confidence recovery candidates surface as risk warnings only, never in this array." + }, + "matched_value": { + "type": "string", + "nullable": true, + "description": "The value that matched: the shared IP address, the persistent device identifier or legacy fingerprint hash, or — for `recovered_high` — the UUID of the recovered device." + }, + "status": { + "type": "string", + "description": "Lifecycle status of the matching session." + }, + "is_blocklisted": { + "type": "boolean", + "description": "Whether the matched value is on a blocklist. Currently always `false` for device/IP matches." + }, + "api_service": { + "type": "string", + "nullable": true, + "description": "Standalone API service of the matching session; `null` for workflow sessions." + }, + "source": { + "type": "string", + "enum": [ + "session" + ], + "description": "Always `session` for device/IP matches — every match is another verification session in your application." + }, + "device_info": { + "type": "object", + "description": "Device details observed on the matching session.", + "properties": { + "device_brand": { + "type": "string", + "nullable": true + }, + "device_model": { + "type": "string", + "nullable": true + }, + "browser_family": { + "type": "string", + "nullable": true + }, + "os_family": { + "type": "string", + "nullable": true + }, + "platform": { + "type": "string", + "nullable": true + }, + "device_fingerprint": { + "type": "string", + "nullable": true + } + } + }, + "location_info": { + "type": "object", + "description": "Network and geolocation details observed on the matching session.", + "properties": { + "ip_address": { + "type": "string", + "nullable": true + }, + "ip_country": { + "type": "string", + "nullable": true + }, + "ip_country_code": { + "type": "string", + "nullable": true + }, + "ip_state": { + "type": "string", + "nullable": true + }, + "ip_city": { + "type": "string", + "nullable": true + }, + "is_vpn_or_tor": { + "type": "boolean" + }, + "is_data_center": { + "type": "boolean" + } + } + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Server-side identification confidence, computed as `1 - P(false positive)`: `1.0` for deterministic matches (`persistent_id`, or hardware-rooted `recovered_high`), `0.5` for `legacy_fp`, `0.0` for `ip_address` (co-location only), and a corroboration-adjusted value below `1.0` for probabilistic recovered matches (higher when the TLS JA4 fingerprint and IP country independently agree)." + }, + "match_mode": { + "type": "string", + "enum": [ + "deterministic", + "probabilistic", + "co_occurrence" + ], + "description": "Evidence class of the match: `deterministic` (exact stored or hardware-rooted identifier that cannot collide between physical devices), `probabilistic` (fuzzy fingerprint evidence), or `co_occurrence` (shared network only, no device-identity claim)." + }, + "recovery_similarity": { + "type": "number", + "description": "Cosine similarity of the fingerprint-recovery match, rounded to 4 decimals. Present only when `match_source` is `recovered_high`." + }, + "tls_ja4_corroborated": { + "type": "boolean", + "description": "Whether the TLS JA4 fingerprints of the two observations agree — a server-observed signal the browser cannot spoof. Present only when `match_source` is `recovered_high`." + }, + "recovery_gate_reason": { + "type": "string", + "description": "Why the recovery match passed the false-positive hard gate: `hardware_root_match` (a hardware-rooted device identifier matched exactly) or, on configurations that admit probe-only evidence, `exact_composite_plus__families` / `very_high_similarity_plus__families`. Present only when `match_source` is `recovered_high`." + } + } + } + } + } + } + }, + "database_validations": { + "type": "array", + "nullable": true, + "description": "**Array of Database Validation reports** — always a JSON array, never a singular `database_validation` object. `null` until at least one DB validation step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `screened_data`, the per-service `validations[]` (each with its registry `source_data`), and `warnings[]`. The `screened_data` and `source_data` fields vary by country:\n\n**ARG (Argentina)**: screened_data: {document_number, selfie, gender, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, date_of_birth, date_of_death, tax_id, tax_id_type, face_match_score, sit_1_since, banks, last_position, highest_position, rejected_checks}. Uses RENAPER biometric face-match validation. Gender is auto-inferred from face analysis or given name when not present in the document (e.g., driver licenses)\n\n**BOL (Bolivia)**: screened_data: {document_number, date_of_birth, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, date_of_birth, gender}\n\n**BRA (Brazil)**: screened_data: {tax_number, first_name, last_name, date_of_birth}. source_data always includes {identification_number, first_name, last_name, date_of_birth, lgpd_minor, minor_under_18, minor_under_16} for successful CPF matches. Successful adult lookups set those flags to false. The Receita Federal Consulta CPF service may return HTTP 422 (minor under 18) or 451 (under 16) with LGPD-withheld data; in those cases source_data sets the minor flags to true as appropriate, includes the upstream HTTP status, and field-level validation is no_match. See the Brazil section of the Database Validation Outcome Codes reference for the full code list.\n\n**CHL (Chile)**: screened_data: {personal_number, first_name, last_name, date_of_birth}. source_data: {identification_number, first_name, last_name, date_of_birth, gender}\n\n**COL (Colombia)**: screened_data: {document_number, document_type, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, date_of_birth, document_type}\n\n**CRI (Costa Rica)**: screened_data: {personal_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name}\n\n**DOM (Dominican Republic)**: screened_data: {personal_number}. source_data: {identification_number} (returns valid/invalid only)\n\n**ECU (Ecuador)**: screened_data: {personal_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, street, formatted_address, gender, nationality, education, marital_status, spouse, mother_name, father_name, profession}\n\n**ESP (Spain)**: screened_data: {personal_number, document_type, expiration_date, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, document_type, expiration_date}\n\n**GTM (Guatemala)**: screened_data: {document_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name}\n\n**HND (Honduras)**: screened_data: {document_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name}\n\n**MEX (Mexico)**: screened_data: {personal_number (CURP), first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, date_of_birth, gender, nationality, curp_status, state_of_birth, doc_probatorio, registration_year, registration_state, registration_municipality, num_acta, crip, issuing_state_code, foreign_registry_number, folio_carta, folio_certificado}. Uses the RENAPO civil registry. A separate INE validity-check service is also available for MEX as an additional one-by-one validation.\n\n**PAN (Panama)**: screened_data: {personal_number, selfie}. source_data: {identification_number}. Uses SIB biometric validation\n\n**PER (Peru)**: screened_data: {personal_number (DNI), first_name, last_name}. source_data: {identification_number, first_name, last_name, paternal_name, maternal_name, verification_number, verification_letter}\n\n**PRY (Paraguay)**: screened_data: {document_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name}\n\n**SLV (El Salvador)**: screened_data: {document_number, date_of_birth, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name}\n\n**URY (Uruguay)**: screened_data: {personal_number, date_of_birth, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name}\n\n**VEN (Venezuela)**: screened_data: {document_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, gender, date_of_birth, document_type}", + "items": { + "type": "object", + "properties": { + "node_id": { + "type": "string", + "description": "Unique identifier for this database validation step in the workflow graph." + }, + "issuing_state": { + "type": "string", + "description": "ISO 3166-1 alpha-3 code of the country whose government/registry sources were queried (e.g. `BRA`, `MEX`, `ESP`, `NGA`, `IDN`). Database Validation covers 60+ countries across the Americas, Europe, Africa, the Middle East and Asia-Pacific, plus global multi-country sources — coverage expands regularly, so treat the value as an open set rather than a fixed enum." + }, + "validation_type": { "type": "string", "enum": [ - "incoming", - "outgoing", - "connected" + "one_by_one", + "two_by_two", + "not_enabled" ], - "description": "Whether the funds came from (`incoming`) or went to (`outgoing`) this entity." + "description": "How the result was corroborated: `one_by_one` (single-service outcome), `two_by_two` (two or more distinct services independently full-matched the identification number), or `not_enabled` (persisted when no service produced a usable match). The value is never `null`. Dedicated biometric 2x2 validation modes are currently available for `ECU` and `PER`." + }, + "screened_data": { + "type": "object", + "description": "The input data used for validation. Fields vary by country — see the Database Validation docs (`/core-technology/database-validation/overview`) for country-specific formats.", + "properties": { + "last_name": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "tax_number": { + "type": "string", + "description": "Brazil: CPF (11 digits)" + }, + "personal_number": { + "type": "string", + "description": "Chile: RUT, DOM: Cédula (11 digits), ECU: Cédula (10 digits), ESP: DNI/NIE, MEX: CURP (18 chars), PER: DNI (8 digits), URY: CI" + }, + "document_number": { + "type": "string", + "description": "ARG: DNI, BOL: CI, COL: Cédula, GTM: DPI, HND: DNI, PAN: Cédula, PRY: CI, SLV: DUI, VEN: Cédula" + }, + "date_of_birth": { + "type": "string", + "format": "date" + }, + "document_type": { + "type": "string", + "description": "COL: CC/CE/TI/NIT, ESP: ID/PASSPORT" + }, + "expiration_date": { + "type": "string", + "format": "date", + "description": "Required for ESP" + } + } + }, + "validations": { + "type": "array", + "description": "One entry per registry service that returned a validation payload. Each entry identifies the service (`service_id`, `service_name`) and carries the per-field `validation` comparison, the service `outcome_code` / `outcome_detail` when reported, and the normalised registry record in `source_data`.", + "items": { + "type": "object", + "properties": { + "service_id": { + "type": "string", + "description": "Identifier of the registry service that produced this validation result.", + "example": "bra_cpf" + }, + "service_name": { + "type": "string", + "description": "Human-readable name of the registry service.", + "example": "Brazil - CPF status check" + }, + "validation": { + "type": "object", + "description": "The results of the field validations.", + "properties": { + "full_name": { + "type": "string", + "description": "Match result for full name (e.g., 'full_match', 'partial_match', 'no_match')." + }, + "date_of_birth": { + "type": "string", + "description": "Match result for date of birth." + }, + "identification_number": { + "type": "string", + "description": "Match result for identification number." + } + } + }, + "outcome_code": { + "type": "string", + "nullable": true, + "description": "Machine-readable outcome of the registry lookup (e.g. `MATCH`, `NO_MATCH`, `PARTIAL_MATCH`, `INCONCLUSIVE`, source-specific codes). Present when the service reports one." + }, + "outcome_detail": { + "type": "string", + "nullable": true, + "description": "Human-readable explanation of `outcome_code`, when provided by the service." + }, + "source_data": { + "type": "object", + "description": "The data returned from the government database. Common fields: identification_number, first_name, last_name, full_name, date_of_birth, gender. Additional fields by country: BRA always includes lgpd_minor, minor_under_18, and minor_under_16 on successful CPF matches; it also includes the upstream HTTP status when CPF data is withheld under LGPD (HTTP 422/451). ECU adds street, formatted_address, nationality, education, marital_status, spouse, mother_name, father_name, profession. PER adds paternal_name, maternal_name, verification_number, verification_letter. ESP adds document_type, expiration_date. VEN/COL add document_type.", + "additionalProperties": true + } + } + } + }, + "errors": { + "type": "array", + "description": "Errors returned by registry services while validating, each tagged with the `service_id` that produced it. **This key is omitted entirely when there are no errors.**", + "items": { + "type": "object", + "description": "The upstream service's error object spread as-is — its keys vary by service (commonly a message/code and any provider-specific details) — plus `service_id` identifying the service that errored.", + "properties": { + "service_id": { + "type": "string", + "nullable": true, + "description": "Identifier of the registry service that errored." + } + }, + "additionalProperties": true + } }, - "exposure_type": { + "match_type": { "type": "string", "enum": [ - "direct", - "indirect" + "no_match", + "partial_match", + "full_match" ], - "description": "`direct` (1 hop) or `indirect` (multi-hop) exposure." - }, - "provider_entity_id": { - "type": "string", "nullable": true, - "description": "The provider's own identifier for the entity." + "description": "The aggregate match outcome across the queried services. `null` until the validation has been evaluated." }, - "provider_entity_type_id": { + "status": { "type": "string", - "nullable": true, - "description": "The provider's own identifier for `entity_type`." + "description": "Feature-level status of this database validation. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`.", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ] }, - "provider_entity_subtype_id": { + "warnings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "enum": [ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE" + ], + "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload." + }, + "risk": { + "type": "string" + }, + "additional_data": { + "type": "object", + "nullable": true + }, + "log_type": { + "type": "string" + }, + "short_description": { + "type": "string" + }, + "long_description": { + "type": "string" + }, + "node_id": { + "type": "string", + "description": "The workflow graph node ID that generated this warning" + } + } + }, + "description": "Warnings related to the database validation." + } + } + } + }, + "reviews": { + "type": "array", + "items": { + "type": "object", + "properties": { + "user": { "type": "string", - "nullable": true, - "description": "The provider's own identifier for `entity_subtype`." + "description": "Who performed the review. Usually the console reviewer's identifier (their email), but the value is `\"API Client\"` for status updates made with an API key and `\"Unknown\"` when no actor could be resolved — so do not parse this field as an email address." }, - "country": { - "type": "string", - "nullable": true, - "description": "Primary ISO 3166-1 alpha-2 country of the entity. Empty or `null` when the provider does not know it." + "new_status": { + "type": "string" }, - "raw_amount": { - "type": "number", - "nullable": true, - "description": "Amount in the asset's own unit, when the provider reports it." + "comment": { + "type": "string" }, - "raw_currency": { + "created_at": { "type": "string", - "nullable": true, - "description": "Unit of `raw_amount`, when the provider reports it." + "format": "date-time" } } } }, - "counterparty_connections": { + "created_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp at which the session was created (ISO 8601, UTC)." + }, + "expires_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Timestamp at which this session is scheduled to expire. Set on User Verification (KYC) sessions when an expiration was configured at creation time (or once `Approved` status triggers KYC expiry computation). `null` for sessions without a configured expiration, and for kinds that do not use expiry (e.g. Business Verification (KYB) sessions that never received an `expires_at`)." + }, + "registry_checks": { "type": "array", - "description": "Entities the provider attributes the screened address to. For Merkle Science these come from the address's own owner and user tags, so the same entity can appear twice, and they carry no fund flows: `received_usd`, `sent_usd`, `received_hops` and `sent_hops` are `null` and `percentage` is 0. Do not route on `risk_level` or `is_direct` here: use `sanctions_hit`, `dominant_risk_category` and `risk_factors[].is_high_risk` for the verdict, and `exposure_type` in `source_of_funds`/`destination_of_funds` for directness.", + "nullable": true, + "description": "Company registry checks. Present only when `session_kind = business`. Each item wraps the full company-registry payload.", "items": { "type": "object", + "description": "One company registry check — the full company-registry payload enriched with ownership structure and warnings. Present only when `session_kind = business`.", "properties": { - "entity_name": { - "type": "string", - "nullable": true, - "description": "Attributed entity name." - }, - "entity_type": { - "type": "string", - "nullable": true, - "description": "Provider entity type." - }, - "entity_subtype": { + "status": { "type": "string", - "nullable": true, - "description": "Provider entity subtype." + "description": "Feature lifecycle status." }, - "risk_level": { + "node_id": { "type": "string", - "description": "Lowercase and provider-dependent. For Merkle Science it is the address's own 0-5 risk grade (`no_risk`, `low`, `medium_low`, `medium`, `high`, `critical`), raised to `high` when the entity's category is high risk; it is not a separate grade of the entity." - }, - "categories": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Normalised risk categories of the entity." - }, - "received_usd": { - "type": "number", - "nullable": true, - "description": "USD received from the entity, when the provider reports it." - }, - "sent_usd": { - "type": "number", - "nullable": true, - "description": "USD sent to the entity, when the provider reports it." - }, - "received_hops": { - "type": "integer", - "nullable": true, - "description": "Hops to the entity on the receiving side, when the provider reports them." - }, - "sent_hops": { - "type": "integer", - "nullable": true, - "description": "Hops to the entity on the sending side, when the provider reports them." - }, - "percentage": { - "type": "number", - "description": "Share of the address's exposure, when the provider reports it; otherwise 0." + "nullable": true }, - "is_direct": { - "type": "boolean", - "description": "Always `true` for Merkle Science, whose connections are attributions rather than counterparties." + "data_resolved": { + "type": "boolean" }, - "country": { - "type": "string", - "nullable": true, - "description": "Country of the entity, when known." + "company": { + "type": "object", + "description": "Full registry payload for the company (registry record merged with any user-provided or console-edited data).", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "node_id": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string" + }, + "registry_status": { + "type": "string", + "nullable": true + }, + "data_resolved": { + "type": "boolean" + }, + "company_name": { + "type": "string" + }, + "registration_number": { + "type": "string", + "nullable": true + }, + "country_code": { + "type": "string", + "description": "Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `DE`, `US`)." + }, + "region": { + "type": "string", + "nullable": true, + "description": "ISO 3166-2 subdivision code when applicable (e.g. `CA`, `NY` for US states)." + }, + "company_type": { + "type": "string", + "nullable": true + }, + "incorporation_date": { + "type": "string", + "format": "date", + "nullable": true + }, + "registered_address": { + "type": "string", + "nullable": true + }, + "tax_number": { + "type": "string", + "nullable": true + }, + "risk_level": { + "type": "string", + "enum": [ + "LOW", + "MEDIUM", + "HIGH" + ], + "nullable": true + }, + "verification_status": { + "type": "string", + "nullable": true + }, + "is_from_registry": { + "type": "boolean" + }, + "fetch_status": { + "type": "string", + "nullable": true + }, + "alternative_names": { + "type": "string", + "nullable": true, + "description": "Alternative, trading, former or original-language company names as text, when reported." + }, + "nature_of_business": { + "type": "string", + "nullable": true + }, + "registered_capital": { + "type": "string", + "nullable": true + }, + "registered_capital_amount": { + "type": "string", + "format": "decimal", + "nullable": true, + "description": "Registered/share capital amount as a decimal string (e.g. `\"100.00\"`), parsed from the registry filing. `null` when not available." + }, + "registered_capital_currency": { + "type": "string", + "nullable": true, + "description": "ISO 4217 currency code of `registered_capital_amount` (e.g. `\"GBP\"`). `null` when not available." + }, + "website": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "format": "email", + "nullable": true + }, + "phone": { + "type": "string", + "nullable": true + }, + "legal_entity_identifier": { + "type": "string", + "nullable": true + }, + "location_of_registration": { + "type": "string", + "nullable": true + }, + "vat_number": { + "type": "string", + "nullable": true, + "description": "EU VAT number, when collected during the hosted registry step or edited afterwards." + }, + "vat_validation_status": { + "type": "string", + "enum": [ + "valid", + "invalid", + "format_valid", + "invalid_format", + "could_not_validate", + "not_applicable" + ], + "description": "Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session." + }, + "vat_validated_number": { + "type": "string", + "nullable": true, + "description": "The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran." + }, + "vat_validation_source": { + "type": "string", + "nullable": true, + "enum": [ + "eu_vies", + "pl_vat_white_list", + "no_brreg", + "ch_uid_register", + "br_receita_federal", + "co_rues", + "cr_hacienda", + "ec_sri", + "py_dnit", + "format_rules", + null + ], + "description": "The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran." + }, + "vat_validated_name": { + "type": "string", + "nullable": true, + "description": "Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." + }, + "vat_validated_address": { + "type": "string", + "nullable": true, + "description": "Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`." + }, + "vat_checked_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "Timestamp of the check. `null` when no check ran." + }, + "financial_summary": { + "type": "object", + "nullable": true + }, + "officers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "designation": { + "type": "string", + "nullable": true + }, + "role": { + "type": "string", + "nullable": true + }, + "nationality": { + "type": "string", + "nullable": true + }, + "is_active": { + "type": "boolean" + }, + "kyc_status": { + "type": "string", + "enum": [ + "Approved", + "Declined", + "Pending" + ], + "nullable": true + }, + "kyc_session_url": { + "type": "string", + "format": "uri", + "nullable": true + }, + "first_name": { + "type": "string", + "nullable": true, + "description": "Given name when explicitly reported by the registry; not inferred from the full name." + }, + "last_name": { + "type": "string", + "nullable": true, + "description": "Family name when explicitly reported by the registry." + }, + "address": { + "type": "string", + "nullable": true, + "description": "Officer address reported by the registry." + }, + "appointment_date": { + "type": "string", + "nullable": true, + "description": "Date the officer was appointed.", + "format": "date" + }, + "termination_date": { + "type": "string", + "nullable": true, + "description": "Date the appointment ended, when reported.", + "format": "date" + }, + "date_of_birth": { + "type": "string", + "nullable": true, + "description": "Complete registry-reported birth date. Null when only a birth year or month is known.", + "format": "date" + }, + "date_of_birth_year": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 9999, + "description": "Registry-reported birth year, including when the full date is unavailable." + }, + "date_of_birth_month": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 12, + "description": "Registry-reported birth month. Null when only the year is known." + } + } + } + }, + "beneficial_owners": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "first_name": { + "type": "string", + "nullable": true + }, + "last_name": { + "type": "string", + "nullable": true + }, + "entity_type": { + "type": "string", + "enum": [ + "person", + "company" + ] + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "ownership_min_shares": { + "type": "string", + "nullable": true, + "description": "Lower bound of the ownership percentage as reported by the registry. Free-form string (e.g. `\"25.00\"`, `\"50\"`); use `effective_ownership_percent` for a parsed numeric value." + }, + "ownership_max_shares": { + "type": "string", + "nullable": true, + "description": "Upper bound of the ownership percentage as reported by the registry. Free-form string; use `effective_ownership_percent` for a parsed numeric value." + }, + "is_active": { + "type": "boolean" + }, + "kyc_status": { + "type": "string", + "enum": [ + "Approved", + "Declined", + "Pending" + ], + "nullable": true + }, + "kyc_session_url": { + "type": "string", + "format": "uri", + "nullable": true + }, + "effective_ownership_percent": { + "type": "number", + "nullable": true + }, + "company_name": { + "type": "string", + "nullable": true + }, + "registration_number": { + "type": "string", + "nullable": true + }, + "country_of_birth": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "nullable": true + }, + "nationality": { + "type": "string", + "nullable": true + }, + "designation": { + "type": "string", + "nullable": true + }, + "voting_min_shares": { + "type": "string", + "nullable": true + }, + "voting_max_shares": { + "type": "string", + "nullable": true + }, + "date_of_birth": { + "type": "string", + "nullable": true, + "description": "Complete birth date when reported.", + "format": "date" + }, + "appointment_date": { + "type": "string", + "nullable": true, + "description": "Appointment date when reported.", + "format": "date" + }, + "shareholding": { + "$ref": "#/components/schemas/KYBRegistryShareholding" + } + } + } + }, + "addresses": { + "type": "array", + "nullable": true, + "description": "Every address the registry states, one canonical shape. Null when it states none.", + "items": { + "$ref": "#/components/schemas/KYBRegistryAddress" + } + }, + "industries": { + "type": "array", + "nullable": true, + "description": "The activities the registry classifies the company under, one canonical shape. Null when it states none.", + "items": { + "$ref": "#/components/schemas/KYBRegistryIndustry" + } + }, + "accounts": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts." + }, + "registry_data": { + "type": "object", + "description": "Raw record returned by the registry source, kept for reviewers. Its shape differs by country and source and is not a stable contract: read the canonical fields (`addresses`, `industries`, `ownership`, `beneficial_owners[].shareholding`) instead." + }, + "user_provided_data": { + "type": "object", + "description": "Data confirmed or edited by the end user." + }, + "confirmed_by_user_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "last_console_edit_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "is_editable": { + "type": "boolean" + }, + "ownership": { + "$ref": "#/components/schemas/KYBRegistryOwnership" + }, + "registry_tier": { + "type": "string", + "nullable": true, + "description": "The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`." + }, + "registry_tier_delivered": { + "type": "string", + "nullable": true, + "description": "The tier the registry delivered and that is billed. Also in `ownership.tier_delivered`." + }, + "registry_ownership_status": { + "type": "string", + "nullable": true, + "description": "`not_requested`, `pending`, `delivered` or `unavailable`. Also in `ownership.status`." + } + } }, - "provider_entity_id": { - "type": "string", - "nullable": true, - "description": "The provider's own identifier for the entity." + "ownership_structure": { + "type": "object", + "description": "Raw ownership snapshot kept for reviewers; its shape differs by registry source and is not a stable contract. Read `company.beneficial_owners` and `company.ownership` instead." + }, + "warnings": { + "type": "array", + "items": { + "type": "object" + } } } } }, - "network_nodes": { - "type": "array", - "items": { - "type": "object" - }, - "description": "Exposure graph nodes for transaction screening. Empty on a wallet screening result." - }, - "network_edges": { + "key_people_checks": { "type": "array", - "items": { - "type": "object" - }, - "description": "Exposure graph edges for transaction screening. Empty on a wallet screening result." - }, - "sanctions_hit": { - "type": "boolean", - "description": "True if the address has direct or indirect sanctions exposure." - }, - "pep_counterparty": { - "type": "boolean", - "description": "Politically exposed person exposure. `null` when no PEP finding is available, which is every result today because no provider reports one; `false` only for an explicit negative screening result. Do not read `null` as a completed PEP check.", - "nullable": true - }, - "dominant_risk_category": { - "type": "string", "nullable": true, - "description": "Highest-weighted high-risk category, or `null` when none is dominant (e.g. `sanctioned`, `mixer`, `stolen_funds`)." - }, - "risk_factors": { - "type": "array", - "description": "Ranked explanation of `risk_score`: what produced this verdict. Providers grade an address on their own model and return only a verdict (Merkle Science returns an integer 0-5), so the score alone is not actionable. Ordered high-risk first, then by the value that moved through each driver, capped at the five strongest; `source_of_funds` and `destination_of_funds` always carry the complete breakdown. A high `risk_score` with no `is_high_risk` entry means the provider reported no sanctions or attributed-entity match and graded the address purely on its own exposure model - `summary` states this explicitly.", + "description": "Aggregate key-people checks. Present only when `session_kind = business`. Each item uses the two-bucket shape (`registry`, `submitted`, `ubo_kyc_summary`).", "items": { "type": "object", + "description": "Aggregate check over the company's officers + UBOs. Uses the `registry` / `submitted` shape. Present only when session_kind = business.", "properties": { - "source": { - "type": "string", - "enum": [ - "attribution", - "exposure" - ], - "description": "`attribution` is what the provider says the address itself is (its own tag or cluster); `exposure` is where its funds actually moved." - }, - "category": { - "type": "string", - "description": "Normalised risk category, same vocabulary as the exposure breakdown." - }, - "label": { - "type": "string", - "description": "The provider's own wording for the driver (e.g. `Gambling / Crypto only`), which is what you would also see in the provider's console." + "status": { + "type": "string" }, - "entity_name": { + "node_id": { "type": "string", - "nullable": true, - "description": "Attributed entity behind the driver, for example a gambling site or an exchange." + "nullable": true }, - "direction": { - "type": "string", - "enum": [ - "incoming", - "outgoing", - "connected", - "unknown", - "attribution" - ], - "description": "Direction of the fund flow, `attribution` when the driver is the address's own tag, or `unknown` when the provider did not report a direction." + "registry": { + "type": "object", + "description": "Parties discovered via the registry check.", + "properties": { + "officers": { + "type": "array", + "description": "Officers discovered via the registry check, each enriched with the status of any child KYC session and AML screening spawned for them.", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "description": "Officer's full name as recorded in the registry." + }, + "designation": { + "type": "string", + "nullable": true, + "description": "Free-text designation from the registry (e.g. `Director`, `Company Secretary`)." + }, + "role": { + "type": "string", + "enum": [ + "director", + "non_executive_director", + "secretary", + "chairman", + "ubo", + "shareholder", + "representative", + "founder", + "legal_advisor", + "authorized_signatory", + "trustee", + "beneficiary", + "settlor", + "protector", + "company_officer", + "investor", + "other" + ], + "description": "Normalized officer role." + }, + "nationality": { + "type": "string", + "nullable": true + }, + "appointment_date": { + "type": "string", + "nullable": true, + "description": "Date the officer was appointed.", + "format": "date" + }, + "termination_date": { + "type": "string", + "nullable": true, + "description": "Date the appointment ended, when reported.", + "format": "date" + }, + "is_active": { + "type": "boolean" + }, + "address": { + "type": "string", + "nullable": true, + "description": "Officer address reported by the registry." + }, + "aml_status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ], + "nullable": true, + "description": "Status of the AML screening run for this officer. `null` when no AML screening was run." + }, + "kyc_status": { + "type": "string", + "enum": [ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User" + ], + "nullable": true, + "description": "Raw lifecycle status of the child KYC session spawned for this officer. This is the full session status enum (not a reduced Approved/Declined/Pending set). `null` when no child KYC session exists." + }, + "kyc_session_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Shareable URL of the child KYC session. `null` when no child session exists." + }, + "kyc_session_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Session id of the child KYC session. Pass it to this same decision endpoint to fetch the person's full KYC report. `null` when no child session exists." + }, + "verification_workflow_type": { + "type": "string", + "nullable": true, + "description": "Base workflow type of the child session's workflow (e.g. `kyc`). `null` for graph-based workflows or when no child session exists." + }, + "verification_workflow_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Workflow id used by the child session. `null` when no child session exists." + }, + "verification_workflow_label": { + "type": "string", + "nullable": true, + "description": "Human-readable label of the child session's workflow. `null` when unset." + }, + "verification_workflow_features": { + "type": "array", + "nullable": true, + "description": "Per-feature progress of the child verification session, e.g. `[{\"feature\": \"ID_VERIFICATION\", \"status\": \"Approved\"}]`. `null` when the child session has no resolvable feature list.", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "description": "Feature display name (e.g. `ID_VERIFICATION`, `LIVENESS`, `AML`)." + }, + "status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ], + "description": "Feature-level status inside the child session." + } + } + } + }, + "didit_internal_id": { + "type": "string", + "nullable": true, + "description": "Stable internal identifier of the verified individual behind the child session — useful for cross-session linking. `null` when no child session exists or the user has not been resolved yet." + }, + "kyc_document_type": { + "type": "string", + "nullable": true, + "description": "Document type presented in the child KYC session (e.g. `P`, `ID`, `DL`). `null` until the child session has an ID verification result." + }, + "kyc_nationality": { + "type": "string", + "nullable": true, + "description": "Nationality extracted from the document in the child KYC session. `null` until available." + }, + "kyc_liveness_passed": { + "type": "boolean", + "nullable": true, + "description": "Whether the child KYC session's liveness check passed. `null` when no liveness check has run." + }, + "kyc_verified_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the child KYC session reached `Approved`. `null` while the child session has any other status." + }, + "kyc_aml_categories": { + "type": "object", + "nullable": true, + "description": "Per-category summary derived from the person's AML hits. `null` when there are no hits.", + "properties": { + "sanctions": { + "type": "string", + "enum": [ + "match", + "clear" + ] + }, + "pep": { + "type": "string", + "enum": [ + "match", + "clear" + ] + }, + "adverse_media": { + "type": "string", + "enum": [ + "match", + "clear" + ] + }, + "warnings": { + "type": "string", + "enum": [ + "match", + "clear" + ] + } + } + }, + "kyc_notification_sent_at": { + "type": "string", + "nullable": true, + "description": "ISO 8601 timestamp of the last verification-invite notification sent for this person's child session. `null` when none was sent." + }, + "first_name": { + "type": "string", + "nullable": true, + "description": "Given name when explicitly reported by the registry; not inferred from the full name." + }, + "last_name": { + "type": "string", + "nullable": true, + "description": "Family name when explicitly reported by the registry." + }, + "date_of_birth": { + "type": "string", + "nullable": true, + "description": "Complete registry-reported birth date. Null when only a birth year or month is known.", + "format": "date" + }, + "date_of_birth_year": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 9999, + "description": "Registry-reported birth year, including when the full date is unavailable." + }, + "date_of_birth_month": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 12, + "description": "Registry-reported birth month. Null when only the year is known." + } + } + } + }, + "beneficial_owners": { + "type": "array", + "description": "Beneficial owners discovered via the registry check, each enriched with the status of any child KYC session and AML screening spawned for them.", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "description": "Beneficial owner's full name (or company name for corporate UBOs)." + }, + "first_name": { + "type": "string", + "nullable": true + }, + "last_name": { + "type": "string", + "nullable": true + }, + "date_of_birth": { + "type": "string", + "nullable": true, + "description": "Complete birth date when reported.", + "format": "date" + }, + "country_of_birth": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "nullable": true + }, + "company_name": { + "type": "string", + "nullable": true + }, + "registration_number": { + "type": "string", + "nullable": true + }, + "entity_type": { + "type": "string", + "enum": [ + "person", + "company" + ] + }, + "nationality": { + "type": "string", + "nullable": true + }, + "designation": { + "type": "string", + "nullable": true + }, + "roles": { + "type": "array", + "description": "Role names held by this owner, e.g. `[\"ubo\", \"shareholder\"]`.", + "items": { + "type": "string" + } + }, + "ownership_min_shares": { + "type": "string", + "nullable": true, + "description": "Lower bound of the ownership percentage as reported by the registry. Free-form string (e.g. `\"25\"`, `\"25-50%\"`); use `effective_ownership_percent` for a parsed numeric value." + }, + "ownership_max_shares": { + "type": "string", + "nullable": true, + "description": "Upper bound of the ownership percentage as reported by the registry. Free-form string; use `effective_ownership_percent` for a parsed numeric value." + }, + "is_active": { + "type": "boolean" + }, + "aml_status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ], + "nullable": true, + "description": "Status of the AML screening run for this beneficial owner. `null` when no AML screening was run." + }, + "kyc_status": { + "type": "string", + "enum": [ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User" + ], + "nullable": true, + "description": "Raw lifecycle status of the child KYC session spawned for this beneficial owner. This is the full session status enum (not a reduced Approved/Declined/Pending set). `null` when no child KYC session exists." + }, + "kyc_session_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Shareable URL of the child KYC session. `null` when no child session exists." + }, + "kyc_session_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Session id of the child KYC session. Pass it to this same decision endpoint to fetch the person's full KYC report. `null` when no child session exists." + }, + "verification_workflow_type": { + "type": "string", + "nullable": true, + "description": "Base workflow type of the child session's workflow (e.g. `kyc`). `null` for graph-based workflows or when no child session exists." + }, + "verification_workflow_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Workflow id used by the child session. `null` when no child session exists." + }, + "verification_workflow_label": { + "type": "string", + "nullable": true, + "description": "Human-readable label of the child session's workflow. `null` when unset." + }, + "verification_workflow_features": { + "type": "array", + "nullable": true, + "description": "Per-feature progress of the child verification session, e.g. `[{\"feature\": \"ID_VERIFICATION\", \"status\": \"Approved\"}]`. `null` when the child session has no resolvable feature list.", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "description": "Feature display name (e.g. `ID_VERIFICATION`, `LIVENESS`, `AML`)." + }, + "status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ], + "description": "Feature-level status inside the child session." + } + } + } + }, + "didit_internal_id": { + "type": "string", + "nullable": true, + "description": "Stable internal identifier of the verified individual behind the child session — useful for cross-session linking. `null` when no child session exists or the user has not been resolved yet." + }, + "kyc_document_type": { + "type": "string", + "nullable": true, + "description": "Document type presented in the child KYC session (e.g. `P`, `ID`, `DL`). `null` until the child session has an ID verification result." + }, + "kyc_nationality": { + "type": "string", + "nullable": true, + "description": "Nationality extracted from the document in the child KYC session. `null` until available." + }, + "kyc_liveness_passed": { + "type": "boolean", + "nullable": true, + "description": "Whether the child KYC session's liveness check passed. `null` when no liveness check has run." + }, + "kyc_verified_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the child KYC session reached `Approved`. `null` while the child session has any other status." + }, + "kyc_aml_categories": { + "type": "object", + "nullable": true, + "description": "Per-category summary derived from the person's AML hits. `null` when there are no hits.", + "properties": { + "sanctions": { + "type": "string", + "enum": [ + "match", + "clear" + ] + }, + "pep": { + "type": "string", + "enum": [ + "match", + "clear" + ] + }, + "adverse_media": { + "type": "string", + "enum": [ + "match", + "clear" + ] + }, + "warnings": { + "type": "string", + "enum": [ + "match", + "clear" + ] + } + } + }, + "kyc_notification_sent_at": { + "type": "string", + "nullable": true, + "description": "ISO 8601 timestamp of the last verification-invite notification sent for this person's child session. `null` when none was sent." + }, + "child_beneficial_owners": { + "type": "array", + "description": "For corporate UBOs: the owning entity's own beneficial owners, as raw objects extracted from the registry. Empty array when not applicable.", + "items": { + "type": "object" + } + }, + "effective_ownership_percent": { + "type": "number", + "nullable": true, + "description": "Parsed numeric ownership percentage — the max bound when available, otherwise the min bound. `null` when neither bound could be parsed." + }, + "voting_min_shares": { + "type": "string", + "nullable": true + }, + "voting_max_shares": { + "type": "string", + "nullable": true + }, + "appointment_date": { + "type": "string", + "nullable": true, + "description": "Appointment date when reported.", + "format": "date" + } + } + } + } + } }, - "exposure_type": { - "type": "string", - "enum": [ - "direct", - "indirect", - "unknown", - "attribution" - ], - "description": "`direct` (1 hop) or `indirect` (multi-hop), `attribution` for the address's own tag, or `unknown` when the provider did not state how many hops away the counterparty is. Treat `unknown` as unclassified, not as `direct`." + "submitted": { + "type": "object", + "description": "Parties submitted by the business admin during the Key People flow (BusinessParty rows where source=USER).", + "properties": { + "parties": { + "type": "array", + "items": { + "type": "object", + "description": "Party submitted by the business admin via the Key People flow (source=USER).", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "entity_type": { + "type": "string", + "enum": [ + "person", + "company" + ] + }, + "source": { + "type": "string", + "enum": [ + "USER" + ], + "description": "Always `USER` here — this bucket only contains parties entered or confirmed by the end user in the Key People flow (registry-discovered people appear under `registry`)." + }, + "name": { + "type": "string" + }, + "first_name": { + "type": "string", + "nullable": true + }, + "last_name": { + "type": "string", + "nullable": true + }, + "company_name": { + "type": "string", + "nullable": true + }, + "registration_number": { + "type": "string", + "nullable": true + }, + "email": { + "type": "string", + "format": "email", + "nullable": true + }, + "nationality": { + "type": "string", + "nullable": true, + "description": "Nationality for persons; country of incorporation for company parties." + }, + "phone_number": { + "type": "string", + "nullable": true + }, + "date_of_birth": { + "type": "string", + "format": "date", + "nullable": true + }, + "position": { + "type": "string", + "nullable": true, + "description": "Free-text job title / position (persons only)." + }, + "custom_fields": { + "type": "object", + "description": "Workflow-defined custom fields collected for this party; keys are defined by the workflow's key-people field descriptors." + }, + "is_active": { + "type": "boolean" + }, + "is_skipped": { + "type": "boolean", + "description": "True when the end user skipped verification for this party (only possible when the role allows skipping)." + }, + "requires_verification": { + "type": "boolean", + "description": "True when a child KYC / KYB sub-session is required for this party based on role settings and ownership thresholds." + }, + "roles": { + "type": "array", + "description": "All roles held by this party, each with its own ownership and voting percentages. There is no scalar `role`/`ownership_percent` on the party itself.", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "director", + "non_executive_director", + "secretary", + "chairman", + "ubo", + "shareholder", + "representative", + "founder", + "legal_advisor", + "authorized_signatory", + "trustee", + "beneficiary", + "settlor", + "protector", + "company_officer", + "investor", + "other" + ] + }, + "ownership_percent": { + "type": "number", + "nullable": true + }, + "voting_percent": { + "type": "number", + "nullable": true + } + } + } + }, + "kyc_session_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Child KYC session id for person parties requiring verification. `null` otherwise." + }, + "kyc_session_status": { + "type": "string", + "enum": [ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User" + ], + "nullable": true, + "description": "Lifecycle status of the child KYC session. `null` when no child KYC session exists." + }, + "kyc_session_url": { + "type": "string", + "format": "uri", + "nullable": true + }, + "kyb_sub_session_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Child KYB sub-session id for corporate parties requiring verification. `null` otherwise." + }, + "kyb_sub_session_status": { + "type": "string", + "enum": [ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User" + ], + "nullable": true, + "description": "Lifecycle status of the child KYB sub-session. `null` when no sub-session exists." + }, + "kyb_sub_session_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Shareable URL of the child KYB sub-session. `null` when no sub-session exists." + }, + "verification_workflow_type": { + "type": "string", + "nullable": true, + "description": "Base workflow type of the child session's workflow (KYC session preferred, falling back to the KYB sub-session). `null` for graph-based workflows or when no child session exists." + }, + "verification_workflow_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "Workflow id used by the child session. `null` when no child session exists." + }, + "verification_workflow_label": { + "type": "string", + "nullable": true, + "description": "Human-readable label of the child session's workflow. `null` when unset." + }, + "verification_workflow_features": { + "type": "array", + "nullable": true, + "description": "Per-feature progress of the child verification session, e.g. `[{\"feature\": \"ID_VERIFICATION\", \"status\": \"Approved\"}]`. `null` when the child session has no resolvable feature list.", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "description": "Feature display name (e.g. `ID_VERIFICATION`, `LIVENESS`, `AML`)." + }, + "status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ], + "description": "Feature-level status inside the child session." + } + } + } + }, + "didit_internal_id": { + "type": "string", + "nullable": true, + "description": "Stable internal identifier of the verified individual behind the child session. `null` when not resolved." + } + } + } + } + } }, - "amount_usd": { - "type": "number", - "format": "float", + "ubo_kyc_summary": { + "type": "object", "nullable": true, - "description": "USD value attributed to this driver." - }, - "percentage": { - "type": "number", - "format": "float", - "description": "Share of the address's TOTAL screened volume, both directions combined - unlike `percentage` inside `source_of_funds`/`destination_of_funds`, which is normalised per direction. Comparable and summable across the whole list." - }, - "is_high_risk": { - "type": "boolean", - "description": "Whether the category is one Didit treats as high risk (`sanctioned`, `mixer`, `stolen_funds`, `ransomware`, `scam`, `darknet_market`, `terrorist_financing`, `child_exploitation`, `hacked_exchange`, `high_risk_exchange`, `seized_funds`)." + "description": "Aggregate UBO KYC progress, or null when no UBOs have linked KYC sessions.", + "properties": { + "total": { + "type": "integer" + }, + "approved": { + "type": "integer" + }, + "flagged": { + "type": "integer", + "description": "UBOs whose KYC is Declined or In Review." + }, + "pending": { + "type": "integer" + } + } }, - "description": { - "type": "string", - "description": "One-line rendering of the driver, e.g. `9.01% indirect outgoing exposure to Gambling / Crypto only (Example Gambling)`." + "warnings": { + "type": "array", + "items": { + "type": "object" + } } } } }, - "raw_response": { - "type": "object", - "additionalProperties": true, - "description": "The provider's own payload. Its shape is provider-specific and can change without notice: read the normalised fields instead of parsing it." - }, - "report_signature": { - "type": "string", - "description": "Opaque HMAC-SHA256 signature over this exact result, scoped to your application. Not meaningful on its own — repost the full response body, unmodified and including this field, to `POST /v3/wallet-screening/pdf/` to render it as a PDF. The PDF endpoint rejects the request with `400` if `report_signature` is missing or if any signed field (e.g. `risk_score`, `severity`, `sanctions_hit`) was edited before repost." - } - }, - "required": [ - "provider", - "screening_type", - "risk_score", - "severity", - "status", - "summary", - "wallet_address", - "blockchain", - "sanctions_hit", - "dominant_risk_category", - "source_of_funds", - "destination_of_funds", - "counterparty_connections", - "report_signature" - ] - }, - "NetworkRiskDisplay": { - "type": "object", - "properties": { - "score": { - "type": "integer", - "description": "Risk score, 0-100." - }, - "band": { - "type": "string", - "enum": [ - "low", - "medium", - "high" - ], - "description": "Band the score renders as. high >= 70, medium >= 40, otherwise low." - }, - "bar": { - "type": "number", - "format": "float", - "description": "Normalized 0-1 value for rendering a risk bar." - } - } - }, - "NetworkNodeTag": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "color": { - "type": "string", - "nullable": true, - "description": "Null for guard tags, whose color token the renderer owns." - }, - "source": { - "type": "string", - "enum": [ - "subject_tag", - "session_tag", - "transaction_tag", - "signal_guard" - ], - "description": "Where the tag came from: a tag on the user/business, a tag on a session in this network, a tag on a transaction a party belongs to, or a signal guard condition (e.g. hub, datacenter IP)." - }, - "category": { - "type": "string", - "enum": [ - "customer", - "quality" - ], - "nullable": true, - "description": "customer for customer-defined tags, quality for signal-quality guard tags. Null when not applicable." - } - } - }, - "NetworkListItem": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "network_id": { - "type": "string", - "description": "Human-readable per-organization sequential id, e.g. N-0001." - }, - "name": { - "type": "string", - "nullable": true - }, - "status": { - "type": "string", - "enum": [ - "active", - "in_review", - "resolved", - "dismissed" - ], - "description": "Network lifecycle status. Distinct from the per-applicant outcome_status inside a network (approved/in_review/declined/not_completed)." - }, - "risk": { - "$ref": "#/components/schemas/NetworkRiskDisplay" - }, - "pattern_types": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "exact_same_device", - "similar_device", - "same_ip_address", - "same_address", - "similar_selfie_backgrounds", - "similar_poa_documents", - "same_document_number", - "same_phone_number", - "same_email" - ] - }, - "description": "The clustering patterns detected in this network." - }, - "signal_types": { + "document_verifications": { "type": "array", - "items": { - "type": "string", - "enum": [ - "ip_address", - "device", - "face", - "document_number", - "phone", - "email", - "address" - ] - }, - "description": "The distinct signal types present in this network." - }, - "member_counts": { - "type": "object", - "description": "Counts of members by category (e.g. total, per outcome_status)." - }, - "member_status": { - "type": "object", - "description": "Breakdown of members by outcome_status." - }, - "signal_coverage": { - "type": "object", - "description": "Per signal_type coverage of this network's members." - }, - "first_activity_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "last_activity_at": { - "type": "string", - "format": "date-time", - "nullable": true - } - } - }, - "NetworkDetail": { - "allOf": [ - { - "$ref": "#/components/schemas/NetworkListItem" - }, - { - "type": "object", - "properties": { - "stats": { - "type": "object", - "description": "Aggregate stats for the network (e.g. member totals)." - }, - "fraud_tags": { - "type": "array", - "items": { - "type": "object" - }, - "description": "Fraud-pattern tags surfaced on the network." - }, - "risk_signals": { - "type": "object", - "description": "Risk-signal summaries: residential_proxy_reuse, email_intelligence, phone_intelligence." - }, - "reviewed_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "reviewed_by": { - "type": "string", - "nullable": true - }, - "review_reason": { - "type": "string", - "nullable": true - } - } - } - ], - "properties": { - "notes_count": { - "type": "integer", - "minimum": 0, - "readOnly": true, - "description": "Number of private analyst notes in this investigation." - } - } - }, - "NetworkGraphNode": { - "type": "object", - "description": "A subject node or a signal node, distinguished by kind. Subject nodes (kind: vendor_user | vendor_business | transaction_party) carry id, kind, subject, node_status (approved | in_review | declined | not_completed), and tags. Signal nodes (kind: signal) carry id, kind, uuid, signal_type, subject_count, coverage, is_hub, is_datacenter_ip, is_high_collision_device, node_status (signal_active | signal_guarded), tags, risk_evidence, first_seen_at, and last_seen_at.", - "properties": { - "id": { - "type": "string" - }, - "kind": { - "type": "string", - "enum": [ - "vendor_user", - "vendor_business", - "transaction_party", - "signal" - ] - } - } - }, - "NetworkGraphEdge": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "source": { - "type": "string", - "description": "Subject node id." - }, - "target": { - "type": "string", - "description": "Signal node id." - }, - "subject_kind": { - "type": "string", - "enum": [ - "vendor_user", - "vendor_business", - "transaction_party" - ] - }, - "signal_type": { - "type": "string", - "enum": [ - "ip_address", - "device", - "face", - "document_number", - "phone", - "email", - "address" - ] - }, - "confidence": { - "type": "number", - "format": "float", - "description": "1.0 for an exact match." - }, - "observation_count": { - "type": "integer" - }, - "first_seen_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "last_seen_at": { - "type": "string", - "format": "date-time", - "nullable": true - } - } - }, - "NetworkGraph": { - "type": "object", - "properties": { - "network": { - "$ref": "#/components/schemas/NetworkDetail" - }, - "depth": { - "type": "integer", "nullable": true, - "description": "Effective depth applied, or null when unbounded (depth=all)." - }, - "truncated": { - "type": "boolean", - "description": "True if the neighborhood was cut off by depth." - }, - "nodes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NetworkGraphNode" - } - }, - "edges": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NetworkGraphEdge" - } - }, - "metrics": { - "type": "object", - "properties": { - "node_count": { - "type": "integer" - }, - "edge_count": { - "type": "integer" - }, - "hub_signal_count": { - "type": "integer" - } - } - } - } - }, - "NetworkMember": { - "type": "object", - "properties": { - "subject": { - "type": "object", - "description": "The vendor_user, vendor_business, or transaction_party this member represents." - }, - "outcome_status": { - "type": "string", - "description": "The subject's verification/decision outcome." - }, - "node_status": { - "type": "string", - "enum": [ - "approved", - "in_review", - "declined", - "not_completed", - "signal_active", - "signal_guarded" - ], - "description": "For a member row this is always one of the subject-outcome values: approved, in_review, declined, or not_completed." - }, - "tags": { - "type": "array", + "description": "Document verification checks grouped by workflow node. Present only when `session_kind = business`.", "items": { - "$ref": "#/components/schemas/NetworkNodeTag" - } - }, - "risk": { - "$ref": "#/components/schemas/NetworkRiskDisplay" - }, - "signal_coverage": { - "type": "object" - }, - "shared_infrastructure": { - "type": "object" - }, - "joined_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "last_seen_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "provenance": { - "type": "object", - "properties": { - "session_ids": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - } - }, - "business_session_ids": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - } - }, - "transaction_ids": { - "type": "array", - "items": { + "type": "object", + "description": "Documents grouped by workflow node. Present only when session_kind = business.", + "properties": { + "status": { + "type": "string" + }, + "node_id": { "type": "string", - "format": "uuid" + "nullable": true + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "document_type": { + "type": "string", + "enum": [ + "LEGAL_PRESENCE", + "COMPANY_DETAILS", + "OWNERSHIP_STRUCTURE", + "REPRESENTATIVES_AUTHORIZATION", + "OTHER" + ], + "nullable": true, + "description": "High-level KYB document group the file was classified into. `null` while unclassified." + }, + "document_subtype": { + "type": "string", + "nullable": true, + "description": "Specific OCR-detected subtype (e.g. `CERTIFICATE_OF_INCORPORATION`, `SHAREHOLDER_REGISTRY`); `UNKNOWN` when the OCR could not confidently classify the document." + }, + "document_group": { + "type": "string", + "enum": [ + "LEGAL_PRESENCE", + "COMPANY_DETAILS", + "OWNERSHIP_STRUCTURE", + "REPRESENTATIVES_AUTHORIZATION", + "OTHER" + ], + "nullable": true, + "description": "Alias of `document_type` — the group name used by `groups` and `required_groups`." + }, + "file_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Short-lived presigned link to the uploaded file. `null` for analyst-created document request placeholders (`is_requested = true`) that have not been fulfilled yet." + }, + "original_filename": { + "type": "string", + "nullable": true, + "description": "Filename as uploaded by the user or analyst." + }, + "file_size": { + "type": "integer", + "nullable": true, + "description": "File size in bytes." + }, + "status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ], + "description": "Per-document verification status." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "cross_check_result": { + "type": "object", + "nullable": true, + "description": "Cross-reference of the OCR-extracted document fields against the registry company profile. An object with `critical`, `non_critical` and `people` sections, each mapping a field name (e.g. `company_name`, `registration_number`, `incorporation_date`, `registered_address`) to a comparison record like `{\"document\": \"...\", \"registry\": \"...\", \"match\": true, \"score\": 0.97}`. `null` when the document had no OCR data or there was no registry company to compare against." + }, + "document_metadata": { + "type": "object", + "nullable": true, + "description": "Metadata extracted from the uploaded corporate document. When metadata extraction succeeded, all 16 keys are always present (individual values are `null` when not applicable to the file type); the whole object is `null` when nothing could be extracted. For PDFs, `overlay_manipulation` carries forensic overlay-text analysis.", + "properties": { + "file_size": { + "type": "integer", + "nullable": true, + "description": "Size of the document." + }, + "content_type": { + "type": "string", + "nullable": true, + "description": "Content type of the document." + }, + "creation_date": { + "type": "string", + "nullable": true, + "description": "Date when the document was created." + }, + "modified_date": { + "type": "string", + "nullable": true, + "description": "Date when the document was modified." + }, + "creator": { + "type": "string", + "nullable": true, + "description": "Authoring tool/user recorded in the file metadata (PDF `Creator`)." + }, + "producer": { + "type": "string", + "nullable": true, + "description": "Software that produced the file (PDF `Producer`)." + }, + "software": { + "type": "string", + "nullable": true, + "description": "Editing software recorded in the file metadata." + }, + "encryption": { + "type": "string", + "nullable": true, + "description": "Encryption scheme of the file, when encrypted." + }, + "is_signed": { + "type": "boolean", + "nullable": true, + "description": "Whether the document carries a digital signature." + }, + "is_tampered": { + "type": "boolean", + "nullable": true, + "description": "Whether the digital signature failed validation (post-signing modification)." + }, + "signature_info": { + "type": "object", + "nullable": true, + "description": "Details of the digital signature(s), when present." + }, + "exif_original_date": { + "type": "string", + "nullable": true, + "description": "EXIF original capture date, for image uploads." + }, + "exif_digitized_date": { + "type": "string", + "nullable": true, + "description": "EXIF digitization date, for image uploads." + }, + "processed_by_known_editor": { + "type": "boolean", + "nullable": true, + "description": "Whether metadata indicates the file passed through a known image/PDF editor." + }, + "has_different_creation_mod_date": { + "type": "boolean", + "nullable": true, + "description": "Whether creation and modification dates differ — a possible (but not conclusive) editing signal." + }, + "overlay_manipulation": { + "type": "object", + "nullable": true, + "description": "PDF forensic analysis for suspected overlay-text manipulation. Null when no overlay manipulation evidence was found or the document was not a PDF.", + "properties": { + "detected": { + "type": "boolean", + "description": "Whether overlay-text manipulation evidence was detected." + }, + "analyzed": { + "type": "boolean", + "description": "Whether the PDF could be analyzed." + }, + "signals": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Forensic signals that fired, such as duplicate_font_subset or glyph_fragmentation." + }, + "manipulated_regions": { + "type": "array", + "description": "Suspected manipulated areas in PDF page coordinates.", + "items": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "width": { + "type": "number" + }, + "height": { + "type": "number" + }, + "page_width": { + "type": "number" + }, + "page_height": { + "type": "number" + } + } + } + } + } + } + } + }, + "description": { + "type": "string", + "nullable": true, + "description": "Analyst-provided label or reason for a document request. `null` for regular uploads." + }, + "is_requested": { + "type": "boolean", + "description": "True for analyst-created document request placeholders awaiting an upload." + }, + "ocr_data": { + "type": "object", + "nullable": true + }, + "is_console_upload": { + "type": "boolean", + "description": "True for documents uploaded from the console rather than the end-user flow. Console uploads never change the feature-level aggregate status." + }, + "is_confirmed": { + "type": "boolean", + "description": "Console uploads start `false` and flip `true` once the admin confirms them; end-user uploads are always `true`." + } + } + } + }, + "groups": { + "type": "object", + "description": "Per-group progress counters keyed by group name (e.g. legal_presence, ownership_structure).", + "additionalProperties": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "approved": { + "type": "integer" + }, + "pending": { + "type": "integer" + }, + "declined": { + "type": "integer" + }, + "in_review": { + "type": "integer" + }, + "other": { + "type": "integer" + }, + "missing": { + "type": "integer", + "description": "Requested placeholders with no file uploaded yet." + } + } + } + }, + "required_groups": { + "type": "array", + "items": { + "type": "string" + } + }, + "warnings": { + "type": "array", + "items": { + "type": "object" + } } } } - } - } - }, - "NetworkSignal": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "signal_type": { - "type": "string", - "enum": [ - "ip_address", - "device", - "face", - "document_number", - "phone", - "email", - "address" - ] - }, - "subject_count": { - "type": "integer" - }, - "coverage": { - "type": "number", - "format": "float" - }, - "is_hub": { - "type": "boolean", - "description": "True when this signal links an unusually large number of subjects." - }, - "is_datacenter_ip": { - "type": "boolean" - }, - "is_high_collision_device": { - "type": "boolean" }, - "node_status": { + "environment": { "type": "string", "enum": [ - "signal_active", - "signal_guarded" - ] - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NetworkNodeTag" - } - }, - "risk_evidence": { - "type": "object" - }, - "first_seen_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "last_seen_at": { - "type": "string", - "format": "date-time", - "nullable": true - } - } - }, - "NetworkTimelineEvent": { - "type": "object", - "properties": { - "event_type": { - "type": "string", - "description": "e.g. network_detected, signal_observed, network_in_review, network_resolved, network_dismissed." - }, - "occurred_at": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "label": { - "type": "string" - }, - "metadata": { - "type": "object" - } - } - }, - "NetworkMap": { - "type": "object", - "properties": { - "network": { - "$ref": "#/components/schemas/NetworkDetail" - }, - "convergence_count": { - "type": "integer", - "description": "Number of distinct address/location points members converge on." - }, - "countries": { - "type": "array", - "items": { - "type": "string" - } + "live", + "sandbox" + ], + "description": "Whether the session was created by a live application (`live`) or a sandbox application (`sandbox`). Sandbox sessions are free, fully simulated, and never bill credits." }, - "points": { + "resubmissions": { "type": "array", + "description": "Returned only with `include=resubmissions`, for user and business sessions. One entry per resubmission request, oldest first. Each entry carries the results the resubmitted steps returned before the request, so earlier submissions stay readable. Deleting the session deletes every submission.", "items": { "type": "object", "properties": { - "uuid": { - "type": "string", - "format": "uuid" + "number": { + "type": "integer", + "description": "1 for the first resubmission of the session, 2 for the second, and so on." }, - "country": { + "created_at": { "type": "string", - "nullable": true + "format": "date-time", + "description": "When the resubmission was requested." }, - "country_code": { + "previous_status": { "type": "string", - "nullable": true + "description": "The session status just before the request." }, - "city": { - "type": "string", - "nullable": true + "nodes": { + "type": "array", + "description": "The workflow steps sent back to the user.", + "items": { + "type": "object", + "properties": { + "node_id": { + "type": "string" + }, + "feature": { + "type": "string" + } + } + } }, - "latitude": { - "type": "number", - "format": "float", - "nullable": true + "reasons": { + "type": "object", + "nullable": true, + "additionalProperties": { + "type": "string" + }, + "description": "The reason given for each step, keyed by `node_id`." }, - "longitude": { - "type": "number", - "format": "float", - "nullable": true + "requested_by": { + "type": "object", + "nullable": true, + "description": "Who requested the resubmission.", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "type": { + "type": "string" + } + } }, - "observed_at": { - "type": "string", - "format": "date-time", - "nullable": true + "results": { + "type": "object", + "additionalProperties": true, + "description": "What the resubmitted steps returned before the request, under the same keys and in the same shape as the current results of this response (`id_verifications`, `nfc_verifications`, `liveness_checks`, `face_matches`, `poa_verifications`, `phone_verifications`, `email_verifications`, `aml_screenings`, `ip_analyses`, `database_validations`, `questionnaire_responses`, `document_ai_documents`, `geolocation_checks`; for business sessions also `document_verifications`). Only the keys of the steps that were sent back appear." } } } + }, + "investigator": { + "$ref": "#/components/schemas/InvestigatorBlock" } } }, - "EntityNetworkMembership": { + "WorkflowDetail": { "type": "object", + "description": "The fields below are typed; the object also carries every other column of the workflow configuration (feature toggles, thresholds, warning actions, allowed documents, callbacks) as additional properties. Fields that do not apply to the `workflow_type` are `null`.", "properties": { - "network": { - "$ref": "#/components/schemas/NetworkListItem" - }, - "subject": { - "type": "object" - }, - "signal_types": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "ip_address", - "device", - "face", - "document_number", - "phone", - "email", - "address" - ] - } - }, - "signal_count": { - "type": "integer" - }, - "joined_at": { + "uuid": { "type": "string", - "format": "date-time", - "nullable": true + "format": "uuid", + "description": "Per-version identifier (`settings_uuid`). Every version of a workflow has its own." }, - "last_seen_at": { - "type": "string", - "format": "date-time", - "nullable": true - } - } - }, - "RiskSignals": { - "type": "object", - "description": "Optional device and network context observed by your application. All fields are optional. Passing them feeds cross-session match detection in Networks.", - "properties": { - "ip": { + "workflow_id": { "type": "string", - "format": "ip", - "description": "IPv4 or IPv6 address observed for the end user.", - "example": "192.0.2.1" + "format": "uuid", + "description": "Stable identifier shared by every version of the workflow. Sessions created with it use the latest published version." }, - "device_id": { + "workflow_label": { "type": "string", - "maxLength": 255, - "description": "Stable device identifier collected by your application (Android `ANDROID_ID`, iOS `identifierForVendor`)." + "description": "Display name of the workflow." }, - "device_platform": { + "workflow_type": { "type": "string", + "nullable": true, "enum": [ - "android", - "ios", - "ipados", - "tvos", - "web" + "kyc", + "kyb", + "adaptive_age_verification", + "biometric_authentication", + "address_verification", + "questionnaire_verification", + "email_verification", + "phone_verification", + null ], - "description": "Platform of the end user's device." + "description": "Workflow template. `null` for graph workflows created without a template." }, - "device_model": { - "type": "string", - "maxLength": 255, - "description": "Device model reported by your application." + "is_default": { + "type": "boolean", + "description": "Whether this is the application's default workflow." }, - "os_version": { - "type": "string", - "maxLength": 64, - "description": "Operating-system version." + "is_archived": { + "type": "boolean", + "description": "Archived workflows cannot start new sessions." }, - "app_version": { - "type": "string", - "maxLength": 64, - "description": "Version of your application." + "version": { + "type": "integer", + "description": "Version number within the workflow." }, - "user_agent": { - "type": "string", - "maxLength": 512, - "description": "Browser or app user agent." - } - } - }, - "PhoneRiskRequest": { - "type": "object", - "required": [ - "phone_number" - ], - "properties": { - "phone_number": { + "status": { "type": "string", - "maxLength": 20, - "description": "Phone number to score. It is validated and normalized to E.164.", - "example": "+14155552671" + "enum": [ + "draft", + "published" + ], + "description": "Draft versions are editable; published versions are immutable and used for new sessions." }, - "country_code": { + "published_at": { "type": "string", - "maxLength": 3, - "description": "Optional ISO 3166-1 alpha-2 or alpha-3 country hint for intelligence coverage.", - "example": "US" + "format": "date-time", + "nullable": true, + "description": "When this version was published. `null` for drafts." }, - "signals": { - "$ref": "#/components/schemas/RiskSignals" + "is_simple_workflow": { + "type": "boolean", + "description": "`true` when the workflow has no custom `workflow_graph`." }, - "vendor_data": { + "is_editable": { + "type": "boolean", + "description": "`true` only for `draft` versions." + }, + "workflow_url": { "type": "string", - "description": "Your stable reference for this request. Stored on the API session and echoed in the response." + "format": "uri", + "description": "Public URL that starts a session with this workflow." }, - "metadata": { + "workflow_graph": { "type": "object", "nullable": true, "additionalProperties": true, - "description": "Your non-sensitive JSON metadata. Stored on the API session and echoed in the response." + "description": "Graph structure of the workflow (nodes and edges). `null` for simple workflows." }, - "enable_social": { - "type": "boolean", - "default": false, - "description": "When true, also look up the social footprint of the number - the messaging, social, e-commerce and professional platforms it is registered on. Billed as one `phone_social` unit per request in addition to this endpoint's own price.", - "example": true - } - } - }, - "EmailRiskRequest": { - "type": "object", - "required": [ - "email" - ], - "properties": { - "email": { + "features": { "type": "string", - "format": "email", - "description": "Email address to score.", - "example": "alice@example.com" + "description": "Enabled features joined with `\" + \"`, for example `\"OCR + LIVENESS\"`." }, - "country_code": { - "type": "string", - "maxLength": 3, - "description": "Optional ISO 3166-1 alpha-2 or alpha-3 country hint for intelligence coverage.", - "example": "US" + "total_price": { + "type": "number", + "nullable": true, + "description": "Price per session; for graph workflows the most expensive path." }, - "signals": { - "$ref": "#/components/schemas/RiskSignals" + "min_price": { + "type": "number", + "nullable": true, + "description": "Price of the cheapest path through the workflow." }, - "vendor_data": { - "type": "string", - "description": "Your stable reference for this request. Stored on the API session and echoed in the response." + "max_price": { + "type": "number", + "nullable": true, + "description": "Price of the most expensive path through the workflow." + }, + "special_add_on_prices": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Organization-specific add-on prices, or `null` when none are set." + }, + "max_retry_attempts": { + "type": "integer", + "description": "Retries allowed after a declined verification." + }, + "retry_window_days": { + "type": "integer", + "nullable": true, + "description": "Days during which the retries are allowed." }, - "metadata": { + "session_expiration_time": { + "type": "integer", + "description": "Seconds before an unfinished session expires." + }, + "questionnaire": { "type": "object", "nullable": true, "additionalProperties": true, - "description": "Your non-sensitive JSON metadata. Stored on the API session and echoed in the response." + "description": "The questionnaire attached to the workflow, or `null`." }, - "enable_social": { - "type": "boolean", - "default": false, - "description": "When true, also look up the social footprint of the address - the email, social, professional, e-commerce and entertainment platforms it is registered on. Billed as one `email_social` unit per request in addition to this endpoint's own price.", - "example": true + "kyb_role_settings": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + }, + "description": "Per-role verification settings for KYB key people." + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" } - } + }, + "additionalProperties": true }, - "IPRiskRequest": { + "UserSessionListItem": { + "title": "KYC session row (session_kind: user)", "type": "object", + "description": "Summary row for a User Verification (KYC) session. Identity fields (`full_name`, `country`, `document_type`, `portrait_image`, contact fields) are populated progressively as the user completes verification steps, and are `null` until the relevant step finishes. Fields restricted by the workflow's response-attributes configuration are also returned as `null`.", "required": [ - "ip_address" + "session_id", + "session_kind", + "session_number", + "session_url", + "portrait_image", + "document_type", + "full_name", + "country", + "status", + "vendor_data", + "didit_internal_id", + "created_at", + "features", + "phone_number", + "email_address", + "additional_details" ], "properties": { - "ip_address": { + "session_id": { "type": "string", - "format": "ip", - "description": "IPv4 or IPv6 address to score.", - "example": "8.8.8.8" + "format": "uuid", + "description": "Unique identifier of the verification session. Use it with `GET /v3/session/{sessionId}/decision/` to fetch the full decision payload.", + "example": "5b3720ed-d429-42ef-b67f-37ea805f48ee" }, - "claimed_country": { + "session_kind": { "type": "string", - "maxLength": 3, - "description": "Optional ISO 3166-1 alpha-2 or alpha-3 country claimed by the user or session. Used to calculate `country_mismatch`.", - "example": "ESP" + "enum": [ + "user" + ], + "description": "Row discriminator. Always `\"user\"` for KYC rows; Business Verification (KYB) rows have `\"business\"` and a different shape.", + "example": "user" }, - "user_agent": { - "type": "string", - "maxLength": 512, - "description": "User agent observed by your backend. Stored as request context; it does not provide device fingerprinting." + "session_number": { + "type": "integer", + "description": "Human-friendly sequential number of the session within your application. Also matched by the `search` parameter.", + "example": 720 }, - "vendor_data": { + "session_url": { "type": "string", - "description": "Your stable reference for this request. Stored on the API session and echoed in the response." + "nullable": true, + "description": "Hosted verification URL the end user opens to complete the flow. `null` when the session has no hosted URL.", + "example": "https://verify.didit.me/session/CuXoBYCYVO5Y" }, - "metadata": { - "type": "object", + "portrait_image": { + "type": "string", "nullable": true, - "additionalProperties": true, - "description": "Your non-sensitive JSON metadata. Stored on the API session and echoed in the response." - } - } - }, - "RiskWarning": { - "type": "object", - "required": [ - "feature", - "risk", - "log_type", - "short_description", - "long_description" - ], - "properties": { - "feature": { + "description": "Expiring (presigned) URL of the best available portrait: the NFC chip photo when available, otherwise the ID document portrait, otherwise the liveness reference image. `null` until one of those exists. Download promptly; the link expires.", + "example": "https:///portraits/5b3720ed.jpg?X-Amz-Expires=3600&X-Amz-Signature=..." + }, + "document_type": { "type": "string", - "description": "Feature that produced the warning." + "nullable": true, + "enum": [ + "Passport", + "Identity Card", + "Driver's License", + "Residence Permit", + "Health Insurance Card", + "Tax Card" + ], + "description": "Human-readable type of the verified ID document. `null` before document verification completes, and also `null` after completion when the verified code has no display mapping (`SSC`, `WP`, `VISA`, `PSC`, `BC`, `OTHER`). Note: the `document_type` query parameter filters by short codes (`P`, `ID`, `DL`, `RP`, ...), not by these display values.", + "example": "Passport" }, - "risk": { + "full_name": { "type": "string", - "description": "Machine-readable risk code. New values can be added over time." + "nullable": true, + "description": "Best available full name for the session, resolved in order: ID document OCR, AML screened data, database-validation screened data, proof-of-address document, then the expected details provided at session creation. `null` when none is available yet.", + "example": "Jane Marie Doe" }, - "additional_data": { - "type": "object", + "country": { + "type": "string", "nullable": true, - "additionalProperties": true, - "description": "Risk-specific context, or `null`." + "description": "ISO 3166-1 alpha-3 country associated with the session, resolved in order: ID document issuing state, database-validation issuing state, proof-of-address issuing state, expected details, then the verified phone's country. May differ from nationality.", + "example": "ESP" }, - "log_type": { + "status": { "type": "string", "enum": [ - "error", - "warning", - "information" + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User" ], - "description": "Severity used by the decision rules." + "description": "Overall session status. See the [Verification Statuses](/integration/verification-statuses) page for the lifecycle.", + "example": "Approved" }, - "short_description": { + "vendor_data": { "type": "string", - "description": "Human-readable one-line summary." + "nullable": true, + "description": "Your own identifier for the end user, echoed back exactly as sent at session creation.", + "example": "your-user-id-123" }, - "long_description": { - "type": "string", - "description": "Human-readable explanation." - } - } - }, - "RiskLifecycleEvent": { - "type": "object", - "required": [ - "type", - "timestamp", - "details", - "fee" - ], - "properties": { - "type": { + "didit_internal_id": { "type": "string", - "description": "Machine-readable event type. New values can be added over time." + "format": "uuid", + "nullable": true, + "description": "Didit's internal identifier for the verified user profile behind this session. Stable across multiple sessions of the same user; filterable via the `didit_internal_id` query parameter.", + "example": "d9e78474-6c8d-4f2a-bb90-6f5c2f3a1147" }, - "timestamp": { + "created_at": { "type": "string", "format": "date-time", - "description": "When the event happened." + "description": "UTC timestamp when the session was created.", + "example": "2026-06-09T12:26:54.328364Z" }, - "details": { + "features": { + "type": "array", + "description": "Per-feature status for every verification step configured in the session's workflow, in workflow order.", + "items": { + "type": "object", + "required": [ + "feature", + "status" + ], + "properties": { + "feature": { + "type": "string", + "enum": [ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACE_MATCH", + "POA", + "QUESTIONNAIRE", + "EMAIL_VERIFICATION", + "PHONE", + "AML", + "IP_ANALYSIS", + "AGE_ESTIMATION", + "DATABASE_VALIDATION" + ], + "description": "Verification feature identifier.", + "example": "ID_VERIFICATION" + }, + "status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ], + "description": "Status of this feature. `Not Finished` until the step completes.", + "example": "Approved" + } + } + } + }, + "phone_number": { "type": "object", "nullable": true, - "additionalProperties": true, - "description": "Event-specific payload, or `null`." + "description": "Phone associated with the session: the verified phone when phone verification ran, otherwise the contact phone provided at session creation. `null` when neither exists.", + "required": [ + "number", + "is_verified" + ], + "properties": { + "number": { + "type": "string", + "description": "Phone number in E.164 format.", + "example": "+14155552671" + }, + "is_verified": { + "type": "boolean", + "description": "`true` only when the phone-verification feature approved this number.", + "example": true + } + } }, - "fee": { - "type": "number", - "description": "USD amount billed for this event." + "email_address": { + "type": "object", + "nullable": true, + "description": "Email associated with the session: the verified email when email verification ran, otherwise the contact email provided at session creation. `null` when neither exists.", + "required": [ + "email", + "is_verified" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "jane@example.com" + }, + "is_verified": { + "type": "boolean", + "description": "`true` only when the email-verification feature approved this address.", + "example": true + } + } + }, + "additional_details": { + "type": "object", + "nullable": true, + "description": "Compact per-feature extras. Only features that produced data appear as keys; `null` when no feature produced extras. Fields excluded by the workflow's response-attributes configuration are omitted.", + "properties": { + "aml": { + "type": "object", + "description": "Present when AML screening ran.", + "properties": { + "total_hits": { + "type": "integer", + "description": "Number of AML screening hits.", + "example": 0 + }, + "entity_type": { + "type": "string", + "description": "Screened entity type.", + "example": "person" + }, + "is_adverse_media_fetched": { + "type": "boolean", + "example": false + }, + "screened_data": { + "type": "object", + "description": "Subset of the data that was screened. Only non-empty keys are included.", + "properties": { + "nationality": { + "type": "string", + "example": "ESP" + }, + "document_number": { + "type": "string", + "example": "XX1234567" + }, + "date_of_birth": { + "type": "string", + "example": "1990-05-21" + } + } + } + } + }, + "database_validation": { + "type": "object", + "description": "Present when database validation ran.", + "properties": { + "validation_type": { + "type": "string", + "example": "DOCUMENT_VALIDATION" + }, + "match_type": { + "type": "string", + "example": "FULL_MATCH" + }, + "identification_number": { + "type": "string", + "description": "Tax, personal, or document number that was validated (first available).", + "example": "12345678Z" + } + } + }, + "poa": { + "type": "object", + "description": "Present when proof of address ran.", + "properties": { + "document_type": { + "type": "string", + "example": "UTILITY_BILL" + }, + "issuer": { + "type": "string", + "example": "Energy Co" + }, + "address": { + "type": "string", + "example": "Calle Mayor 1, 28013 Madrid" + }, + "name_on_document": { + "type": "string", + "example": "Jane Marie Doe" + } + } + }, + "liveness": { + "type": "object", + "description": "Present when a liveness check ran.", + "properties": { + "method": { + "type": "string", + "example": "PASSIVE" + }, + "score": { + "type": "number", + "description": "Liveness confidence score, rounded to 2 decimals.", + "example": 0.98 + } + } + }, + "face_match": { + "type": "object", + "description": "Present when face match ran.", + "properties": { + "score": { + "type": "number", + "description": "Face-match similarity score, rounded to 2 decimals.", + "example": 0.95 + }, + "source_image": { + "type": "string", + "description": "Expiring (presigned) URL of the source face image.", + "example": "https:///faces/source.jpg?X-Amz-Expires=3600&..." + }, + "target_image": { + "type": "string", + "description": "Expiring (presigned) URL of the target face image.", + "example": "https:///faces/target.jpg?X-Amz-Expires=3600&..." + } + } + }, + "phone_verification": { + "type": "object", + "description": "Present when phone verification ran.", + "properties": { + "country_code": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country of the phone number.", + "example": "US" + }, + "is_temporary": { + "type": "boolean", + "example": false + }, + "is_ported": { + "type": "boolean", + "example": false + }, + "is_blocked": { + "type": "boolean", + "example": false + } + } + }, + "email_verification": { + "type": "object", + "description": "Present when email verification ran.", + "properties": { + "is_breached": { + "type": "boolean", + "example": false + }, + "is_disposable": { + "type": "boolean", + "example": false + }, + "is_undeliverable": { + "type": "boolean", + "example": false + }, + "is_blocklisted": { + "type": "boolean", + "example": false + } + } + } + } } } }, - "PhoneMatchItem": { + "BusinessSessionListItem": { + "title": "KYB session row (session_kind: business)", "type": "object", - "description": "Another session of your application that used the same phone number with a different `vendor_data`.", + "description": "Summary row for a Business Verification (KYB) session. Returned only when `session_kind` is `business` or `all`.", + "required": [ + "session_id", + "session_kind", + "session_number", + "status", + "vendor_data", + "created_at", + "workflow_id", + "workflow_type", + "workflow_label", + "company_name", + "registration_number", + "country" + ], "properties": { "session_id": { "type": "string", "format": "uuid", - "description": "Session id of the other verification that used this phone number." + "description": "Unique identifier of the business verification session.", + "example": "8a1f63c2-1b9e-4f3a-9c41-2f6a1f0b7d52" + }, + "session_kind": { + "type": "string", + "enum": [ + "business" + ], + "description": "Row discriminator. Always `\"business\"` for KYB rows.", + "example": "business" }, "session_number": { "type": "integer", - "description": "Human-friendly sequential number of that session." + "description": "Human-friendly sequential number of the business session within your application. Numbered independently from KYC sessions.", + "example": 42 + }, + "status": { + "type": "string", + "enum": [ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User" + ], + "description": "Overall session status. See the [Verification Statuses](/integration/verification-statuses) page.", + "example": "Approved" }, "vendor_data": { "type": "string", "nullable": true, - "description": "`vendor_data` of that session." + "description": "Your own identifier for the business, echoed back exactly as sent at session creation.", + "example": "your-company-id-77" }, - "verification_date": { + "created_at": { "type": "string", - "description": "When that session was created (`YYYY-MM-DDTHH:MM:SSZ`)." + "format": "date-time", + "description": "UTC timestamp when the session was created.", + "example": "2026-06-08T09:14:02.118450Z" }, - "phone_number": { + "workflow_id": { "type": "string", - "description": "The matched phone number." + "format": "uuid", + "nullable": true, + "description": "Identifier of the workflow this session was created with.", + "example": "9f9b1234-aaaa-bbbb-cccc-1234567890ab" }, - "status": { + "workflow_type": { "type": "string", - "description": "Status of that session (e.g., `Approved`, `Declined`, `In Progress`)." + "nullable": true, + "description": "Workflow type. `kyb` for business workflows.", + "example": "kyb" }, - "is_blocklisted": { - "type": "boolean", - "description": "Whether the phone number is on your application's blocklist." + "workflow_label": { + "type": "string", + "nullable": true, + "description": "Display name of the workflow.", + "example": "Standard KYB" }, - "api_service": { + "company_name": { "type": "string", "nullable": true, - "description": "For API-type sessions, the standalone API that created it; `null` for regular verification sessions." + "description": "Name of the company being verified, from the registry company linked to the session. `null` until a company is selected.", + "example": "Acme Holdings S.L." }, - "source": { + "registration_number": { "type": "string", - "description": "Where the match was found. Usually `session`; a blocklist hit via an organization list entry uses `list_entry`." + "nullable": true, + "description": "Company registration number from the registry company linked to the session.", + "example": "B12345678" + }, + "country": { + "type": "string", + "nullable": true, + "description": "Country code of the registry company linked to the session.", + "example": "ES" } } }, - "EmailMatchItem": { + "TransactionVerificationSessionAction": { + "title": "Verification session", "type": "object", - "description": "Another session of your application that used the same email address with a different `vendor_data`.", "properties": { - "session_id": { + "type": { "type": "string", - "format": "uuid", - "description": "Session id of the other verification that used this email address." - }, - "session_number": { - "type": "integer", - "description": "Human-friendly sequential number of that session." + "enum": [ + "verification_session" + ] }, - "vendor_data": { + "url": { "type": "string", - "nullable": true, - "description": "`vendor_data` of that session." + "format": "uri", + "description": "Hosted verification URL to open for the end user." }, - "verification_date": { + "session_id": { "type": "string", - "description": "When that session was created (`YYYY-MM-DDTHH:MM:SSZ`)." + "format": "uuid", + "description": "Verification session id." }, - "email": { + "session_token": { "type": "string", - "description": "The matched email address." + "description": "Session token, used by the mobile SDKs to launch the flow natively." }, "status": { "type": "string", - "description": "Status of that session (e.g., `Approved`, `Declined`, `In Progress`)." - }, - "is_blocklisted": { - "type": "boolean", - "description": "Whether the email address is on your application's blocklist." - }, - "api_service": { - "type": "string", - "nullable": true, - "description": "For API-type sessions, the standalone API that created it; `null` for regular verification sessions." - }, - "source": { - "type": "string", - "description": "Where the match was found. Usually `session`; a blocklist hit via an organization list entry uses `list_entry`." + "description": "Verification session status." } - } + }, + "required": [ + "type", + "url" + ] }, - "PhoneIntelligence": { + "TransactionWalletOwnershipAction": { + "title": "Wallet ownership", "type": "object", - "description": "Normalized phone-intelligence block. Present only when the intelligence add-on ran for this lookup; when present it is also exposed under the `enrichment` alias with identical values.", "properties": { - "score": { - "type": "number", - "nullable": true, - "description": "Normalized 0-100 risk score. Higher is riskier." - }, - "phone_type_risk": { + "type": { "type": "string", - "nullable": true, - "description": "`Low`, `Medium` or `High`, derived from the score." - }, - "phone_trust_index": { - "type": "number", - "nullable": true, - "description": "Inverse of the score, for dashboards that prefer higher is better." - }, - "network_stability_index": { - "type": "number", - "nullable": true, - "description": "Stability of the line's network." - }, - "is_disposable": { - "type": "boolean", - "nullable": true, - "description": "`true` when the number looks disposable or virtual." - }, - "is_ported": { - "type": "boolean", - "nullable": true, - "description": "`true` when the number has moved carrier." + "enum": [ + "wallet_ownership" + ] }, - "is_recently_ported": { - "type": "boolean", - "nullable": true, - "description": "`true` when the port happened recently." + "url": { + "type": "string", + "format": "uri", + "description": "Hosted wallet-ownership widget URL. Open it in a real browser surface - on mobile use SFSafariViewController or a Chrome Custom Tab, never an embedded webview." }, - "carrier_type": { + "widget_session_id": { "type": "string", - "nullable": true, - "description": "Line classification reported by the lookup (e.g., `mobile`)." + "format": "uuid", + "description": "Wallet-ownership widget session id." }, - "reason_codes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Machine-readable codes explaining the score. Values can grow over time, so do not reject unknown codes." + "expires_at": { + "type": "string", + "format": "date-time", + "description": "When the widget session expires." } - } + }, + "required": [ + "type", + "url" + ] }, - "EmailIntelligence": { + "TransactionRuleActionAddScore": { "type": "object", - "description": "Normalized email-intelligence block. Present only when the intelligence add-on ran for this email; when present it is also exposed as the `enrichment` alias with identical values.", + "required": [ + "type", + "value" + ], "properties": { - "score": { - "type": "number", - "nullable": true, - "description": "Normalized 0-100 risk score. Higher is riskier." - }, - "deliverable": { - "type": "string", - "nullable": true, - "description": "Deliverability band (e.g., `high`)." - }, - "breach_risk_level": { + "type": { "type": "string", - "nullable": true, - "description": "`Low`, `Medium` or `High`." - }, - "breach_risk_index": { - "type": "number", - "nullable": true, - "description": "Numeric breach-risk index." - }, - "is_recent_breach": { - "type": "boolean", - "nullable": true, - "description": "`true` when the address appears in a recent breach." - }, - "is_frequent_breached": { - "type": "boolean", - "nullable": true, - "description": "`true` when the address appears in many breaches." - }, - "is_disposable": { - "type": "boolean", - "nullable": true, - "description": "`true` when the domain is a throwaway provider." - }, - "is_free_domain": { - "type": "boolean", - "nullable": true, - "description": "`true` when the domain is a free mail provider." - }, - "is_breached": { - "type": "boolean", - "nullable": true, - "description": "`true` when the address appears in known breaches." - }, - "no_of_breaches": { - "type": "number", - "nullable": true, - "description": "Number of known breaches containing the address." + "enum": [ + "add_score" + ] }, - "reason_codes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Machine-readable codes explaining the score. Values can grow over time, so do not reject unknown codes." + "value": { + "type": "integer", + "description": "Points added to the transaction's risk score. May be negative." } - } + }, + "description": "Add points to the transaction's cumulative risk score; the total decides the status via the application's review/decline thresholds." }, - "IPRiskResult": { + "TransactionRuleActionChangeStatus": { "type": "object", - "description": "IP risk result. Individual enrichment fields can be `null` when the lookup returned no usable data.", "required": [ - "status", - "threat" + "type", + "value" ], "properties": { - "status": { + "type": { "type": "string", "enum": [ - "Approved", - "In Review" - ], - "description": "`In Review` when the resolved country conflicts with `claimed_country`, or when the address is associated with VPN, Tor, or data-center infrastructure. Otherwise `Approved`." + "change_status" + ] }, - "ip_address": { + "value": { "type": "string", - "nullable": true, - "description": "The scored address. `null` when no enrichment source returned usable data." + "enum": [ + "APPROVED", + "IN_REVIEW", + "DECLINED", + "AWAITING_USER" + ], + "description": "Status to set immediately, regardless of score. Alias key: status." }, - "ip_country": { + "workflow_id": { "type": "string", - "nullable": true, - "description": "Resolved country name." + "format": "uuid", + "description": "REQUIRED when value is AWAITING_USER: the verification workflow the subject is asked to complete. Alias key: remediation_workflow_id. Didit creates a hosted remediation session for the subject when the rule fires." + } + }, + "description": "Directly set the transaction status. DECLINED always wins across matched rules." + }, + "TransactionRuleActionAddTags": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "add_tags" + ] }, - "ip_country_code": { + "tag_uuid": { "type": "string", - "nullable": true, - "description": "Resolved country code. Compare countries through `country_mismatch` instead of comparing raw country strings yourself." + "format": "uuid", + "description": "Existing application tag to attach." }, - "ip_state": { + "tag_name": { "type": "string", - "nullable": true, - "description": "Resolved state or region." + "description": "Tag name to attach; created on first use (stored uppercased). Provide tag_uuid or tag_name." }, - "ip_city": { + "tag_color": { "type": "string", - "nullable": true, - "description": "Resolved city." + "description": "Hex color used when tag_name creates a new tag, e.g. #007AFE." + } + }, + "description": "Attach an application tag to the matched transaction." + }, + "TransactionRuleActionAddNote": { + "type": "object", + "required": [ + "type", + "note" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "add_note" + ] }, - "latitude": { - "type": "number", - "nullable": true, - "description": "Approximate network geolocation, not a precise device location." + "note": { + "type": "string", + "description": "Note text attached to the transaction (author: Rule Engine). Alias key: value." + } + }, + "description": "Attach an automated analyst note to the matched transaction." + }, + "TransactionRuleActionAddToList": { + "type": "object", + "required": [ + "type", + "list_id" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "add_to_list" + ] }, - "longitude": { - "type": "number", - "nullable": true, - "description": "Approximate network geolocation, not a precise device location." + "list_id": { + "type": "string", + "format": "uuid", + "description": "List to add the subject's vendor_data to." + } + }, + "description": "Add the subject's vendor_data to a blocklist, allowlist or custom list." + }, + "TransactionRuleActionOpenCase": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "open_case" + ] }, - "isp": { + "blueprint": { "type": "string", - "nullable": true, - "description": "Internet service provider serving the address." + "format": "uuid", + "description": "Case blueprint UUID; defaults to the application's default blueprint." }, - "organization": { + "grouping": { "type": "string", - "nullable": true, - "description": "Organization owning the address block." + "enum": [ + "by_applicant", + "by_rule_and_applicant" + ], + "description": "Whether repeat matches join one case per applicant or one per rule+applicant." }, - "is_vpn_or_tor": { + "attach_matched_transaction": { "type": "boolean", - "nullable": true, - "description": "`true` when the address is associated with VPN or Tor infrastructure." + "description": "Attach the matched transaction to the case." + } + }, + "description": "Open (or add to) an investigation case. Only the first open_case action on a rule is applied." + }, + "KYBRegistrySearchResults": { + "type": "object", + "properties": { + "companies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "kyb_response_id": { + "type": "string", + "nullable": true, + "description": "Handle for this candidate — pass it to `POST /v3/kyb/select/` to retrieve the full profile. Ephemeral and scoped to this search: do not store it long-term; re-search to get a fresh one.", + "example": "69aeeb95febb0f1704042259" + }, + "name": { + "type": "string", + "nullable": true, + "description": "Registered company name.", + "example": "Tesco PLC" + }, + "registration_number": { + "type": "string", + "nullable": true, + "description": "Registry registration number.", + "example": "00445790" + }, + "status": { + "type": "string", + "nullable": true, + "description": "Normalized registry status of the company (e.g. `active`, `dissolved`, `inactive`, `struck off`).", + "example": "active" + }, + "type": { + "type": "string", + "nullable": true, + "description": "Legal form as reported by the registry.", + "example": "Public Limited Company" + }, + "registered_address": { + "type": "string", + "nullable": true, + "description": "Registered address of the candidate, when the registry returns one. Use it, with the registration number, to tell same-named companies apart: in the United States a candidate can be listed under the state where the company operates rather than the state you searched.", + "example": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, AL7 1GA" + }, + "risk_level": { + "type": "string", + "nullable": true, + "description": "Registry-reported risk level, when available. Usually `null` at search time." + }, + "fetch_status": { + "type": "string", + "nullable": true, + "description": "Always `pending` for search candidates — the full company profile is only fetched (and billed) when you select the candidate.", + "example": "pending" + } + } + }, + "description": "Deduplicated candidate list. Empty while `search_resolved=false` (webhook mode) or when nothing matched." }, - "is_data_center": { - "type": "boolean", - "nullable": true, - "description": "`true` when the address belongs to a hosting or data-center network." + "pagination": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "description": "Number of candidates returned." + }, + "page": { + "type": "integer" + }, + "per_page": { + "type": "integer", + "example": 25 + } + } }, - "time_zone": { + "search_status": { "type": "string", "nullable": true, - "description": "Time zone of the resolved location." + "enum": [ + "pending", + "resolved" + ], + "description": "Registry search lifecycle status." }, - "time_zone_offset": { + "search_resolved": { + "type": "boolean", + "description": "`true` once the candidate list is final." + } + } + }, + "KYBRegistryCandidateRequest": { + "type": "object", + "required": [ + "kyb_response_id" + ], + "properties": { + "kyb_response_id": { "type": "string", - "nullable": true, - "description": "UTC offset of the resolved location." - }, - "asn_number": { - "type": "number", - "nullable": true, - "description": "Autonomous system number." + "maxLength": 255, + "description": "Candidate handle from `POST /v3/kyb/search/` (`kyb_registry.companies[].kyb_response_id`). Ephemeral — use it shortly after the search that produced it.", + "example": "69aeeb95febb0f1704042259" }, - "asn_organization": { + "vendor_data": { "type": "string", - "nullable": true, - "description": "Organization operating the autonomous system." + "description": "Your identifier for this company; echoed back and stored with the business session." }, - "connection_type": { - "type": "string", + "metadata": { + "type": "object", "nullable": true, - "description": "Connection classification (e.g., `cellular`, `hosting`)." + "description": "Free-form JSON stored with the business session and echoed back." }, - "carrier": { + "save_api_request": { + "type": "boolean", + "default": true, + "description": "Must be `true` (the default). KYB registry selects are always persisted so billing and retrieval can be tracked; `false` returns `400`." + } + } + }, + "WebhookEventEnvelope": { + "type": "object", + "description": "Fields Didit adds to every signed webhook delivery, whatever the event type. Read `webhook_type` to know which payload you received.", + "required": [ + "event_id", + "webhook_type", + "timestamp", + "created_at", + "application_id", + "environment" + ], + "properties": { + "event_id": { "type": "string", - "nullable": true, - "description": "Mobile carrier when the address is on a mobile network." + "format": "uuid", + "description": "Identifier of the event. Every destination receives the same value and a redelivery keeps it, so use it to deduplicate." }, - "proxy_type": { - "type": "string", - "nullable": true, - "description": "Proxy classification when the address is behind a proxy." + "webhook_type": { + "$ref": "#/components/schemas/WebhookEvent" }, - "claimed_country": { - "type": "string", - "nullable": true, - "description": "Echo of the `claimed_country` you sent." + "timestamp": { + "type": "integer", + "description": "Unix time in seconds at which this delivery was signed. Equal to the `X-Timestamp` header; a redelivery carries a new value." }, - "country_mismatch": { - "type": "boolean", - "nullable": true, - "description": "`true` when the resolved country differs from `claimed_country`. `null` when no enrichment data or no claimed country is available." + "created_at": { + "type": "integer", + "description": "Unix time in seconds of the change that produced the event." }, - "threat": { - "type": "object", - "additionalProperties": true, - "description": "Free-form network-threat evidence from the enrichment source (e.g., `is_proxy`, `is_tor`). Empty object when no enrichment data is available. Treat as diagnostic detail, not a stable contract." + "application_id": { + "type": "string", + "format": "uuid", + "description": "The application the event belongs to." + }, + "environment": { + "type": "string", + "enum": [ + "live", + "sandbox" + ], + "description": "`sandbox` for sandbox sessions and sandbox applications, otherwise `live`." } } }, - "PhoneRiskResponse": { + "SessionWebhookEventData": { "type": "object", + "description": "Session fields of `status.updated` and `data.updated`. Optional fields are omitted, never sent as `null`, when they do not apply. Business (KYB) sessions add `business_session_id`, `session_kind` and `vendor_business_id`.", "required": [ - "request_id", + "session_id", "status", - "created_at" + "sandbox_scenario" ], "properties": { - "request_id": { + "session_id": { "type": "string", "format": "uuid", - "description": "Id of the API session created for this request. Store it with `vendor_data` if you need to correlate the result with later webhooks or console activity." + "description": "The verification session." + }, + "business_session_id": { + "type": "string", + "format": "uuid", + "description": "Business (KYB) sessions only: the same value as `session_id`." + }, + "session_kind": { + "type": "string", + "enum": [ + "business" + ], + "description": "Business (KYB) sessions only. User (KYC) session events carry no top-level `session_kind`; read `decision.session_kind` instead." }, "status": { "type": "string", "enum": [ + "Not Started", + "In Progress", "Approved", + "Declined", "In Review", - "Declined" + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User" ], - "description": "Final decision for this API session. Normally `Approved`; the risk evaluation can produce `In Review` or `Declined`." + "description": "Session status after the change." + }, + "trigger": { + "type": "string", + "description": "Present when the event was not produced by the verification flow itself, for example `manual_review`, `manual_step_update`, `ongoing_monitoring`, `webhook_reconciliation`, `manual_webhook_recovery`, `kyb_registry_change` or a `console_*` edit." + }, + "workflow_id": { + "type": "string", + "format": "uuid", + "description": "The session's workflow. Omitted when the session has none." + }, + "workflow_version": { + "type": "integer", + "description": "Version of the workflow the session ran. Sent together with `workflow_id`." }, "vendor_data": { "type": "string", - "nullable": true, - "description": "Echo of the `vendor_data` you sent, `null` when you did not send one." + "description": "Your `vendor_data` for the session. Omitted when empty." + }, + "vendor_business_id": { + "type": "string", + "description": "Business (KYB) sessions only: the business the session belongs to. Omitted when none." }, "metadata": { "type": "object", - "nullable": true, "additionalProperties": true, - "description": "Echo of the `metadata` you sent, `null` when you did not send one." + "description": "Your session `metadata`. Omitted when empty." }, - "created_at": { + "decision": { + "$ref": "#/components/schemas/SessionDecision" + }, + "resubmit_info": { + "type": "object", + "description": "Only when `status` is `Resubmitted`: the steps the user must redo and why.", + "required": [ + "nodes_to_resubmit", + "reasons" + ], + "properties": { + "nodes_to_resubmit": { + "type": "array", + "items": { + "type": "object", + "required": [ + "node_id", + "feature" + ], + "properties": { + "node_id": { + "type": "string", + "description": "Node identifier as it appears in the session's workflow definition (for example `feature_ocr`, `feature_liveness`).", + "example": "feature_poa" + }, + "feature": { + "type": "string", + "description": "Feature type of the node, for example `OCR`, `LIVENESS` or `PROOF_OF_ADDRESS`.", + "example": "PROOF_OF_ADDRESS" + } + } + }, + "description": "Workflow steps the user must redo. The same `{node_id, feature}` entries as `nodes_to_resubmit` in [Update Status](#patch-/v3/session/-sessionId-/update-status/)." + }, + "reasons": { + "type": "object", + "additionalProperties": true, + "description": "Reason per `node_id` in `nodes_to_resubmit`." + } + } + }, + "sandbox_scenario": { "type": "string", - "format": "date-time", - "description": "Timestamp of this response." + "nullable": true, + "description": "The simulated outcome armed on a sandbox session, or `null`. Always `null` for live sessions." + } + } + }, + "SessionStatusUpdatedEvent": { + "description": "A verification session changed status. `decision` is sent only for `Approved`, `Declined`, `In Review` and `Abandoned`; it has the shape of `GET /v3/session/{sessionId}/decision/` for destinations on webhook version 3 (the default), and the older reduced shape for version 1 or 2 destinations. Business sessions always receive the version 3 business decision.", + "allOf": [ + { + "$ref": "#/components/schemas/WebhookEventEnvelope" + }, + { + "$ref": "#/components/schemas/SessionWebhookEventData" + }, + { + "type": "object", + "properties": { + "webhook_type": { + "type": "string", + "enum": [ + "status.updated" + ], + "description": "Always `status.updated`." + } + }, + "required": [ + "webhook_type" + ] + } + ] + }, + "SessionDataUpdatedEvent": { + "description": "Data of a verification session changed without a status change, for example after a manual review edit, a step override or an ongoing-monitoring rescreen. `decision` is sent only for `Approved`, `Declined`, `In Review` and `Abandoned`; it has the shape of `GET /v3/session/{sessionId}/decision/` for destinations on webhook version 3 (the default), and the older reduced shape for version 1 or 2 destinations. Business sessions always receive the version 3 business decision.", + "allOf": [ + { + "$ref": "#/components/schemas/WebhookEventEnvelope" + }, + { + "$ref": "#/components/schemas/SessionWebhookEventData" + }, + { + "type": "object", + "properties": { + "webhook_type": { + "type": "string", + "enum": [ + "data.updated" + ], + "description": "Always `data.updated`." + } + }, + "required": [ + "webhook_type" + ] + } + ] + }, + "UserStatusUpdatedEvent": { + "description": "A user changed status. Not sent when the user is created.", + "allOf": [ + { + "$ref": "#/components/schemas/WebhookEventEnvelope" + }, + { + "type": "object", + "properties": { + "webhook_type": { + "type": "string", + "enum": [ + "user.status.updated" + ], + "description": "Always `user.status.updated`." + }, + "vendor_user_id": { + "type": "string", + "format": "uuid", + "description": "The user the event is about." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your `vendor_data` for the user, or `null`." + }, + "status": { + "type": "string", + "enum": [ + "ACTIVE", + "IN_REVIEW", + "FLAGGED", + "BLOCKED" + ], + "description": "User status after the change." + }, + "previous_status": { + "type": "string", + "enum": [ + "ACTIVE", + "IN_REVIEW", + "FLAGGED", + "BLOCKED" + ], + "description": "Status before the change." + } + }, + "required": [ + "webhook_type", + "vendor_user_id", + "vendor_data", + "status", + "previous_status" + ] + } + ] + }, + "UserDataUpdatedEvent": { + "description": "Fields of a user changed. Not sent when the user is created. One save can produce both a status and a data event.", + "allOf": [ + { + "$ref": "#/components/schemas/WebhookEventEnvelope" + }, + { + "type": "object", + "properties": { + "webhook_type": { + "type": "string", + "enum": [ + "user.data.updated" + ], + "description": "Always `user.data.updated`." + }, + "vendor_user_id": { + "type": "string", + "format": "uuid", + "description": "The user the event is about." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your `vendor_data` for the user, or `null`." + }, + "status": { + "type": "string", + "enum": [ + "ACTIVE", + "IN_REVIEW", + "FLAGGED", + "BLOCKED" + ], + "description": "User status after the change." + }, + "changed_fields": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "vendor_data", + "display_name", + "full_name", + "date_of_birth", + "metadata", + "portrait_image", + "issuing_states", + "approved_emails", + "approved_phones", + "features", + "session_count", + "approved_count", + "declined_count", + "in_review_count", + "first_session_at", + "last_session_at" + ] + }, + "description": "Names of the fields that changed." + }, + "changes": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "previous": { + "nullable": true, + "description": "Value before the change. Dates are ISO 8601 strings." + }, + "current": { + "nullable": true, + "description": "Value after the change. Dates are ISO 8601 strings." + } + } + }, + "description": "Previous and current value of each changed field, keyed by field name. Values can be personal data (names, dates of birth, portrait links, emails, phone numbers); handle them accordingly." + } + }, + "required": [ + "webhook_type", + "vendor_user_id", + "vendor_data", + "status", + "changed_fields", + "changes" + ] + } + ] + }, + "BusinessStatusUpdatedEvent": { + "description": "A business changed status. Not sent when the business is created.", + "allOf": [ + { + "$ref": "#/components/schemas/WebhookEventEnvelope" + }, + { + "type": "object", + "properties": { + "webhook_type": { + "type": "string", + "enum": [ + "business.status.updated" + ], + "description": "Always `business.status.updated`." + }, + "vendor_business_id": { + "type": "string", + "format": "uuid", + "description": "The business the event is about." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your `vendor_data` for the business, or `null`." + }, + "status": { + "type": "string", + "enum": [ + "ACTIVE", + "IN_REVIEW", + "FLAGGED", + "BLOCKED" + ], + "description": "Business status after the change." + }, + "previous_status": { + "type": "string", + "enum": [ + "ACTIVE", + "IN_REVIEW", + "FLAGGED", + "BLOCKED" + ], + "description": "Status before the change." + } + }, + "required": [ + "webhook_type", + "vendor_business_id", + "vendor_data", + "status", + "previous_status" + ] + } + ] + }, + "BusinessDataUpdatedEvent": { + "description": "Fields of a business changed. Not sent when the business is created. One save can produce both a status and a data event.", + "allOf": [ + { + "$ref": "#/components/schemas/WebhookEventEnvelope" + }, + { + "type": "object", + "properties": { + "webhook_type": { + "type": "string", + "enum": [ + "business.data.updated" + ], + "description": "Always `business.data.updated`." + }, + "vendor_business_id": { + "type": "string", + "format": "uuid", + "description": "The business the event is about." + }, + "vendor_data": { + "type": "string", + "nullable": true, + "description": "Your `vendor_data` for the business, or `null`." + }, + "status": { + "type": "string", + "enum": [ + "ACTIVE", + "IN_REVIEW", + "FLAGGED", + "BLOCKED" + ], + "description": "Business status after the change." + }, + "changed_fields": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "vendor_data", + "display_name", + "metadata", + "legal_name", + "registration_number", + "country_code", + "session_count", + "approved_count", + "declined_count", + "in_review_count", + "features", + "first_session_at", + "last_session_at" + ] + }, + "description": "Names of the fields that changed." + }, + "changes": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "previous": { + "nullable": true, + "description": "Value before the change. Dates are ISO 8601 strings." + }, + "current": { + "nullable": true, + "description": "Value after the change. Dates are ISO 8601 strings." + } + } + }, + "description": "Previous and current value of each changed field, keyed by field name." + } + }, + "required": [ + "webhook_type", + "vendor_business_id", + "vendor_data", + "status", + "changed_fields", + "changes" + ] + } + ] + }, + "TransactionCreatedEvent": { + "description": "A transaction was created and screened. Sent for live applications only.", + "allOf": [ + { + "$ref": "#/components/schemas/WebhookEventEnvelope" }, - "phone": { + { "type": "object", - "description": "Normalized phone report, the same block used by Phone Verification responses.", - "required": [ - "status" - ], "properties": { - "status": { + "webhook_type": { "type": "string", "enum": [ - "Approved", - "In Review", - "Declined" + "transaction.created" ], - "description": "Decision for this phone number. Mirrors the top-level `status`." + "description": "Always `transaction.created`." }, - "phone_number_prefix": { + "transaction_id": { "type": "string", - "description": "Country calling code with leading `+` (e.g., `+1`).", - "example": "+1" + "format": "uuid", + "description": "Didit's identifier of the transaction." }, - "phone_number": { + "txn_id": { "type": "string", - "description": "National number without the country calling code.", - "example": "4155552671" + "nullable": true, + "description": "The `transaction_id` you supplied at create time, or `null`." }, - "full_number": { + "status": { "type": "string", - "description": "Complete number in E.164 format.", - "example": "+14155552671" + "enum": [ + "APPROVED", + "IN_REVIEW", + "DECLINED", + "AWAITING_USER" + ], + "description": "Monitoring status after the change." }, - "country_code": { + "score": { + "type": "integer", + "description": "Risk score accumulated by rules." + }, + "severity": { "type": "string", "nullable": true, - "description": "ISO 3166-1 alpha-2 country of the number.", - "example": "US" + "enum": [ + "UNKNOWN", + "LOW", + "MEDIUM", + "HIGH", + "CRITICAL", + null + ], + "description": "Risk severity, or `null` until a rule or provider sets one." }, - "country_name": { + "amount": { "type": "string", - "description": "Full country name derived from `country_code`.", - "example": "United States" + "description": "Transaction amount as a decimal string. Unlike the API it is not normalized, so it can carry trailing zeros (up to 18 decimal places)." }, - "carrier": { + "currency": { + "type": "string", + "description": "Currency code or crypto asset symbol of `amount`." + }, + "direction": { + "type": "string", + "enum": [ + "INBOUND", + "OUTBOUND" + ], + "description": "Direction relative to the subject." + }, + "action_required": { "type": "object", "nullable": true, - "description": "Current network serving the number. `null` when the lookup could not resolve a carrier.", - "properties": { - "name": { - "type": "string", - "description": "Carrier name. Empty string when the lookup could not identify it." + "oneOf": [ + { + "$ref": "#/components/schemas/TransactionVerificationSessionAction" }, - "type": { - "type": "string", - "description": "Line type reported by the lookup: `mobile`, `fixed_line`, `voip`, `isp`, `vpn`, `toll_free`, `premium_rate`, or similar." + { + "$ref": "#/components/schemas/TransactionWalletOwnershipAction" } - } - }, - "is_disposable": { - "type": "boolean", - "description": "`true` when the number belongs to a temporary or burner number service." + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "verification_session": "#/components/schemas/TransactionVerificationSessionAction", + "wallet_ownership": "#/components/schemas/TransactionWalletOwnershipAction" + } + }, + "description": "Pending end-user action, or `null`. Same object as `action_required` on the transaction; `session_token` is a credential, so never log it." + } + }, + "required": [ + "webhook_type", + "transaction_id", + "txn_id", + "status", + "score", + "severity", + "amount", + "currency", + "direction", + "action_required" + ] + } + ] + }, + "TransactionStatusUpdatedEvent": { + "description": "A transaction changed status, from a rule, a review or an end-user action. Sent for live applications only.", + "allOf": [ + { + "$ref": "#/components/schemas/WebhookEventEnvelope" + }, + { + "type": "object", + "properties": { + "webhook_type": { + "type": "string", + "enum": [ + "transaction.status.updated" + ], + "description": "Always `transaction.status.updated`." }, - "is_virtual": { - "type": "boolean", - "description": "`true` when the line type is virtual (`voip`, `isp`, or `vpn`)." + "transaction_id": { + "type": "string", + "format": "uuid", + "description": "Didit's identifier of the transaction." }, - "verification_method": { + "txn_id": { "type": "string", "nullable": true, - "description": "OTP channel of the underlying verification. `null` for risk lookups, which never send a code." + "description": "The `transaction_id` you supplied at create time, or `null`." }, - "verification_attempts": { + "status": { + "type": "string", + "enum": [ + "APPROVED", + "IN_REVIEW", + "DECLINED", + "AWAITING_USER" + ], + "description": "Monitoring status after the change." + }, + "score": { "type": "integer", - "description": "OTP sends for this number. `0` for risk lookups." + "description": "Risk score accumulated by rules." }, - "verified_at": { + "severity": { "type": "string", - "format": "date-time", "nullable": true, - "description": "When the number was last verified by an OTP. `null` for risk lookups." + "enum": [ + "UNKNOWN", + "LOW", + "MEDIUM", + "HIGH", + "CRITICAL", + null + ], + "description": "Risk severity, or `null` until a rule or provider sets one." }, - "warnings": { - "type": "array", - "description": "Risk warnings recorded for this request. Empty array when there are none.", - "items": { - "$ref": "#/components/schemas/RiskWarning" - } + "amount": { + "type": "string", + "description": "Transaction amount as a decimal string. Unlike the API it is not normalized, so it can carry trailing zeros (up to 18 decimal places)." }, - "lifecycle": { - "type": "array", - "description": "Chronological audit trail of the request. Empty array for risk lookups without send events.", - "items": { - "$ref": "#/components/schemas/RiskLifecycleEvent" - } + "currency": { + "type": "string", + "description": "Currency code or crypto asset symbol of `amount`." }, - "matches": { - "type": "array", - "description": "Other sessions of your application where the same phone number was used by a different user (different `vendor_data`). Up to 5 entries. Empty array when there are no matches.", - "items": { - "$ref": "#/components/schemas/PhoneMatchItem" - } + "direction": { + "type": "string", + "enum": [ + "INBOUND", + "OUTBOUND" + ], + "description": "Direction relative to the subject." }, - "phone_intelligence": { - "$ref": "#/components/schemas/PhoneIntelligence" + "action_required": { + "type": "object", + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/TransactionVerificationSessionAction" + }, + { + "$ref": "#/components/schemas/TransactionWalletOwnershipAction" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "verification_session": "#/components/schemas/TransactionVerificationSessionAction", + "wallet_ownership": "#/components/schemas/TransactionWalletOwnershipAction" + } + }, + "description": "Pending end-user action, or `null`. Same object as `action_required` on the transaction; `session_token` is a credential, so never log it." + } + }, + "required": [ + "webhook_type", + "transaction_id", + "txn_id", + "status", + "score", + "severity", + "amount", + "currency", + "direction", + "action_required" + ] + } + ] + }, + "TravelRuleStatusUpdatedEvent": { + "description": "A Travel Rule exchange changed status. Sent on every transition up to and including `FINISHED`, `CANCELLED` or `EXPIRED`, never after. Sent for live applications only. There is no top-level `status`.", + "allOf": [ + { + "$ref": "#/components/schemas/WebhookEventEnvelope" + }, + { + "type": "object", + "properties": { + "webhook_type": { + "type": "string", + "enum": [ + "travel_rule.status.updated" + ], + "description": "Always `travel_rule.status.updated`." }, - "enrichment": { - "$ref": "#/components/schemas/PhoneIntelligence" + "transaction_id": { + "type": "string", + "format": "uuid", + "description": "The transaction the exchange belongs to." }, - "phone_social": { - "$ref": "#/components/schemas/PhoneSocialFootprint" + "txn_id": { + "type": "string", + "nullable": true, + "description": "The `transaction_id` you supplied at create time, or `null`." + }, + "travel_rule_status": { + "type": "string", + "enum": [ + "AWAITING_COUNTERPARTY", + "ON_HOLD", + "COMPLETED", + "FINISHED", + "CANCELLED", + "EXPIRED", + "COUNTERPARTY_MISMATCHED_DATA", + "COUNTERPARTY_VASP_NOT_FOUND", + "COUNTERPARTY_VASP_NOT_REACHABLE", + "UNCONFIRMED_OWNERSHIP", + "COUNTERPARTY_VASP_GENERAL_DECLINE", + "NOT_ENOUGH_COUNTERPARTY_DATA", + "NOT_APPLICABLE" + ], + "description": "Exchange status after the change." + }, + "rail": { + "type": "string", + "enum": [ + "", + "INTERNAL", + "TRP", + "GTR", + "TRUST", + "VERIFYVASP", + "SYGNA", + "EMAIL" + ], + "description": "Network the exchange runs on, or an empty string when none could be resolved." + }, + "direction": { + "type": "string", + "enum": [ + "INBOUND", + "OUTBOUND" + ], + "description": "Direction of the transfer." } - } + }, + "required": [ + "webhook_type", + "transaction_id", + "txn_id", + "travel_rule_status", + "rail", + "direction" + ] + } + ] + }, + "WorkflowComplianceChangedEvent": { + "description": "The compliance status of a published workflow changed after a re-evaluation. One event per workflow. There is no top-level `status`.", + "allOf": [ + { + "$ref": "#/components/schemas/WebhookEventEnvelope" + }, + { + "type": "object", + "properties": { + "webhook_type": { + "type": "string", + "enum": [ + "workflow.compliance_changed" + ], + "description": "Always `workflow.compliance_changed`." + }, + "workflow_id": { + "type": "string", + "format": "uuid", + "description": "The workflow that changed." + }, + "workflow_version": { + "type": "integer", + "description": "Version that was evaluated." + }, + "workflow_label": { + "type": "string", + "nullable": true, + "description": "Display name of the workflow." + }, + "transition": { + "type": "object", + "required": [ + "from", + "to" + ], + "properties": { + "from": { + "type": "string", + "enum": [ + "compliant", + "gaps_detected", + "pending_change", + "unknown_coverage", + "not_evaluated" + ], + "description": "Status before the re-evaluation." + }, + "to": { + "type": "string", + "enum": [ + "compliant", + "gaps_detected", + "pending_change", + "unknown_coverage", + "not_evaluated" + ], + "description": "Status after the re-evaluation." + } + } + }, + "severity": { + "type": "string", + "enum": [ + "actionable", + "indicative", + "informative" + ], + "description": "`actionable` when gaps were detected, `indicative` for a pending change, `informative` when gaps were resolved." + }, + "kb_version": { + "type": "string", + "description": "Version of the regulatory knowledge base used." + } + }, + "required": [ + "webhook_type", + "workflow_id", + "workflow_version", + "workflow_label", + "transition", + "severity", + "kb_version" + ] + } + ] + }, + "WebhookEventPayload": { + "description": "Body of a signed webhook delivery, discriminated by `webhook_type`. `activity.created` can be subscribed to but is not sent today. Treat any other `webhook_type` as a new event type and keep the raw body.", + "oneOf": [ + { + "$ref": "#/components/schemas/SessionStatusUpdatedEvent" + }, + { + "$ref": "#/components/schemas/SessionDataUpdatedEvent" + }, + { + "$ref": "#/components/schemas/UserStatusUpdatedEvent" + }, + { + "$ref": "#/components/schemas/UserDataUpdatedEvent" + }, + { + "$ref": "#/components/schemas/BusinessStatusUpdatedEvent" + }, + { + "$ref": "#/components/schemas/BusinessDataUpdatedEvent" + }, + { + "$ref": "#/components/schemas/TransactionCreatedEvent" + }, + { + "$ref": "#/components/schemas/TransactionStatusUpdatedEvent" + }, + { + "$ref": "#/components/schemas/TravelRuleStatusUpdatedEvent" + }, + { + "$ref": "#/components/schemas/WorkflowComplianceChangedEvent" + } + ], + "discriminator": { + "propertyName": "webhook_type", + "mapping": { + "status.updated": "#/components/schemas/SessionStatusUpdatedEvent", + "data.updated": "#/components/schemas/SessionDataUpdatedEvent", + "user.status.updated": "#/components/schemas/UserStatusUpdatedEvent", + "user.data.updated": "#/components/schemas/UserDataUpdatedEvent", + "business.status.updated": "#/components/schemas/BusinessStatusUpdatedEvent", + "business.data.updated": "#/components/schemas/BusinessDataUpdatedEvent", + "transaction.created": "#/components/schemas/TransactionCreatedEvent", + "transaction.status.updated": "#/components/schemas/TransactionStatusUpdatedEvent", + "travel_rule.status.updated": "#/components/schemas/TravelRuleStatusUpdatedEvent", + "workflow.compliance_changed": "#/components/schemas/WorkflowComplianceChangedEvent" } } }, - "EmailRiskResponse": { + "KYBRegistrySearchResolvedCallback": { "type": "object", + "description": "Body of the callback sent to the `webhook_url` of `POST /v3/kyb/search/` once the registry search resolves. It is not signed: the request carries `X-Didit-Unsigned-Callback: true` and none of the signature headers, so confirm it by matching `request_id` against a search you started. Uses `event_type`, not `webhook_type`.", "required": [ + "event_id", + "event_type", "request_id", - "status", + "vendor_data", + "metadata", + "search_status", + "search_resolved", + "kyb_registry", + "timestamp", "created_at" ], "properties": { - "request_id": { + "event_id": { "type": "string", "format": "uuid", - "description": "Id of the API session created for this request. Store it with `vendor_data` if you need to correlate the result with later webhooks or console activity." + "description": "Identifier of the callback." }, - "status": { + "event_type": { "type": "string", "enum": [ - "Approved", - "In Review", - "Declined" + "kyb.registry_search.resolved" ], - "description": "Final decision for this API session. Normally `Approved`; the risk evaluation can produce `In Review` or `Declined`." + "description": "Always `kyb.registry_search.resolved`." + }, + "request_id": { + "type": "string", + "format": "uuid", + "description": "The `request_id` returned by `POST /v3/kyb/search/`." }, "vendor_data": { "type": "string", "nullable": true, - "description": "Echo of the `vendor_data` you sent, `null` when you did not send one." + "description": "Echo of the `vendor_data` you sent." }, "metadata": { "type": "object", "nullable": true, "additionalProperties": true, - "description": "Echo of the `metadata` you sent, `null` when you did not send one." + "description": "Echo of the `metadata` you sent." + }, + "search_status": { + "type": "string", + "description": "Always `resolved` today." + }, + "search_resolved": { + "type": "boolean", + "description": "`true` when `search_status` is `resolved`." + }, + "kyb_registry": { + "$ref": "#/components/schemas/KYBRegistrySearchResults" + }, + "timestamp": { + "type": "integer", + "description": "Unix time in seconds at which the callback was queued." }, "created_at": { + "type": "integer", + "description": "Unix time in seconds at which the callback was queued." + } + } + }, + "AMLScreening": { + "type": "object", + "properties": { + "node_id": { "type": "string", - "format": "date-time", - "description": "Timestamp of this response." + "description": "Unique identifier for this AML step in the workflow graph." }, - "email": { - "type": "object", - "description": "Normalized email report, the same block used by Email Verification responses.", - "required": [ - "status", - "email", - "is_breached", - "breaches", - "is_disposable", - "is_undeliverable" - ], - "properties": { - "status": { - "type": "string", - "enum": [ - "Approved", - "In Review", - "Declined" - ], - "description": "Decision for this email address. Mirrors the top-level `status`." - }, - "email": { - "type": "string", - "description": "The scored address.", - "example": "alice@example.com" - }, - "is_breached": { - "type": "boolean", - "description": "Whether the address appears in a known credential breach." - }, - "breaches": { - "type": "array", - "description": "Up to 5 most recent known breaches containing this address. Empty array when `is_breached` is `false`.", - "items": { + "status": { + "type": "string", + "description": "Feature-level status of this AML screening. Independent of the top-level session `status` — a session can reach its overall decision while an individual feature stays `In Review`.", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ] + }, + "total_hits": { + "type": "integer", + "description": "Total number of matches found" + }, + "entity_type": { + "type": "string", + "description": "Type of entity screened: person or company" + }, + "hits": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier of the match" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Source URL for the match" + }, + "match": { + "type": "boolean", + "description": "Whether this is a confirmed match" + }, + "score": { + "type": "number", + "format": "float", + "nullable": true, + "minimum": 0, + "maximum": 1, + "description": "Deprecated weighted match score in the 0-1 range (e.g. `0.73`). Use `match_score` (0-100) instead.", + "deprecated": true + }, + "target": { + "type": "boolean", + "nullable": true, + "description": "Whether the matched entity is the direct target of the listing (as opposed to a related party). `null` when the source does not specify it." + }, + "caption": { + "type": "string", + "description": "Caption or title of the match" + }, + "datasets": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "PEP", + "PEP Level 1", + "PEP Level 2", + "PEP Level 3", + "PEP Level 4", + "Sanctions", + "Adverse Media", + "Warnings and Regulatory Enforcement", + "Fitness and Probity", + "SIP", + "SIE", + "Insolvency" + ] + }, + "description": "List of dataset categories where the match was found. Indicates whether the entity appeared in PEP lists (with level), sanctions databases, adverse media, warnings/regulatory enforcement, or other compliance datasets." + }, + "features": { + "type": "object", + "nullable": true, + "description": "Additional features of the match" + }, + "rca_name": { + "type": "string", + "description": "RCA name if applicable" + }, + "last_seen": { + "type": "string", + "format": "date-time", + "description": "Date when match was last seen" + }, + "risk_view": { + "type": "object", + "properties": { + "crimes": { + "type": "object", + "properties": { + "score": { + "type": "integer" + }, + "weightage": { + "type": "integer" + }, + "risk_level": { + "type": "string" + }, + "risk_scores": { + "type": "object" + } + } + }, + "countries": { + "type": "object", + "properties": { + "score": { + "type": "integer" + }, + "weightage": { + "type": "integer" + }, + "risk_level": { + "type": "string" + }, + "risk_scores": { + "type": "object" + } + } + }, + "categories": { + "type": "object", + "properties": { + "score": { + "type": "integer" + }, + "weightage": { + "type": "integer" + }, + "risk_level": { + "type": "string" + }, + "risk_scores": { + "type": "object" + } + } + }, + "custom_list": { + "type": "object" + } + } + }, + "first_seen": { + "type": "string", + "format": "date-time", + "description": "Date when match was first seen" + }, + "properties": { "type": "object", "properties": { "name": { + "type": "array", + "items": { + "type": "string" + } + }, + "alias": { + "type": "array", + "items": { + "type": "string" + } + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "type": "string", + "nullable": true + }, + "gender": { + "type": "array", + "items": { + "type": "string" + } + }, + "height": { + "type": "string", + "nullable": true + }, + "topics": { + "type": "string", + "nullable": true + }, + "weight": { + "type": "string", + "nullable": true + }, + "address": { + "type": "string", + "nullable": true + }, + "country": { + "type": "string", + "nullable": true + }, + "website": { + "type": "string", + "nullable": true + }, + "eyeColor": { + "type": "string", + "nullable": true + }, + "keywords": { + "type": "string", + "nullable": true + }, + "lastName": { + "type": "array", + "items": { + "type": "string" + } + }, + "position": { + "type": "string", + "nullable": true + }, + "religion": { + "type": "string", + "nullable": true + }, + "birthDate": { + "type": "array", + "items": { + "type": "string" + } + }, + "education": { + "type": "array", + "items": { + "type": "string" + } + }, + "ethnicity": { + "type": "string", + "nullable": true + }, + "firstName": { + "type": "array", + "items": { + "type": "string" + } + }, + "hairColor": { "type": "string", - "description": "Breach identifier." + "nullable": true }, - "domain": { + "weakAlias": { "type": "string", - "description": "Domain of the breached service." + "nullable": true }, - "breach_date": { + "birthPlace": { + "type": "array", + "items": { + "type": "string" + } + }, + "modifiedAt": { "type": "string", - "format": "date", - "description": "Date the breach occurred." + "nullable": true }, - "breach_emails_count": { + "wikidataId": { + "type": "string", + "nullable": true + }, + "citizenship": { + "type": "string", + "nullable": true + }, + "nationality": { + "type": "string", + "nullable": true + } + } + }, + "match_score": { + "type": "number", + "description": "Weighted identity confidence score (0-100) calculated from name similarity, date of birth, and country matching. Used to determine if hit is a False Positive or Possible Match.", + "format": "float", + "nullable": true + }, + "risk_score": { + "type": "number", + "description": "Entity risk score (0-100). Used to determine final AML status (Approved/In Review/Declined) based on the highest risk_score among non-False-Positive hits." + }, + "review_status": { + "type": "string", + "description": "Review status for this hit. Can be 'Unreviewed', 'Confirmed Match', 'False Positive', or 'Inconclusive'.", + "enum": [ + "Unreviewed", + "Confirmed Match", + "False Positive", + "Inconclusive" + ] + }, + "score_breakdown": { + "type": "object", + "description": "Detailed breakdown of how the match_score was calculated.", + "properties": { + "name_score": { "type": "integer", - "description": "Total number of accounts exposed in the breach." + "description": "Raw name similarity percentage (0-100)." }, - "description": { + "name_weight": { + "type": "integer", + "description": "Original weight for name from config (0-100)." + }, + "name_weight_normalized": { + "type": "number", + "description": "Normalized weight for name after re-weighting for missing data (0-100)." + }, + "name_contribution": { + "type": "number", + "description": "Points contributed by name to the total score." + }, + "dob_score": { + "type": "integer", + "description": "DOB match score (-100 to 100)." + }, + "dob_weight": { + "type": "integer", + "description": "Original weight for DOB from config (0-100)." + }, + "dob_weight_normalized": { + "type": "number", + "description": "Normalized weight for DOB after re-weighting for missing data (0-100)." + }, + "dob_contribution": { + "type": "number", + "description": "Points contributed by DOB to the total score." + }, + "country_score": { + "type": "integer", + "description": "Country match score (-100 to 100)." + }, + "country_weight": { + "type": "integer", + "description": "Original weight for country from config (0-100)." + }, + "country_weight_normalized": { + "type": "number", + "description": "Normalized weight for country after re-weighting for missing data (0-100)." + }, + "country_contribution": { + "type": "number", + "description": "Points contributed by country to the total score." + }, + "document_number_match_type": { "type": "string", - "description": "Human-readable description of the breach (may contain HTML markup)." + "description": "Result of document number 'Golden Key' evaluation.", + "enum": [ + "MATCH", + "NEUTRAL", + "HARD_MISMATCH" + ] }, - "logo_path": { + "document_number_effect": { "type": "string", - "description": "URL of the breached service's logo." + "description": "Human-readable description of the document number effect on the score." }, - "data_classes": { - "type": "array", - "items": { + "total_score": { + "type": "integer", + "description": "Final weighted match score (0-100)." + } + } + }, + "pep_matches": { + "type": "array", + "items": { + "type": "object", + "properties": { + "aliases": { + "type": "array", + "items": { + "type": "string" + } + }, + "education": { + "type": "array", + "items": { + "type": "string" + } + }, + "list_name": { "type": "string" }, - "description": "Categories of data exposed, in snake_case (e.g., `email_addresses`, `passwords`)." + "publisher": { + "type": "string" + }, + "source_url": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + }, + "matched_name": { + "type": "string" + }, + "pep_position": { + "type": "string" + }, + "date_of_birth": { + "type": "string" + }, + "other_sources": { + "type": "array", + "items": { + "type": "string" + } + }, + "place_of_birth": { + "type": "string" + } + } + } + }, + "linked_entities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "array", + "items": { + "type": "string" + } + }, + "active": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "type": "array", + "items": { + "type": "string" + } + }, + "details": { + "type": "array", + "items": { + "type": "string" + } + }, + "relation": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "warning_matches": { + "type": "array", + "description": "List of warning and regulatory enforcement matches. Includes entries from categories: Warning, SIP, Warnings and Regulatory Enforcement, Fitness and Probity, SIE, and Insolvency.", + "items": { + "type": "object", + "properties": { + "list_name": { + "type": "string", + "description": "Name of the warning list publisher" + }, + "matched_name": { + "type": "string", + "description": "Name matched in the warning list" + }, + "description": { + "type": "string", + "description": "Description of the warning entry" + }, + "publisher": { + "type": "string", + "description": "Publisher or source of the warning" + }, + "source_url": { + "type": "string", + "format": "uri", + "description": "URL to the source of the warning" + }, + "other_sources": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional source URLs" + }, + "countries": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "description": "Countries associated with the warning" + }, + "created_at": { + "type": "string", + "nullable": true, + "description": "Date the warning entry was created" + }, + "updated_at": { + "type": "string", + "nullable": true, + "description": "Date the warning entry was last updated" + }, + "additional_data": { + "type": "object", + "description": "Additional data from the warning source. May contain a `data` object with `case_details` array.", + "properties": { + "data": { + "type": "object", + "properties": { + "case_details": { + "type": "array", + "description": "Details of warning cases associated with this match", + "items": { + "type": "object", + "properties": { + "case_type": { + "type": "string", + "description": "Type of the warning case", + "enum": [ + "Accusation", + "Arrest", + "Bribery", + "Criminal", + "Fraud", + "Illegal Fundraising", + "Non-conviction Terror", + "Proclaimed Offender", + "Regulatory Enforcement", + "Terrorism", + "Terrorism-related", + "Terrorism-related listing" + ] + } + } + } + } + } + } + } + } + } + } + }, + "sanction_matches": { + "type": "array", + "description": "List of sanction list matches found for this entity.", + "items": { + "type": "object", + "properties": { + "list_name": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "description": "Name(s) of the sanction list(s)" + }, + "matched_name": { + "type": "string", + "description": "Name matched in the sanction list" + }, + "description": { + "type": "string", + "description": "Description of the sanction entry" + }, + "reason": { + "type": "string", + "nullable": true, + "description": "Free-text reason why the entity was sanctioned. Joined from the sanction list's reason entries. Examples: 'Executive Order 13224 (Terrorism)', 'Support for actions undermining Ukraine's sovereignty', 'Listed due to national security concerns.'" + }, + "source_url": { + "type": "string", + "format": "uri", + "description": "URL to the source of the sanction" + }, + "other_sources": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional source URLs" + }, + "details": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Additional details about the sanction" + }, + "legal_basis": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Legal basis for the sanction" + }, + "listed_on": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "description": "Date(s) when the entity was listed" + }, + "sanction_list": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "description": "Sanction list identifier(s)" + }, + "sanction_program": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Sanction program(s) the entity is listed under" + }, + "sanctioning_authority": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Authority/authorities that imposed the sanction" + }, + "updated_on": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Date(s) when the sanction entry was last updated" + }, + "additional_data": { + "type": "object", + "description": "Additional data from the sanction source not captured in the main fields" + } + } + } + }, + "adverse_media_details": { + "type": "object", + "properties": { + "sentiment": { + "type": "string" }, - "is_verified": { - "type": "boolean", - "description": "Whether the breach has been verified as legitimate." + "entity_type": { + "type": "string" + }, + "sentiment_score": { + "type": "integer" + }, + "adverse_keywords": { + "type": "object", + "additionalProperties": { + "type": "integer" + } } } + }, + "adverse_media_matches": { + "type": "array", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "headline": { + "type": "string" + }, + "sentiment": { + "type": "string" + }, + "thumbnail": { + "type": "string", + "format": "uri" + }, + "source_url": { + "type": "string", + "format": "uri" + }, + "author_name": { + "type": "string", + "nullable": true + }, + "other_sources": { + "type": "array", + "items": { + "type": "string" + } + }, + "adverse_keywords": { + "type": "array", + "items": { + "type": "string" + } + }, + "sentiment_score": { + "type": "integer" + }, + "publication_date": { + "type": "string", + "format": "date-time" + } + } + } + }, + "additional_information": { + "type": "object" } - }, - "is_disposable": { - "type": "boolean", - "description": "Whether the domain belongs to a throwaway provider." - }, - "is_undeliverable": { - "type": "boolean", - "description": "Whether the address was found undeliverable." - }, - "verification_attempts": { - "type": "integer", - "description": "OTP sends for this address. `0` for risk lookups." - }, - "verified_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "description": "When the address was last verified by an OTP. `null` for risk lookups." - }, - "warnings": { - "type": "array", - "description": "Risk warnings recorded for this request. Empty array when there are none.", - "items": { - "$ref": "#/components/schemas/RiskWarning" - } - }, - "lifecycle": { - "type": "array", - "description": "Chronological audit trail of the request. Empty array for risk lookups without sends.", - "items": { - "$ref": "#/components/schemas/RiskLifecycleEvent" - } - }, - "matches": { - "type": "array", - "description": "Other sessions of your application where the same email address was used by a different user (different `vendor_data`). Up to 5 entries. Empty array when there are no matches.", - "items": { - "$ref": "#/components/schemas/EmailMatchItem" - } - }, - "email_intelligence": { - "$ref": "#/components/schemas/EmailIntelligence" - }, - "enrichment": { - "$ref": "#/components/schemas/EmailIntelligence" - }, - "email_social": { - "$ref": "#/components/schemas/EmailSocialFootprint" } } - } - } - }, - "IPRiskResponse": { - "type": "object", - "required": [ - "request_id", - "status", - "created_at" - ], - "properties": { - "request_id": { - "type": "string", - "format": "uuid", - "description": "Id of the API session created for this request. Store it with `vendor_data` if you need to correlate the result with later webhooks or console activity." - }, - "status": { - "type": "string", - "enum": [ - "Approved", - "In Review" - ], - "description": "Decision for this API session: `In Review` on network or country-mismatch risk, otherwise `Approved`." - }, - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Echo of the `vendor_data` you sent, `null` when you did not send one." }, - "metadata": { - "type": "object", + "score": { + "type": "number", + "format": "float", "nullable": true, - "additionalProperties": true, - "description": "Echo of the `metadata` you sent, `null` when you did not send one." - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "Timestamp of this response." - }, - "ip": { - "type": "object", - "description": "IP risk result. Individual enrichment fields can be `null` when the lookup returned no usable data.", - "required": [ - "status", - "threat" - ], - "properties": { - "status": { - "type": "string", - "enum": [ - "Approved", - "In Review" - ], - "description": "`In Review` when the resolved country conflicts with `claimed_country`, or when the address is associated with VPN, Tor, or data-center infrastructure. Otherwise `Approved`." - }, - "ip_address": { - "type": "string", - "nullable": true, - "description": "The scored address. `null` when no enrichment source returned usable data." - }, - "ip_country": { - "type": "string", - "nullable": true, - "description": "Resolved country name." - }, - "ip_country_code": { - "type": "string", - "nullable": true, - "description": "Resolved country code. Compare countries through `country_mismatch` instead of comparing raw country strings yourself." - }, - "ip_state": { - "type": "string", - "nullable": true, - "description": "Resolved state or region." - }, - "ip_city": { - "type": "string", - "nullable": true, - "description": "Resolved city." - }, - "latitude": { - "type": "number", - "nullable": true, - "description": "Approximate network geolocation, not a precise device location." - }, - "longitude": { - "type": "number", - "nullable": true, - "description": "Approximate network geolocation, not a precise device location." - }, - "isp": { - "type": "string", - "nullable": true, - "description": "Internet service provider serving the address." - }, - "organization": { - "type": "string", - "nullable": true, - "description": "Organization owning the address block." - }, - "is_vpn_or_tor": { - "type": "boolean", - "nullable": true, - "description": "`true` when the address is associated with VPN or Tor infrastructure." - }, - "is_data_center": { - "type": "boolean", - "nullable": true, - "description": "`true` when the address belongs to a hosting or data-center network." - }, - "time_zone": { - "type": "string", - "nullable": true, - "description": "Time zone of the resolved location." - }, - "time_zone_offset": { - "type": "string", - "nullable": true, - "description": "UTC offset of the resolved location." - }, - "asn_number": { - "type": "number", - "nullable": true, - "description": "Autonomous system number." - }, - "asn_organization": { - "type": "string", - "nullable": true, - "description": "Organization operating the autonomous system." - }, - "connection_type": { - "type": "string", - "nullable": true, - "description": "Connection classification (e.g., `cellular`, `hosting`)." + "minimum": 0, + "maximum": 100, + "description": "Overall AML screening score — the highest risk score across all hits (float, 0-100). `null` until computed." + }, + "screened_data": { + "type": "object", + "properties": { + "full_name": { + "type": "string" }, - "carrier": { - "type": "string", - "nullable": true, - "description": "Mobile carrier when the address is on a mobile network." + "nationality": { + "type": "string" }, - "proxy_type": { + "date_of_birth": { "type": "string", - "nullable": true, - "description": "Proxy classification when the address is behind a proxy." + "format": "date" }, - "claimed_country": { + "document_number": { "type": "string", - "nullable": true, - "description": "Echo of the `claimed_country` you sent." - }, - "country_mismatch": { - "type": "boolean", - "nullable": true, - "description": "`true` when the resolved country differs from `claimed_country`. `null` when no enrichment data or no claimed country is available." - }, - "threat": { - "type": "object", - "additionalProperties": true, - "description": "Free-form network-threat evidence from the enrichment source (e.g., `is_proxy`, `is_tor`). Empty object when no enrichment data is available. Treat as diagnostic detail, not a stable contract." + "nullable": true } } - } - } - }, - "SocialFootprintPlatform": { - "type": "object", - "properties": { - "platform": { - "type": "string", - "description": "Platform identifier.", - "example": "whatsapp" }, - "category": { + "is_ongoing_monitoring_enabled": { + "type": "boolean", + "description": "Whether AML ongoing monitoring is enabled for this screening. When `true`, the entity is re-screened periodically and any new hits update the report." + }, + "next_ongoing_monitoring_bill_date": { "type": "string", - "description": "Platform category, e.g. MESSAGING, SOCIAL_MEDIA, ECOMMERCE, PROFESSIONAL, EMAIL_PROVIDER.", - "example": "MESSAGING" + "format": "date", + "nullable": true, + "description": "Date (YYYY-MM-DD) when this ongoing-monitoring subscription will next be billed. Computed as the date of the last ongoing-monitoring billing plus 365 days when `is_ongoing_monitoring_enabled = true`; otherwise `null`. This is a billing date, not a screening date - use `last_ongoing_screening_at` for that." }, - "scope": { + "last_ongoing_screening_at": { "type": "string", - "enum": [ - "global", - "local" - ], - "description": "`global` for worldwide platforms, `local` for region-specific ones.", - "example": "global" - } - } - }, - "SocialFootprintCategorySummary": { - "type": "object", - "properties": { - "registered": { - "type": "integer", + "format": "date-time", "nullable": true, - "description": "Platforms in this category where registration is confirmed.", - "example": 2 + "description": "When ongoing monitoring last completed a re-screening of this record, whether or not the result changed. `null` when the record has never been re-screened; it is never back-filled from the enablement or billing date." }, - "ratio": { - "type": "number", - "format": "float", + "last_ongoing_screening_datasets_updated_at": { + "type": "string", + "format": "date-time", "nullable": true, - "description": "Share of the category's checked platforms with a registration, 0.0-1.0.", - "example": 0.6667 + "description": "The most recent AML dataset update date the provider reported at the time of that screening. `null` when the provider reported no dataset dates; never estimated." + }, + "warnings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "enum": [ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE" + ], + "description": "The feature that generated this warning. The value identifies the feature block the warning belongs to. Within an AML screening this is typically `AML`, but the enum above is authoritative across all feature warnings on the decision payload." + }, + "risk": { + "type": "string" + }, + "additional_data": { + "type": "object", + "nullable": true + }, + "log_type": { + "type": "string" + }, + "short_description": { + "type": "string" + }, + "long_description": { + "type": "string" + }, + "node_id": { + "type": "string", + "description": "The workflow graph node ID that generated this warning" + } + } + } } } }, - "SocialFootprintProfile": { + "MonitoringEvent": { "type": "object", + "description": "One stored monitoring event as returned by the list and subject-timeline endpoints. Restricted evidence (payload, device context, rule evidence) is only returned by `GET /v3/events/{event_uuid}/`.", + "required": [ + "event_uuid", + "event_id", + "event_category", + "action_type", + "occurred_at", + "received_at", + "status", + "score", + "subject_vendor_data", + "subject_entity_type", + "subject_uuid", + "counterparty_vendor_data", + "session_id", + "device_fingerprint", + "ip_address", + "ip_country", + "amount", + "currency", + "game_id", + "round_id", + "bonus_id", + "affiliate_code", + "payment_reference_id", + "retention_class", + "rules_matched_count", + "correlation_pending", + "is_droppable" + ], "properties": { - "name": { - "type": "string", - "nullable": true, - "description": "Profile name, when the platform exposes one." - }, - "photo": { + "event_uuid": { "type": "string", - "nullable": true, - "description": "Profile photo URL, when the platform exposes one." + "format": "uuid", + "description": "Didit identifier of the event. Use it as `{event_uuid}` in the retrieve and delete calls." }, - "about": { + "event_id": { "type": "string", - "nullable": true, - "description": "Profile about/bio text, when the platform exposes one." + "maxLength": 128, + "description": "Your own identifier, the idempotency key you sent." }, - "privacy_status": { + "event_category": { "type": "string", - "nullable": true, "enum": [ - "PUBLIC", - "PRIVATE", - null + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom" ], - "description": "Profile visibility, when the platform exposes it." - }, - "business_account": { - "type": "boolean", - "nullable": true, - "description": "Whether the profile is a business account, when the platform exposes it." - } - } - }, - "PhoneSocialFootprint": { - "type": "object", - "nullable": true, - "description": "Social footprint of the phone number: the online platforms it is registered on. Present only when `enable_social` was set and the check completed. Every count may be null when no data was returned for it. `profiles_registered: 0` means the platforms were checked and the phone number was found on none of them - a valid, billable answer and a common synthetic-identity signal.", - "properties": { - "profiles_registered": { - "type": "integer", - "nullable": true, - "description": "Platforms where the phone number is confirmed registered.", - "example": 5 - }, - "profiles_checked": { - "type": "integer", - "nullable": true, - "description": "Platforms evaluated for registration.", - "example": 14 - }, - "names_found": { - "type": "integer", - "nullable": true, - "description": "Platforms that returned an associated name.", - "example": 0 - }, - "photos_found": { - "type": "integer", - "nullable": true, - "description": "Platforms that returned a profile photo.", - "example": 0 - }, - "communication_breadth": { - "type": "integer", - "nullable": true, - "description": "Communication platforms with detected presence.", - "example": 2 + "description": "Event category." }, - "digital_footprint_diversity": { - "type": "integer", - "nullable": true, - "description": "Distinct platform categories with detected presence.", - "example": 5 - }, - "digital_sophistication_index": { - "type": "integer", - "nullable": true, - "description": "Composite digital-maturity indicator, 0-10.", - "example": 5 - }, - "engagement_depth_index": { - "type": "integer", - "nullable": true, - "description": "Profile-richness indicator, 0-10.", - "example": 3 - }, - "subscription_affordability_index": { - "type": "integer", - "nullable": true, - "description": "Free vs paid platform mix, 0-10.", - "example": 2 - }, - "free_platforms": { - "type": "integer", - "nullable": true, - "description": "Registrations on free platforms.", - "example": 4 + "action_type": { + "type": "string", + "maxLength": 128, + "description": "What happened within the category, as you sent it." }, - "full_paid_platforms": { - "type": "integer", - "nullable": true, - "description": "Registrations on fully paid platforms.", - "example": 0 + "occurred_at": { + "type": "string", + "format": "date-time", + "description": "When the event happened on your side." }, - "partial_paid_platforms": { - "type": "integer", - "nullable": true, - "description": "Registrations on freemium platforms.", - "example": 1 + "received_at": { + "type": "string", + "format": "date-time", + "description": "When Didit accepted the event." }, - "whatsapp_profile_completeness": { + "status": { "type": "string", - "nullable": true, "enum": [ - "Basic", - "Partial", - "Complete", - "Not Present", - null + "ACCEPTED", + "APPROVED", + "IN_REVIEW", + "DECLINED" ], - "description": "Completeness of the WhatsApp profile. Phone only; null for email.", - "example": "Partial" + "description": "`ACCEPTED`: stored, no matched rule changed its standing (the normal answer for most events, and always the answer in a sandbox application, where no rules run). `APPROVED`: a rule matched and the score stayed under your review threshold. `IN_REVIEW`: a rule pushed the score over the review threshold or set the status. `DECLINED`: a rule declined it or the score crossed your decline threshold." }, - "registered_platforms": { - "type": "array", - "description": "Platforms where registration is confirmed.", - "items": { - "$ref": "#/components/schemas/SocialFootprintPlatform" - } + "score": { + "type": "integer", + "description": "Accumulated rule score. Starts at 0; rule `add_score` actions move it (negative deltas are allowed)." }, - "not_registered_platforms": { - "type": "array", - "description": "Platforms checked with a negative result.", - "items": { - "$ref": "#/components/schemas/SocialFootprintPlatform" - } + "subject_vendor_data": { + "type": "string", + "maxLength": 255, + "description": "`subject.vendor_data` of the event: your id for the user or company." }, - "categories": { - "type": "object", - "description": "Registered count and ratio per platform category.", - "additionalProperties": { - "$ref": "#/components/schemas/SocialFootprintCategorySummary" - } + "subject_entity_type": { + "type": "string", + "description": "`individual` or `company`: which kind of subject `subject_vendor_data` names.", + "example": "individual" }, - "profiles": { - "type": "object", - "description": "Per-platform profile details, keyed by platform, when the platform exposes them.", - "additionalProperties": { - "$ref": "#/components/schemas/SocialFootprintProfile" - } - } - } - }, - "EmailSocialFootprint": { - "type": "object", - "nullable": true, - "description": "Social footprint of the email address: the online platforms it is registered on. Present only when `enable_social` was set and the check completed. Every count may be null when no data was returned for it. `profiles_registered: 0` means the platforms were checked and the email address was found on none of them - a valid, billable answer and a common synthetic-identity signal.", - "properties": { - "profiles_registered": { - "type": "integer", + "subject_uuid": { + "type": "string", + "format": "uuid", "nullable": true, - "description": "Platforms where the email address is confirmed registered.", - "example": 5 + "description": "UUID of the user (individual) or company the event resolved to, or null when that record no longer exists." }, - "profiles_checked": { - "type": "integer", + "counterparty_vendor_data": { + "type": "string", "nullable": true, - "description": "Platforms evaluated for registration.", - "example": 14 + "description": "The other player of a P2P event (promoted from `payload.counterparty_vendor_data`).", + "maxLength": 255 }, - "names_found": { - "type": "integer", + "session_id": { + "type": "string", "nullable": true, - "description": "Platforms that returned an associated name.", - "example": 0 + "description": "`session.session_id` sent with the event.", + "maxLength": 255 }, - "photos_found": { - "type": "integer", + "device_fingerprint": { + "type": "string", "nullable": true, - "description": "Platforms that returned a profile photo.", - "example": 0 + "description": "`device_context.fingerprint` sent with the event.", + "maxLength": 255 }, - "communication_breadth": { - "type": "integer", + "ip_address": { + "type": "string", "nullable": true, - "description": "Communication platforms with detected presence.", - "example": 2 + "description": "IP address of the event. On the server lane this is `device_context.network_context.ip_address` as you sent it; on the device lane it is the IP of the request.", + "maxLength": 64 }, - "digital_footprint_diversity": { - "type": "integer", + "ip_country": { + "type": "string", "nullable": true, - "description": "Distinct platform categories with detected presence.", - "example": 5 + "description": "Country code of `ip_address` from IP enrichment, or null when the address was not enriched (missing, private, or low-value telemetry whose address was not already known).", + "example": "ES" }, - "digital_sophistication_index": { - "type": "integer", + "amount": { + "type": "string", "nullable": true, - "description": "Composite digital-maturity indicator, 0-10.", - "example": 5 + "description": "Amount as a decimal string with 18 decimal places, from `transaction_context.amount`, `payload.amount` or `payload.stake_amount`, in that order.", + "example": "250.000000000000000000" }, - "engagement_depth_index": { - "type": "integer", + "currency": { + "type": "string", "nullable": true, - "description": "Profile-richness indicator, 0-10.", - "example": 3 + "description": "Currency from `transaction_context.currency` or `payload.currency`.", + "maxLength": 10 }, - "subscription_affordability_index": { - "type": "integer", + "game_id": { + "type": "string", "nullable": true, - "description": "Free vs paid platform mix, 0-10.", - "example": 2 + "description": "Promoted from `payload.game_id`.", + "maxLength": 128 }, - "free_platforms": { - "type": "integer", + "round_id": { + "type": "string", "nullable": true, - "description": "Registrations on free platforms.", - "example": 4 + "description": "Promoted from `payload.round_id`.", + "maxLength": 128 }, - "full_paid_platforms": { - "type": "integer", + "bonus_id": { + "type": "string", "nullable": true, - "description": "Registrations on fully paid platforms.", - "example": 0 + "description": "Promoted from `payload.bonus_id`.", + "maxLength": 128 }, - "partial_paid_platforms": { - "type": "integer", + "affiliate_code": { + "type": "string", "nullable": true, - "description": "Registrations on freemium platforms.", - "example": 1 + "description": "Promoted from `payload.affiliate_code`.", + "maxLength": 128 }, - "whatsapp_profile_completeness": { + "payment_reference_id": { "type": "string", "nullable": true, + "description": "Payment reference from `transaction_context.payment_reference_id` or `payload.payment_reference_id`.", + "maxLength": 255 + }, + "retention_class": { + "type": "string", "enum": [ - "Basic", - "Partial", - "Complete", - "Not Present", - null + "low_value", + "standard", + "risk_relevant", + "monetary_or_compliance" ], - "description": "Completeness of the WhatsApp profile. Phone only; null for email.", - "example": "Partial" + "description": "Retention class. Payment and dispute events are `monetary_or_compliance`; security and responsible-gambling events are `risk_relevant`; `session.page_view`, `session.sdk_heartbeat`, `session.sdk_loaded`, `session.device_seen`, `session.bot_signal_observed`, `bonus.promotion_viewed` and `affiliate.referral_clicked` are `low_value`; everything else is `standard`. An event an active rule matched is promoted to `risk_relevant` (never demoted)." }, - "registered_platforms": { - "type": "array", - "description": "Platforms where registration is confirmed.", - "items": { - "$ref": "#/components/schemas/SocialFootprintPlatform" - } + "rules_matched_count": { + "type": "integer", + "minimum": 0, + "description": "Number of rules that matched the event, including rules in `TEST` mode (which record evidence but apply no actions)." }, - "not_registered_platforms": { - "type": "array", - "description": "Platforms checked with a negative result.", - "items": { - "$ref": "#/components/schemas/SocialFootprintPlatform" - } + "correlation_pending": { + "type": "boolean", + "description": "`true` while an asynchronous rule (cross-subject correlation, or a sequence longer than 24 hours) still has to run for this event. It resolves within minutes and `status`, `score` and `rules_matched_count` update in place." }, - "categories": { - "type": "object", - "description": "Registered count and ratio per platform category.", - "additionalProperties": { - "$ref": "#/components/schemas/SocialFootprintCategorySummary" - } + "is_droppable": { + "type": "boolean", + "description": "Whether an SDK under backpressure may drop events of this kind. `false` for every `security`, `payment`, `dispute` and `responsible_gambling` event and for `gameplay.bet_placed`, `gameplay.bet_settled` and `gameplay.p2p_settlement`." + } + } + }, + "MonitoringEventDetail": { + "description": "One monitoring event with its full envelope, enrichment and rule evidence. Every field of `MonitoringEvent` plus the fields below.", + "allOf": [ + { + "$ref": "#/components/schemas/MonitoringEvent" }, - "profiles": { + { "type": "object", - "description": "Per-platform profile details, keyed by platform, when the platform exposes them.", - "additionalProperties": { - "$ref": "#/components/schemas/SocialFootprintProfile" + "required": [ + "payload", + "custom_values", + "device_context", + "field_sources", + "session_sequence", + "table_id", + "hand_id", + "match_id", + "market_id", + "side", + "role", + "net_result_amount", + "payment_fingerprint", + "instrument_reference_hash", + "withdrawal_address_hash", + "dispute_id", + "chargeback_id", + "changed_field", + "identity_match_ref", + "schema_version", + "expires_at", + "rules_evaluated_count", + "rule_runs", + "ip_enrichment", + "subject_profile", + "tags" + ], + "properties": { + "payload": { + "type": "object", + "additionalProperties": true, + "description": "The category fields you sent in `payload`." + }, + "custom_values": { + "type": "object", + "additionalProperties": true, + "description": "The scalar `custom_values` you sent. Didit adds `vpn_or_proxy_detected: true` or `is_data_center: true` when IP enrichment flags the address and you did not set them." + }, + "device_context": { + "type": "object", + "additionalProperties": true, + "description": "The device context as stored: your `device_context` (including `fingerprint`, `persistent_device_id` and `bot_score` when sent) with `network_context` completed by IP enrichment (`ip_country`, `ip_country_code`, `ip_city`, `asn_number`, `asn_organization`, `is_vpn_or_tor`, `is_data_center`, `proxy_type`, `connection_type`) for keys you left empty." + }, + "field_sources": { + "type": "object", + "additionalProperties": { + "type": "string", + "enum": [ + "customer", + "sdk", + "server" + ] + }, + "description": "Field path to provenance: `customer` (sent by your backend), `sdk` (sent through the device lane) or `server` (derived by Didit, such as `received_at` and enriched network fields). Check it before acting on a match." + }, + "session_sequence": { + "type": "integer", + "format": "int64", + "nullable": true, + "description": "`session.sequence` sent with the event." + }, + "table_id": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.table_id`.", + "maxLength": 128 + }, + "hand_id": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.hand_id`.", + "maxLength": 128 + }, + "match_id": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.match_id`.", + "maxLength": 128 + }, + "market_id": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.market_id`.", + "maxLength": 128 + }, + "side": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.side`.", + "maxLength": 64 + }, + "role": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.role`.", + "maxLength": 64 + }, + "net_result_amount": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.net_result_amount`, as a decimal string with 18 decimal places.", + "example": "-120.000000000000000000" + }, + "payment_fingerprint": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.payment_fingerprint`.", + "maxLength": 255 + }, + "instrument_reference_hash": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.instrument_reference_hash`.", + "maxLength": 255 + }, + "withdrawal_address_hash": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.withdrawal_address_hash` (or `payload.destination_reference_hash`).", + "maxLength": 255 + }, + "dispute_id": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.dispute_id`.", + "maxLength": 255 + }, + "chargeback_id": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.chargeback_id`.", + "maxLength": 255 + }, + "changed_field": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.changed_field` (security change events).", + "maxLength": 128 + }, + "identity_match_ref": { + "type": "string", + "nullable": true, + "description": "Promoted from `payload.identity_match_ref` (responsible-gambling registry matches).", + "maxLength": 255 + }, + "schema_version": { + "type": "integer", + "description": "Version of the event envelope schema. Currently `1`.", + "example": 1 + }, + "expires_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the event expires under its retention class and is deleted: `occurred_at` plus the class period (14, 30 or 90 days by default, adjustable per application). Null for `monetary_or_compliance` events, which follow transaction retention." + }, + "rules_evaluated_count": { + "type": "integer", + "minimum": 0, + "description": "Number of event rules evaluated for this event." + }, + "rule_runs": { + "type": "array", + "description": "Every rule that matched the event, newest first.", + "items": { + "$ref": "#/components/schemas/MonitoringEventRuleRun" + } + }, + "ip_enrichment": { + "type": "object", + "nullable": true, + "description": "Enrichment of the event's IP address, or null when the address was not enriched. See [IP enrichment](https://docs.didit.me/transaction-monitoring/ip-enrichment).", + "properties": { + "ip_country_code": { + "type": "string", + "nullable": true, + "description": "Country code of the address." + }, + "ip_city": { + "type": "string", + "nullable": true, + "description": "City of the address." + }, + "asn_number": { + "type": "integer", + "nullable": true, + "description": "Autonomous system number." + }, + "asn_organization": { + "type": "string", + "nullable": true, + "description": "Organization that operates the autonomous system." + }, + "is_vpn_or_tor": { + "type": "boolean", + "description": "Whether the address is a known VPN or Tor exit." + }, + "is_data_center": { + "type": "boolean", + "description": "Whether the address belongs to a hosting or data-centre network." + }, + "proxy_type": { + "type": "string", + "nullable": true, + "description": "Proxy classification of the address as an opaque string, or null." + }, + "last_enriched_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the address was last enriched." + } + }, + "required": [ + "ip_country_code", + "ip_city", + "asn_number", + "asn_organization", + "is_vpn_or_tor", + "is_data_center", + "proxy_type", + "last_enriched_at" + ] + }, + "subject_profile": { + "type": "object", + "nullable": true, + "description": "Cached email and phone risk signals of the subject, or null when no profile exists for the subject.", + "properties": { + "email_risk_score": { + "type": "integer", + "nullable": true, + "description": "Email risk score." + }, + "email_deliverability": { + "type": "string", + "nullable": true, + "description": "Email deliverability verdict." + }, + "email_is_disposable": { + "type": "boolean", + "nullable": true, + "description": "Whether the email address is disposable." + }, + "email_is_breached": { + "type": "boolean", + "nullable": true, + "description": "Whether the email address appears in known data breaches." + }, + "email_social_count": { + "type": "integer", + "nullable": true, + "description": "Number of online accounts linked to the email address." + }, + "email_social_providers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Names of the online services where an account linked to the email address was found." + }, + "phone_risk_score": { + "type": "integer", + "nullable": true, + "description": "Phone risk score." + }, + "phone_line_type": { + "type": "string", + "nullable": true, + "description": "Phone line type." + }, + "phone_carrier": { + "type": "string", + "nullable": true, + "description": "Phone carrier name." + }, + "phone_social_count": { + "type": "integer", + "nullable": true, + "description": "Number of online accounts linked to the phone number." + }, + "phone_social_providers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Names of the online services where an account linked to the phone number was found." + }, + "source_provider": { + "type": "string", + "nullable": true, + "description": "Opaque identifier of the source of the profile." + }, + "last_enriched_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the profile was last refreshed." + } + }, + "required": [ + "email_risk_score", + "email_deliverability", + "email_is_disposable", + "email_is_breached", + "email_social_count", + "email_social_providers", + "phone_risk_score", + "phone_line_type", + "phone_carrier", + "phone_social_count", + "phone_social_providers", + "source_provider", + "last_enriched_at" + ] + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Names of the tags attached to the event (rule `add_tags` actions)." + } } } - } - }, - "KYBRegistryAddress": { - "type": "object", - "description": "An address the registry states, in the same shape for every country and registry source. Every key is always present; a part the registry does not publish is null.", - "properties": { - "type": { - "type": "string", - "nullable": true, - "description": "Address type in lower case: `registered` for the registered office; other types as the registry names them (for example `postal`, `office`). `null` when the registry does not say what the address is." - }, - "address": { - "type": "string", - "description": "The full address as one line. Always present on every address." - }, - "street": { - "type": "string", - "nullable": true, - "description": "Street and number." - }, - "city": { - "type": "string", - "nullable": true, - "description": "City or locality." - }, - "postal_code": { - "type": "string", - "nullable": true, - "description": "Postal code." - }, - "region": { - "type": "string", - "nullable": true, - "description": "Region, state or province." - }, - "country_code": { - "type": "string", - "nullable": true, - "description": "ISO 3166-1 alpha-2 country code." - }, - "description": { - "type": "string", - "nullable": true, - "description": "The registry's own label for the address, when it gives one." - } - }, - "required": [ - "type", - "address", - "street", - "city", - "postal_code", - "region", - "country_code", - "description" ] }, - "KYBRegistryIndustry": { + "MonitoringEventRuleRun": { "type": "object", - "description": "An activity the registry classifies the company under, in the same shape for every country and registry source.", "properties": { - "code": { + "rule_uuid": { "type": "string", - "nullable": true, - "description": "Activity code as the registry prints it (for example `73110`)." + "format": "uuid", + "description": "UUID of the transaction monitoring rule." }, - "description": { + "rule_title": { "type": "string", - "nullable": true, - "description": "Activity label, when the registry publishes one." + "description": "Title of the rule." }, - "scheme": { + "library_key": { "type": "string", "nullable": true, - "description": "Classification system, one name per system whichever source reported it: `UK SIC 2007`, `US SIC`, `NACE Rev. 2`, `NACE Rev. 2.1`, `NAF Rev. 2`, `CNAE`, `SBI`, `NOGA 2008`, `CAE Rev. 4`, `Canada SIC 1977`; other national systems keep the registry's own name. Null when the registry does not name the system." + "description": "Library key when the rule was installed from the rules library, otherwise null." }, - "is_primary": { + "matched": { "type": "boolean", - "description": "True for the company's main activity. Exactly one entry is primary when the list is not empty." - } - }, - "required": [ - "code", - "description", - "scheme", - "is_primary" - ] - }, - "KYBRegistryShareholding": { - "type": "object", - "nullable": true, - "description": "What an owner holds, in the same shape for every country and registry source. Null when the registry states no holding. A band (for example a UK PSC's 25-50%) is not a stake: `percentage` is null and the band stays in `ownership_min_shares` / `ownership_max_shares`.", - "properties": { - "share_class": { - "type": "string", - "nullable": true, - "description": "Share class or type as the registry names it." - }, - "number_of_shares": { - "type": "number", - "nullable": true, - "description": "Number of shares held." - }, - "currency": { - "type": "string", - "nullable": true, - "description": "ISO 4217 currency of the values below." - }, - "nominal_value": { - "type": "number", - "nullable": true, - "description": "Nominal value per share." - }, - "total_value": { - "type": "number", - "nullable": true, - "description": "Total value of the holding." + "description": "Always `true`: only matching runs are recorded." }, - "percentage": { - "type": "number", - "nullable": true, - "description": "Exact stake in percent, to four decimal places." - } - }, - "required": [ - "share_class", - "number_of_shares", - "currency", - "nominal_value", - "total_value", - "percentage" - ] - }, - "KYBRegistryOwnership": { - "type": "object", - "description": "The outcome of the ownership (Shareholders / UBO) tier, in the same shape for every country and registry source.", - "properties": { - "status": { + "mode": { "type": "string", - "nullable": true, "enum": [ - "not_requested", - "pending", - "delivered", - "unavailable", - null + "ACTIVE", + "TEST" ], - "description": "`delivered` when the registry published ownership; `unavailable` when it did not (see `unavailable_reason`); `pending` while the registry is still fetching it; `not_requested` for the Basic tier." + "description": "Mode of the rule when it ran. A `TEST` run records evidence but applied no actions." }, - "tier_requested": { - "type": "string", - "nullable": true, - "enum": [ - "basic", - "shareholders", - "ubo", - null - ], - "description": "The tier the workflow ordered." + "is_async": { + "type": "boolean", + "description": "`true` when the run came from the asynchronous correlation pass rather than the ingest request." }, - "tier_delivered": { + "severity": { "type": "string", "nullable": true, "enum": [ - "basic", - "shareholders", - "ubo", + "UNKNOWN", + "LOW", + "MEDIUM", + "HIGH", + "CRITICAL", null ], - "description": "The tier the registry delivered (and that is billed). A UBO request answered with direct shareholders only is `shareholders`." + "description": "Severity of the rule. Null for `TEST` runs and for rules without a severity." }, - "unavailable_reason": { - "type": "string", - "nullable": true, - "enum": [ - "no_owners_on_record", - "no_natural_person_owner", - "ownership_section_unavailable", - "company_not_found", - "ambiguous_company_match", - "missing_registration_number", - "entity_type_not_covered", - "provider_unavailable", - "other", - null - ], - "description": "Why a requested ownership tier was not delivered. Null unless `status` is `unavailable` or `pending`." + "score_delta": { + "type": "integer", + "description": "How much this run changed the event's score (0 for `TEST` runs)." }, - "direct_ownership_complete": { - "type": "boolean", + "action_summary": { + "type": "string", "nullable": true, - "description": "True when the direct shareholders' stated stakes add up to 100% (within one percentage point of rounding); false when they do not; null when ownership was not delivered or a stake is not stated." + "description": "Short summary of the actions applied, for example `score +40; add_tag:VPN_LOGIN`. Null when nothing was applied." }, - "unresolved": { + "matched_payload": { "type": "array", - "description": "Parts of the ownership chain the registry could not resolve, for example no group structure on record above the direct owners.", + "description": "The conditions and aggregations that matched. Condition entries carry `field`, `operator`, `expected_value`, `current_value`, `matched`, `group_index`, `group_logic` and `field_source` (`server`, `sdk` or `customer`); aggregation entries carry `metric`, `field`, `operator`, `expected_value`, `computed_value`, `window`, `filters`, `matched` and `sample_references`.", "items": { "type": "object", - "properties": { - "reason": { - "type": "string" - }, - "section": { - "type": "string", - "nullable": true - } - } + "additionalProperties": true } + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the rule run was recorded." } }, "required": [ - "status", - "tier_requested", - "tier_delivered", - "unavailable_reason", - "direct_ownership_complete", - "unresolved" + "rule_uuid", + "rule_title", + "library_key", + "matched", + "mode", + "is_async", + "severity", + "score_delta", + "action_summary", + "matched_payload", + "created_at" ] }, - "DigitalIdWalletApiWalletVerificationError": { + "MonitoringEventPage": { "type": "object", + "description": "One page of monitoring events, newest `occurred_at` first.", + "required": [ + "count", + "next", + "previous", + "results" + ], "properties": { - "error": { - "type": "string", - "description": "Stable machine-readable code, e.g. wallet_unavailable." + "count": { + "type": "integer", + "description": "Number of matching events, exact up to 100 and capped at 100 above that. Do not compute page counts from it; follow `next` until it is null." }, - "detail": { - "type": "string" - } - }, - "required": [ - "detail", - "error" - ] - }, - "DigitalIdWalletApiWalletVerificationStartForbidden": { - "oneOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiClientCredentialsForbiddenErrorMessage" + "next": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL of the next page, or null on the last page." }, - { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" - } - ] - }, - "DigitalIdWalletApiClientCredentialsForbiddenErrorMessage": { - "type": "object", - "properties": { - "detail": { + "previous": { "type": "string", - "default": "Forbidden - User doesn't have permission" - } - } - }, - "DigitalIdWalletApiWalletVerificationStartBadRequest": { - "oneOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationError" + "format": "uri", + "nullable": true, + "description": "URL of the previous page, or null on the first page." }, - { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationFieldErrors" - } - ] - }, - "DigitalIdWalletApiWalletVerificationFieldErrors": { - "type": "object", - "description": "Request validation errors: each rejected field with its messages.", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MonitoringEvent" + } } } }, - "DigitalIdWalletApiWalletVerificationResponse": { + "MonitoringEventCreateRequest": { "type": "object", - "description": "One standalone wallet verification, as every wallet endpoint answers it.", + "description": "The event envelope, the same for every category. camelCase aliases are accepted: `eventId`, `eventCategory` (or `category`), `actionType`, `occurredAt`, `timeZone` (or `zoneId`), `deviceContext` (or `device`), `transactionContext`, `customValues` (or `props`); inside `subject`: `vendorData`, `entityType`, `fullName`; inside `session`: `sessionId`, `pageUrl`; inside `transaction_context`: `paymentReferenceId`.", + "required": [ + "event_id", + "event_category", + "action_type", + "occurred_at", + "subject" + ], "properties": { - "request_id": { - "type": "string", - "format": "uuid" - }, - "wallet_verification": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResult" - }, - "vendor_data": { + "event_id": { "type": "string", - "nullable": true - }, - "metadata": { - "type": "object", - "additionalProperties": {}, - "nullable": true, - "description": "The metadata object sent at start, or null." + "maxLength": 128, + "description": "Your identifier for the event, unique per application. It is the idempotency key: resending the same `event_category`, `action_type`, `occurred_at`, `subject.vendor_data` and `payload` under it is a replay, anything different is a conflict.", + "example": "login-2026-10-17-000001" }, - "created_at": { + "event_category": { "type": "string", - "format": "date-time" - } - }, - "required": [ - "created_at", - "metadata", - "request_id", - "vendor_data", - "wallet_verification" - ] - }, - "DigitalIdWalletApiWalletVerificationResult": { - "type": "object", - "properties": { - "wallet": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiWallet" - } + "enum": [ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom" ], - "readOnly": true - }, - "country": { - "type": "string" + "description": "Event category.", + "example": "auth" }, - "status": { + "action_type": { "type": "string", - "description": "Not Finished while pending, then Approved (verified) or Declined." + "minLength": 1, + "maxLength": 128, + "description": "What happened within the category. Open vocabulary; the managed actions listed in [Event ingestion](https://docs.didit.me/transaction-monitoring/events) (for example `login`, `deposit_completed`, `chargeback_opened`) unlock typed validation and managed rule fields. Leading and trailing whitespace is trimmed.", + "example": "login" }, - "outcome": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletVerificationResultOutcomeEnum" + "occurred_at": { + "type": "string", + "format": "date-time", + "description": "When the event happened on your side, ISO 8601 (a value without an offset is read as UTC). Must be no more than 7 days in the past and no more than 5 minutes in the future.", + "example": "2026-10-17T09:15:02.481Z" }, - "error": { + "time_zone": { "type": "string", "nullable": true, - "readOnly": true + "maxLength": 64, + "description": "Time zone of the occurrence on your side, for example `Europe/Madrid`. Stored with the event; not returned by the read endpoints." }, - "interaction": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiInteractionEnum" - } + "subject": { + "type": "object", + "description": "The user or company the event belongs to. It resolves to the user (or company) with this `vendor_data` in your application; one is created when none exists.", + "required": [ + "vendor_data" ], - "readOnly": true - }, - "simulated": { - "type": "boolean", - "readOnly": true - }, - "expires_at": { - "type": "string", - "format": "date-time" - }, - "next_action": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletNextAction" + "properties": { + "vendor_data": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Your id for the subject.", + "example": "user-42" + }, + "entity_type": { + "type": "string", + "maxLength": 32, + "default": "individual", + "description": "`company` resolves the subject to a company; any other value (default `individual`) resolves it to an individual. The same `vendor_data` can name one of each.", + "example": "individual" + }, + "full_name": { + "type": "string", + "nullable": true, + "maxLength": 512, + "description": "Display name, used only when this event creates the user or company." + }, + "email": { + "type": "string", + "format": "email", + "nullable": true, + "description": "Validated as an email address; not stored on the event." + }, + "phone": { + "type": "string", + "nullable": true, + "maxLength": 32, + "description": "Validated; not stored on the event." + }, + "country": { + "type": "string", + "nullable": true, + "maxLength": 3, + "description": "Validated; not stored on the event." } - ], - "nullable": true, - "readOnly": true + } }, - "identity": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletIdentity" + "business": { + "type": "object", + "description": "The business the event belongs to, when it differs from the subject. Only `vendor_data` is read.", + "additionalProperties": true, + "properties": { + "vendor_data": { + "type": "string", + "maxLength": 255, + "description": "Your id for the business. Filterable as `business_vendor_data`.", + "example": "merchant-7" } - ], - "nullable": true, - "readOnly": true + } }, - "attributes": { + "session": { "type": "object", - "additionalProperties": {}, - "nullable": true, - "readOnly": true - }, - "level_of_assurance": { - "type": "string", - "nullable": true + "description": "Your session context for the event.", + "properties": { + "session_id": { + "type": "string", + "nullable": true, + "maxLength": 255, + "description": "Your session identifier. Filterable and returned as `session_id`." + }, + "sequence": { + "type": "integer", + "nullable": true, + "description": "Position of the event within the session. Returned as `session_sequence`." + }, + "page_url": { + "type": "string", + "nullable": true, + "maxLength": 2048, + "description": "Validated; not stored on the event." + }, + "referrer": { + "type": "string", + "nullable": true, + "maxLength": 2048, + "description": "Validated; not stored on the event." + } + } }, - "verified_at": { - "type": "string", - "format": "date-time", - "nullable": true + "device_context": { + "type": "object", + "additionalProperties": true, + "description": "Device and network context, stored as sent and returned by `GET /v3/events/{event_uuid}/`. On this server lane Didit does not see the end user's connection, so the IP enriched and used by rules is `network_context.ip_address` as you send it.", + "properties": { + "fingerprint": { + "type": "string", + "maxLength": 255, + "description": "Device fingerprint. Filterable as `device_fingerprint`." + }, + "persistent_device_id": { + "type": "string", + "maxLength": 255, + "description": "Your persistent device identifier." + }, + "bot_score": { + "type": "integer", + "description": "Bot score from your own detection, as an integer." + }, + "network_context": { + "type": "object", + "additionalProperties": true, + "properties": { + "ip_address": { + "type": "string", + "maxLength": 64, + "description": "End user's IP address. Public addresses are enriched once per unique address.", + "example": "203.0.113.42" + } + } + } + } }, - "signature_valid": { - "type": "boolean", - "nullable": true, - "description": "True once the credential's signature was verified; null for a simulated one." + "transaction_context": { + "type": "object", + "description": "Money context. Non-empty values here also satisfy the required payload keys of the action.", + "properties": { + "amount": { + "type": "string", + "nullable": true, + "description": "Amount as a decimal string (a number is also accepted).", + "example": "1000.00" + }, + "currency": { + "type": "string", + "nullable": true, + "maxLength": 10, + "description": "Currency code." + }, + "direction": { + "type": "string", + "nullable": true, + "maxLength": 16, + "description": "Direction of the money movement, for example `inbound` or `outbound`." + }, + "payment_reference_id": { + "type": "string", + "nullable": true, + "maxLength": 255, + "description": "Your payment reference. Filterable as `payment_reference_id`." + } + } }, - "credential_type": { - "type": "string", - "nullable": true - } - }, - "required": [ - "attributes", - "country", - "credential_type", - "error", - "expires_at", - "identity", - "interaction", - "level_of_assurance", - "next_action", - "outcome", - "signature_valid", - "simulated", - "status", - "verified_at", - "wallet" - ] - }, - "DigitalIdWalletApiWalletIdentity": { - "type": "object", - "properties": { - "full_name": { - "type": "string", - "nullable": true + "payload": { + "type": "object", + "additionalProperties": true, + "description": "The category's domain fields, at most 16384 bytes once JSON-encoded. Some actions require keys (in `payload` or `transaction_context`): `gameplay.p2p_settlement` needs `game_id`, `counterparty_vendor_data`, `stake_amount`, `net_result_amount`, `role` and one of `hand_id`, `round_id`, `match_id`; `gameplay.bet_placed` and `gameplay.bet_settled` need `game_id`; `payment.deposit_initiated`, `payment.deposit_completed`, `payment.withdrawal_requested` and `payment.withdrawal_completed` need `payment_reference_id`, `amount`, `currency`, `payment_method`, `status`; `payment.withdrawal_address_changed` needs `withdrawal_address_hash`; `dispute.chargeback_opened`, `dispute.chargeback_won` and `dispute.chargeback_lost` need `payment_reference_id` and one of `dispute_id`, `chargeback_id`; `session.bot_signal_observed` needs `telemetry_schema_version` and one of `bot_score`, `actions_per_minute`, `timing_variance_ms`, `pointer_entropy`, `keystroke_entropy`, `reaction_time_ms_p50`; `responsible_gambling.self_exclusion_match` needs `registry_source`, `exclusion_status`, `identity_match_ref`. Every `security` action ending in `_changed` needs `changed_field`, plus `previous_reference_hash` or `new_reference_hash` when the field is `password`, `mfa_method`, `mfa`, `email`, `phone`, `withdrawal_address`, `payout_method` or `api_key`. Send hashes and references, never a card number, a full withdrawal address, a password hash or an MFA secret." }, - "first_name": { - "type": "string", - "nullable": true + "custom_values": { + "type": "object", + "description": "Up to 100 keys of your own, addressable from rules as `custom_values.`. Values must be scalars (string, number, boolean or null); nested objects and arrays are rejected.", + "additionalProperties": { + "nullable": true, + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + } + }, + "example": { + "event_id": "login-2026-10-17-000001", + "event_category": "auth", + "action_type": "login", + "occurred_at": "2026-10-17T09:15:02.481Z", + "time_zone": "Europe/Madrid", + "subject": { + "vendor_data": "user-42", + "entity_type": "individual" }, - "last_name": { - "type": "string", - "nullable": true + "session": { + "session_id": "web-session-abc", + "sequence": 142 }, - "date_of_birth": { - "type": "string", - "format": "date", - "nullable": true + "device_context": { + "fingerprint": "fp-a8c1", + "network_context": { + "ip_address": "203.0.113.42" + } }, - "nationality": { - "type": "string", - "nullable": true, - "description": "ISO3 nationality, only when the wallet asserts one (never inferred)." + "payload": { + "method": "password" }, - "issuing_state": { - "type": "string", - "description": "ISO3 country whose scheme issued the identity." + "custom_values": { + "vip_tier": "gold" } - }, - "required": [ - "date_of_birth", - "first_name", - "full_name", - "issuing_state", - "last_name", - "nationality" - ] + } }, - "DigitalIdWalletApiWalletNextAction": { + "MonitoringEventIngestResult": { "type": "object", + "description": "Outcome of ingesting one event. The same shape is returned for a new event and for an idempotent replay.", + "required": [ + "event_uuid", + "event_id", + "status", + "score", + "rules_evaluated_count", + "rules_matched_count", + "correlation_pending", + "retention_class", + "expires_at", + "idempotent_replay", + "rule_matches" + ], "properties": { - "type": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletNextActionTypeEnum" - } - ], - "description": "redirect: send the user's browser to url. confirm_in_app: show verification_code to the user, who checks it matches the code in their wallet app and confirms there; then poll.\n\n* `redirect` - redirect\n* `confirm_in_app` - confirm_in_app" + "event_uuid": { + "type": "string", + "format": "uuid", + "description": "Didit identifier of the stored event." }, - "url": { + "event_id": { "type": "string", - "format": "uri", - "description": "redirect only: where the user signs in with the wallet." + "description": "Your `event_id`." }, - "verification_code": { + "status": { "type": "string", - "description": "confirm_in_app only: the 4-digit comparison code the wallet app shows too." + "enum": [ + "ACCEPTED", + "APPROVED", + "IN_REVIEW", + "DECLINED" + ], + "description": "Status after the real-time rules ran. See `MonitoringEvent.status`." }, - "poll_interval_seconds": { + "score": { "type": "integer", - "description": "confirm_in_app only: how often to call POST .../verifications/{request_id}/poll/ for prompt feedback. Didit also polls pending sign-ins in the background, so the webhook reports the outcome even when you do not poll." - } - }, - "required": [ - "type" - ] - }, - "DigitalIdWalletApiWalletNextActionTypeEnum": { - "enum": [ - "redirect", - "confirm_in_app" - ], - "type": "string", - "description": "* `redirect` - redirect\n* `confirm_in_app` - confirm_in_app" - }, - "DigitalIdWalletApiInteractionEnum": { - "enum": [ - "redirect", - "in_app" - ], - "type": "string", - "description": "* `redirect` - redirect\n* `in_app` - in_app" - }, - "DigitalIdWalletApiWalletVerificationResultOutcomeEnum": { - "enum": [ - "pending", - "verified", - "cancelled", - "timeout", - "failed" - ], - "type": "string", - "description": "* `pending` - Pending\n* `verified` - Verified\n* `cancelled` - Cancelled by the user\n* `timeout` - Timed out\n* `failed` - Failed" - }, - "DigitalIdWalletApiWallet": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Catalog id of the wallet, e.g. bankid_se." + "description": "Score after the real-time rules ran." }, - "name": { - "type": "string" + "rules_evaluated_count": { + "type": "integer", + "minimum": 0, + "description": "Number of event rules evaluated in the request. 0 in a sandbox application, where no rules run." }, - "issuing_authority": { - "type": "string", - "nullable": true - } - }, - "required": [ - "id", - "issuing_authority", - "name" - ] - }, - "DigitalIdWalletApiEudiWalletVerificationStartRequest": { - "type": "object", - "description": "Start a EUDI Wallet verification (AUT, BEL, BGR, HRV, CYP, CZE, DNK, EST, FIN, FRA, DEU, GRC, HUN, IRL, ITA, LVA, LTU, LUX, MLT, NLD, POL, PRT, ROU, SVK, SVN, ESP, SWE, ISL, LIE, NOR).", - "properties": { - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "rules_matched_count": { + "type": "integer", + "minimum": 0, + "description": "Number of rules that matched, including `TEST` mode rules." }, - "metadata": { - "type": "object", - "additionalProperties": {}, - "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "correlation_pending": { + "type": "boolean", + "description": "`true` when an asynchronous rule still has to run. Read `GET /v3/events/{event_uuid}/` a few minutes later for the final `status` and `score`." }, - "sandbox_scenario": { + "retention_class": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." - }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiEudiWalletVerificationStartCountryEnum" - } + "enum": [ + "low_value", + "standard", + "risk_relevant", + "monetary_or_compliance" ], - "description": "ISO 3166-1 alpha-3 country of the EUDI Wallet identity.\n\n* `AUT` - AUT\n* `BEL` - BEL\n* `BGR` - BGR\n* `HRV` - HRV\n* `CYP` - CYP\n* `CZE` - CZE\n* `DNK` - DNK\n* `EST` - EST\n* `FIN` - FIN\n* `FRA` - FRA\n* `DEU` - DEU\n* `GRC` - GRC\n* `HUN` - HUN\n* `IRL` - IRL\n* `ITA` - ITA\n* `LVA` - LVA\n* `LTU` - LTU\n* `LUX` - LUX\n* `MLT` - MLT\n* `NLD` - NLD\n* `POL` - POL\n* `PRT` - PRT\n* `ROU` - ROU\n* `SVK` - SVK\n* `SVN` - SVN\n* `ESP` - ESP\n* `SWE` - SWE\n* `ISL` - ISL\n* `LIE` - LIE\n* `NOR` - NOR" + "description": "Retention class, after any promotion by a matched rule." }, - "return_url": { + "expires_at": { "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "format": "date-time", + "nullable": true, + "description": "When the event expires under its retention class and is deleted. Null for `monetary_or_compliance` (payment and dispute) events." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + "idempotent_replay": { + "type": "boolean", + "description": "`true` when an event with this `event_id` and the same content was already stored; the values are those of the stored event and no rule ran again." + }, + "rule_matches": { + "type": "array", + "description": "Rules that matched the event.", + "items": { + "type": "object", + "required": [ + "rule_id", + "library_key", + "mode", + "is_async" + ], + "properties": { + "rule_id": { + "type": "string", + "format": "uuid", + "description": "UUID of the rule." + }, + "library_key": { + "type": "string", + "nullable": true, + "description": "Library key when the rule was installed from the rules library, otherwise null." + }, + "mode": { + "type": "string", + "enum": [ + "ACTIVE", + "TEST" + ], + "description": "`TEST` rules record the match but apply no actions." + }, + "is_async": { + "type": "boolean", + "description": "`true` when the match came from the asynchronous pass." + } } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + } } - }, - "required": [ - "country", - "return_url" - ] - }, - "DigitalIdWalletApiDeviceEnum": { - "enum": [ - "same_device", - "cross_device" - ], - "type": "string", - "description": "* `same_device` - same_device\n* `cross_device` - cross_device" - }, - "DigitalIdWalletApiEudiWalletVerificationStartCountryEnum": { - "enum": [ - "AUT", - "BEL", - "BGR", - "HRV", - "CYP", - "CZE", - "DNK", - "EST", - "FIN", - "FRA", - "DEU", - "GRC", - "HUN", - "IRL", - "ITA", - "LVA", - "LTU", - "LUX", - "MLT", - "NLD", - "POL", - "PRT", - "ROU", - "SVK", - "SVN", - "ESP", - "SWE", - "ISL", - "LIE", - "NOR" - ], - "type": "string", - "description": "* `AUT` - AUT\n* `BEL` - BEL\n* `BGR` - BGR\n* `HRV` - HRV\n* `CYP` - CYP\n* `CZE` - CZE\n* `DNK` - DNK\n* `EST` - EST\n* `FIN` - FIN\n* `FRA` - FRA\n* `DEU` - DEU\n* `GRC` - GRC\n* `HUN` - HUN\n* `IRL` - IRL\n* `ITA` - ITA\n* `LVA` - LVA\n* `LTU` - LTU\n* `LUX` - LUX\n* `MLT` - MLT\n* `NLD` - NLD\n* `POL` - POL\n* `PRT` - PRT\n* `ROU` - ROU\n* `SVK` - SVK\n* `SVN` - SVN\n* `ESP` - ESP\n* `SWE` - SWE\n* `ISL` - ISL\n* `LIE` - LIE\n* `NOR` - NOR" + } }, - "DigitalIdWalletApiConnectidWalletVerificationStartRequest": { + "ComplianceList": { "type": "object", - "description": "Start a ConnectID verification (AUS).", + "description": "A blocklist, allowlist or custom list of one application.", + "required": [ + "uuid", + "name", + "description", + "list_type", + "entry_type", + "is_system", + "entry_count", + "last_entry_added_at", + "created_at", + "updated_at" + ], "properties": { - "vendor_data": { + "uuid": { "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "format": "uuid", + "description": "List identifier. Use it as `list_uuid` in the list and entry endpoints, and as the value of a workflow or transaction rule condition with `value_type: \"list\"`." }, - "metadata": { - "type": "object", - "additionalProperties": {}, - "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "name": { + "type": "string", + "maxLength": 255, + "description": "List name, unique among the application's lists." }, - "sandbox_scenario": { + "description": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "description": "Free-text description. Empty string when not set." }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiConnectidWalletVerificationStartCountryEnum" - } + "list_type": { + "type": "string", + "enum": [ + "blocklist", + "allowlist", + "custom" ], - "default": "AUS", - "description": "ISO 3166-1 alpha-3 country of the ConnectID identity. Defaults to AUS, the only country it serves.\n\n* `AUS` - AUS" + "description": "`blocklist`: a match declines the session or transaction (system lists only, one per entry type). `allowlist`: matching items are trusted, for example a face allowlist skips the duplicated-face action; never overrides a blocklist match. `custom`: used in workflow and transaction rule conditions (`value_type: \"list\"`)." }, - "authorisation_server_id": { + "entry_type": { "type": "string", - "minLength": 1, - "description": "Authorisation server ID selected with ConnectID's bank selector. Sandbox defaults to Mock Bank A.", - "maxLength": 36 + "enum": [ + "face", + "document", + "phone", + "email", + "ip_address", + "device_fingerprint", + "wallet_address", + "bank_account", + "country", + "user", + "business", + "key" + ], + "description": "What the list holds: `face`, `document` (document number), `phone`, `email`, `ip_address` (IP address or CIDR range), `device_fingerprint`, `wallet_address`, `bank_account`, `country` (ISO 3166-1 alpha-3), `user` or `business` (your `vendor_data`), or `key` (any string). It decides how entry values are validated and matched." }, - "return_url": { - "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "is_system": { + "type": "boolean", + "description": "`true` for the lists Didit provisions: one blocklist per entry type (`face`, `document`, `phone`, `email`, `ip_address`, `device_fingerprint`, `wallet_address`, `bank_account`, `user`, `business`), the **High Risk Countries** custom list, and system allowlists. System lists cannot be renamed, edited or deleted; you can add and delete their entries." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" - } - }, - "required": [ - "return_url" - ] - }, - "DigitalIdWalletApiConnectidWalletVerificationStartCountryEnum": { - "enum": [ - "AUS" - ], - "type": "string", - "description": "* `AUS` - AUS" - }, - "DigitalIdWalletApiAudkenniWalletVerificationStartRequest": { - "type": "object", - "description": "Start a Auðkenni verification (ISL).", - "properties": { - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "entry_count": { + "type": "integer", + "minimum": 0, + "description": "Number of entries currently in the list." }, - "metadata": { - "type": "object", - "additionalProperties": {}, + "last_entry_added_at": { + "type": "string", + "format": "date-time", "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "description": "When the newest current entry was added. `null` for an empty list." }, - "sandbox_scenario": { + "created_at": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." - }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiAudkenniWalletVerificationStartCountryEnum" - } - ], - "default": "ISL", - "description": "ISO 3166-1 alpha-3 country of the Auðkenni identity. Defaults to ISL, the only country it serves.\n\n* `ISL` - ISL" + "format": "date-time", + "description": "When the list was created." }, - "return_url": { + "updated_at": { "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 - }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + "format": "date-time", + "description": "When the list was last updated." } - }, - "required": [ - "return_url" - ] - }, - "DigitalIdWalletApiAudkenniWalletVerificationStartCountryEnum": { - "enum": [ - "ISL" - ], - "type": "string", - "description": "* `ISL` - ISL" + } }, - "DigitalIdWalletApiFranceconnectWalletVerificationStartRequest": { + "ComplianceListEntry": { "type": "object", - "description": "Start a FranceConnect verification (FRA).", + "description": "One value in a list.", + "required": [ + "uuid", + "value", + "display_label", + "comment", + "added_by", + "reference_session_id", + "reference_session_number", + "reference_session_type", + "reference_object_uuid", + "metadata", + "created_at" + ], "properties": { - "vendor_data": { + "uuid": { "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "format": "uuid", + "description": "Entry identifier. Use it as `entry_uuid` in Delete a list entry." }, - "metadata": { - "type": "object", - "additionalProperties": {}, - "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "value": { + "type": "string", + "description": "The listed value: a phone number in E.164, an email address, a document number, an IP address or CIDR range, a device fingerprint, a wallet address, a bank account, an ISO 3166-1 alpha-3 country code, a `vendor_data`, any string on a `key` list, or the UUID of the stored face on a face list. Values you send are stored in lower case, except phone numbers and country codes. Matching is case-insensitive." }, - "sandbox_scenario": { + "display_label": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "nullable": true, + "maxLength": 512, + "description": "Human-readable label. Didit fills it for entries taken from a session (for example the holder's name and document number) when you send none." }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiFranceconnectWalletVerificationStartCountryEnum" - } - ], - "default": "FRA", - "description": "ISO 3166-1 alpha-3 country of the FranceConnect identity. Defaults to FRA, the only country it serves.\n\n* `FRA` - FRA" + "comment": { + "type": "string", + "description": "Free-text note. Empty string when not set." }, - "return_url": { + "added_by": { "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "nullable": true, + "description": "Who added the entry: `api` (an API key), a console user's email address or `console`, `system` or `migration` (entries Didit created), or `rule` (a transaction or monitoring rule action)." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" - } - }, - "required": [ - "return_url" - ] - }, - "DigitalIdWalletApiFranceconnectWalletVerificationStartCountryEnum": { - "enum": [ - "FRA" - ], - "type": "string", - "description": "* `FRA` - FRA" - }, - "DigitalIdWalletApiDiiaWalletVerificationStartRequest": { - "type": "object", - "description": "Start a Diia verification (UKR).", - "properties": { - "vendor_data": { + "reference_session_id": { "type": "string", + "format": "uuid", "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "description": "Verification session the entry was taken from. `null` when the entry was added by value." }, - "metadata": { - "type": "object", - "additionalProperties": {}, + "reference_session_number": { + "type": "integer", "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "description": "Application-scoped number of that session, as shown in the console." }, - "sandbox_scenario": { + "reference_session_type": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." - }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDiiaWalletVerificationStartCountryEnum" - } + "nullable": true, + "enum": [ + "kyc", + "kyb", + null ], - "default": "UKR", - "description": "ISO 3166-1 alpha-3 country of the Diia identity. Defaults to UKR, the only country it serves.\n\n* `UKR` - UKR" + "description": "`kyc` for a person verification session, `kyb` for a business verification session, `null` when there is no session." }, - "return_url": { + "reference_object_uuid": { "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "format": "uuid", + "nullable": true, + "description": "The record the entry stands for: the face, document, phone or email record read from a session, the face created by Upload a face to a list, or the UUID you sent (for example a transaction UUID or a user's `didit_internal_id`)." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" - } - }, - "required": [ - "return_url" - ] - }, - "DigitalIdWalletApiDiiaWalletVerificationStartCountryEnum": { - "enum": [ - "UKR" - ], - "type": "string", - "description": "* `UKR` - UKR" + "metadata": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Extra context. Holds the keys Didit sets (listed here) merged with any `metadata` you sent when creating the entry, returned as stored. `null` when there is none.", + "properties": { + "reference_image_url": { + "type": "string", + "description": "Face entries: short-lived signed URL, issued again on every call; `null` for a scoped API key without `media: read`. Absent when no image belongs to the entry." + }, + "front_image_url": { + "type": "string", + "description": "Document entries that carry a document front image: short-lived signed URL, issued again on every call; `null` for a scoped API key without `media: read`." + }, + "full_name": { + "type": "string", + "description": "Document entries taken from a session: holder's full name." + }, + "issuing_state": { + "type": "string", + "description": "Document entries taken from a session: ISO 3166-1 alpha-3 issuing country." + }, + "document_type": { + "type": "string", + "description": "Document entries taken from a session: document type code, for example `P`, `ID` or `DL`." + }, + "region": { + "type": "string", + "description": "Document entries taken from a session: issuing state or province of a regionally issued document, empty when not applicable. Not set by Add session items to the blocklist." + }, + "document_number": { + "type": "string", + "description": "Document entries taken from a session: the document number." + }, + "face_image_source": { + "type": "string", + "enum": [ + "front_image_camera_front", + "back_image_camera_front" + ], + "description": "Face entries taken from a document-capture selfie: which selfie. Absent for liveness faces." + }, + "reference_node_id": { + "type": "string", + "nullable": true, + "description": "Face entries taken from a document-capture selfie: the ID verification node the selfie came from." + }, + "fingerprint_tier": { + "type": "string", + "nullable": true, + "enum": [ + "persistent_id", + "composite_hash", + "legacy_fp", + "unknown", + null + ], + "description": "Device fingerprint entries taken from a session: how the fingerprint was derived. `persistent_id` identifies one device; `composite_hash` and `legacy_fp` can be shared by devices of the same model." + }, + "sandbox": { + "type": "boolean", + "description": "`true` on a face entry uploaded in a sandbox application; such an entry matches nothing." + }, + "reference_type": { + "type": "string", + "enum": [ + "transaction", + "vendor_user", + "vendor_business" + ], + "description": "Set by you: what `reference_object_uuid` points to, so the console links back to it." + }, + "reference_id": { + "type": "string", + "description": "Set by you: identifier of the source record." + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the entry was added." + } + } }, - "DigitalIdWalletApiMojeidWalletVerificationStartRequest": { + "BlocklistedItem": { "type": "object", - "description": "Start a MojeID verification (CZE).", + "description": "One blocklisted face, identity document, phone number or email address of a verification session. Fields that do not apply to the item's `type` are `null`.", + "required": [ + "type", + "session_id", + "session_number", + "blocklisted_at", + "face_image", + "document_type", + "document_number", + "front_image", + "phone_number", + "country_code", + "email" + ], "properties": { - "vendor_data": { + "type": { "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "enum": [ + "face", + "document", + "phone", + "email" + ], + "description": "What was blocklisted." }, - "metadata": { - "type": "object", - "additionalProperties": {}, + "session_id": { + "type": "string", + "format": "uuid", + "description": "Verification session the item belongs to." + }, + "session_number": { + "type": "integer", "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "description": "Application-scoped number of that session, as shown in the console." }, - "sandbox_scenario": { + "blocklisted_at": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." - }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiCountryE2dEnum" - } - ], - "default": "CZE", - "description": "ISO 3166-1 alpha-3 country of the MojeID identity. Defaults to CZE, the only country it serves.\n\n* `CZE` - CZE" + "format": "date-time", + "description": "When the item was last blocklisted." }, - "return_url": { + "face_image": { "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "nullable": true, + "description": "Face items: short-lived signed URL, issued again on every call; `null` for a scoped API key without `media: read`. Also `null` when no reference image is stored." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" - } - }, - "required": [ - "return_url" - ] - }, - "DigitalIdWalletApiCountryE2dEnum": { - "enum": [ - "CZE" - ], - "type": "string", - "description": "* `CZE` - CZE" - }, - "DigitalIdWalletApiBankidCzWalletVerificationStartRequest": { - "type": "object", - "description": "Start a Bank iD verification (CZE).", - "properties": { - "vendor_data": { + "document_type": { "type": "string", "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "description": "Document items: document type code, for example `P` (passport), `ID` (national ID), `DL` (driver's license) or `RP` (residence permit)." }, - "metadata": { - "type": "object", - "additionalProperties": {}, + "document_number": { + "type": "string", "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "description": "Document items: the document number, or the personal number when the document has none." }, - "sandbox_scenario": { + "front_image": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "nullable": true, + "description": "Document items: front image of the document, short-lived signed URL, issued again on every call; `null` for a scoped API key without `media: read`." }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiCountryE2dEnum" - } - ], - "default": "CZE", - "description": "ISO 3166-1 alpha-3 country of the Bank iD identity. Defaults to CZE, the only country it serves.\n\n* `CZE` - CZE" + "phone_number": { + "type": "string", + "nullable": true, + "description": "Phone items: the phone number in E.164." }, - "return_url": { + "country_code": { "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "nullable": true, + "description": "Phone items: ISO 3166-1 alpha-2 country of the number." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + "email": { + "type": "string", + "nullable": true, + "description": "Email items: the email address." } - }, - "required": [ - "return_url" - ] + } }, - "DigitalIdWalletApiEparakstsMobileWalletVerificationStartRequest": { + "TransactionNote": { "type": "object", - "description": "Start a eParaksts mobile verification (LVA).", + "description": "One note on a monitored transaction's review timeline.", + "required": [ + "uuid", + "note", + "actor_email", + "actor_name", + "metadata", + "created_at" + ], "properties": { - "vendor_data": { + "uuid": { "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "format": "uuid", + "description": "Note identifier." }, - "metadata": { - "type": "object", - "additionalProperties": {}, + "note": { + "type": "string", + "description": "Plain-text note body." + }, + "actor_email": { + "type": "string", + "format": "email", "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "description": "Email of the Console user who wrote the note or changed the status. `null` for notes created with an API key and for notes Didit adds automatically." }, - "sandbox_scenario": { + "actor_name": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "nullable": true, + "description": "Display name of the Console user (their email when they have no name). `Rule Engine` on notes Didit adds automatically. `null` for notes created with an API key." }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiEparakstsMobileWalletVerificationStartCountryEnum" + "metadata": { + "type": "object", + "description": "Tells the note types apart. Notes written through the API or the Console carry `mentioned_emails`. A status change made in the Console carries `type: status_change` with `previous_status`, `new_status`, `reason_code` and `reason_label`. Notes Didit adds carry `source`: `rule_action` (with `rule_uuid`) for a rule's `add_note` action, or `remediation_session` for a problem creating the user-action verification session.", + "properties": { + "mentioned_emails": { + "type": "array", + "items": { + "type": "string", + "format": "email" + }, + "description": "Mentioned organization members, lower-cased and de-duplicated. Empty when nobody was mentioned." + }, + "type": { + "type": "string", + "enum": [ + "status_change" + ], + "description": "Present on notes recording a status change made in the Console." + }, + "previous_status": { + "type": "string", + "enum": [ + "APPROVED", + "IN_REVIEW", + "DECLINED", + "AWAITING_USER" + ], + "description": "Status before the change." + }, + "new_status": { + "type": "string", + "enum": [ + "APPROVED", + "IN_REVIEW", + "DECLINED", + "AWAITING_USER" + ], + "description": "Status after the change." + }, + "reason_code": { + "type": "string", + "nullable": true, + "description": "Reason code given with the change." + }, + "reason_label": { + "type": "string", + "nullable": true, + "description": "Reason label given with the change." + }, + "source": { + "type": "string", + "enum": [ + "rule_action", + "remediation_session" + ], + "description": "Present on notes Didit adds automatically." + }, + "rule_uuid": { + "type": "string", + "format": "uuid", + "description": "Rule whose `add_note` action wrote the note (with `source: rule_action`)." } - ], - "default": "LVA", - "description": "ISO 3166-1 alpha-3 country of the eParaksts mobile identity. Defaults to LVA, the only country it serves.\n\n* `LVA` - LVA" + } }, - "return_url": { + "created_at": { "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 - }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + "format": "date-time", + "description": "When the note was stored." } - }, - "required": [ - "return_url" - ] - }, - "DigitalIdWalletApiEparakstsMobileWalletVerificationStartCountryEnum": { - "enum": [ - "LVA" - ], - "type": "string", - "description": "* `LVA` - LVA" + } }, - "DigitalIdWalletApiIdcardEeWalletVerificationStartRequest": { + "TransactionSummary": { "type": "object", - "description": "Start a Estonian ID-card verification (EST).", + "description": "Summary of a monitored transaction, the shape `GET /v3/transactions/` lists. Call `GET /v3/transactions/{transaction_id}/` with `uuid` for parties, alerts and rule runs.", "properties": { - "vendor_data": { + "uuid": { "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "format": "uuid", + "description": "Didit transaction UUID. Use it as `{transaction_id}` on the transaction endpoints." }, - "metadata": { - "type": "object", - "additionalProperties": {}, + "transaction_number": { + "type": "integer", "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "description": "Application-scoped sequential number shown in the Console." }, - "sandbox_scenario": { + "txn_id": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." - }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiIdcardEeWalletVerificationStartCountryEnum" - } - ], - "default": "EST", - "description": "ISO 3166-1 alpha-3 country of the Estonian ID-card identity. Defaults to EST, the only country it serves.\n\n* `EST` - EST" + "nullable": true, + "description": "Your own identifier from the create call. Unique per application." }, - "return_url": { + "transaction_type": { "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "description": "Category as stored: `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet`, `gamblingLimitChange`, `gamblingBonusChange` or `auditTrailEvent`." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" - } - }, - "required": [ - "return_url" - ] - }, - "DigitalIdWalletApiIdcardEeWalletVerificationStartCountryEnum": { - "enum": [ - "EST" - ], - "type": "string", - "description": "* `EST` - EST" - }, - "DigitalIdWalletApiMobileIdWalletVerificationStartRequest": { - "type": "object", - "description": "Start a Mobile-ID verification (EST, LTU).", - "properties": { - "vendor_data": { + "action_type": { "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "description": "Sub-type within the category, for example `withdrawal`." }, - "metadata": { - "type": "object", - "additionalProperties": {}, - "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "status": { + "type": "string", + "enum": [ + "APPROVED", + "IN_REVIEW", + "DECLINED", + "AWAITING_USER" + ], + "description": "Current monitoring verdict." }, - "sandbox_scenario": { + "source": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "enum": [ + "LIVE", + "IMPORTED" + ], + "description": "`LIVE` for transactions submitted for monitoring, `IMPORTED` for historical imports." }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiMobileIdWalletVerificationStartCountryEnum" - } + "direction": { + "type": "string", + "enum": [ + "INBOUND", + "OUTBOUND" ], - "description": "ISO 3166-1 alpha-3 country of the Mobile-ID identity.\n\n* `EST` - EST\n* `LTU` - LTU" + "description": "Direction relative to the subject." }, - "personal_code": { + "amount": { "type": "string", - "minLength": 1, - "description": "The user's national personal identification code. It addresses the user's Mobile-ID app; the user confirms the comparison code and enters their PIN in the app. Never collect the PIN.", - "pattern": "^[0-9-]{6,20}$" + "description": "Amount as a decimal string with trailing zeros removed (up to 18 decimal places)." }, - "phone_number": { + "currency": { "type": "string", - "minLength": 1, - "description": "The Mobile-ID phone number in E.164 format (+372... or +370...).", - "maxLength": 16 - } - }, - "required": [ - "country", - "personal_code", - "phone_number" - ] - }, - "DigitalIdWalletApiMobileIdWalletVerificationStartCountryEnum": { - "enum": [ - "EST", - "LTU" - ], - "type": "string", - "description": "* `EST` - EST\n* `LTU` - LTU" - }, - "DigitalIdWalletApiSmartIdWalletVerificationStartRequest": { - "type": "object", - "description": "Start a Smart-ID verification (EST, LVA, LTU, BEL).", - "properties": { - "vendor_data": { + "description": "Currency code or crypto asset symbol of `amount`." + }, + "currency_type": { "type": "string", "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "description": "`fiat` or `crypto` when known." }, - "metadata": { - "type": "object", - "additionalProperties": {}, + "preferred_currency_amount": { + "type": "string", "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "description": "`amount` converted to the application's preferred currency, when a rate was available." }, - "sandbox_scenario": { + "preferred_currency_code": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "nullable": true, + "description": "Currency of `preferred_currency_amount`." }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiSmartIdWalletVerificationStartCountryEnum" - } - ], - "description": "ISO 3166-1 alpha-3 country of the Smart-ID identity.\n\n* `EST` - EST\n* `LVA` - LVA\n* `LTU` - LTU\n* `BEL` - BEL" + "payment_details": { + "type": "string", + "nullable": true, + "description": "Free-text payment reference." }, - "personal_code": { + "txn_date": { "type": "string", - "minLength": 1, - "description": "The user's national personal identification code. It addresses the user's Smart-ID app; the user confirms the comparison code and enters their PIN in the app. Never collect the PIN.", - "pattern": "^[0-9-]{6,20}$" - } - }, - "required": [ - "country", - "personal_code" - ] - }, - "DigitalIdWalletApiSmartIdWalletVerificationStartCountryEnum": { - "enum": [ - "EST", - "LVA", - "LTU", - "BEL" - ], - "type": "string", - "description": "* `EST` - EST\n* `LVA` - LVA\n* `LTU` - LTU\n* `BEL` - BEL" - }, - "DigitalIdWalletApiGovukWalletWalletVerificationStartRequest": { - "type": "object", - "description": "Start a GOV.UK Wallet verification (GBR).", - "properties": { - "vendor_data": { + "format": "date-time", + "description": "When the transaction occurred." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When Didit recorded it." + }, + "score": { + "type": "integer", + "description": "Risk score. Higher is riskier." + }, + "severity": { "type": "string", "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "enum": [ + "UNKNOWN", + "LOW", + "MEDIUM", + "HIGH", + "CRITICAL", + null + ], + "description": "Risk severity, `null` until something sets it." }, - "metadata": { - "type": "object", - "additionalProperties": {}, + "decision_reason_code": { + "type": "string", "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "description": "Machine-readable reason for the current `status`." }, - "sandbox_scenario": { + "decision_reason_label": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "nullable": true, + "description": "Human-readable label for `decision_reason_code`." }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiCountry520Enum" + "tags": { + "type": "array", + "description": "Tags on the transaction.", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string", + "description": "Hex color, for example `#F59E0B`." + }, + "description": { + "type": "string", + "nullable": true + }, + "source": { + "type": "string", + "enum": [ + "didit", + "custom" + ], + "description": "Didit preset or custom tag." + } } - ], - "default": "GBR", - "description": "ISO 3166-1 alpha-3 country of the GOV.UK Wallet identity. Defaults to GBR, the only country it serves.\n\n* `GBR` - GBR" + } }, - "return_url": { + "subject_name": { "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "nullable": true, + "description": "Full name of the `APPLICANT` party." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" - } - }, - "required": [ - "return_url" - ] - }, - "DigitalIdWalletApiCountry520Enum": { - "enum": [ - "GBR" - ], - "type": "string", - "description": "* `GBR` - GBR" - }, - "DigitalIdWalletApiOneidWalletVerificationStartRequest": { - "type": "object", - "description": "Start a OneID verification (GBR).", - "properties": { - "vendor_data": { + "subject_vendor_data": { + "type": "string", + "nullable": true, + "description": "Your `vendor_data` for the applicant." + }, + "subject_type": { "type": "string", "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "enum": [ + "user", + "business", + null + ], + "description": "Whether the applicant is linked to a Didit user or business; `null` when it is linked to neither." }, - "metadata": { - "type": "object", - "additionalProperties": {}, + "subject_uuid": { + "type": "string", + "format": "uuid", "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "description": "UUID of the Didit user or business the applicant is linked to." }, - "sandbox_scenario": { + "subject_country": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "nullable": true, + "description": "Country code of the applicant." }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiCountry520Enum" - } - ], - "default": "GBR", - "description": "ISO 3166-1 alpha-3 country of the OneID identity. Defaults to GBR, the only country it serves.\n\n* `GBR` - GBR" + "counterparty_name": { + "type": "string", + "nullable": true, + "description": "Full name of the `COUNTERPARTY` party, if any." }, - "return_url": { + "counterparty_vendor_data": { "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "nullable": true, + "description": "Your `vendor_data` for the counterparty." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" - } - }, - "required": [ - "return_url" - ] - }, - "DigitalIdWalletApiGovbrWalletVerificationStartRequest": { - "type": "object", - "description": "Start a gov.br verification (BRA).", - "properties": { - "vendor_data": { + "counterparty_country": { "type": "string", "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "description": "Country code of the counterparty." }, - "metadata": { - "type": "object", - "additionalProperties": {}, + "assigned_to_name": { + "type": "string", "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "description": "Name of the Console reviewer assigned to the transaction." }, - "sandbox_scenario": { + "travel_rule_status": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "nullable": true, + "description": "Status of the transaction's Travel Rule transfer, or `null` when it has none." }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiGovbrWalletVerificationStartCountryEnum" - } - ], - "default": "BRA", - "description": "ISO 3166-1 alpha-3 country of the gov.br identity. Defaults to BRA, the only country it serves.\n\n* `BRA` - BRA" + "application_id": { + "type": "string", + "format": "uuid", + "description": "Application that owns the transaction." }, - "return_url": { + "application_name": { "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "nullable": true, + "description": "Name of that application." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + "organization_id": { + "type": "string", + "format": "uuid", + "description": "Organization that owns the application." + }, + "organization_name": { + "type": "string", + "nullable": true, + "description": "Name of that organization." } - }, - "required": [ - "return_url" - ] - }, - "DigitalIdWalletApiGovbrWalletVerificationStartCountryEnum": { - "enum": [ - "BRA" - ], - "type": "string", - "description": "* `BRA` - BRA" + } }, - "DigitalIdWalletApiUaePassWalletVerificationStartRequest": { + "ApplicationAnalyticsDataPoint": { "type": "object", - "description": "Start a UAE PASS verification (ARE).", + "description": "Session counts for one day, by creation date (UTC). Each count reflects the session's current status. Points of the previous period carry at least the required fields.", + "required": [ + "rounded_date", + "total", + "approved", + "declined", + "canceled", + "pending" + ], "properties": { - "vendor_data": { + "rounded_date": { "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "format": "date", + "description": "The day, `YYYY-MM-DD`." }, - "metadata": { - "type": "object", - "additionalProperties": {}, - "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "total": { + "type": "integer", + "description": "Sessions created that day." }, - "sandbox_scenario": { - "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "approved": { + "type": "integer", + "description": "Of those, sessions `Approved` or `Kyc Expired`." }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiUaePassWalletVerificationStartCountryEnum" - } - ], - "default": "ARE", - "description": "ISO 3166-1 alpha-3 country of the UAE PASS identity. Defaults to ARE, the only country it serves.\n\n* `ARE` - ARE" + "declined": { + "type": "integer", + "description": "Sessions `Declined`." }, - "return_url": { - "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "canceled": { + "type": "integer", + "description": "Sessions `Expired` or `Abandoned`." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + "pending": { + "type": "integer", + "description": "Sessions `Not Started`, `In Progress` or `In Review`." + }, + "in_review": { + "type": "integer", + "description": "Sessions `In Review`." + }, + "expired": { + "type": "integer", + "description": "Sessions `Expired`." + }, + "abandoned": { + "type": "integer", + "description": "Sessions `Abandoned`." + }, + "in_progress": { + "type": "integer", + "description": "Sessions `In Progress`." + }, + "not_started": { + "type": "integer", + "description": "Sessions `Not Started`." + }, + "manually_approved": { + "type": "integer", + "description": "Sessions a reviewer approved after the workflow gave another verdict." + }, + "manually_declined": { + "type": "integer", + "description": "Sessions a reviewer declined after the workflow gave another verdict." } - }, - "required": [ - "return_url" - ] + } }, - "DigitalIdWalletApiUaePassWalletVerificationStartCountryEnum": { - "enum": [ - "ARE" + "ApplicationAnalyticsRequestBreakdown": { + "type": "object", + "description": "Status totals for the range. The previous-period breakdown carries at least the required fields.", + "required": [ + "total_approved", + "total_in_review", + "total_declined", + "total_canceled", + "total_pending" ], - "type": "string", - "description": "* `ARE` - ARE" + "properties": { + "total_approved": { + "type": "integer", + "description": "Sessions `Approved` or `Kyc Expired`." + }, + "total_in_review": { + "type": "integer", + "description": "Sessions `In Review`." + }, + "total_declined": { + "type": "integer", + "description": "Sessions `Declined`." + }, + "total_canceled": { + "type": "integer", + "description": "Sessions `Expired` or `Abandoned`." + }, + "total_pending": { + "type": "integer", + "description": "Sessions `Not Started`, `In Progress` or `In Review` (includes `total_in_review`)." + }, + "total_expired": { + "type": "integer", + "description": "Sessions `Expired`." + }, + "total_abandoned": { + "type": "integer", + "description": "Sessions `Abandoned`." + }, + "total_in_progress": { + "type": "integer", + "description": "Sessions `In Progress`." + }, + "total_not_started": { + "type": "integer", + "description": "Sessions `Not Started`." + }, + "total_manually_approved": { + "type": "integer", + "description": "Approved by a reviewer." + }, + "total_manually_declined": { + "type": "integer", + "description": "Declined by a reviewer." + } + } }, - "DigitalIdWalletApiFrejaidWalletVerificationStartRequest": { + "ApplicationAnalyticsCountryCount": { "type": "object", - "description": "Start a Freja eID verification (SWE).", "properties": { - "vendor_data": { + "country": { "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." - }, - "metadata": { - "type": "object", - "additionalProperties": {}, - "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "description": "Country name." }, - "sandbox_scenario": { + "country_code": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "description": "Two-letter country code." }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiCountryEe9Enum" - } - ], - "default": "SWE", - "description": "ISO 3166-1 alpha-3 country of the Freja eID identity. Defaults to SWE, the only country it serves.\n\n* `SWE` - SWE" + "session_count": { + "type": "integer", + "description": "Sessions from that country." + } + } + }, + "ApplicationAnalyticsLocationStats": { + "type": "object", + "properties": { + "ip_countries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsCountryCount" + }, + "description": "Countries of the users' IP addresses, most sessions first." }, - "return_url": { - "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "countries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsCountryCount" + }, + "description": "Same list as `ip_countries`." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + "id_countries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsCountryCount" + }, + "description": "Issuing countries of the verified ID documents, most sessions first." + }, + "conversion_by_country": { + "type": "array", + "description": "Per document issuing country: finished sessions (approved, declined or in review) and how many were approved. Most finished first.", + "items": { + "type": "object", + "properties": { + "country": { + "type": "string" + }, + "country_code": { + "type": "string" + }, + "finished": { + "type": "integer" + }, + "approved": { + "type": "integer" + } } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + } + }, + "cities": { + "type": "array", + "description": "Up to 100 IP cities, most sessions first.", + "items": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "`City, Country`." + }, + "session_count": { + "type": "integer" + }, + "latitude": { + "type": "number", + "nullable": true + }, + "longitude": { + "type": "number", + "nullable": true + } + } + } } - }, - "required": [ - "return_url" - ] - }, - "DigitalIdWalletApiCountryEe9Enum": { - "enum": [ - "SWE" - ], - "type": "string", - "description": "* `SWE` - SWE" + } }, - "DigitalIdWalletApiPersonalausweisWalletVerificationStartRequest": { + "ApplicationAnalyticsDemographicStats": { "type": "object", - "description": "Start a Personalausweis verification (DEU).", "properties": { - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." - }, - "metadata": { + "age_distribution": { "type": "object", - "additionalProperties": {}, - "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." - }, - "sandbox_scenario": { - "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "description": "Sessions per age band from the ID document. Empty when there is no data.", + "properties": { + "under_18": { + "type": "integer" + }, + "18_24": { + "type": "integer" + }, + "25_34": { + "type": "integer" + }, + "35_44": { + "type": "integer" + }, + "45_64": { + "type": "integer" + }, + "65_plus": { + "type": "integer" + } + }, + "additionalProperties": { + "type": "integer" + } }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiPersonalausweisWalletVerificationStartCountryEnum" + "gender_distribution": { + "type": "object", + "description": "Sessions per document gender code: `M`, `F` or `U` (unknown).", + "additionalProperties": { + "type": "integer" + } + } + } + }, + "ApplicationAnalyticsDeviceDetails": { + "type": "object", + "description": "Top five of each, most sessions first.", + "properties": { + "device_models": { + "type": "array", + "description": "Device brands: `Apple`, `Android`, `LG`, everything else grouped as `Other`.", + "items": { + "type": "object", + "properties": { + "device": { + "type": "string" + }, + "count": { + "type": "integer" + } } - ], - "default": "DEU", - "description": "ISO 3166-1 alpha-3 country of the Personalausweis identity. Defaults to DEU, the only country it serves.\n\n* `DEU` - DEU" + } }, - "return_url": { - "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "browsers": { + "type": "array", + "description": "Browsers: `Chrome`, `Safari`, `Didit App`, `Firefox`, everything else grouped as `Other`.", + "items": { + "type": "object", + "properties": { + "browser": { + "type": "string" + }, + "count": { + "type": "integer" + } + } + } }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + "operating_systems": { + "type": "array", + "description": "Operating systems.", + "items": { + "type": "object", + "properties": { + "os": { + "type": "string" + }, + "count": { + "type": "integer" + } } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + } } - }, - "required": [ - "return_url" - ] + } }, - "DigitalIdWalletApiPersonalausweisWalletVerificationStartCountryEnum": { - "enum": [ - "DEU" - ], - "type": "string", - "description": "* `DEU` - DEU" + "ApplicationAnalyticsWorkflowFunnel": { + "type": "object", + "description": "Keyed by workflow ID. `total` is the sessions started on that workflow; `steps` counts the sessions that reached each step (`OCR`, `LIVENESS`, `FACE_MATCH`, `AML`, `POA`, `PHONE`, `EMAIL`, `NFC`, `DATABASE_VALIDATION`, `QUESTIONNAIRE`, `IP_ANALYSIS`, `AGE_ESTIMATION`, `KYB_REGISTRY`, `KYB_DOCUMENTS`, `KYB_KEY_PEOPLE`). Reaching a step is not passing it.", + "additionalProperties": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "steps": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "reached": { + "type": "integer" + } + } + } + } + } + } }, - "DigitalIdWalletApiFtnWalletVerificationStartRequest": { + "ApplicationAnalyticsVerificationTimeStats": { "type": "object", - "description": "Start a Finnish Trust Network verification (FIN).", + "description": "Active time users spent verifying, ignoring idle gaps. An empty object when there is no data.", "properties": { - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "overall_avg_seconds": { + "type": "number", + "description": "Average per session, one decimal." }, - "metadata": { - "type": "object", - "additionalProperties": {}, - "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "overall_count": { + "type": "integer", + "description": "Sessions measured." }, - "sandbox_scenario": { - "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "per_feature": { + "type": "object", + "description": "Keyed by feature, for example `OCR` or `LIVENESS`.", + "additionalProperties": { + "type": "object", + "properties": { + "avg_seconds": { + "type": "number" + }, + "total_seconds": { + "type": "number" + }, + "count": { + "type": "integer" + } + } + } }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiFtnWalletVerificationStartCountryEnum" + "per_sub_step": { + "type": "object", + "description": "Keyed by feature, then by sub-step, for example `front_id_photo`.", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "avg_seconds": { + "type": "number" + }, + "total_seconds": { + "type": "number" + }, + "count": { + "type": "integer" + } + } } - ], - "default": "FIN", - "description": "ISO 3166-1 alpha-3 country of the Finnish Trust Network identity. Defaults to FIN, the only country it serves.\n\n* `FIN` - FIN" + } + } + } + }, + "ApplicationAnalyticsResubmissionStats": { + "type": "object", + "properties": { + "first_pass": { + "type": "integer", + "description": "Finished sessions with no resubmission." }, - "return_url": { - "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "second": { + "type": "integer", + "description": "Finished sessions resubmitted once." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + "third_plus": { + "type": "integer", + "description": "Finished sessions resubmitted two or more times." + }, + "abandoned": { + "type": "integer", + "description": "Abandoned sessions." } - }, - "required": [ - "return_url" - ] + } }, - "DigitalIdWalletApiFtnWalletVerificationStartCountryEnum": { - "enum": [ - "FIN" - ], - "type": "string", - "description": "* `FIN` - FIN" + "ApplicationAnalyticsOriginalStatusStats": { + "type": "object", + "description": "The workflow's automatic verdict, before any reviewer changed it.", + "properties": { + "approved": { + "type": "integer" + }, + "in_review": { + "type": "integer" + }, + "declined": { + "type": "integer" + } + } + }, + "ApplicationAnalyticsWarningStats": { + "type": "object", + "description": "Keyed by feature (`ID_VERIFICATION`, `LIVENESS`, `FACEMATCH`, `AML`, `PROOF_OF_ADDRESS`, `PHONE`, `EMAIL`, `NFC`, `LOCATION`, `DATABASE_VALIDATION`, `QUESTIONNAIRE` and the KYB features). `total` is the sessions that reached the feature; `risks` counts sessions per warning code and level.", + "additionalProperties": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "risks": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "error": { + "type": "integer" + }, + "warning": { + "type": "integer" + }, + "information": { + "type": "integer" + } + } + } + } + } + } }, - "DigitalIdWalletApiIdinWalletVerificationStartRequest": { + "ApplicationAnalyticsDemographicPoint": { "type": "object", - "description": "Start a iDIN verification (NLD).", "properties": { - "vendor_data": { + "date": { "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "description": "Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`." }, - "metadata": { + "age_distribution": { "type": "object", - "additionalProperties": {}, - "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "additionalProperties": { + "type": "integer" + } }, - "sandbox_scenario": { + "gender_distribution": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + } + } + }, + "ApplicationAnalyticsDevicePoint": { + "type": "object", + "properties": { + "date": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "description": "Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`." }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiIdinWalletVerificationStartCountryEnum" - } - ], - "default": "NLD", - "description": "ISO 3166-1 alpha-3 country of the iDIN identity. Defaults to NLD, the only country it serves.\n\n* `NLD` - NLD" + "devices": { + "type": "object", + "additionalProperties": { + "type": "integer" + } }, - "return_url": { - "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "browsers": { + "type": "object", + "additionalProperties": { + "type": "integer" + } }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + "operating_systems": { + "type": "object", + "additionalProperties": { + "type": "integer" + } } - }, - "required": [ - "return_url" - ] - }, - "DigitalIdWalletApiIdinWalletVerificationStartCountryEnum": { - "enum": [ - "NLD" - ], - "type": "string", - "description": "* `NLD` - NLD" + } }, - "DigitalIdWalletApiItsmeWalletVerificationStartRequest": { + "ApplicationAnalyticsVerificationTimePoint": { "type": "object", - "description": "Start a itsme verification (BEL, LUX, NLD).", "properties": { - "vendor_data": { + "date": { "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "description": "Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`." }, - "metadata": { - "type": "object", - "additionalProperties": {}, - "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "overall_avg": { + "type": "number", + "description": "Average active seconds per session." }, - "sandbox_scenario": { + "per_feature": { + "type": "object", + "description": "Average active seconds per feature.", + "additionalProperties": { + "type": "number" + } + } + } + }, + "ApplicationAnalyticsResubmissionPoint": { + "type": "object", + "properties": { + "date": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "description": "Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`." }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiItsmeWalletVerificationStartCountryEnum" - } - ], - "description": "ISO 3166-1 alpha-3 country of the itsme identity.\n\n* `BEL` - BEL\n* `LUX` - LUX\n* `NLD` - NLD" + "first_pass": { + "type": "integer" }, - "return_url": { - "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "second": { + "type": "integer" }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + "third_plus": { + "type": "integer" + }, + "abandoned": { + "type": "integer" } - }, - "required": [ - "country", - "return_url" - ] - }, - "DigitalIdWalletApiItsmeWalletVerificationStartCountryEnum": { - "enum": [ - "BEL", - "LUX", - "NLD" - ], - "type": "string", - "description": "* `BEL` - BEL\n* `LUX` - LUX\n* `NLD` - NLD" + } }, - "DigitalIdWalletApiBuypassWalletVerificationStartRequest": { + "ApplicationAnalyticsOriginalStatusPoint": { "type": "object", - "description": "Start a Buypass ID verification (NOR).", "properties": { - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." - }, - "metadata": { - "type": "object", - "additionalProperties": {}, - "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." - }, - "sandbox_scenario": { + "date": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "description": "Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`." }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiCountryAfcEnum" - } - ], - "default": "NOR", - "description": "ISO 3166-1 alpha-3 country of the Buypass ID identity. Defaults to NOR, the only country it serves.\n\n* `NOR` - NOR" + "approved": { + "type": "integer" }, - "return_url": { - "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "in_review": { + "type": "integer" }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + "declined": { + "type": "integer" } - }, - "required": [ - "return_url" - ] - }, - "DigitalIdWalletApiCountryAfcEnum": { - "enum": [ - "NOR" - ], - "type": "string", - "description": "* `NOR` - NOR" + } }, - "DigitalIdWalletApiVippsWalletVerificationStartRequest": { + "ApplicationAnalyticsSummary": { "type": "object", - "description": "Start a Vipps verification (NOR).", + "description": "User verification (KYC) summary, returned when neither `scope` nor `widget_keys` is sent.", + "required": [ + "data_points", + "request_breakdown", + "previous_data_points", + "previous_request_breakdown", + "conversion_rate", + "location_stats", + "demographic_stats", + "device_details", + "workflow_funnel_stats", + "verification_time_stats", + "resubmission_stats", + "warning_stats", + "original_status_stats", + "grouping_mode", + "demographic_time_series", + "device_time_series", + "verification_time_time_series", + "resubmission_time_series", + "original_status_time_series" + ], "properties": { - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." + "data_points": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsDataPoint" + }, + "description": "One point per day, from the first day with sessions in the range to `end_date`. Always daily, whatever `grouping_mode` says." }, - "metadata": { - "type": "object", - "additionalProperties": {}, + "request_breakdown": { + "$ref": "#/components/schemas/ApplicationAnalyticsRequestBreakdown" + }, + "previous_data_points": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsDataPoint" + }, + "description": "Daily points for the period of the same length right before `start_date`." + }, + "previous_request_breakdown": { + "$ref": "#/components/schemas/ApplicationAnalyticsRequestBreakdown" + }, + "conversion_rate": { + "type": "number", "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "description": "`total_approved / (total_approved + total_declined + total_in_review) x 100`, one decimal. `null` when nothing finished." }, - "sandbox_scenario": { - "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "location_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsLocationStats" }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiCountryAfcEnum" - } - ], - "default": "NOR", - "description": "ISO 3166-1 alpha-3 country of the Vipps identity. Defaults to NOR, the only country it serves.\n\n* `NOR` - NOR" + "demographic_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsDemographicStats" }, - "return_url": { - "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "device_details": { + "$ref": "#/components/schemas/ApplicationAnalyticsDeviceDetails" }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } + "workflow_funnel_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsWorkflowFunnel" + }, + "verification_time_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsVerificationTimeStats" + }, + "resubmission_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsResubmissionStats" + }, + "warning_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsWarningStats" + }, + "original_status_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsOriginalStatusStats" + }, + "grouping_mode": { + "type": "string", + "enum": [ + "day", + "week", + "month" ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + "description": "How the `*_time_series` lists are grouped: `day` for up to 14 days, `week` up to 90, `month` beyond. Measured from the first day with sessions in the range." + }, + "demographic_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsDemographicPoint" + } + }, + "device_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsDevicePoint" + } + }, + "verification_time_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsVerificationTimePoint" + } + }, + "resubmission_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsResubmissionPoint" + } + }, + "original_status_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsOriginalStatusPoint" + } } - }, - "required": [ - "return_url" - ] + } }, - "DigitalIdWalletApiBankidNoWalletVerificationStartRequest": { + "ApplicationAnalyticsWidget": { "type": "object", - "description": "Start a BankID verification (NOR).", + "description": "One dashboard widget. Every widget has `kind` and `title`; the other fields depend on `kind`. The session widgets reuse the summary objects (`data_points`, `request_breakdown`, `workflow_funnel_stats` and so on); list widgets put their entries in `items`; transaction metrics use `value`.", + "required": [ + "kind", + "title" + ], "properties": { - "vendor_data": { + "kind": { "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." - }, - "metadata": { - "type": "object", - "additionalProperties": {}, - "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "enum": [ + "sessions_volume", + "workflow_funnel", + "average_verification_time", + "country_distribution", + "warning_insights", + "conversion_rate", + "conversion_by_country", + "resubmission_insights", + "original_status_distribution", + "database_validation_coverage", + "database_validation_latency", + "demographics", + "device_details", + "horizontal_bar", + "kyb_registry_coverage", + "kyb_registry_data_completeness", + "metric", + "time_series", + "breakdown", + "status_time_series", + "leaderboard", + "histogram" + ], + "description": "Widget type." }, - "sandbox_scenario": { + "title": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." + "description": "Display title, for example `Verification volume`." }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiCountryAfcEnum" - } + "grouping_mode": { + "type": "string", + "enum": [ + "day", + "week", + "month" ], - "default": "NOR", - "description": "ISO 3166-1 alpha-3 country of the BankID identity. Defaults to NOR, the only country it serves.\n\n* `NOR` - NOR" + "description": "How the `*_time_series` lists are grouped: `day` for up to 14 days, `week` up to 90, `month` beyond. Measured from the first day with sessions in the range." }, - "return_url": { + "data_points": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsDataPoint" + } + }, + "request_breakdown": { + "$ref": "#/components/schemas/ApplicationAnalyticsRequestBreakdown" + }, + "previous_data_points": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsDataPoint" + } + }, + "previous_request_breakdown": { + "$ref": "#/components/schemas/ApplicationAnalyticsRequestBreakdown" + }, + "conversion_rate": { + "type": "number", + "nullable": true + }, + "workflow_funnel_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsWorkflowFunnel" + }, + "verification_time_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsVerificationTimeStats" + }, + "verification_time_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsVerificationTimePoint" + } + }, + "warning_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsWarningStats" + }, + "resubmission_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsResubmissionStats" + }, + "resubmission_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsResubmissionPoint" + } + }, + "original_status_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsOriginalStatusStats" + }, + "original_status_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsOriginalStatusPoint" + } + }, + "demographic_stats": { + "$ref": "#/components/schemas/ApplicationAnalyticsDemographicStats" + }, + "demographic_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsDemographicPoint" + } + }, + "device_details": { + "$ref": "#/components/schemas/ApplicationAnalyticsDeviceDetails" + }, + "device_time_series": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationAnalyticsDevicePoint" + } + }, + "items": { + "type": "array", + "description": "Entries of a list widget. `country_distribution` (KYC): `country`, `country_code`, `session_count`. `conversion_by_country`: `country`, `country_code`, `finished`, `approved`. `time_series`: `date`, `value`. `status_time_series`: `date`, `approved`, `declined`, `flagged`. `breakdown`: `transaction_type` or `action_type` with `count` and `volume`, or `tag_id`, `tag_name`, `count`. `leaderboard`: `rule_title`, `trigger_count` or `assignee`, `handled_count`, `average_processing_seconds`, each with `approved_percentage`, `declined_percentage`, `flagged_percentage`. `histogram`: `score`, `count`.", + "items": { + "type": "object", + "additionalProperties": {} + } + }, + "previous_items": { + "type": "array", + "description": "Transaction time series for the period of the same length right before `start_date`.", + "items": { + "type": "object", + "additionalProperties": {} + } + }, + "value": { + "type": "number", + "description": "`metric` widgets: the figure for the range." + }, + "currency": { "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "nullable": true, + "description": "Transaction volume and amount widgets: the application's default transaction currency." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" + "thresholds": { + "type": "object", + "description": "`histogram` widget: the application's review and decline score thresholds.", + "properties": { + "review": { + "type": "integer" + }, + "decline": { + "type": "integer" } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + } + }, + "search": { + "type": "object", + "description": "`kyb_registry_coverage` widget: registry search totals.", + "properties": { + "searches_total": { + "type": "integer" + }, + "searches_with_no_candidates": { + "type": "integer" + } + } } - }, - "required": [ - "return_url" - ] + } }, - "DigitalIdWalletApiBankidSeWalletVerificationStartRequest": { + "ApplicationAnalyticsWidgetResponse": { "type": "object", - "description": "Start a BankID verification (SWE).", + "description": "Dashboard widgets, returned when `scope` or `widget_keys` is sent.", + "required": [ + "scope", + "available_widget_keys", + "requested_widget_keys", + "widgets" + ], "properties": { - "vendor_data": { - "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." - }, - "metadata": { - "type": "object", - "additionalProperties": {}, - "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." - }, - "sandbox_scenario": { + "scope": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." - }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiCountryEe9Enum" - } + "enum": [ + "home", + "kyc", + "kyb", + "transactions" ], - "default": "SWE", - "description": "ISO 3166-1 alpha-3 country of the BankID identity. Defaults to SWE, the only country it serves.\n\n* `SWE` - SWE" + "description": "Dashboard tab." }, - "return_url": { - "type": "string", - "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "available_widget_keys": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Every widget key this scope accepts." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" + "requested_widget_keys": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The widget keys answered, in order: your valid `widget_keys`, or the scope's defaults." + }, + "widgets": { + "type": "object", + "description": "Keyed by widget key, in the order of `requested_widget_keys`.", + "additionalProperties": { + "$ref": "#/components/schemas/ApplicationAnalyticsWidget" + } } - }, - "required": [ - "return_url" - ] + } }, - "DigitalIdWalletApiMitidWalletVerificationStartRequest": { + "WebhookConfigDestination": { "type": "object", - "description": "Start a MitID verification (DNK).", + "description": "A webhook destination as listed in the webhook configuration, without delivery metrics. `GET /v3/webhook/destinations/{destination_uuid}/` adds its delivery summary.", + "required": [ + "uuid", + "label", + "url", + "enabled", + "webhook_version", + "secret_shared_key", + "subscribed_events", + "created_at", + "updated_at" + ], "properties": { - "vendor_data": { + "uuid": { "type": "string", - "nullable": true, - "description": "Your own reference for this verification (e.g. your user id). Returned in results and webhooks." - }, - "metadata": { - "type": "object", - "additionalProperties": {}, - "nullable": true, - "description": "Optional JSON object stored with the verification and returned in results and webhooks." + "format": "uuid", + "description": "Destination ID." }, - "sandbox_scenario": { + "label": { "type": "string", - "minLength": 1, - "description": "Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/." - }, - "country": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiMitidWalletVerificationStartCountryEnum" - } - ], - "default": "DNK", - "description": "ISO 3166-1 alpha-3 country of the MitID identity. Defaults to DNK, the only country it serves.\n\n* `DNK` - DNK" + "maxLength": 255, + "description": "Human-readable name." }, - "return_url": { + "url": { "type": "string", "format": "uri", - "minLength": 1, - "description": "https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.", - "maxLength": 2048 + "maxLength": 2000, + "description": "HTTPS endpoint Didit posts events to." }, - "device": { - "allOf": [ - { - "$ref": "#/components/schemas/DigitalIdWalletApiDeviceEnum" - } - ], - "default": "same_device", - "description": "same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code).\n\n* `same_device` - same_device\n* `cross_device` - cross_device" - } - }, - "required": [ - "return_url" - ] - }, - "DigitalIdWalletApiMitidWalletVerificationStartCountryEnum": { - "enum": [ - "DNK" - ], - "type": "string", - "description": "* `DNK` - DNK" - }, - "DigitalIdWalletApiWalletCatalogResponse": { - "type": "object", - "properties": { - "wallets": { + "enabled": { + "type": "boolean", + "description": "`false` pauses deliveries to this destination." + }, + "webhook_version": { + "type": "string", + "enum": [ + "v1", + "v2", + "v3" + ], + "description": "Payload version sent to this destination." + }, + "secret_shared_key": { + "type": "string", + "description": "This destination's signing secret (43-character URL-safe token). Verify each webhook with the secret of the destination it was sent to. API-key callers always receive it in full; Console users without `read:webhooks` receive an empty string." + }, + "subscribed_events": { "type": "array", + "uniqueItems": true, + "description": "Event types delivered to this destination. Empty when the destination was created without events, in which case it receives nothing.", "items": { - "$ref": "#/components/schemas/DigitalIdWalletApiWalletCatalogEntry" + "$ref": "#/components/schemas/WebhookEvent" } + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" } - }, - "required": [ - "wallets" - ] + } }, - "DigitalIdWalletApiWalletCatalogEntry": { + "WebhookConfig": { "type": "object", + "description": "Application-level webhook configuration, retention policy and Digital ID wallet return hosts, with every webhook destination.", + "required": [ + "webhook_url", + "webhook_version", + "secret_shared_key", + "capture_method", + "data_retention_months", + "face_retention_policy", + "face_retention_days", + "available_events", + "allowed_return_hosts", + "endpoints" + ], "properties": { - "id": { - "type": "string" + "webhook_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Application-level (legacy) webhook URL. Didit sends to it only while the application has no enabled destination, and then only `status.updated` and `data.updated`. Every create, update or delete of a destination sets it to the URL of the oldest enabled destination, or `null` when none is enabled." }, - "name": { - "type": "string" + "webhook_version": { + "type": "string", + "enum": [ + "v1", + "v2", + "v3" + ], + "description": "Payload version of the legacy `webhook_url`. Each destination has its own `webhook_version`." }, - "path": { + "secret_shared_key": { "type": "string", - "description": "The wallet's own start endpoint." + "description": "Application-level signing secret (43-character URL-safe token). It signs only deliveries to the legacy `webhook_url`; each destination signs with its own `secret_shared_key`. API-key callers always receive it in full; Console users without `read:webhooks` receive an empty string." }, - "countries": { - "type": "array", - "items": { - "type": "string" - } + "capture_method": { + "type": "string", + "enum": [ + "MOBILE", + "MOBILE_AND_DESKTOP" + ], + "description": "Legacy application setting. The verification flow does not read it: capture options are configured per workflow." }, - "issuing_authority": { - "type": "string" + "data_retention_months": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 120, + "description": "Automatic deletion window. User verification sessions created more than this many months ago (a month counts as 30 days) are deleted automatically, and that deletion is irreversible. `null` means sessions are never deleted automatically. See [Data retention](https://docs.didit.me/console/data-retention)." }, - "level_of_assurance": { + "face_retention_policy": { "type": "string", - "nullable": true + "enum": [ + "delete_with_session", + "retain_until_user_deleted" + ], + "description": "What happens to a session's face embedding when the session is deleted. `delete_with_session` (default) deletes it with the session. `retain_until_user_deleted` is the opt-in [biometric template retention](https://docs.didit.me/console/data-retention#biometric-template-retention): an image-free biometric template linked to the User is managed separately until its retention period ends, the User is deleted, or you purge it." }, - "interaction": { - "$ref": "#/components/schemas/DigitalIdWalletApiInteractionEnum" + "face_retention_days": { + "type": "integer", + "nullable": true, + "minimum": 1, + "maximum": 3650, + "description": "Retention period, in days, of biometric templates under `retain_until_user_deleted`. Required when that policy is set." }, - "required_fields": { + "available_events": { "type": "array", + "description": "Every event type a destination can subscribe to, with a display label.", "items": { - "type": "string" + "type": "object", + "required": [ + "value", + "label" + ], + "properties": { + "value": { + "$ref": "#/components/schemas/WebhookEvent" + }, + "label": { + "type": "string", + "description": "Display label, for example `Session status updated`." + } + } } }, - "available": { - "type": "boolean", - "description": "Whether this application can start it in this environment now. Listed does not mean live." + "allowed_return_hosts": { + "type": "array", + "maxItems": 20, + "description": "Hostnames (exact match, HTTPS only) a standalone Digital ID wallet verification may send the user back to with its `return_url`. Your white-label domain is always allowed as well. See [Register your return destination](https://docs.didit.me/standalone-apis/digital-id-wallets#register-your-return-destination).", + "items": { + "type": "string", + "maxLength": 253 + } + }, + "endpoints": { + "type": "array", + "description": "Every webhook destination of the application, oldest first, each with its signing secret. Read only here: manage destinations with the `/v3/webhook/destinations/` endpoints.", + "items": { + "$ref": "#/components/schemas/WebhookConfigDestination" + } } - }, - "required": [ - "available", - "countries", - "id", - "interaction", - "issuing_authority", - "level_of_assurance", - "name", - "path", - "required_fields" - ] + } }, - "CaseDuplicateCaseDuplicate": { + "WebhookDestinationDelivery": { "type": "object", + "description": "One delivery attempt to a webhook destination. The first send of an event, each automatic retry and each resend are separate deliveries.", + "required": [ + "uuid", + "session_id", + "request_url", + "response_status", + "response_time_ms", + "status", + "webhook_type", + "created_at" + ], "properties": { "uuid": { "type": "string", "format": "uuid", - "readOnly": true + "description": "Delivery ID, for the detail and resend calls." }, - "case_number": { + "session_id": { "type": "string", + "format": "uuid", "nullable": true, - "maxLength": 32 + "description": "User verification session the event is about. `null` for business session, user, business, transaction, Travel Rule and workflow events." }, - "title": { + "request_url": { "type": "string", - "maxLength": 255 + "format": "uri", + "nullable": true, + "description": "URL the delivery was sent to: the destination's URL when the event was generated." + }, + "response_status": { + "type": "integer", + "nullable": true, + "description": "HTTP status your endpoint returned. `null` while the delivery is queued and not yet attempted. When no HTTP answer was received Didit records a status of its own: `504` when your endpoint did not answer within 5 seconds, `503` for a connection failure, `400` when the URL was refused as unsafe (for example a private or internal address), and `404` for any other request error. `400` or above counts as a failed delivery." + }, + "response_time_ms": { + "type": "integer", + "nullable": true, + "minimum": 0, + "description": "Time the attempt took, in milliseconds. `null` while the delivery is queued." }, "status": { - "$ref": "#/components/schemas/CaseDuplicateStatusEnum" + "type": "string", + "nullable": true, + "description": "Status the event reported when it was generated: the session status for session events (for example `Approved` or `In Review`), the person's or company's status for user and business events, and the transaction status for transaction events. `null` for events that carry no status." }, - "priority": { - "$ref": "#/components/schemas/CaseDuplicatePriorityEnum" + "webhook_type": { + "$ref": "#/components/schemas/WebhookEvent" }, "created_at": { "type": "string", "format": "date-time", - "readOnly": true + "description": "When the delivery was created." } - }, - "required": [ - "created_at", - "title", - "uuid" - ] + } }, - "CaseDuplicateCaseDuplicatePage": { + "WebhookDestinationDeliveryDetail": { "type": "object", + "description": "One delivery attempt to a webhook destination, with the exact request headers and body Didit sent.", + "required": [ + "uuid", + "session_id", + "request_url", + "response_status", + "response_time_ms", + "status", + "webhook_type", + "created_at", + "request_header", + "request_body" + ], "properties": { - "count": { - "type": "integer" + "uuid": { + "type": "string", + "format": "uuid", + "description": "Delivery ID, for the detail and resend calls." }, - "next_offset": { + "session_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "User verification session the event is about. `null` for business session, user, business, transaction, Travel Rule and workflow events." + }, + "request_url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "URL the delivery was sent to: the destination's URL when the event was generated." + }, + "response_status": { "type": "integer", - "nullable": true + "nullable": true, + "description": "HTTP status your endpoint returned. `null` while the delivery is queued and not yet attempted. When no HTTP answer was received Didit records a status of its own: `504` when your endpoint did not answer within 5 seconds, `503` for a connection failure, `400` when the URL was refused as unsafe (for example a private or internal address), and `404` for any other request error. `400` or above counts as a failed delivery." }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaseDuplicateCaseDuplicate" - } + "response_time_ms": { + "type": "integer", + "nullable": true, + "minimum": 0, + "description": "Time the attempt took, in milliseconds. `null` while the delivery is queued." + }, + "status": { + "type": "string", + "nullable": true, + "description": "Status the event reported when it was generated: the session status for session events (for example `Approved` or `In Review`), the person's or company's status for user and business events, and the transaction status for transaction events. `null` for events that carry no status." + }, + "webhook_type": { + "$ref": "#/components/schemas/WebhookEvent" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the delivery was created." + }, + "request_header": { + "type": "string", + "description": "JSON text (a string) of the request headers Didit sent: `Content-Type`, `X-Signature`, `X-Signature-V2`, `X-Signature-Simple`, `X-Timestamp` and `User-Agent`. Parse it with a JSON parser. It holds signatures, never the signing secret." + }, + "request_body": { + "type": "string", + "description": "JSON text (a string) of the exact payload Didit sent, keys sorted, including `event_id` and `timestamp`. Parse it with a JSON parser; see [Payload shape](https://docs.didit.me/integration/webhooks#payload-shape). For a scoped key without `media: read`, media URLs inside it are `null`; without `sessions: write`, session tokens and hosted verification links are `null`. A body changed that way no longer matches its signatures. Didit does not record the response body your endpoint returned." } - }, - "required": [ - "count", - "next_offset", - "results" - ] + } }, - "RoutingPublicationPolicyDraftResponse": { + "SessionWebhookDelivery": { "type": "object", - "properties": { - "draft": { - "allOf": [ - { - "$ref": "#/components/schemas/RoutingPublicationPolicyDraft" - } - ], - "nullable": true - } - }, + "description": "One webhook delivery attempt recorded for a verification session, with the exact request headers and body Didit sent.", "required": [ - "draft" - ] - }, - "RoutingPublicationPolicyDraftWriteRequest": { - "type": "object", + "uuid", + "session_id", + "request_url", + "response_status", + "status", + "webhook_type", + "request_header", + "request_body", + "created_at" + ], "properties": { - "expected_version": { - "type": "integer", - "minimum": 0 + "uuid": { + "type": "string", + "format": "uuid", + "description": "Delivery ID. Use it as `{webhook_uuid}` to get or resend this delivery." }, - "expected_revision": { + "session_id": { "type": "string", "format": "uuid", - "nullable": true + "description": "The session the delivery belongs to (the session in the path)." }, - "changes": { - "type": "object", - "additionalProperties": {} - } - }, - "required": [ - "changes", - "expected_revision", - "expected_version" - ] - }, - "CasePublicationPolicyDraft": { - "type": "object", - "properties": { - "revision": { + "request_url": { "type": "string", - "format": "uuid" + "format": "uri", + "nullable": true, + "description": "URL the delivery was sent to." }, - "base_version": { + "response_status": { "type": "integer", - "maximum": 2147483647, - "minimum": 0 + "nullable": true, + "description": "HTTP status your endpoint returned. `null` while the delivery is queued and not yet attempted. When no HTTP answer was received Didit records a status of its own: `504` when your endpoint did not answer within 5 seconds, `503` for a connection failure, `400` when the URL was refused as unsafe (for example a private or internal address), and `404` for any other request error. `400` or above counts as a failed delivery." }, - "changes": {}, - "actor_email": { + "status": { "type": "string", - "format": "email", "nullable": true, - "maxLength": 254 + "description": "Session status when the event was generated, for example `In Progress`, `Approved` or `Declined`." }, - "updated_at": { + "webhook_type": { + "$ref": "#/components/schemas/WebhookEvent" + }, + "request_header": { "type": "string", - "format": "date-time", - "readOnly": true - } - }, - "required": [ - "base_version", - "updated_at" - ] - }, - "CasePublicationPolicyDraftPreconditionRequest": { - "type": "object", - "properties": { - "expected_version": { - "type": "integer", - "minimum": 0 + "description": "JSON text (a string) of the request headers Didit sent: `Content-Type`, `X-Signature`, `X-Signature-V2`, `X-Signature-Simple`, `X-Timestamp` and `User-Agent`. Parse it with a JSON parser. It holds signatures, never the signing secret." }, - "expected_revision": { + "request_body": { "type": "string", - "format": "uuid", - "nullable": true + "description": "JSON text (a string) of the exact payload Didit sent, keys sorted, including `event_id` and `timestamp`. Parse it with a JSON parser; see [Payload shape](https://docs.didit.me/integration/webhooks#payload-shape). For a scoped key without `media: read`, media URLs inside it are `null`; without `sessions: write`, session tokens and hosted verification links are `null`. A body changed that way no longer matches its signatures. Didit does not record the response body your endpoint returned." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "When the delivery was created." } - }, - "required": [ - "expected_revision", - "expected_version" - ] + } }, - "CaseCollaborationCaseNote": { + "SessionEvent": { "type": "object", + "description": "One entry of a verification session's event timeline.", + "required": [ + "uuid", + "event_type", + "created_at", + "component", + "feature_step", + "step", + "details", + "ip_address", + "user_agent", + "device_info", + "file_url", + "sent_at" + ], "properties": { "uuid": { "type": "string", "format": "uuid", - "readOnly": true + "description": "Event identifier." }, - "comment": { - "type": "string" + "event_type": { + "type": "string", + "enum": [ + "SESSION_CREATED", + "SESSION_COMPLETED", + "SESSION_STATUS_CHANGED", + "LINK_OPENED", + "FORM_SUBMITTED", + "BUTTON_CLICKED", + "FILE_UPLOADED", + "ATTEMPT_FAILED", + "FEATURE_STEP_CHANGED", + "STEP_RENDERED", + "NOTE_ADDED", + "MANUAL_REVIEW_STARTED", + "WORKFLOW_BRANCH_EVALUATED", + "WORKFLOW_STATUS_NODE_REACHED", + "LOG" + ], + "description": "What happened. Reported by the device during the hosted flow or SDK: `LINK_OPENED`, `STEP_RENDERED`, `BUTTON_CLICKED`, `FORM_SUBMITTED`, `FILE_UPLOADED`, `ATTEMPT_FAILED` and `LOG` (debugging). Recorded by the server: `SESSION_CREATED`, `SESSION_STATUS_CHANGED` (`details.old_status`, `details.current_status`), `FEATURE_STEP_CHANGED` (`details.old_feature_step`, `details.current_feature_step`), `WORKFLOW_BRANCH_EVALUATED` and `WORKFLOW_STATUS_NODE_REACHED` (the workflow routing trace). `SESSION_COMPLETED`, `NOTE_ADDED` and `MANUAL_REVIEW_STARTED` can appear on older sessions." }, - "actor_email": { + "created_at": { + "type": "string", + "format": "date-time", + "description": "When Didit stored the event. Events are stored asynchronously, a few seconds after `sent_at`." + }, + "component": { "type": "string", - "format": "email", "nullable": true, - "maxLength": 255 + "description": "UI component that reported the event (device events only)." }, - "actor_name": { + "feature_step": { "type": "string", "nullable": true, - "maxLength": 255 + "enum": [ + "OCR", + "OCR_BACK", + "NFC", + "AML", + "FACE", + "LIVENESS", + "FACE_MATCH", + "IP_ANALYSIS", + "AGE_ESTIMATION", + "PROOF_OF_ADDRESS", + "PHONE_VERIFICATION", + "EMAIL_VERIFICATION", + "FACE_SEARCH", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + "DOCUMENT_AI", + "GEOLOCATION", + null + ], + "description": "Verification feature the session was on when the event happened." }, - "mentioned_emails": {}, - "is_internal": { - "type": "boolean" + "step": { + "type": "string", + "nullable": true, + "description": "Screen within the feature, for example `DOCUMENT_CAPTURE_FRONT` or `FACE_PASSIVE_LIVENESS`." }, - "attachments": {}, - "tags": {}, - "created_at": { + "details": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Event-specific data. The keys depend on `event_type`." + }, + "ip_address": { "type": "string", - "format": "date-time", - "readOnly": true + "nullable": true, + "description": "IP address the device reported the event from. `null` for server-recorded events." }, - "reply_to": { - "allOf": [ - { - "$ref": "#/components/schemas/CaseCollaborationCaseNoteReply" - } - ], + "user_agent": { + "type": "string", "nullable": true, - "readOnly": true + "description": "User agent of the device. `null` for server-recorded events." }, - "is_pinned": { - "type": "boolean" + "device_info": { + "type": "object", + "nullable": true, + "additionalProperties": true, + "description": "Device details the SDK or browser reported." }, - "client_reference": { + "file_url": { "type": "string", - "format": "uuid", - "nullable": true + "format": "uri", + "nullable": true, + "description": "Short-lived presigned link to a file attached to the event (for example a captured image). `null` when there is none, and always `null` for a scoped API key without `media: read`." + }, + "sent_at": { + "type": "integer", + "format": "int64", + "description": "When the event happened, as a Unix timestamp in milliseconds. Results are ordered by it." } - }, - "required": [ - "comment", - "created_at", - "reply_to", - "uuid" - ] + } }, - "CustomerMessagingPaginatedCaseRequestList": { + "AMLDatasetSourceFreshness": { "type": "object", + "description": "Freshness of one family of AML datasets.", "required": [ - "count", - "results" + "last_updated_at", + "oldest_updated_at", + "dataset_count", + "dated_dataset_count" ], "properties": { - "count": { - "type": "integer", - "example": 123 + "last_updated_at": { + "type": "string", + "format": "date-time", + "description": "Most recent update of any dataset in the family." }, - "next": { + "oldest_updated_at": { "type": "string", + "format": "date-time", "nullable": true, - "format": "uri", - "example": "http://api.example.org/accounts/?offset=400&limit=100" + "description": "Staleness bound: every dataset in the family is at least this current. `null` when at least one dataset in the family reported no update date, because the bound is then unknown." }, - "previous": { + "dataset_count": { + "type": "integer", + "description": "Number of datasets in the family." + }, + "dated_dataset_count": { + "type": "integer", + "description": "Number of those datasets that reported an update date." + } + } + }, + "AMLDatasetFreshness": { + "type": "object", + "description": "How current the AML datasets were when a screening ran. Dates are never estimated.", + "required": [ + "captured_at", + "provider", + "catalog_version", + "sources" + ], + "properties": { + "captured_at": { + "type": "string", + "format": "date-time", + "description": "When Didit read the dataset catalog this freshness comes from." + }, + "provider": { + "type": "string", + "description": "Opaque identifier of the AML data source. Treat it as an opaque string; do not branch on it.", + "example": "aml_data_source" + }, + "catalog_version": { "type": "string", "nullable": true, - "format": "uri", - "example": "http://api.example.org/accounts/?offset=200&limit=100" + "description": "Version identifier of the dataset catalog, when the source reports one." }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomerMessagingCaseRequest" + "sources": { + "type": "object", + "description": "Freshness per dataset family. A family whose datasets reported no update date is absent rather than filled with `null`.", + "properties": { + "sanctions": { + "$ref": "#/components/schemas/AMLDatasetSourceFreshness" + }, + "pep": { + "$ref": "#/components/schemas/AMLDatasetSourceFreshness" + }, + "warnings": { + "$ref": "#/components/schemas/AMLDatasetSourceFreshness" + }, + "adverse_media": { + "$ref": "#/components/schemas/AMLDatasetSourceFreshness" + } } } } }, - "FraudCheckResponse": { + "AMLScreeningRun": { "type": "object", + "description": "One completed ongoing-monitoring re-screen of an AML check.", + "required": [ + "uuid", + "completed_at", + "started_at", + "outcome", + "trigger", + "provider", + "status", + "score", + "total_hits", + "dataset_freshness" + ], "properties": { - "check_id": { + "uuid": { "type": "string", - "format": "uuid" + "format": "uuid", + "description": "Identifier of this screening run." }, - "request_id": { + "completed_at": { "type": "string", - "format": "uuid", - "description": "Native verification session created for this check." + "format": "date-time", + "description": "When this subject's re-screen finished. Results are ordered by it, newest first." }, - "client_reference": { + "started_at": { "type": "string", - "nullable": true + "format": "date-time", + "description": "When the monitoring batch this re-screen belongs to started." }, - "processing_status": { - "$ref": "#/components/schemas/FraudProcessingStatusEnum" + "outcome": { + "type": "string", + "enum": [ + "NO_CHANGE", + "CHANGED", + "FAILED" + ], + "description": "`NO_CHANGE`: completed and the result did not move. `CHANGED`: completed and the score, status, hit count or hits changed. `FAILED`: no usable answer for this subject, so the check was left as it was." }, - "profile_id": { - "type": "string" + "trigger": { + "type": "string", + "enum": [ + "ONGOING_MONITORING" + ], + "description": "What started the screening. Always `ONGOING_MONITORING` today." }, - "profile_version": { - "type": "string" + "provider": { + "type": "string", + "description": "Opaque identifier of the AML data source that answered. Treat it as an opaque string; do not branch on it.", + "example": "aml_data_source" }, - "created_at": { + "status": { "type": "string", - "format": "date-time" + "nullable": true, + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested", + null + ], + "description": "AML check status this run left the check in." }, - "completed_at": { + "score": { + "type": "number", + "format": "float", + "nullable": true, + "minimum": 0, + "maximum": 100, + "description": "AML score this run left the check in." + }, + "total_hits": { + "type": "integer", + "nullable": true, + "description": "Number of hits this run left the check in." + }, + "dataset_freshness": { + "allOf": [ + { + "$ref": "#/components/schemas/AMLDatasetFreshness" + } + ], + "nullable": true, + "description": "Dataset freshness behind this run, or `null` when no dates were available." + } + } + }, + "KYBKeyPeopleParty": { + "type": "object", + "description": "A Key People party of a Business Verification (KYB) session, with its child verification. The same party appears in `key_people_checks[].submitted.parties[]` of the business session decision.", + "required": [ + "uuid", + "entity_type", + "source", + "name", + "is_active", + "is_skipped", + "requires_verification", + "roles", + "kyc_session_deleted", + "kyb_sub_session_deleted" + ], + "properties": { + "uuid": { "type": "string", - "format": "date-time", - "nullable": true + "format": "uuid", + "description": "Party identifier." }, - "inputs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FraudCheckInput" - } + "entity_type": { + "type": "string", + "enum": [ + "person", + "company" + ], + "description": "Whether the party is an individual or a company." }, - "modules": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FraudCheckModuleReport" - } + "source": { + "type": "string", + "enum": [ + "REGISTRY", + "USER" + ], + "description": "`REGISTRY`: taken from the company registry. `USER`: entered in the Key People screen." }, - "attribute_evidence": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": {} - } + "name": { + "type": "string", + "description": "Display name of the party." }, - "compound_evidence": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": {} - } + "first_name": { + "type": "string", + "nullable": true, + "description": "First name (persons)." + }, + "last_name": { + "type": "string", + "nullable": true, + "description": "Last name (persons)." }, - "risk": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FraudCheckRisk" - } + "company_name": { + "type": "string", + "nullable": true, + "description": "Company name (company parties)." }, - "network": { - "type": "object", - "additionalProperties": {} + "registration_number": { + "type": "string", + "nullable": true, + "description": "Company registration number (company parties)." }, - "enrichment": { - "type": "object", - "additionalProperties": {} + "email": { + "type": "string", + "format": "email", + "nullable": true, + "description": "Email the verification link is sent to." }, - "identity_relationship": { - "allOf": [ - { - "$ref": "#/components/schemas/FraudCheckIdentityRelationship" - } - ], - "nullable": true + "nationality": { + "type": "string", + "nullable": true, + "description": "Nationality for persons, country of incorporation for companies." }, - "recommendation": { - "$ref": "#/components/schemas/FraudRecommendationEnum" + "phone_number": { + "type": "string", + "nullable": true, + "description": "Phone number collected for the party." }, - "next_action": { - "$ref": "#/components/schemas/FraudNextActionEnum" + "date_of_birth": { + "type": "string", + "format": "date", + "nullable": true, + "description": "Date of birth (persons)." }, - "decision_reasons": { - "type": "array", - "items": { - "type": "string" - } + "position": { + "type": "string", + "nullable": true, + "description": "Free-text job title (persons)." }, - "coverage": { + "custom_fields": { "type": "object", - "additionalProperties": {} + "additionalProperties": true, + "description": "Workflow-defined custom fields collected for the party, keyed by field key." }, - "usage": { + "is_active": { + "type": "boolean", + "description": "Whether the party is active." + }, + "is_skipped": { + "type": "boolean", + "description": "Whether the party is exempt from verification. Regenerating clears it." + }, + "requires_verification": { + "type": "boolean", + "description": "Whether the role and ownership rules require a verification for this party." + }, + "roles": { "type": "array", + "description": "Roles the party holds in the company.", "items": { "type": "object", - "additionalProperties": {} + "required": [ + "role", + "ownership_percent", + "voting_percent" + ], + "properties": { + "role": { + "type": "string", + "enum": [ + "director", + "non_executive_director", + "secretary", + "chairman", + "ubo", + "shareholder", + "representative", + "founder", + "legal_advisor", + "authorized_signatory", + "trustee", + "beneficiary", + "settlor", + "protector", + "company_officer", + "investor", + "other" + ], + "description": "Role code." + }, + "ownership_percent": { + "type": "number", + "nullable": true, + "description": "Ownership percentage for this role, when known." + }, + "voting_percent": { + "type": "number", + "nullable": true, + "description": "Voting-rights percentage for this role, when known." + } + } } }, - "vendor_data": { + "kyc_session_id": { "type": "string", - "nullable": true + "format": "uuid", + "nullable": true, + "description": "Child individual KYC session (persons)." }, - "metadata": { - "nullable": true - } - }, - "required": [ - "attribute_evidence", - "check_id", - "client_reference", - "completed_at", - "compound_evidence", - "coverage", - "created_at", - "decision_reasons", - "inputs", - "modules", - "network", - "next_action", - "processing_status", - "profile_id", - "profile_version", - "recommendation", - "risk", - "usage" - ] - }, - "CaseReviewTeam": { - "type": "object", - "properties": { - "name": { + "kyc_session_status": { "type": "string", - "maxLength": 100 + "nullable": true, + "enum": [ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + null + ], + "description": "Status of the child KYC session. A deleted session keeps the status it had when it was deleted." }, - "description": { + "kyc_session_url": { "type": "string", - "maxLength": 500 - }, - "members": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaseReviewTeamMember" - }, - "maxItems": 250 + "format": "uri", + "nullable": true, + "description": "Hosted link of the child KYC session. `null` when there is none or it was deleted, and for a scoped API key without `sessions: write`." }, - "is_active": { - "type": "boolean" + "kyc_session_deleted": { + "type": "boolean", + "description": "The child KYC session was deleted, so it can never settle." }, - "uuid": { + "kyb_sub_session_id": { "type": "string", "format": "uuid", - "readOnly": true + "nullable": true, + "description": "Child KYB session (company parties)." }, - "created_at": { + "kyb_sub_session_status": { "type": "string", - "format": "date-time", - "readOnly": true + "nullable": true, + "enum": [ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + null + ], + "description": "Status of the child KYB session." }, - "updated_at": { + "kyb_sub_session_url": { "type": "string", - "format": "date-time", - "readOnly": true - } - } - }, - "CaseActionCaseEntityActionRequest": { - "type": "object", - "properties": { - "client_reference": { - "type": "string", - "format": "uuid" + "format": "uri", + "nullable": true, + "description": "Hosted link of the child KYB session, with the same `null` rules as `kyc_session_url`." }, - "retry": { + "kyb_sub_session_deleted": { "type": "boolean", - "default": false + "description": "Company-party counterpart of `kyc_session_deleted`." }, - "action": { - "$ref": "#/components/schemas/CaseActionActionEnum" + "verification_workflow_type": { + "type": "string", + "nullable": true, + "enum": [ + "kyc", + "kyb", + "adaptive_age_verification", + "biometric_authentication", + "address_verification", + "questionnaire_verification", + "email_verification", + "phone_verification", + null + ], + "description": "Type of the workflow the child session runs." }, - "status": { + "verification_workflow_id": { "type": "string", - "nullable": true + "format": "uuid", + "nullable": true, + "description": "Workflow the child session runs." }, - "tags": { + "verification_workflow_label": { + "type": "string", + "nullable": true, + "description": "Label of that workflow." + }, + "verification_workflow_features": { "type": "array", + "nullable": true, + "description": "Features of the child session's workflow with their current status.", "items": { - "type": "string", - "minLength": 1 + "type": "object", + "required": [ + "feature", + "status" + ], + "properties": { + "feature": { + "type": "string", + "description": "Feature code, for example `ID_VERIFICATION` or `LIVENESS`." + }, + "status": { + "type": "string", + "description": "Feature status, `Not Finished` until it runs." + } + } } }, - "note": { + "didit_internal_id": { "type": "string", - "nullable": true + "format": "uuid", + "nullable": true, + "description": "Didit's identifier of the verified person, once known." } - }, - "required": [ - "action" - ] + } }, - "RoutingPublicationPaginatedPolicyRevisionList": { + "TravelRuleDelivery": { "type": "object", + "description": "Where the latest message the transfer owes the other side stands. Didit contacts the counterparty after the request that created or changed the transfer has returned, so this object says whether that message got through: queued, being sent, waiting to be retried after an error, delivered, or failed. A failure the network may still recover from (no answer in time, a failed connection, HTTP `408`, `429` or `5xx`) is tried again after 30 seconds, 2 minutes, 10 minutes, 30 minutes and 1 hour. When a first message (`kind: SEND`) fails for good, because the network refused it or the sixth attempt failed too, the transfer moves to `COUNTERPARTY_VASP_NOT_REACHABLE` with `last_error` in its `timeline`; any other message that fails for good is noted in the `timeline`. The transfer's `status` stays the source of truth for the exchange itself.", "required": [ - "count", - "results" + "kind", + "status", + "rail", + "attempts", + "last_error", + "next_attempt_at", + "delivered_at" ], "properties": { - "count": { + "kind": { + "type": "string", + "enum": [ + "SEND", + "ANSWER", + "TXID", + "CANCEL", + "POLL" + ], + "description": "What the message carries. `SEND`: the transfer's first message to the counterparty (outbound; the rail is chosen when it is sent). `ANSWER`: your accept or decline, owed to the network that asked (inbound). `TXID`: the on-chain transaction hash, after you finish the transfer. `CANCEL`: the cancellation, after you cancel it. `POLL`: a status query to a network that offers one, repeated every 5 minutes while the transfer waits on the counterparty." + }, + "status": { + "type": "string", + "enum": [ + "QUEUED", + "SENDING", + "RETRYING", + "DELIVERED", + "FAILED" + ], + "description": "`QUEUED`: waiting to be sent. `SENDING`: an attempt is running. `RETRYING`: an attempt failed and the next one runs at `next_attempt_at`. `DELIVERED`: the other side took the message. `FAILED`: the other side refused it, or the last attempt failed." + }, + "rail": { + "type": "string", + "enum": [ + "INTERNAL", + "TRP", + "GTR", + "TRUST", + "VERIFYVASP", + "SYGNA", + "EMAIL", + "" + ], + "description": "The rail that carries the message. Empty until Didit has chosen one for a first message." + }, + "attempts": { "type": "integer", - "example": 123 + "minimum": 0, + "description": "Attempts made so far for this message. A message that is not a status poll is attempted at most 6 times." }, - "next": { + "last_error": { "type": "string", - "nullable": true, - "format": "uri", - "example": "http://api.example.org/accounts/?offset=400&limit=100" + "description": "The kind of failure the last attempt hit, `\"\"` when none did: for example `email undeliverable`, `trp identity endpoint unreachable` or `trp transfer inquiry delivery failed`. It never carries the counterparty's own answer, which stays on the transfer's `timeline`.\nWhen GTR answers with an error, its own status code and message are included. Response bodies are never included.", + "example": "email undeliverable" }, - "previous": { + "next_attempt_at": { "type": "string", + "format": "date-time", "nullable": true, - "format": "uri", - "example": "http://api.example.org/accounts/?offset=200&limit=100" + "description": "When the next attempt runs while the message is `QUEUED` or `RETRYING`." }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RoutingPublicationPolicyRevision" - } + "delivered_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the other side took the message." } } }, - "FraudRecommendationEnum": { - "enum": [ - "approve", - "review", - "reject" - ], - "type": "string", - "description": "* `approve` - Approve\n* `review` - Review\n* `reject` - Reject" - }, - "CustomerMessagingCaseRequestPreviewResponse": { + "TravelRuleDiscoveryStep": { "type": "object", + "description": "One step of the counterparty discovery Didit ran for an outbound transfer, in the order it ran them. The first step that names a VASP with a route ends the chain.", "properties": { - "request_id": { + "step": { "type": "string", - "format": "uuid" + "enum": [ + "internal_address_book", + "wallet_attribution", + "network_lookup", + "customer_hint", + "end_user" + ], + "description": "`internal_address_book`: another Didit customer that proved control of the destination wallet. `wallet_attribution`: the wallet's blockchain analytics attribution. `network_lookup`: a network that can say which VASP holds the address. `customer_hint`: the VASP name, LEI or jurisdiction you sent in `beneficiary_data`. `end_user`: the counterparty the end user (or you) picked, or the request to pick one." }, - "expires_at": { + "outcome": { "type": "string", - "format": "date-time" + "enum": [ + "match", + "miss", + "skipped" + ], + "description": "`match`: the step named a counterparty. `miss`: it ran and found none. `skipped`: it could not run, and `reason` says why." }, - "recipient": { + "reason": { "type": "string", - "format": "email" - }, - "sender": { - "type": "string" - }, - "subject": { - "type": "string" - }, - "html": { - "type": "string" - }, - "requires_user_action": { - "type": "boolean" + "description": "Machine-readable reason, `\"\"` on a match: `no_proven_internal_wallet`, `attribution_disabled`, `already_attributed`, `no_attributed_entity`, `directory_match_no_route`, `gtr_address_lookup_runs_on_dispatch`, `no_membership`, `network_switched_off`, `no_network_address_lookup`, `no_directory_match`, `awaiting_end_user` or `picked_member_without_proven_wallet`." }, - "preview_token": { - "type": "string" - } - }, - "required": [ - "expires_at", - "html", - "preview_token", - "recipient", - "request_id", - "requires_user_action", - "sender", - "subject" - ] - }, - "CustomerMessagingCaseCustomerResponseRequest": { - "type": "object", - "properties": { - "full_name": { + "vasp": { "type": "string", - "minLength": 1, - "maxLength": 512 + "format": "uuid", + "nullable": true, + "description": "The directory VASP the step named, when it named one." }, - "date_of_birth": { + "rail": { "type": "string", - "format": "date" + "nullable": true, + "description": "The rail that reaches that counterparty, when the step found one." }, - "file": { + "at": { "type": "string", - "format": "binary", - "writeOnly": true + "format": "date-time" } } }, - "CaseCollaborationCaseNoteReply": { + "TravelRuleNextAction": { "type": "object", + "description": "One thing that moves a `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE` transfer on. See [Why a transfer is stuck](/transaction-monitoring/travel-rule#why-a-transfer-is-stuck).", "properties": { - "uuid": { - "type": "string", - "format": "uuid" - }, - "actor_name": { - "type": "string", - "nullable": true - }, - "actor_email": { + "action": { "type": "string", - "format": "email", - "nullable": true + "enum": [ + "select_counterparty_vasp", + "declare_self_hosted", + "add_vasp_hint", + "update_compliance_email", + "resend" + ], + "description": "`select_counterparty_vasp`: the end user picks the destination VASP in the wallet-ownership widget. `declare_self_hosted`: the end user declares the wallet self-hosted in the widget. `add_vasp_hint`: tell Didit who the counterparty is, with a VASP name, LEI or travel address (for example a `travel_address` on the destination wallet's address-book entry), then resend. `update_compliance_email` and `resend` are `action` values of `PATCH /v3/transactions/{transaction_id}/travel-rule/`: give the counterparty's compliance email again, and re-run routing." }, - "comment": { + "url": { "type": "string", - "maxLength": 300 + "format": "uri", + "description": "The wallet-ownership widget link, on `select_counterparty_vasp` and `declare_self_hosted` while the transfer has an open widget session." } }, "required": [ - "actor_email", - "actor_name", - "comment", - "uuid" + "action" ] }, - "FraudCheckModuleReport": { + "TransactionTravelRule": { "type": "object", + "description": "A transaction's Travel Rule transfer, as `GET /v3/transactions/{transaction_id}/`, the `201` of `POST /v3/transactions/` and each row of `GET /v3/transactions/` return it. `direction`, `rail`, `counterparty_vasp`, `delivery`, `discovery`, `reason` and `next_actions` have the same shape as on `PATCH /v3/transactions/{transaction_id}/travel-rule/`, so one read of a transaction says why its transfer is stuck and what moves it on.", "properties": { - "module": { - "$ref": "#/components/schemas/FraudExcludeEnum" + "uuid": { + "type": "string", + "format": "uuid" }, "status": { - "$ref": "#/components/schemas/FraudStatusEnum" + "type": "string" }, - "reason": { + "protocol": { "type": "string", "nullable": true }, - "latency_ms": { + "required": { + "type": "boolean" + }, + "obligations_count": { "type": "integer" }, - "freshness": { - "type": "string", - "nullable": true + "originator_data": { + "type": "object", + "description": "Originator party. For an inbound transfer the counterparty's values are masked until ownership is confirmed." }, - "billable_units": { - "type": "integer" - } - }, - "required": [ - "billable_units", - "freshness", - "latency_ms", - "module", - "reason", - "status" - ] - }, - "CaseDuplicateEntityTypeEnum": { - "enum": [ - "session", - "business_session", - "transaction" - ], - "type": "string", - "description": "* `session` - Session\n* `business_session` - Business Session\n* `transaction` - Transaction" - }, - "FraudClientCredentialsErrorMessage": { - "type": "object", - "properties": { - "detail": { - "type": "string", - "default": "Authentication failed: Invalid client access token." - } - } - }, - "FraudAvailabilityEnum": { - "enum": [ - "available", - "not_requested", - "no_reference", - "insufficient_history", - "unsupported_region", - "source_unavailable" - ], - "type": "string", - "description": "* `available` - Available\n* `not_requested` - Not Requested\n* `no_reference` - No Reference\n* `insufficient_history` - Insufficient History\n* `unsupported_region` - Unsupported Region\n* `source_unavailable` - Source Unavailable" - }, - "CasePublicationPolicyDraftResponse": { - "type": "object", - "properties": { - "draft": { - "allOf": [ - { - "$ref": "#/components/schemas/CasePublicationPolicyDraft" - } - ], - "nullable": true - } - }, - "required": [ - "draft" - ] - }, - "FraudCheckAttributesRequest": { - "type": "object", - "description": "Corroborating attributes. All optional; they widen module coverage.", - "properties": { - "first_name": { - "type": "string", - "maxLength": 128 + "beneficiary_data": { + "type": "object", + "description": "Beneficiary party. For an inbound transfer the counterparty's values are masked until ownership is confirmed." }, - "last_name": { - "type": "string", - "maxLength": 128 + "name_match": { + "type": "object", + "nullable": true, + "description": "The name comparison that decided the transfer (score and strictness). Null until one ran. The compared names are masked together with the parties." }, - "full_name": { - "type": "string", - "maxLength": 256 + "data_match": { + "$ref": "#/components/schemas/TravelRuleDataMatch" }, - "date_of_birth": { - "type": "string", - "format": "date" + "mismatched_fields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Shortcut for `data_match.mismatched_fields`. Empty when nothing differs." }, - "nationality": { + "data_unmasked": { + "$ref": "#/components/schemas/TravelRuleDataUnmasked" + }, + "data_masked": { + "type": "boolean", + "description": "True while the counterparty's personal data is masked in this response." + }, + "metadata": { + "type": "object" + }, + "direction": { "type": "string", - "maxLength": 3 + "enum": [ + "INBOUND", + "OUTBOUND" + ], + "description": "Which side of the exchange this transfer is." }, - "gender": { + "rail": { "type": "string", - "maxLength": 16 + "nullable": true, + "description": "The rail the transfer went over: `INTERNAL`, `TRP`, `GTR`, `TRUST`, `VERIFYVASP`, `SYGNA` or `EMAIL` (`TEST_VASP` on a sandbox application, when the Didit Test VASP answered), or `null` until a rail is chosen." + }, + "counterparty_vasp": { + "type": "object", + "nullable": true, + "properties": { + "name": { + "type": "string" + }, + "lei": { + "type": "string" + }, + "dd_score": { + "type": "integer", + "description": "Due-diligence score from 0 to 100, where higher is better. Same scale as in the VASP directory." + }, + "dd_score_breakdown": { + "type": "object", + "description": "Per-component contributions to dd_score, one object per component as in the VASP directory (jurisdiction, verification, protocol, lei, history, essentials, responsiveness), each {points, max, reason, detail}. {} until the counterparty has been assessed.", + "additionalProperties": { + "type": "object", + "properties": { + "points": { + "type": "number", + "description": "Points awarded for this component." + }, + "max": { + "type": "integer", + "description": "Maximum points this component can award." + }, + "reason": { + "type": "string", + "description": "Machine-readable code of the component's verdict." + }, + "detail": { + "type": "string", + "description": "Human-readable reason for the awarded points." + } + } + } + }, + "dd_assessed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the due-diligence assessment last ran. Null until the counterparty has been assessed." + } + }, + "description": "The counterparty's directory entry, or `null` when the transfer links none." + }, + "delivery": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/TravelRuleDelivery" + } + ], + "description": "The newest message the transfer owes another party. `null` when it has owed none, for example below `threshold_amount` or for a declared self-hosted wallet." + }, + "discovery": { + "type": "array", + "description": "How Didit looked for the counterparty, one entry per step that ran. Empty when no discovery ran, for example on an inbound transfer.", + "items": { + "$ref": "#/components/schemas/TravelRuleDiscoveryStep" + } }, - "address": { + "reason": { "type": "string", - "maxLength": 512 + "description": "Why a `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE` transfer is stuck; `\"\"` for any other status. A failed delivery decides it first, on `COUNTERPARTY_VASP_NOT_REACHABLE` only: `email_undeliverable` (the mail provider refused the compliance email and no address is left to try: when it was sent, on a reminder, or as a permanent bounce reported later) or `delivery_failed` (the rail that was delivering failed for good: a network rail refused the message or ran out of retries, a `TRP` identity host did not answer, or the counterparty's `TRP` host did not accept the inquiry). Otherwise the `discovery` trace decides it: `awaiting_end_user` (no step named a counterparty Didit can reach), `directory_match_no_route` (a step named a directory VASP that has no route), `picked_member_without_proven_wallet` (the end user picked a Didit customer that has not proven control of the wallet), or `no_directory_match` when the trace gives no other reason. A resend moves the transfer on, so the reason clears. See [Why a transfer is stuck](/transaction-monitoring/travel-rule#why-a-transfer-is-stuck)." }, - "city": { - "type": "string", - "maxLength": 128 + "next_actions": { + "type": "array", + "description": "What moves the transfer on; `[]` for any status but `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE`. `email_undeliverable` offers `update_compliance_email`, then `resend`. `delivery_failed` offers `resend`. A discovery reason offers `select_counterparty_vasp`, `declare_self_hosted`, `add_vasp_hint` and `resend`.", + "items": { + "$ref": "#/components/schemas/TravelRuleNextAction" + } }, - "state": { - "type": "string", - "maxLength": 128 + "chain_details": { + "$ref": "#/components/schemas/TravelRuleChainDetails" + } + } + }, + "NetworkListCounts": { + "type": "object", + "description": "Network counts for the list's views and filter options, each network counted once. Returned only when `summary` is not `false`. `scopes` count every network visible to the application. `views` count the visible networks inside the selected `scope`. `facets` count the networks in the selected `scope` and `status` view (dismissed networks are left out unless `status` includes them). The other list filters do not change these counts.", + "properties": { + "scopes": { + "type": "object", + "description": "Visible networks by product.", + "properties": { + "all": { + "type": "integer" + }, + "identity": { + "type": "integer", + "description": "Every other network." + }, + "transactions": { + "type": "integer", + "description": "Networks whose members are all transaction parties." + } + } }, - "postal_code": { - "type": "string", - "maxLength": 32 + "views": { + "type": "object", + "description": "Visible networks inside the selected `scope`, by status, plus two saved views.", + "properties": { + "all": { + "type": "integer" + }, + "active": { + "type": "integer" + }, + "in_review": { + "type": "integer" + }, + "resolved": { + "type": "integer" + }, + "dismissed": { + "type": "integer" + }, + "mine": { + "type": "integer", + "description": "Networks assigned to the calling console member. Always 0 for an API key." + }, + "high_risk": { + "type": "integer", + "description": "Networks with a risk score of 80 or more that are not dismissed." + } + } + }, + "facets": { + "type": "object", + "description": "Counts for each filter option, inside the selected `scope` and status view.", + "properties": { + "total": { + "type": "integer", + "description": "Networks in the selected `scope` and status view." + }, + "last_activity_days": { + "type": "object", + "description": "Networks with activity in the last 7, 30 and 90 days, keyed by the number of days.", + "properties": { + "7": { + "type": "integer" + }, + "30": { + "type": "integer" + }, + "90": { + "type": "integer" + } + } + }, + "risk_min": { + "type": "object", + "description": "Networks with a risk score of at least 50 and at least 80, keyed by the threshold.", + "properties": { + "50": { + "type": "integer" + }, + "80": { + "type": "integer" + } + } + }, + "signal_types": { + "type": "object", + "description": "Networks with at least one signal of each type.", + "properties": { + "ip_address": { + "type": "integer" + }, + "device": { + "type": "integer" + }, + "face": { + "type": "integer" + }, + "document_number": { + "type": "integer" + }, + "phone": { + "type": "integer" + }, + "email": { + "type": "integer" + }, + "address": { + "type": "integer" + } + } + }, + "priority": { + "type": "object", + "description": "Networks by priority. `none` counts networks without a priority.", + "properties": { + "low": { + "type": "integer" + }, + "medium": { + "type": "integer" + }, + "high": { + "type": "integer" + }, + "none": { + "type": "integer" + } + } + }, + "assignee": { + "type": "object", + "description": "Networks by assignee.", + "properties": { + "me": { + "type": "integer", + "description": "Assigned to the calling console member. Always 0 for an API key." + }, + "unassigned": { + "type": "integer", + "description": "Assigned to nobody." + }, + "users": { + "type": "array", + "description": "One entry per assignee, most networks first.", + "items": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "full_name": { + "type": "string", + "description": "The assignee's name, or their email when no name is set." + }, + "email": { + "type": "string" + }, + "count": { + "type": "integer" + } + } + } + } + } + }, + "has_case": { + "type": "integer", + "description": "Networks this application opened a case for." + }, + "min_size": { + "type": "object", + "description": "Networks with at least 10 members, keyed by the minimum size.", + "properties": { + "10": { + "type": "integer" + } + } + } + } } } }, - "NetworkNote": { + "NetworkTag": { "type": "object", - "required": [ - "uuid", - "actor_email", - "comment", - "client_reference", - "created_at" - ], + "description": "One of your application's tags on the network.", "properties": { "uuid": { "type": "string", "format": "uuid", - "readOnly": true + "description": "The tag's UUID, the same value you pass as `tag_uuid`." }, - "actor_email": { + "name": { + "type": "string" + }, + "color": { "type": "string", - "format": "email", - "readOnly": true, - "description": "Authenticated author, assigned by the server." + "description": "Hex color, for example `#6366F1`." }, - "comment": { + "source": { "type": "string", - "minLength": 1, - "maxLength": 10000 + "enum": [ + "didit", + "custom" + ], + "description": "`didit` for a Didit preset tag, `custom` for a tag your team created." }, - "client_reference": { + "added_by": { "type": "string", - "format": "uuid" + "nullable": true, + "description": "Email of the console member who added the tag. Null when an API key added it." }, - "created_at": { + "added_at": { "type": "string", - "format": "date-time", - "readOnly": true + "format": "date-time" } } }, - "CaseActionEntityTypeEnum": { - "enum": [ - "user", - "business" - ], - "type": "string", - "description": "* `user` - user\n* `business` - business" - }, - "CaseRoutingCondition": { + "NetworkOutcomes": { "type": "object", + "description": "Where the network's people and businesses stand. Each member you may see counts once, by the status of their most recent verification that reached a decision. Transaction parties are not counted.", "properties": { - "field": { - "type": "string", - "enum": [ - "source", - "priority", - "tags" - ] + "approved": { + "type": "integer", + "description": "Members whose most recent decided verification was approved." }, - "operator": { - "type": "string", - "enum": [ - "equals", - "in", - "contains" - ] + "declined": { + "type": "integer", + "description": "Members whose most recent decided verification was declined." }, - "value": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] + "in_review": { + "type": "integer", + "description": "Members whose most recent decided verification is in review." + }, + "total": { + "type": "integer", + "description": "Members with at least one verification, decided or not. A member whose verifications are all undecided (not started, in progress, expired or abandoned) counts here and in no other key." } - }, - "required": [ - "field", - "operator", - "value" - ], - "description": "Source and priority support equals or in with supported values. Tags support contains with one non-empty tag." + } }, - "RoutingPublicationPolicyRevision": { + "NetworkPatternScore": { "type": "object", + "description": "How alike one kind of the members' data is across the whole network. A pattern is not a signal: no shared value links the members.", "properties": { - "version": { - "type": "integer", - "maximum": 2147483647, - "minimum": 0 - }, - "snapshot": {}, - "actor_email": { + "type": { "type": "string", - "format": "email", + "enum": [ + "background" + ], + "description": "`background`: the mean similarity of every pair of member selfie backgrounds, with the person masked out. It reads the newest selfie of each person, at most the 50 newest, and leaves out age assurance sessions. It covers the whole network, not only the members of your applications." + }, + "score": { + "type": "number", "nullable": true, - "maxLength": 254 + "minimum": 0, + "maximum": 1, + "description": "From 0 to 1, rounded to 4 decimals. Null when `reason` is set." }, - "published_at": { - "type": "string", - "format": "date-time" - } - }, - "required": [ - "published_at", - "snapshot", - "version" - ] - }, - "CasePublicationPolicyRevision": { - "type": "object", - "properties": { - "version": { + "members": { "type": "integer", - "maximum": 2147483647, - "minimum": 0 + "nullable": true, + "description": "How many members' selfies the score compared. A selfie that cannot be read, or shows too little background, is not compared. Null while `computing`; 0 or 1 with `not_enough_selfies`." }, - "snapshot": {}, - "actor_email": { + "reason": { "type": "string", - "format": "email", + "enum": [ + "computing", + "not_enough_selfies" + ], "nullable": true, - "maxLength": 254 + "description": "Why there is no score. `computing`: the score for the current members is being computed; read the network again later. `not_enough_selfies`: fewer than two members have a usable selfie. Null when there is a score." }, - "published_at": { + "computed_at": { "type": "string", - "format": "date-time" + "format": "date-time", + "nullable": true, + "description": "When the score was computed. Null while `computing`." } - }, - "required": [ - "published_at", - "snapshot", - "version" - ] + } }, - "FraudCheckIdentityRelationship": { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/FraudResultEnum" - }, - "reference_type": { - "type": "string" - }, - "country": { - "type": "string" - }, - "availability": { - "$ref": "#/components/schemas/FraudAvailabilityEnum" + "NetworkDetailResponse": { + "description": "The network detail, plus the organization's sharing mode, whether cross-organization signals apply to it, your application's tags on the network, member outcomes and pattern scores.", + "allOf": [ + { + "$ref": "#/components/schemas/NetworkDetail" }, - "reason_codes": { - "type": "array", - "items": { - "type": "string" + { + "type": "object", + "properties": { + "sharing_mode": { + "type": "string", + "enum": [ + "off", + "private", + "global" + ], + "description": "The organization's cross-organization sharing mode. Only `global` compares networks across organizations; `private` and `off` keep them to the organization's own history.", + "example": "global" + }, + "cross_org_available": { + "type": "boolean", + "description": "Whether cross-organization signals apply to this organization. They need both the Identity Enterprise plan and the `global` sharing mode.", + "example": true + }, + "cross_org_unavailable_reason": { + "type": "string", + "enum": [ + "plan", + "sharing_mode" + ], + "nullable": true, + "description": "Why cross-organization signals do not apply, checked in this order. `plan`: the organization's Identity plan does not include the cross-organization fraud network (it needs Identity Enterprise), whatever the sharing mode. `sharing_mode`: the plan includes it, but the sharing mode is `private` or `off`. Null when `cross_org_available` is `true`.", + "example": null + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkTag" + }, + "description": "Your application's tags on the network, oldest first. Each application sees only its own tags. `fraud_tags` is separate and unchanged: the tags the network's members carry." + }, + "outcomes": { + "$ref": "#/components/schemas/NetworkOutcomes" + }, + "patterns": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkPatternScore" + }, + "description": "Pattern scores over the whole network. Today one item, `background`." + } } } - }, - "required": [ - "availability", - "country", - "reason_codes", - "reference_type", - "result" ] }, - "CasePublicationPaginatedPolicyRevisionList": { + "NetworkTagRequest": { "type": "object", "required": [ - "count", - "results" + "tag_uuid" ], "properties": { - "count": { - "type": "integer", - "example": 123 - }, - "next": { - "type": "string", - "nullable": true, - "format": "uri", - "example": "http://api.example.org/accounts/?offset=400&limit=100" - }, - "previous": { + "tag_uuid": { "type": "string", - "nullable": true, - "format": "uri", - "example": "http://api.example.org/accounts/?offset=200&limit=100" - }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CasePublicationPolicyRevision" - } + "format": "uuid", + "description": "UUID of one of your application's tags." } + }, + "example": { + "tag_uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7" } }, - "CaseDuplicateBlankEnum": { - "enum": [ - "" - ] - }, - "FraudCheckPlan": { + "NetworkTimeline": { + "title": "Default timeline (no contract)", "type": "object", + "description": "Without `contract`: the network's own events, oldest first, not paginated.", "properties": { - "profile_id": { - "type": "string" - }, - "profile_version": { - "type": "string" - }, - "modules": { + "results": { "type": "array", "items": { - "$ref": "#/components/schemas/FraudCheckPlannedModule" + "$ref": "#/components/schemas/NetworkTimelineEvent" } - }, - "estimated_price": { - "type": "string", - "nullable": true - }, - "currency": { - "type": "string", - "nullable": true - }, - "is_sandbox": { - "type": "boolean" - }, - "can_afford": { - "type": "boolean" } - }, - "required": [ - "can_afford", - "currency", - "estimated_price", - "is_sandbox", - "modules", - "profile_id", - "profile_version" - ] + } }, - "CaseReviewTeamUpdate": { + "NetworkTimelinePage": { + "title": "Typed timeline page (contract=2)", "type": "object", + "description": "With `contract=2`: one page of typed events, newest first.", "properties": { - "name": { - "type": "string", - "maxLength": 100 - }, - "description": { - "type": "string", - "maxLength": 500 - }, - "members": { + "results": { "type": "array", "items": { - "$ref": "#/components/schemas/CaseReviewTeamMember" - }, - "maxItems": 250 + "$ref": "#/components/schemas/NetworkTypedTimelineEvent" + } }, - "is_active": { - "type": "boolean" + "next_cursor": { + "type": "string", + "nullable": true, + "description": "Pass as `cursor` to read the next page. Null on the last page." } } }, - "RoutingPublicationPolicyDraft": { + "NetworkTypedTimelineEvent": { "type": "object", + "description": "One typed timeline event: the four default fields, plus its id, when Didit stored it, the member account and record it belongs to, and the detail block of its event type. The other detail blocks are null.", "properties": { - "revision": { + "id": { "type": "string", - "format": "uuid" - }, - "base_version": { - "type": "integer", - "maximum": 2147483647, - "minimum": 0 + "description": "`:`, unique within the timeline.", + "example": "session:3fa85f64-5717-4562-b3fc-2c963f66afa6" }, - "changes": {}, - "actor_email": { + "event_type": { "type": "string", - "format": "email", - "nullable": true, - "maxLength": 254 + "enum": [ + "network_detected", + "signal_observed", + "network_active", + "network_in_review", + "network_resolved", + "network_dismissed", + "verification_attempt", + "transaction", + "login", + "rule_hit", + "case_linked", + "note_added" + ] }, - "updated_at": { + "occurred_at": { "type": "string", "format": "date-time", - "readOnly": true - } - }, - "required": [ - "base_version", - "updated_at" - ] - }, - "FraudCheckRequestRequest": { - "type": "object", - "properties": { - "client_reference": { - "type": "string", - "minLength": 1, - "description": "Your own reference for this check. Doubles as the idempotency key: reusing it with the same body returns the original check without running or billing the modules again; reusing it with a different body returns 409.", - "maxLength": 255 + "description": "When it happened." }, - "profile": { + "ingested_at": { "type": "string", - "minLength": 1, - "default": "standard", - "description": "Orchestration profile id. Defaults to the Didit `standard` profile.", - "maxLength": 64 + "format": "date-time", + "description": "When Didit stored it." }, - "profile_version": { + "label": { "type": "string", - "minLength": 1, - "description": "Pin a frozen profile version. Defaults to the current version of the profile.", - "maxLength": 32 + "description": "A short English label, for example `Verification attempt 2 of 3`." }, - "subject": { - "$ref": "#/components/schemas/FraudCheckSubjectRequest" + "metadata": { + "type": "object", + "description": "For the network's own events, the same metadata as the default timeline. Empty otherwise." }, - "context": { - "$ref": "#/components/schemas/FraudCheckContextRequest" + "account": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineAccount" + } + ], + "nullable": true, + "description": "The member account. Null for the network's own events, `case_linked` and `note_added`." }, - "checks": { - "$ref": "#/components/schemas/FraudCheckModuleSelectionRequest" + "source_ref": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineSourceRef" + } + ], + "nullable": true, + "description": "The source record. Null for the network's own events and `note_added`." }, - "vendor_data": { - "type": "string", - "maxLength": 1000 + "verification_attempt": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineAttempt" + } + ], + "nullable": true, + "description": "Set on `verification_attempt` events." }, - "metadata": {} - }, - "required": [ - "subject" - ] - }, - "CaseDuplicateCaseSubjectInputRequest": { - "type": "object", - "properties": { - "type": { - "$ref": "#/components/schemas/CaseDuplicateTypeEnum" + "transaction": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineTransaction" + } + ], + "nullable": true, + "description": "Set on `transaction` events." }, - "uuid": { - "type": "string", - "format": "uuid" + "login": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineLogin" + } + ], + "nullable": true, + "description": "Set on `login` events." + }, + "rule_hit": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineRuleHit" + } + ], + "nullable": true, + "description": "Set on `rule_hit` events." + }, + "case_linked": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineCaseLinked" + } + ], + "nullable": true, + "description": "Set on `case_linked` events." + }, + "note_added": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineNoteAdded" + } + ], + "nullable": true, + "description": "Set on `note_added` events. Never returned to an API key." } - }, - "required": [ - "type", - "uuid" - ] + } }, - "CaseReviewTeamMember": { + "NetworkTimelineAccount": { "type": "object", + "description": "The member account the event belongs to.", "properties": { - "user_id": { + "node_id": { "type": "string", - "format": "uuid" + "description": "`:`, where kind is `vendor_user`, `vendor_business` or `transaction_party`.", + "example": "vendor_user:3fa85f64-5717-4562-b3fc-2c963f66afa6" }, - "availability": { + "node_type": { "type": "string", "enum": [ - "available", - "unavailable" - ], - "default": "available" - }, - "capacity": { - "type": "integer", - "minimum": 1, - "maximum": 1000, - "default": 10 + "account", + "business_account", + "transaction_party" + ] }, - "skills": { - "type": "array", - "items": { - "type": "string", - "maxLength": 64 - }, - "maxItems": 25 + "uuid": { + "type": "string", + "format": "uuid" }, - "available_from": { + "vendor_data": { "type": "string", - "format": "date-time", "nullable": true }, - "available_until": { + "display_name": { "type": "string", - "format": "date-time", "nullable": true + }, + "ownership": { + "type": "string", + "enum": [ + "owned", + "organization" + ], + "description": "`owned` when the record belongs to the application you called (for a console member, to an application they were granted). `organization` when it belongs to another application of your organization." + }, + "route": { + "$ref": "#/components/schemas/NetworkTimelineRoute" } - }, - "required": [ - "user_id" - ] + } }, - "CaseRoutingPreviewRequest": { + "NetworkTimelineSourceRef": { "type": "object", + "description": "The record the event was read from.", "properties": { - "source": { + "kind": { "type": "string", "enum": [ - "MANUAL", - "AML_SCREENING", - "TRANSACTION_RULE", - "WORKFLOW_BUILDER" + "session", + "business_session", + "transaction", + "monitoring_event", + "case" ] }, - "priority": { + "id": { + "type": "string", + "format": "uuid" + }, + "ownership": { "type": "string", "enum": [ - "LOW", - "MEDIUM", - "HIGH" + "owned", + "organization" ], - "default": "MEDIUM" + "description": "`owned` when the record belongs to the application you called (for a console member, to an application they were granted). `organization` when it belongs to another application of your organization." }, - "tags": { - "type": "array", - "items": { - "type": "string", - "maxLength": 64 - }, - "maxItems": 50 + "route": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineRoute" + } + ], + "description": "Absent for `monitoring_event` and `case`." + } + } + }, + "NetworkTimelineRoute": { + "type": "object", + "description": "Where the record opens: its kind, its UUID and the application it belongs to.", + "properties": { + "kind": { + "type": "string", + "description": "`user`, `business` or `transaction_party` for an account; `session`, `business_session` or `transaction` for a source record." }, - "rule_uuid": { + "id": { "type": "string", "format": "uuid" }, - "draft": { - "$ref": "#/components/schemas/CaseRoutingRuleCreate" + "application_id": { + "type": "string", + "format": "uuid" } - }, - "required": [ - "source" - ] - }, - "FraudExcludeEnum": { - "enum": [ - "normalization", - "email_risk", - "email_social", - "phone_social", - "phone_risk", - "ip_risk", - "database_validation", - "networks_same_org", - "networks_cross_org", - "biometric_identity" - ], - "type": "string", - "description": "* `normalization` - Normalization\n* `email_risk` - Email Risk\n* `email_social` - Email Social\n* `phone_social` - Phone Social\n* `phone_risk` - Phone Risk\n* `ip_risk` - Ip Risk\n* `database_validation` - Database Validation\n* `networks_same_org` - Networks Same Org\n* `networks_cross_org` - Networks Cross Org\n* `biometric_identity` - Biometric Identity" + } }, - "CaseDuplicateCaseLinkCreateRequest": { + "NetworkTimelineAttempt": { "type": "object", + "description": "One verification session of the account.", "properties": { - "entity_type": { - "$ref": "#/components/schemas/CaseDuplicateEntityTypeEnum" + "session_id": { + "type": "string", + "format": "uuid", + "description": "The session UUID, or the business session UUID for a company." }, - "entity_uuid": { + "session_number": { + "type": "integer", + "nullable": true + }, + "attempt": { + "type": "integer", + "description": "This session's position among the account's sessions, in creation order." + }, + "attempt_count": { + "type": "integer", + "description": "How many sessions the account has." + }, + "status": { "type": "string", - "format": "uuid" + "description": "The session status, for example `Approved` or `Declined`." + }, + "status_changed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "decline_reasons": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkTimelineDeclineReason" + }, + "description": "For a declined session only: its warnings, oldest first, once per warning. Empty otherwise." } - }, - "required": [ - "entity_type", - "entity_uuid" - ] - }, - "CaseDuplicateStatusEnum": { - "enum": [ - "OPEN", - "AWAITING_USER", - "RESOLVED" - ], - "type": "string", - "description": "* `OPEN` - Open\n* `AWAITING_USER` - Awaiting user\n* `RESOLVED` - Resolved" - }, - "FraudBandEnum": { - "enum": [ - "low", - "medium", - "high", - "unknown" - ], - "type": "string", - "description": "* `low` - Low\n* `medium` - Medium\n* `high` - High\n* `unknown` - Unknown" - }, - "CaseDuplicatePriorityEnum": { - "enum": [ - "LOW", - "MEDIUM", - "HIGH" - ], - "type": "string", - "description": "* `LOW` - Low\n* `MEDIUM` - Medium\n* `HIGH` - High" + } }, - "FraudStatusEnum": { - "enum": [ - "planned", - "completed", - "skipped", - "failed" - ], - "type": "string", - "description": "* `planned` - Planned\n* `completed` - Completed\n* `skipped` - Skipped\n* `failed` - Failed" + "NetworkTimelineDeclineReason": { + "type": "object", + "properties": { + "risk": { + "type": "string", + "description": "The warning code." + }, + "feature": { + "type": "string", + "description": "The feature that raised the warning." + }, + "short_description": { + "type": "string" + } + } }, - "FraudCheckInput": { + "NetworkTimelineTransaction": { "type": "object", + "description": "A transaction a member account took part in, dated by its transaction date.", "properties": { - "reference": { + "transaction_id": { "type": "string", - "description": "Redacted handle for the submitted identifier." + "format": "uuid" }, - "type": { - "$ref": "#/components/schemas/FraudTypeEnum" + "transaction_number": { + "type": "integer", + "nullable": true }, - "namespace": { - "type": "object", - "additionalProperties": {} + "transaction_type": { + "type": "string" }, - "quality": { + "action_type": { "type": "string" }, - "normalization": { - "type": "object", - "additionalProperties": {} + "amount": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineAmount" + } + ], + "nullable": true, + "description": "Null when the transaction has no amount." }, - "availability": { - "$ref": "#/components/schemas/FraudAvailabilityEnum" + "status": { + "type": "string" + }, + "decision_reason_code": { + "type": "string", + "nullable": true + }, + "decision_reason_label": { + "type": "string", + "nullable": true + }, + "party_role": { + "type": "string", + "description": "The account's role in the transaction: `APPLICANT`, `REMITTER`, `BENEFICIARY` or `COUNTERPARTY`." } - }, - "required": [ - "availability", - "namespace", - "normalization", - "quality", - "reference", - "type" - ] - }, - "CustomerMessagingPurposesEnum": { - "enum": [ - "identity_document", - "proof_of_address", - "business_document", - "confirm_details", - "review_in_progress" - ], - "type": "string", - "description": "* `identity_document` - identity_document\n* `proof_of_address` - proof_of_address\n* `business_document` - business_document\n* `confirm_details` - confirm_details\n* `review_in_progress` - review_in_progress" - }, - "FraudTypeEnum": { - "enum": [ - "email", - "phone", - "national_id", - "tax_number", - "passport_number", - "driving_licence_number", - "residence_permit_number", - "voter_id", - "external_id" - ], - "type": "string", - "description": "* `email` - Email\n* `phone` - Phone\n* `national_id` - National Id\n* `tax_number` - Tax Number\n* `passport_number` - Passport Number\n* `driving_licence_number` - Driving Licence Number\n* `residence_permit_number` - Residence Permit Number\n* `voter_id` - Voter Id\n* `external_id` - External Id" + } }, - "CaseDuplicateCaseSubjectInput": { + "NetworkTimelineAmount": { "type": "object", "properties": { - "type": { - "$ref": "#/components/schemas/CaseDuplicateTypeEnum" + "value": { + "type": "string", + "description": "Decimal amount as a string." }, - "uuid": { + "currency": { + "type": "string" + }, + "direction": { "type": "string", - "format": "uuid" + "nullable": true } - }, - "required": [ - "type", - "uuid" - ] + } }, - "CaseReviewTeamCreate": { + "NetworkTimelineLogin": { "type": "object", + "description": "A sign-in event you posted for a person through the events API.", "properties": { - "name": { + "action": { "type": "string", - "maxLength": 100 + "description": "`.`: `auth.login` or `security.login_succeeded` for a sign-in, `auth.login_failed` or `security.login_failed` for a failed one." }, - "description": { + "succeeded": { + "type": "boolean" + }, + "status": { "type": "string", - "maxLength": 500 + "description": "The event's status." }, - "members": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaseReviewTeamMember" - }, - "maxItems": 250 + "ip": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineIP" + } + ], + "nullable": true, + "description": "Null when the event has no IP address." }, - "is_active": { - "type": "boolean" + "device": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTimelineDevice" + } + ], + "nullable": true, + "description": "Null when the event has no device fingerprint or context." + }, + "is_first_login": { + "type": "boolean", + "description": "The account's first successful sign-in on record." + }, + "is_new_device": { + "type": "boolean", + "description": "A successful sign-in from a device none of the account's earlier successful sign-ins came from. Never true on the first sign-in." } - }, - "required": [ - "name", - "members" - ] + } }, - "CaseDuplicateCaseDuplicateQueryRequest": { + "NetworkTimelineIP": { "type": "object", "properties": { - "subject": { - "$ref": "#/components/schemas/CaseDuplicateCaseSubjectInputRequest" + "address": { + "type": "string" }, - "links": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaseDuplicateCaseLinkCreateRequest" - } + "country_code": { + "type": "string", + "nullable": true }, - "offset": { - "type": "integer", - "maximum": 1000000, - "minimum": 0, - "default": 0 + "city": { + "type": "string", + "nullable": true + }, + "connection_type": { + "type": "string", + "nullable": true, + "description": "The connection type label for the address, as free text." + }, + "is_vpn_or_tor": { + "type": "boolean" + }, + "is_data_center": { + "type": "boolean" } } }, - "CaseDuplicateResolutionEnum": { - "enum": [ - "FALSE_POSITIVE", - "VALID_THREAT" - ], - "type": "string", - "description": "* `FALSE_POSITIVE` - False positive\n* `VALID_THREAT` - Valid threat" + "NetworkTimelineDevice": { + "type": "object", + "properties": { + "fingerprint": { + "type": "string", + "nullable": true + }, + "label": { + "type": "string", + "nullable": true, + "description": "A readable device label built from the event's device context." + } + } }, - "CasePublicationPolicyDraftWriteRequest": { + "NetworkTimelineRuleHit": { "type": "object", + "description": "A live rule that matched a member's transaction or event. Test-mode matches are not included.", "properties": { - "expected_version": { - "type": "integer", - "minimum": 0 + "rule_id": { + "type": "string", + "format": "uuid" }, - "expected_revision": { + "rule_title": { + "type": "string" + }, + "rule_category": { + "type": "string" + }, + "rule_source": { + "type": "string" + }, + "severity": { "type": "string", - "format": "uuid", "nullable": true }, - "changes": { - "type": "object", - "additionalProperties": {} + "score_delta": { + "type": "integer" + }, + "status_target": { + "type": "string", + "nullable": true + }, + "action_summary": { + "type": "string", + "nullable": true } - }, - "required": [ - "changes", - "expected_revision", - "expected_version" - ] + } }, - "FraudCheckSubjectRequest": { + "NetworkTimelineCaseLinked": { "type": "object", + "description": "A case opened for the network in your organization. `source_ref.ownership` says whether it belongs to the application you called.", "properties": { - "selfie": { + "case_id": { "type": "string", - "minLength": 1, - "description": "Base64 JPEG, PNG or WebP selfie, up to 6 MB. No reference image or URL.", - "maxLength": 8388672 + "format": "uuid" }, - "identifiers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FraudCheckIdentifierRequest" - }, - "description": "One or more typed identifiers. One is enough.", - "maxItems": 10, - "minItems": 1 + "case_number": { + "type": "string", + "nullable": true }, - "attributes": { - "$ref": "#/components/schemas/FraudCheckAttributesRequest" + "status": { + "type": "string" + }, + "author_email": { + "type": "string", + "nullable": true + }, + "author_name": { + "type": "string", + "nullable": true } - }, - "required": [ - "identifiers" - ] + } }, - "RoutingPublicationRoutingCondition": { + "NetworkTimelineNoteAdded": { "type": "object", + "description": "A note left on the network. The note text is not included.", "properties": { - "field": { - "$ref": "#/components/schemas/RoutingPublicationFieldEnum" - }, - "operator": { - "$ref": "#/components/schemas/RoutingPublicationOperatorEnum" + "note_id": { + "type": "string", + "format": "uuid" }, - "value": {} - }, - "required": [ - "field", - "operator", - "value" - ] + "author_email": { + "type": "string" + } + } }, - "CaseActionCaseActionReceipt": { + "FeatureStatusOverride": { "type": "object", + "description": "One override of a check (feature) status made with `PATCH /v3/session/{sessionId}/features/{nodeId}/update-status/`: what it changes, whether it applied or still waits for the person to finish the flow, who asked and why.", + "required": [ + "id", + "node_id", + "feature_type", + "feature_model", + "previous_status", + "new_status", + "state", + "forced", + "reason", + "requested_by", + "requested_at", + "applied_at", + "cancelled_at", + "cancel_reason" + ], "properties": { - "uuid": { + "id": { "type": "string", "format": "uuid", - "readOnly": true + "description": "Identifier of the override. Pass it to the cancel endpoint.", + "example": "7c9e6679-7425-40de-944b-e07fc1f90ae7" }, - "client_reference": { + "node_id": { "type": "string", - "format": "uuid" + "description": "The workflow node the request named (`default` for a step without a node).", + "example": "feature_ocr_1" }, - "action": { + "feature_type": { "type": "string", - "maxLength": 32 + "description": "The `feature_type` the request passed, or an empty string when it passed none.", + "example": "" }, - "status": { - "$ref": "#/components/schemas/CaseActionStatusEnum" + "feature_model": { + "type": "string", + "description": "The kind of check that was overridden, for example `KYC`, `Face`, `AML` or `GeolocationCheck`.", + "example": "KYC" + }, + "previous_status": { + "type": "string", + "enum": [ + "Not Finished", + "Approved", + "Declined", + "In Review", + "Resub Requested" + ], + "description": "The check's status when the override was requested.", + "example": "In Review" + }, + "new_status": { + "type": "string", + "enum": [ + "Approved", + "Declined", + "In Review" + ], + "description": "The status the override sets.", + "example": "Approved" + }, + "state": { + "type": "string", + "enum": [ + "applied", + "pending_until_flow_finishes", + "cancelled" + ], + "description": "`applied`: the check has the new status. `pending_until_flow_finishes`: the override waits and applies when the person finishes the flow. `cancelled`: it waited and never applied (see `cancel_reason`).", + "example": "applied" + }, + "forced": { + "type": "boolean", + "description": "`true` when the override applied at once while the person was still in the flow.", + "example": false + }, + "reason": { + "type": "string", + "nullable": true, + "description": "The `comment` sent with the request, or `null`.", + "example": "Document manually verified by compliance." + }, + "requested_by": { + "type": "object", + "nullable": true, + "description": "Who requested the override, or `null` when that is not known.", + "properties": { + "email": { + "type": "string", + "format": "email", + "nullable": true, + "description": "Email of the console user, or `null` for API keys and the system.", + "example": "reviewer@example.com" + }, + "name": { + "type": "string", + "nullable": true, + "description": "Display name of the requester, or `null`.", + "example": "Alex Reviewer" + }, + "type": { + "type": "string", + "enum": [ + "CONSOLE_USER", + "API_KEY", + "SYSTEM" + ], + "description": "Who made the request: a console user, an API key, or Didit itself.", + "example": "CONSOLE_USER" + } + } }, - "result": {}, - "error": { + "requested_at": { "type": "string", - "maxLength": 255 + "format": "date-time", + "description": "When the override was requested.", + "example": "2026-10-13T09:15:00Z" }, - "actor_email": { + "applied_at": { "type": "string", - "format": "email", + "format": "date-time", "nullable": true, - "maxLength": 254 - }, - "attempts": { - "type": "integer", - "maximum": 2147483647, - "minimum": 0 + "description": "When the check took the new status, or `null` while it waits or once it is cancelled.", + "example": "2026-10-13T09:15:00Z" }, - "created_at": { + "cancelled_at": { "type": "string", "format": "date-time", - "readOnly": true + "nullable": true, + "description": "When a waiting override was cancelled, or `null`.", + "example": null }, - "updated_at": { + "cancel_reason": { "type": "string", - "format": "date-time", - "readOnly": true + "nullable": true, + "enum": [ + "cancelled_by_reviewer", + "replaced", + "check_changed", + "session_closed", + null + ], + "description": "Why a waiting override never applied, or `null`. `cancelled_by_reviewer`: cancelled with the cancel endpoint. `replaced`: a newer override on the same check replaced it. `check_changed`: the person redid the check before finishing the flow. `session_closed`: the session left the flow before the person finished it (abandoned, expired, or decided mid-flow).", + "example": null } - }, - "required": [ - "action", - "client_reference", - "created_at", - "status", - "updated_at", - "uuid" - ] + } }, - "FraudCheckContextRequest": { + "SandboxSimulateRequest": { "type": "object", + "description": "Force a sandbox session's status. Every field is optional, but at least one of `new_status`, `scenario` or `risk` is required.", "properties": { - "ip_address": { + "new_status": { "type": "string", - "minLength": 1 + "enum": [ + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned" + ], + "description": "The status to set. When you leave it out, it defaults to the default status of the risk case (`risk`, or the first risk the scenario's magic values carry), else to the scenario's default status.", + "example": "Declined" }, - "user_agent": { + "comment": { "type": "string", - "maxLength": 512 + "maxLength": 500, + "description": "Note added to the activity entry the status change records.", + "example": "Testing the decline path" }, - "country": { + "scenario": { "type": "string", - "description": "Country the subject claims to be in. Contextual only - never proof of identity ownership.", - "maxLength": 3 + "maxLength": 100, + "description": "A scenario slug from the catalog (`GET /v1/sandbox/scenarios/`). It supplies the default status and, without `risk`, the warning its magic values carry. An unknown slug returns `400`.", + "example": "decline_aml_hit" }, - "purpose": { + "risk": { "type": "string", - "maxLength": 128 + "maxLength": 128, + "description": "A risk case from the catalog. Its warning is added to the session, with a log type that follows `new_status`. An unknown value returns `400`.", + "example": "POSSIBLE_MATCH_FOUND" }, - "consent_obtained": { - "type": "boolean" + "node_id": { + "type": "string", + "maxLength": 128, + "description": "Workflow node the risk warning is attached to. Defaults to the session's current node.", + "example": "feature_aml_1" + }, + "send_webhooks": { + "type": "boolean", + "default": true, + "description": "`true` sends exactly the webhooks a real status change sends to the sandbox application's destinations: `status.updated`, plus the `user.status.updated` / `user.data.updated` (or `business.status.updated` / `business.data.updated`) events the change causes, for destinations subscribed to them. `false` changes the status without logging or sending any webhook: the missed-webhook recovery never re-sends that decision, the console still updates live, and the next status change of the session sends webhooks as usual.", + "example": true } } }, - "CustomerMessagingCaseCustomerResponseResult": { + "SandboxSimulateResponse": { "type": "object", - "properties": { - "responded_at": { - "type": "string", - "format": "date-time" - } - }, "required": [ - "responded_at" - ] - }, - "CustomerMessagingCaseRequestContact": { - "type": "object", + "session_id", + "previous_status", + "status", + "scenario", + "risk", + "send_webhooks" + ], "properties": { - "uuid": { + "session_id": { "type": "string", - "format": "uuid" + "format": "uuid", + "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }, - "email": { + "previous_status": { "type": "string", - "format": "email" + "enum": [ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User" + ], + "description": "The session's status before the simulation.", + "example": "In Progress" }, - "verified_at": { + "status": { "type": "string", - "format": "date-time" - } - }, - "required": [ - "email", - "uuid", - "verified_at" - ] - }, - "FraudClientCredentialsForbiddenErrorMessage": { - "type": "object", - "properties": { - "detail": { + "enum": [ + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned" + ], + "description": "The status the simulation set.", + "example": "Declined" + }, + "scenario": { "type": "string", - "default": "Forbidden - User doesn't have permission" + "nullable": true, + "description": "The scenario slug you passed, or `null`.", + "example": "decline_aml_hit" + }, + "risk": { + "type": "string", + "nullable": true, + "description": "The risk case applied (`risk`, or the one taken from the scenario), or `null` when none.", + "example": "POSSIBLE_MATCH_FOUND" + }, + "send_webhooks": { + "type": "boolean", + "description": "The `send_webhooks` value the simulation applied.", + "example": false } } }, - "CasePublicationAssignmentMethodEnum": { - "enum": [ - "MANUAL", - "AUTOMATIC" - ], - "type": "string", - "description": "* `MANUAL` - Manual\n* `AUTOMATIC` - Automatic" - }, - "CaseDuplicateNullEnum": { - "enum": [ - null - ] - }, - "CustomerMessagingLanguageEnum": { - "enum": [ - "en", - "es", - "pt" - ], - "type": "string", - "description": "* `en` - en\n* `es` - es\n* `pt` - pt" - }, - "CaseDuplicateCaseFraudSourceContextRequest": { + "CaseTeamSummary": { "type": "object", + "nullable": true, + "description": "The review team that owns the case, or null when no team owns it. Routing sets it when a routing rule places the case with one of its teams. Change or clear it with `PATCH .../cases/{case_uuid}/` (`team`).", "properties": { - "check_id": { - "type": "string", - "format": "uuid" - }, - "request_id": { + "uuid": { "type": "string", "format": "uuid" }, - "source_section": { - "type": "string", - "minLength": 1, - "pattern": "^[A-Z][A-Z0-9_:-]{0,63}$" + "name": { + "type": "string" } - }, - "required": [ - "check_id", - "request_id", - "source_section" - ] + } }, - "FraudCheckPlannedModule": { + "CaseChecker": { "type": "object", + "nullable": true, + "description": "The member named to approve or reject the pending resolution. Set only while a four-eyes resolution waits on a named checker. Null when nothing is pending, while the pending resolution waits on the checker queue, and once the resolution is approved, rejected, or returned for a new review because the evidence changed.", "properties": { - "module": { - "$ref": "#/components/schemas/FraudExcludeEnum" - }, - "available": { - "type": "boolean" - }, - "availability_reason": { + "member_uuid": { "type": "string", - "nullable": true - }, - "will_run": { - "type": "boolean" + "format": "uuid", + "description": "The member's user uuid: the value you pass as `checker` when you resolve the case." }, - "selection_reason": { + "name": { "type": "string", - "nullable": true + "nullable": true, + "description": "The member's full name, or null when it is not known." }, - "estimated_price": { + "email": { "type": "string", - "nullable": true + "format": "email" } - }, - "required": [ - "availability_reason", - "available", - "estimated_price", - "module", - "selection_reason", - "will_run" - ] + } }, - "CustomerMessagingCaseRequestOptions": { + "ServiceStatusFeed": { "type": "object", + "description": "Public service status: the state of each Didit service, the overall state, and the incidents that are ongoing or were resolved in the last 7 days.", + "required": [ + "status", + "updated_at", + "services", + "incidents" + ], "properties": { - "contacts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomerMessagingCaseRequestContact" - } + "status": { + "type": "string", + "enum": [ + "operational", + "degraded", + "down" + ], + "description": "The worst state of any service right now, decided by the ongoing incidents only.", + "example": "degraded" }, - "purposes": { + "updated_at": { + "type": "string", + "format": "date-time", + "description": "When Didit built this response (ISO 8601, UTC). Responses are cached for 30 seconds, so it can be up to 30 seconds old.", + "example": "2026-10-13T09:41:07.512345Z" + }, + "services": { "type": "array", + "description": "One entry per service, every service always listed.", "items": { - "$ref": "#/components/schemas/CustomerMessagingPurposesEnum" + "$ref": "#/components/schemas/ServiceStatusService" } }, - "languages": { + "incidents": { "type": "array", + "description": "Newest `started_at` first. Every ongoing incident, plus the incidents resolved in the last 7 days (at most 50 resolved incidents). Empty when nothing happened in that time.", "items": { - "$ref": "#/components/schemas/CustomerMessagingLanguagesEnum" + "$ref": "#/components/schemas/ServiceStatusIncident" } } - }, - "required": [ - "contacts", - "languages", - "purposes" - ] + } }, - "CaseCollaborationPaginatedCaseNoteList": { + "ServiceStatusService": { "type": "object", "required": [ - "count", - "results" + "key", + "name", + "status" ], "properties": { - "count": { - "type": "integer", - "example": 123 - }, - "next": { + "key": { "type": "string", - "nullable": true, - "format": "uri", - "example": "http://api.example.org/accounts/?offset=400&limit=100" + "enum": [ + "service_id_verification", + "service_liveness_face_match", + "service_aml_screening", + "service_kyb_registries", + "service_database_validation", + "service_phone_delivery", + "service_email_delivery", + "service_transaction_monitoring" + ], + "description": "Stable service key. Match on this, never on the display name. `service_id_verification`: ID verification, `service_liveness_face_match`: Liveness and face match, `service_aml_screening`: AML screening, `service_kyb_registries`: KYB registries, `service_database_validation`: Database validation, `service_phone_delivery`: Phone verification delivery, `service_email_delivery`: Email verification delivery, `service_transaction_monitoring`: Transaction monitoring.", + "example": "service_database_validation" }, - "previous": { + "name": { "type": "string", - "nullable": true, - "format": "uri", - "example": "http://api.example.org/accounts/?offset=200&limit=100" + "description": "English display name of the service.", + "example": "Database validation" }, - "results": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaseCollaborationCaseNote" - } + "status": { + "type": "string", + "enum": [ + "operational", + "degraded", + "down" + ], + "description": "`operational` when no incident is ongoing for the service, otherwise the worst state of its ongoing incidents.", + "example": "degraded" } } }, - "FraudProcessingStatusEnum": { - "enum": [ - "pending", - "running", - "completed", - "failed" - ], - "type": "string", - "description": "* `pending` - Pending\n* `running` - Running\n* `completed` - Completed\n* `failed` - Failed" - }, - "CasePublicationCaseBlueprint": { + "ServiceStatusIncident": { "type": "object", + "required": [ + "id", + "service", + "service_name", + "state", + "status", + "title", + "body", + "affected", + "country_name", + "started_at", + "resolved_at", + "duration_minutes" + ], "properties": { - "uuid": { + "id": { "type": "string", "format": "uuid", - "readOnly": true + "description": "Incident ID.", + "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }, - "name": { + "service": { "type": "string", - "maxLength": 24 + "enum": [ + "service_id_verification", + "service_liveness_face_match", + "service_aml_screening", + "service_kyb_registries", + "service_database_validation", + "service_phone_delivery", + "service_email_delivery", + "service_transaction_monitoring" + ], + "description": "Key of the affected service (see `services[].key`).", + "example": "service_database_validation" }, - "description": { + "service_name": { "type": "string", - "nullable": true - }, - "assignment_method": { - "$ref": "#/components/schemas/CasePublicationAssignmentMethodEnum" + "description": "English display name of the affected service.", + "example": "Database validation" }, - "assignee_pool": {}, - "max_active_cases": { - "type": "integer", - "maximum": 2147483647, - "minimum": 0, - "nullable": true - }, - "escalation_enabled": { - "type": "boolean" - }, - "escalation_blueprint": { + "state": { "type": "string", - "format": "uuid", - "nullable": true + "enum": [ + "degraded", + "down" + ], + "description": "How the service is affected. A resolved incident keeps the last state it had.", + "example": "degraded" }, - "escalation_blueprint_name": { + "status": { "type": "string", - "readOnly": true - }, - "transfer_enabled": { - "type": "boolean" + "enum": [ + "ongoing", + "resolved" + ], + "description": "`ongoing` until the service recovers, then `resolved`.", + "example": "ongoing" }, - "four_eyes_checker_blueprint": { + "title": { "type": "string", - "format": "uuid", - "nullable": true + "description": "English headline. To translate it, build your own text from `service`, `state` and `country_name`.", + "example": "Database validation is degraded in Brazil." }, - "four_eyes_checker_blueprint_name": { + "body": { "type": "string", - "readOnly": true - }, - "require_resolution_note": { - "type": "boolean" + "description": "English detail line: since when checks may be slow or fail, or when it recovered.", + "example": "Checks against Brazil (CPF) may be slow or fail since 08:55 UTC." }, - "content_config": {}, - "version": { - "type": "integer", - "maximum": 2147483647, - "minimum": 0 + "affected": { + "type": "string", + "description": "Countries or registries affected, for example `Brazil (CPF)`. Empty when the incident is not tied to a country or registry.", + "example": "Brazil (CPF)" }, - "default_deadline_hours": { - "type": "integer", - "maximum": 2147483647, - "minimum": 0, - "nullable": true + "country_name": { + "type": "string", + "description": "Country affected, in English. Empty when the incident is not tied to a country.", + "example": "Brazil" }, - "is_active": { - "type": "boolean" + "started_at": { + "type": "string", + "format": "date-time", + "description": "When the incident started (ISO 8601, UTC).", + "example": "2026-10-13T08:55:00Z" }, - "preset_key": { + "resolved_at": { "type": "string", + "format": "date-time", "nullable": true, - "maxLength": 64 + "description": "When the service recovered (ISO 8601, UTC). `null` while the incident is ongoing." }, - "creation_sources": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": {} - }, - "readOnly": true + "duration_minutes": { + "type": "integer", + "nullable": true, + "description": "Whole minutes from `started_at` to `resolved_at`. `null` while the incident is ongoing." + } + } + }, + "TravelRuleJoinInvitation": { + "type": "object", + "description": "What the join page shows before anything is typed.", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "The address the invitation went to." }, - "receives_escalation_from": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": {} - }, - "readOnly": true + "vasp_name": { + "type": "string" }, - "created_at": { + "source": { "type": "string", - "format": "date-time", - "readOnly": true + "description": "Where the invitation came from, for example `pickup`." }, - "updated_at": { + "expires_at": { "type": "string", "format": "date-time", - "readOnly": true + "nullable": true + }, + "prefill": { + "type": "object", + "properties": { + "vasp_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "legal_name": { + "type": "string" + }, + "lei": { + "type": "string" + }, + "jurisdiction": { + "type": "string" + }, + "domains": { + "type": "array", + "items": { + "type": "string" + } + } + } } - }, - "required": [ - "created_at", - "creation_sources", - "escalation_blueprint_name", - "four_eyes_checker_blueprint_name", - "name", - "receives_escalation_from", - "updated_at", - "uuid" - ] + } }, - "CaseRoutingRuleUpdate": { + "TravelRuleJoinRegisterRequest": { "type": "object", + "required": [ + "email", + "password" + ], "properties": { - "name": { + "email": { "type": "string", - "maxLength": 100 + "format": "email", + "maxLength": 254, + "description": "Must be the address the invitation went to." }, - "description": { + "password": { "type": "string", - "maxLength": 500 + "minLength": 8, + "maxLength": 128 + } + } + }, + "TravelRuleJoinVerifyRequest": { + "type": "object", + "required": [ + "email", + "code" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "maxLength": 254 }, - "priority": { - "type": "integer", - "minimum": 0 + "code": { + "type": "string", + "minLength": 6, + "maxLength": 6, + "description": "The 6-character code emailed after registering." + } + } + }, + "TravelRuleJoinCredentials": { + "type": "object", + "description": "The new free organization and the credentials its application calls the API with.", + "properties": { + "organization": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + } }, - "conditions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaseRoutingCondition" - }, - "maxItems": 20 + "application": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + } }, - "required_skills": { - "type": "array", - "items": { - "type": "string", - "maxLength": 64 - }, - "maxItems": 25 + "client_id": { + "type": "string" }, - "blueprint": { + "api_key": { "type": "string", - "format": "uuid" - }, - "team": { + "description": "Send as the `x-api-key` header on the workspace endpoints." + } + } + }, + "TravelRuleWorkspaceClaimRequest": { + "type": "object", + "required": [ + "vasp_id" + ], + "properties": { + "vasp_id": { "type": "string", "format": "uuid", - "nullable": true - }, - "is_active": { - "type": "boolean" + "description": "The directory entry this workspace is. Read ids from `GET /v3/travel-rule/vasps/`." + } + } + }, + "TravelRuleWorkspaceDomainConfirmRequest": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string", + "minLength": 6, + "maxLength": 6 + } + } + }, + "TravelRuleWorkspaceBillingRequest": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Only `true` is accepted: outbound billing cannot be turned back off through the API." + } + } + }, + "TravelRuleWorkspaceDomainCodeResponse": { + "type": "object", + "properties": { + "domain": { + "type": "string" }, - "expected_version": { - "type": "integer", - "minimum": 1, - "writeOnly": true, - "description": "Version opened by the editor. When supplied, stale writes return 409 without changing the rule." + "status": { + "type": "string", + "enum": [ + "unverified", + "code_sent", + "verified" + ] + }, + "txt": { + "$ref": "#/components/schemas/TravelRuleWorkspaceTxtRecord" } } }, - "CustomerMessagingCaseRequestPreviewRequest": { + "TravelRuleWorkspaceTxtRecord": { "type": "object", + "description": "The TXT record to publish for the DNS proof. Empty until a proof has been started.", "properties": { - "contact_id": { + "host": { "type": "string", - "format": "uuid" - }, - "purpose": { - "$ref": "#/components/schemas/CustomerMessagingPurposeEnum" + "example": "didit-tr-verify.example.com" }, - "language": { - "allOf": [ - { - "$ref": "#/components/schemas/CustomerMessagingLanguageEnum" - } - ], - "default": "en" + "value": { + "type": "string" } - }, - "required": [ - "contact_id", - "purpose" - ] + } }, - "CaseDuplicateCaseFraudSourceContext": { + "TravelRuleWorkspace": { "type": "object", + "description": "The free Travel Rule workspace of an application that joined free.", "properties": { - "check_id": { + "joined_free": { + "type": "boolean" + }, + "work_email": { "type": "string", - "format": "uuid" + "format": "email" }, - "request_id": { + "joined_at": { "type": "string", - "format": "uuid" + "format": "date-time", + "nullable": true }, - "source_section": { + "claim": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "unclaimed", + "pending_review", + "claimed", + "rejected" + ] + }, + "proved_by_domain": { + "type": "boolean" + }, + "claimed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "reviewed_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "review_note": { + "type": "string" + }, + "vasp_id": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "vasp_name": { + "type": "string" + } + } + }, + "prefill": { + "type": "object", + "description": "Profile values taken from the directory entry: `vasp_id`, `legal_name`, `lei`, `jurisdiction`, `compliance_email`, `domains`. Nothing is written until the claim is made." + }, + "domain": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "unverified", + "code_sent", + "verified" + ] + }, + "verified_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "txt": { + "$ref": "#/components/schemas/TravelRuleWorkspaceTxtRecord" + } + } + }, + "billing_enabled": { + "type": "boolean", + "description": "Receiving Travel Rule data is free. Sending is billed only after billing is enabled." + }, + "billing_enabled_at": { "type": "string", - "pattern": "^[A-Z][A-Z0-9_:-]{0,63}$" + "format": "date-time", + "nullable": true + }, + "didit_network": { + "type": "object", + "description": "Whether the Didit network lists this VASP and, when not, each condition it still misses." + }, + "settings": { + "$ref": "#/components/schemas/TravelRuleSettingsDetail" } - }, - "required": [ - "check_id", - "request_id", - "source_section" - ] - }, - "RoutingPublicationOperatorEnum": { - "enum": [ - "equals", - "in", - "contains" - ], - "type": "string", - "description": "* `equals` - equals\n* `in` - in\n* `contains` - contains" + } }, - "CaseRoutingPreview": { + "InvestigatorDecision": { "type": "object", + "description": "How the item left the Investigator queue. `decided_by`, `original_outcome` and `overrides` are returned by the accept and override endpoints and the report; the `investigator` block of a session decision carries the other fields.", "properties": { - "rule_uuid": { + "outcome": { "type": "string", - "format": "uuid", - "nullable": true + "enum": [ + "accepted", + "edited", + "overridden", + "decided_elsewhere", + "automated" + ], + "description": "`accepted` or `edited` took the recommendation (`edited` with a rewritten narrative), `overridden` took another action, `decided_elsewhere` is a decision taken outside the Investigator while the report was open, `automated` is a decision the Investigator took on its own." }, - "rule_name": { + "action": { + "type": "string", + "enum": [ + "approve", + "decline", + "escalate", + "request" + ] + }, + "actor_email": { "type": "string", "nullable": true }, - "rule_version": { - "type": "integer", + "actor_name": { + "type": "string", "nullable": true }, - "blueprint_uuid": { + "override_category": { "type": "string", - "format": "uuid", + "enum": [ + "missing_context", + "needs_information", + "policy_differs", + "evidence_misread", + "other", + "" + ], "nullable": true }, - "team_uuid": { + "override_reason": { "type": "string", - "format": "uuid", "nullable": true }, - "assignee_user_id": { + "final_status": { "type": "string", - "format": "uuid", + "description": "The item's status after the decision." + }, + "agreed": { + "type": "boolean", + "description": "Whether the decision matched the recommendation.", "nullable": true }, - "reason": { - "type": "string" + "decided_at": { + "type": "string", + "format": "date-time" }, - "candidates": { + "billed": { + "type": "boolean", + "description": "The review was charged." + }, + "decided_by": { + "type": "string", + "enum": [ + "investigator", + "member" + ] + }, + "original_outcome": { + "type": "object", + "description": "The decision the Investigator took on its own, unchanged by later overrides. Null for a decision the team took.", + "properties": { + "action": { + "type": "string" + }, + "final_status": { + "type": "string" + }, + "decided_at": { + "type": "string", + "format": "date-time" + } + }, + "nullable": true + }, + "overrides": { "type": "array", + "description": "Every override of a decision the Investigator took on its own, oldest first.", "items": { "type": "object", "properties": { - "user_id": { - "type": "string", - "format": "uuid" - }, - "eligible": { - "type": "boolean" + "action": { + "type": "string" }, - "active_cases": { - "type": "integer", + "actor_email": { + "type": "string", "nullable": true }, - "reason": { + "actor_name": { "type": "string", "nullable": true - } - } - } - }, - "matched_rules": { - "type": "array", - "items": { - "type": "object", - "properties": { - "uuid": { - "type": "string", - "format": "uuid" }, - "name": { + "override_category": { "type": "string" }, - "priority": { - "type": "integer" + "override_reason": { + "type": "string" + }, + "previous_status": { + "type": "string" + }, + "final_status": { + "type": "string" + }, + "agreed": { + "type": "boolean", + "nullable": true + }, + "overridden_at": { + "type": "string", + "format": "date-time" } } } } } }, - "FraudDimensionEnum": { - "enum": [ - "identity", - "contact", - "device_network", - "behavioral", - "fraud_network" - ], - "type": "string", - "description": "* `identity` - Identity\n* `contact` - Contact\n* `device_network` - Device Network\n* `behavioral` - Behavioral\n* `fraud_network` - Fraud Network" - }, - "FraudBadRequestErrorMessage": { + "InvestigatorBlock": { "type": "object", + "nullable": true, + "description": "The item's latest Investigator review, or null when it never had one. Carried by the V3 session and business session decision (and so by `status.updated` webhooks).", "properties": { - "detail": { + "run_id": { "type": "string", - "default": "Bad Request - Invalid request data" - } - } - }, - "FraudNextActionEnum": { - "enum": [ - "none", - "manual_review", - "step_up", - "resubmit_capture" - ], - "type": "string", - "description": "* `none` - None\n* `manual_review` - Manual Review\n* `step_up` - Step Up\n* `resubmit_capture` - Resubmit Capture" - }, - "RoutingPublicationPolicyDraftPreconditionRequest": { - "type": "object", - "properties": { - "expected_version": { - "type": "integer", - "minimum": 0 + "format": "uuid" }, - "expected_revision": { + "status": { "type": "string", - "format": "uuid", + "enum": [ + "running", + "completed", + "failed" + ] + }, + "stage": { + "type": "string", + "enum": [ + "shadow", + "live" + ] + }, + "queue": { + "type": "string", + "enum": [ + "kyc", + "kyb", + "aml", + "transactions", + "cases" + ] + }, + "is_open": { + "type": "boolean" + }, + "recommendation": { + "type": "string", + "enum": [ + "approve", + "decline", + "escalate", + "request" + ], + "nullable": true + }, + "recommendation_detail": { + "type": "string", + "nullable": true + }, + "confidence": { + "type": "string", + "enum": [ + "high", + "moderate", + "low" + ], + "nullable": true + }, + "summary": { + "type": "string", + "nullable": true + }, + "ready_to_accept": { + "type": "boolean" + }, + "prepared_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "automated": { + "type": "boolean", + "description": "The Investigator decided on its own, even if the decision was later overridden." + }, + "decided_by": { + "type": "string", + "enum": [ + "investigator", + "member" + ], "nullable": true + }, + "decision": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/InvestigatorDecision" + } + ], + "type": "object" } - }, - "required": [ - "expected_revision", - "expected_version" - ] + } }, - "FraudCheckRisk": { + "InvestigatorQueueRow": { "type": "object", "properties": { - "dimension": { - "$ref": "#/components/schemas/FraudDimensionEnum" + "run_id": { + "type": "string", + "format": "uuid" }, - "score": { - "type": "integer", - "nullable": true + "kind": { + "type": "string", + "enum": [ + "session", + "business_session", + "transaction", + "case" + ] }, - "band": { - "$ref": "#/components/schemas/FraudBandEnum" + "queue": { + "type": "string", + "enum": [ + "kyc", + "kyb", + "aml", + "transactions", + "cases" + ] }, - "availability": { - "$ref": "#/components/schemas/FraudAvailabilityEnum" + "stage": { + "type": "string", + "enum": [ + "shadow", + "live" + ] }, - "requested_but_unavailable": { - "type": "boolean", - "description": "True when the request supplied everything this dimension needed and the platform still could not answer it. Such a check is never recommended for approval on the strength of the dimensions that did answer." + "subject": { + "type": "object", + "description": "The item under review: `id`, `number`, `status`, `name` and, depending on the kind, `country`, `workflow`, `vendor_data`, `txn_id`, `amount`, `currency`, `direction`, `title` or `priority`.", + "additionalProperties": true }, - "reason_codes": { - "type": "array", - "items": { - "type": "string" - } + "recommendation": { + "type": "string", + "enum": [ + "approve", + "decline", + "escalate", + "request" + ] }, - "evidence_indexes": { - "type": "array", - "items": { - "type": "integer" + "recommendation_detail": { + "type": "string" + }, + "confidence": { + "type": "string", + "enum": [ + "high", + "moderate", + "low" + ] + }, + "summary": { + "type": "string" + }, + "sources_count": { + "type": "integer" + }, + "ready_to_accept": { + "type": "boolean" + }, + "guardrails": { + "description": "The locked guardrails that hold this item, if any." + }, + "waiting_since": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "due_at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "prepared_at": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, + "InvestigatorRunDetail": { + "allOf": [ + { + "$ref": "#/components/schemas/InvestigatorQueueRow" + }, + { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "is_open": { + "type": "boolean" + }, + "lookups": { + "type": "integer", + "description": "How many sections and linked records it read." + }, + "duration_ms": { + "type": "integer", + "nullable": true + }, + "agent_version": { + "type": "string" + }, + "prepared_before_opened": { + "type": "boolean", + "nullable": true + }, + "report": { + "type": "object", + "additionalProperties": true, + "description": "The report: reasoning, evidence points with their verdict (`ok`, `risk`, `look`, `explained`, `context`) and cited sources, screening hit verdicts (`same`, `not_same`, `unclear`) and suggested next steps." + }, + "trail": { + "description": "What I checked: the overview, sections and records it read, in order." + }, + "decision": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/InvestigatorDecision" + } + ], + "type": "object" + }, + "autonomy": { + "type": "object", + "additionalProperties": true, + "description": "What autonomy made of the report (`result`, `reason`, `tier`, `policy_version`, `rule`, `agreement`, `decisions`, `threshold`, `at`). Null when the queue is not automated.", + "nullable": true + } } } - }, - "required": [ - "availability", - "band", - "dimension", - "evidence_indexes", - "reason_codes", - "requested_but_unavailable", - "score" ] }, - "CaseDuplicateSourceEnum": { - "enum": [ - "MANUAL", - "AML_SCREENING", - "TRANSACTION_RULE", - "WORKFLOW_BUILDER" - ], - "type": "string", - "description": "* `MANUAL` - Manual\n* `AML_SCREENING` - AML screening\n* `TRANSACTION_RULE` - Transaction rule\n* `WORKFLOW_BUILDER` - Workflow builder" - }, - "CaseCollaborationPatchedCaseNoteUpdateRequest": { + "InvestigatorAcceptRequest": { "type": "object", "properties": { - "is_pinned": { - "type": "boolean" + "narrative": { + "type": "string", + "maxLength": 4000, + "description": "A rewritten narrative. Sending one records the decision as edited and accepted." + }, + "send_email": { + "type": "boolean", + "default": false, + "description": "Email the person about the decision." + }, + "email_address": { + "type": "string", + "format": "email", + "description": "Required when `send_email` is true." + }, + "email_language": { + "type": "string", + "default": "en", + "maxLength": 16 } } }, - "CustomerMessagingPurposeEnum": { - "enum": [ - "identity_document", - "proof_of_address", - "business_document", - "confirm_details", - "review_in_progress" - ], - "type": "string", - "description": "* `identity_document` - identity_document\n* `proof_of_address` - proof_of_address\n* `business_document` - business_document\n* `confirm_details` - confirm_details\n* `review_in_progress` - review_in_progress" - }, - "CaseDuplicateTypeEnum": { - "enum": [ - "user", - "business" - ], - "type": "string", - "description": "* `user` - user\n* `business` - business" - }, - "CaseCollaborationCaseNoteCreateRequest": { + "InvestigatorOverrideRequest": { "type": "object", + "required": [ + "action", + "category", + "reason" + ], "properties": { - "comment": { + "action": { "type": "string", - "minLength": 1, - "maxLength": 10000 + "enum": [ + "approve", + "decline", + "escalate", + "request" + ] }, - "reply_to": { + "category": { "type": "string", - "format": "uuid", - "nullable": true + "enum": [ + "missing_context", + "needs_information", + "policy_differs", + "evidence_misread", + "other" + ] }, - "client_reference": { + "reason": { "type": "string", - "format": "uuid", - "nullable": true - }, - "mentioned_emails": { - "type": "array", - "items": { - "type": "string", - "format": "email", - "minLength": 1 - } + "minLength": 10, + "maxLength": 500 }, - "is_internal": { + "send_email": { "type": "boolean", - "default": false + "default": false, + "description": "Email the person about the decision." }, - "attachments": { + "email_address": { + "type": "string", + "format": "email", + "description": "Required when `send_email` is true." + }, + "email_language": { + "type": "string", + "default": "en", + "maxLength": 16 + } + } + }, + "InvestigatorBatchAcceptRequest": { + "type": "object", + "required": [ + "run_ids" + ], + "properties": { + "run_ids": { "type": "array", + "minItems": 1, + "maxItems": 50, "items": { - "type": "object", - "additionalProperties": {} + "type": "string", + "format": "uuid" } + } + } + }, + "InvestigatorBatchAcceptResponse": { + "type": "object", + "properties": { + "accepted": { + "type": "integer" + }, + "skipped": { + "type": "integer" }, - "tags": { + "results": { "type": "array", "items": { - "type": "string", - "minLength": 1 + "type": "object", + "properties": { + "run_id": { + "type": "string", + "format": "uuid" + }, + "result": { + "type": "string", + "enum": [ + "accepted", + "skipped" + ] + }, + "reason": { + "type": "string", + "enum": [ + "not_found", + "forbidden", + "already_decided", + "not_ready", + "guardrail", + "invalid" + ] + }, + "detail": {}, + "action": { + "type": "string" + }, + "final_status": { + "type": "string" + } + } } } - }, - "required": [ - "comment" - ] - }, - "CustomerMessagingLanguagesEnum": { - "enum": [ - "en", - "es", - "pt" - ], - "type": "string", - "description": "* `en` - en\n* `es` - es\n* `pt` - pt" + } }, - "CustomerMessagingCaseRequestConfirmRequest": { + "InvestigatorSettings": { "type": "object", "properties": { - "contact_id": { + "stage": { "type": "string", - "format": "uuid" - }, - "purpose": { - "$ref": "#/components/schemas/CustomerMessagingPurposeEnum" - }, - "language": { - "allOf": [ - { - "$ref": "#/components/schemas/CustomerMessagingLanguageEnum" - } - ], - "default": "en" + "enum": [ + "none", + "shadow", + "live" + ] }, - "request_id": { + "stage_changed_at": { "type": "string", - "format": "uuid" + "format": "date-time", + "readOnly": true, + "nullable": true }, - "expires_at": { + "stage_changed_by": { "type": "string", - "format": "date-time" + "readOnly": true }, - "preview_token": { + "monthly_spend_limit": { "type": "string", - "minLength": 1, - "maxLength": 512 + "format": "decimal", + "description": "Monthly limit in your billing currency, 0.01 to 1,000,000. Null turns it off.", + "nullable": true + }, + "spend_limit": { + "type": "object", + "readOnly": true, + "properties": { + "enabled": { + "type": "boolean" + }, + "limit": { + "type": "string", + "format": "decimal", + "nullable": true + }, + "spent": { + "type": "string", + "format": "decimal" + }, + "reviews": { + "type": "integer" + }, + "reserved": { + "type": "string", + "format": "decimal", + "description": "The price of every open live report, held back because accepting it charges it." + }, + "currency": { + "type": "string" + }, + "paused": { + "type": "boolean", + "description": "At the limit: no new review starts." + } + } } - }, - "required": [ - "contact_id", - "expires_at", - "preview_token", - "purpose", - "request_id" - ] + } }, - "CaseRoutingRule": { + "TravelRuleUtxoEntry": { "type": "object", + "description": "One input or output of a UTXO transaction, as the chain recorded it.", + "required": [ + "address" + ], "properties": { - "name": { + "address": { "type": "string", - "maxLength": 100 + "maxLength": 128 }, - "description": { + "amount": { "type": "string", - "maxLength": 500 + "nullable": true, + "description": "Decimal string in the coin's own unit (BTC, not satoshis). Optional.", + "example": "0.0125" }, - "priority": { - "type": "integer", - "minimum": 0 + "txid": { + "type": "string", + "maxLength": 128, + "description": "Optional. The transaction an input spends." }, - "conditions": { + "vout": { + "type": "integer", + "nullable": true, + "minimum": 0, + "description": "Optional. The output index an input spends." + } + } + }, + "TravelRuleUtxo": { + "type": "object", + "description": "The inputs and outputs of a UTXO transfer (BTC, LTC, BCH, DOGE). Only accepted on those chains: on any other chain it is a `400`. From them Didit infers the originator wallet (the declared one, else the input address that contributed the most) and the beneficiary output (the declared one, else the only output that does not pay an input address back as change, else the one output paying the transfer amount). The result is on the transfer's `chain_details`.", + "required": [ + "outputs" + ], + "properties": { + "inputs": { "type": "array", + "maxItems": 1000, "items": { - "$ref": "#/components/schemas/CaseRoutingCondition" + "$ref": "#/components/schemas/TravelRuleUtxoEntry" }, - "maxItems": 20 + "description": "Optional. Without inputs no originator is inferred and no change output is recognised." }, - "required_skills": { + "outputs": { "type": "array", + "minItems": 1, + "maxItems": 1000, "items": { - "type": "string", - "maxLength": 64 - }, - "maxItems": 25 - }, - "blueprint": { + "$ref": "#/components/schemas/TravelRuleUtxoEntry" + } + } + } + }, + "TravelRuleChainSide": { + "type": "object", + "description": "One side's wallet as Didit resolved it from the chain data.", + "properties": { + "address": { "type": "string", - "format": "uuid" + "description": "The wallet. On a memo network the classic address (an XRP X-address is split into its classic address and its tag); on Lightning the payee node id or Lightning Address." }, - "team": { + "memo": { "type": "string", - "format": "uuid", - "nullable": true - }, - "is_active": { - "type": "boolean" + "description": "The destination tag or memo that is part of the wallet; `\"\"` for none." }, - "uuid": { + "source": { "type": "string", - "format": "uuid", - "readOnly": true - }, - "created_at": { + "enum": [ + "declared", + "x_address", + "largest_input", + "only_non_change_output", + "amount_match", + "invoice_payee", + "updated" + ], + "description": "Where the wallet came from: `declared` (your request), `x_address` (split from an XRP X-address), `largest_input` (the UTXO input address that contributed the most), `only_non_change_output` (the only UTXO output that is not change), `amount_match` (the one UTXO output paying the transfer amount), `invoice_payee` (the payee a Lightning payment request names), `updated` (set later with `action: update_originator_wallet`)." + } + } + }, + "TravelRuleChainDetails": { + "type": "object", + "description": "How the transfer's chain names its two wallets, when that is more than one address paying another. Empty (`{}`) for an account-chain transfer (EVM, Solana, Tron), where the two addresses are the wallets. Matching, routing and the address book read the wallets from here first.", + "properties": { + "chain": { "type": "string", - "format": "date-time", - "readOnly": true + "description": "Canonical chain, e.g. `BTC`, `XRP`, or `LIGHTNING` for a Lightning transfer." }, - "updated_at": { + "shape": { "type": "string", - "format": "date-time", - "readOnly": true + "enum": [ + "utxo", + "memo", + "lightning" + ] }, - "version": { - "type": "integer", - "readOnly": true + "originator": { + "$ref": "#/components/schemas/TravelRuleChainSide" }, - "blueprint_name": { - "type": "string", - "readOnly": true + "beneficiary": { + "$ref": "#/components/schemas/TravelRuleChainSide" }, - "team_name": { + "utxo": { + "type": "object", + "description": "On a UTXO transfer: the inputs and outputs as stored (amounts as decimal strings) and which outputs Didit read as what.", + "properties": { + "inputs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TravelRuleUtxoEntry" + } + }, + "outputs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TravelRuleUtxoEntry" + } + }, + "change_output_indexes": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "Outputs that pay an input address back." + }, + "beneficiary_output_index": { + "type": "integer", + "nullable": true, + "description": "The output that pays the beneficiary; null when none could be told apart." + } + } + }, + "lightning": { + "type": "object", + "description": "On a Lightning transfer: what the payment request says. Always `kind` (`bolt11`, `bolt12_offer`, `bolt12_invoice`, `bolt12_invoice_request`, `lnurl`, `lightning_address`, `node_id` or `unknown`); a BOLT11 invoice adds `network`, `payee_node_id`, `payment_hash`, `amount_msat`, `created_at` and `expires_at`; a BOLT12 string `blinded_paths` and `payee_node_id`; an LNURL or a Lightning Address its `domain`. Nothing is resolved over the network: the request is decoded only.", + "additionalProperties": true + }, + "warnings": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "declared_originator_not_an_input", + "declared_beneficiary_not_an_output", + "beneficiary_output_ambiguous", + "beneficiary_output_amount_differs" + ] + }, + "description": "UTXO inconsistencies worth a look: the declared originator is not an input, the declared beneficiary is not an output, no single beneficiary output could be told apart, or the beneficiary output pays a different amount than the transaction states." + }, + "not_applicable_reason": { "type": "string", - "nullable": true, - "readOnly": true + "enum": [ + "LIGHTNING_NO_PAYEE_IDENTIFIER", + "LIGHTNING_BOLT12_BLINDED_PAYEE", + "LIGHTNING_LNURL_NAMES_NO_PAYEE" + ], + "description": "On an outbound Lightning transfer whose payment request names no payee: why the transfer is `NOT_APPLICABLE`. `LIGHTNING_NO_PAYEE_IDENTIFIER` (the request names no payee node, or could not be decoded), `LIGHTNING_BOLT12_BLINDED_PAYEE` (the offer hides its payee behind blinded paths), `LIGHTNING_LNURL_NAMES_NO_PAYEE` (an LNURL names a service, not a payee). The same code opens the transfer's last `timeline` entry." } } }, - "FraudResultEnum": { - "enum": [ - "match", - "mismatch", - "inconclusive", - "no_reference" - ], - "type": "string", - "description": "* `match` - Match\n* `mismatch` - Mismatch\n* `inconclusive` - Inconclusive\n* `no_reference` - No Reference" - }, - "CaseRoutingRuleCreate": { + "WalletAddressImportRow": { "type": "object", + "description": "One wallet to import: by its `address`, its `address_hash`, or both. The camelCase forms of these fields are accepted too, as are `walletAddress`, `walletAddressHash`, `destinationTag` and `externalUserId`.", "properties": { - "name": { + "address": { "type": "string", - "maxLength": 100 + "maxLength": 128, + "description": "The wallet address. Alias: `walletAddress`." }, - "description": { + "address_hash": { "type": "string", - "maxLength": 500 - }, - "priority": { - "type": "integer", - "minimum": 0 - }, - "conditions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaseRoutingCondition" - }, - "maxItems": 20 + "description": "SHA-256 of the lowercased address, as 64 hex characters or 44 base64 characters. Send it alone to register a wallet without disclosing its address; sent with `address`, it must be that address's hash. Alias: `walletAddressHash`." }, - "required_skills": { - "type": "array", - "items": { - "type": "string", - "maxLength": 64 - }, - "maxItems": 25 + "chain": { + "type": "string", + "maxLength": 32 }, - "blueprint": { + "memo": { "type": "string", - "format": "uuid" + "maxLength": 256, + "description": "The destination tag or memo. XRP: a destination tag, a whole number from 0 to 4294967295 (a JSON number is accepted). XLM: a text memo of up to 28 bytes, a 64-bit id or a 32-byte hash in hex. EOS: up to 256 bytes. ATOM: up to 256 characters. HBAR: up to 100 bytes. Bitcoin, EVM, Solana, Tron, Litecoin, Bitcoin Cash, Dogecoin and Lightning wallets take no memo: one there is a `400`." }, - "team": { + "holder_name": { "type": "string", - "format": "uuid", - "nullable": true + "maxLength": 255 }, - "is_active": { - "type": "boolean" + "holder_vendor_data": { + "type": "string", + "maxLength": 255, + "description": "Your internal identifier for the holder. Alias: `externalUserId`." }, - "publish": { - "type": "boolean", - "writeOnly": true, - "description": "Set false to save a never-published draft. The record remains inactive at version 0 until the draft is explicitly published. Omitting this field preserves immediate creation for existing integrations." - } - }, - "required": [ - "name", - "conditions", - "blueprint" - ] - }, - "FraudCheckIdentifierRequest": { - "type": "object", - "properties": { - "type": { - "allOf": [ - { - "$ref": "#/components/schemas/FraudTypeEnum" - } + "entity_type": { + "type": "string", + "enum": [ + "individual", + "company" ], - "description": "Identifier type. Determines which namespace fields are required.\n\n* `email` - Email\n* `phone` - Phone\n* `national_id` - National Id\n* `tax_number` - Tax Number\n* `passport_number` - Passport Number\n* `driving_licence_number` - Driving Licence Number\n* `residence_permit_number` - Residence Permit Number\n* `voter_id` - Voter Id\n* `external_id` - External Id" + "default": "individual" }, - "value": { + "label": { "type": "string", - "minLength": 1, - "description": "The identifier value, in any customary format.", "maxLength": 255 }, - "country": { + "owner_type": { "type": "string", - "description": "ISO-3166 alpha-2 or alpha-3 issuing country. Required for government identifier types: the same digits mean different people in different countries.", - "maxLength": 3 + "enum": [ + "customer", + "internal", + "vasp" + ], + "default": "customer" }, - "issuer": { + "travel_address": { "type": "string", - "description": "Optional issuing authority or sub-national issuer, when the country alone is not unique.", - "maxLength": 128 + "maxLength": 512 + }, + "self_declared": { + "type": "boolean", + "default": false, + "description": "Create the entry already verified through a self-declaration, when your settings allow it." } - }, - "required": [ - "type", - "value" - ] - }, - "CaseActionActionEnum": { - "enum": [ - "set_status", - "add_tags", - "add_note", - "add_to_blocklist" - ], - "type": "string", - "description": "* `set_status` - set_status\n* `add_tags` - add_tags\n* `add_note` - add_note\n* `add_to_blocklist` - add_to_blocklist" + } }, - "CaseActionCaseActionReceiptPage": { + "WalletAddressImportReport": { "type": "object", + "description": "What happened to each row of an import, in the order sent.", "properties": { - "count": { + "total": { "type": "integer" }, - "next_offset": { - "type": "integer", - "nullable": true + "created": { + "type": "integer" }, - "results": { + "existing": { + "type": "integer" + }, + "duplicate": { + "type": "integer" + }, + "failed": { + "type": "integer" + }, + "rows": { "type": "array", "items": { - "$ref": "#/components/schemas/CaseActionCaseActionReceipt" + "type": "object", + "properties": { + "row": { + "type": "integer", + "description": "The row's number, from 1." + }, + "status": { + "type": "string", + "enum": [ + "created", + "existing", + "duplicate", + "failed" + ], + "description": "`created`: added to the book. `existing`: the wallet is already in the book and was left untouched. `duplicate`: repeats an earlier row of this import. `failed`: see `errors`." + }, + "entry_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "The address-book entry the row created or found." + }, + "errors": { + "type": "object", + "description": "Field errors of a `failed` row; empty otherwise." + }, + "duplicate_of_row": { + "type": "integer", + "nullable": true, + "description": "On a `duplicate` row, the row it repeats." + } + } } } - }, - "required": [ - "count", - "next_offset", - "results" - ] + } }, - "CaseActionCaseEntityActionResult": { + "TravelRuleCounterpartyContact": { "type": "object", + "description": "A compliance address the EMAIL rail writes to for one directory entry.", "properties": { - "receipt": { - "$ref": "#/components/schemas/CaseActionCaseActionReceipt" - }, - "entity_type": { - "$ref": "#/components/schemas/CaseActionEntityTypeEnum" - }, - "entity_uuid": { + "uuid": { "type": "string", "format": "uuid" }, - "action": { - "type": "string" - }, - "previous_status": { - "type": "string" + "counterparty": { + "type": "string", + "format": "uuid", + "description": "The directory entry's `id`, as GET /v3/travel-rule/vasps/ shows it." }, - "new_status": { + "counterparty_name": { "type": "string" }, - "tags_added": { - "type": "array", - "items": { - "type": "string" - } - }, - "comment_uuid": { + "email": { "type": "string", - "format": "uuid" + "format": "email" }, - "blocklisted": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "CustomerMessagingCaseRequest": { - "type": "object", - "properties": { - "uuid": { + "name": { "type": "string", - "format": "uuid", - "readOnly": true + "description": "Optional label, for example the team name." }, - "purpose": { + "source": { "type": "string", - "readOnly": true + "enum": [ + "staff", + "application", + "learned" + ], + "description": "staff: curated by Didit. learned: an address at one of the VASP's own domains that answered a request through its response link. application: added by your application." }, - "language": { + "scope": { "type": "string", - "readOnly": true + "enum": [ + "platform", + "application" + ], + "description": "platform: curated by Didit or learned from a verified answer, used for every Didit customer, read-only for you. application: private to your application." }, - "recipient": { + "created_by": { "type": "string", - "format": "email", - "readOnly": true + "description": "Empty for platform contacts and for contacts an API key added." }, - "expires_at": { + "verified_at": { "type": "string", "format": "date-time", - "readOnly": true + "nullable": true, + "description": "When this address last answered a request through its response link." }, - "actor_email": { - "type": "string", - "format": "email", - "readOnly": true, - "nullable": true + "is_invalid": { + "type": "boolean", + "description": "true once the address bounced or complained; the EMAIL rail skips it." }, - "delivery_status": { + "invalid_reason": { "type": "string", - "readOnly": true + "enum": [ + "", + "hard_bounce", + "soft_bounce", + "spam_complaint", + "suppressed" + ] }, - "delivered_at": { + "invalid_detail": { "type": "string", - "format": "date-time", - "readOnly": true, - "nullable": true + "description": "The receiving mail server's reason, when it gave one." }, - "cancelled_at": { + "invalid_at": { "type": "string", "format": "date-time", - "readOnly": true, "nullable": true }, - "responded_at": { + "created_at": { "type": "string", - "format": "date-time", - "readOnly": true, - "nullable": true - }, - "response": { - "readOnly": true, - "nullable": true + "format": "date-time" + } + } + }, + "TravelRuleCounterpartyContactCreate": { + "type": "object", + "required": [ + "counterparty", + "email" + ], + "properties": { + "counterparty": { + "type": "string", + "format": "uuid", + "description": "The directory entry's `id`." }, - "has_attachment": { - "type": "boolean", - "readOnly": true + "email": { + "type": "string", + "format": "email", + "maxLength": 254 }, - "created_at": { + "name": { "type": "string", - "format": "date-time", - "readOnly": true + "description": "Optional label.", + "maxLength": 255 } - }, - "required": [ - "actor_email", - "cancelled_at", - "created_at", - "delivered_at", - "delivery_status", - "expires_at", - "has_attachment", - "language", - "purpose", - "recipient", - "responded_at", - "response", - "uuid" - ] + } }, - "CustomerMessagingCaseRequestPublic": { + "TravelRuleNameMatch": { "type": "object", + "nullable": true, + "description": "The name comparison that decided the transfer, or null until one ran. While the parties are masked, and on the other customer's copy of an INTERNAL exchange, `transfer_name`, `compared_name`, `matched_names` and `mismatched_names` are hidden. `score` and `matched` can disagree: `matched` follows the strictness, `score` is the similarity of the two full names.", "properties": { - "request_id": { - "type": "string", - "format": "uuid" + "score": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "description": "Similarity of the two full names, 0 to 100. 0 when either name is blank." }, - "purpose": { - "$ref": "#/components/schemas/CustomerMessagingPurposeEnum" + "matched": { + "type": "boolean", + "description": "Whether the names satisfy `strictness`. The transfer's outcome follows `data_match`, which compares every field, so a transfer can be COUNTERPARTY_MISMATCHED_DATA while this is true." }, - "language": { - "$ref": "#/components/schemas/CustomerMessagingLanguageEnum" + "method": { + "type": "string", + "enum": [ + "none", + "exact_token", + "token_subset", + "token_containment", + "sequence_ratio" + ], + "description": "How `matched` was decided: none (`NONE`), exact_token (`STRICT`), token_subset (`DEFAULT`), token_containment (`CONTAINS`) or sequence_ratio (`FUZZY`)." + }, + "strictness": { + "type": "string", + "enum": [ + "NONE", + "STRICT", + "CONTAINS", + "DEFAULT", + "FUZZY" + ] }, - "expires_at": { + "compared_with": { "type": "string", - "format": "date-time" + "enum": [ + "wallet_holder", + "counterparty_vasp" + ], + "description": "wallet_holder: the holder recorded for the wallet. counterparty_vasp: the name the counterparty answered on the email response page, or the Didit Test VASP's answer in sandbox." }, - "responded_at": { + "transfer_name": { "type": "string", - "format": "date-time", - "nullable": true - }, - "copy": { - "type": "object", - "additionalProperties": {} + "description": "The name on the transfer." }, - "organization_name": { - "type": "string" + "compared_name": { + "type": "string", + "description": "The name it was compared with." }, - "is_white_label": { - "type": "boolean" + "matched_names": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Words of `transfer_name` found in `compared_name`." }, - "customization": { - "type": "object", - "additionalProperties": {}, - "nullable": true + "mismatched_names": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Words of `transfer_name` missing from `compared_name`, then words of `compared_name` missing from `transfer_name`." } - }, - "required": [ - "copy", - "customization", - "expires_at", - "is_white_label", - "language", - "organization_name", - "purpose", - "request_id", - "responded_at" - ] - }, - "RoutingPublicationFieldEnum": { - "enum": [ - "source", - "priority", - "tags" - ], - "type": "string", - "description": "* `source` - source\n* `priority` - priority\n* `tags` - tags" + } }, - "RoutingPublicationCaseRoutingRule": { + "TravelRuleTransferRow": { "type": "object", + "description": "One Travel Rule transfer as the transfers list shows it: VASP names, wallets and memos only, never the originator or beneficiary person data.", "properties": { - "uuid": { + "id": { "type": "string", "format": "uuid", - "readOnly": true + "description": "The transfer's id." }, - "name": { + "transaction_id": { "type": "string", - "maxLength": 100 + "format": "uuid", + "description": "The Didit transaction the transfer belongs to." }, - "description": { + "txn_id": { "type": "string", - "maxLength": 500 + "nullable": true, + "description": "Your own id for the transaction. Transfers Didit books for you, such as a Didit Test VASP inbound transfer, carry a generated one." }, - "priority": { - "type": "integer", - "maximum": 2147483647, - "minimum": 0 + "txn_date": { + "type": "string", + "format": "date-time" }, - "conditions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RoutingPublicationRoutingCondition" - } + "kind": { + "type": "string", + "enum": [ + "withdrawal", + "deposit", + "message" + ], + "description": "withdrawal: outbound. message: an inbound transfer a rail created from a counterparty's message (a TRP inquiry, the INTERNAL inbound leg, or a GTR, VerifyVASP or Sygna Bridge message). deposit: any other inbound transfer." }, - "required_skills": { - "type": "array", - "items": { - "type": "string", - "maxLength": 64 - }, - "maxItems": 25 + "direction": { + "type": "string", + "enum": [ + "INBOUND", + "OUTBOUND" + ] }, - "blueprint": { + "status": { "type": "string", - "format": "uuid" + "enum": [ + "AWAITING_COUNTERPARTY", + "ON_HOLD", + "COMPLETED", + "FINISHED", + "CANCELLED", + "EXPIRED", + "COUNTERPARTY_MISMATCHED_DATA", + "COUNTERPARTY_VASP_NOT_FOUND", + "COUNTERPARTY_VASP_NOT_REACHABLE", + "UNCONFIRMED_OWNERSHIP", + "COUNTERPARTY_VASP_GENERAL_DECLINE", + "NOT_ENOUGH_COUNTERPARTY_DATA", + "NOT_APPLICABLE" + ] }, - "blueprint_name": { + "reason": { "type": "string", - "readOnly": true + "description": "Why a `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE` transfer is stuck, the same value as the transfer's own `reason`, so the list tells a bounced compliance email from a counterparty with no route without opening each transfer. `\"\"` for any other status. See [Why a transfer is stuck](/transaction-monitoring/travel-rule#why-a-transfer-is-stuck)." }, - "team": { + "rail": { "type": "string", - "format": "uuid", - "nullable": true + "nullable": true, + "enum": [ + "INTERNAL", + "TRP", + "GTR", + "TRUST", + "VERIFYVASP", + "SYGNA", + "EMAIL", + "TEST_VASP", + null + ] }, - "team_name": { + "asset": { "type": "string", - "readOnly": true + "description": "The transaction's currency." }, - "is_active": { - "type": "boolean" + "amount": { + "type": "string", + "description": "The amount as a plain decimal, for example \"1.25\"." }, - "version": { - "type": "integer", - "readOnly": true + "originator_vasp": { + "type": "string", + "description": "Outbound: your VASP's legal name. Inbound: the counterparty's name, from its directory entry, the Didit network member, the TRP sender, the Didit Test VASP, or the name the transfer declared. Empty when none is known." }, - "created_at": { + "beneficiary_vasp": { "type": "string", - "format": "date-time", - "readOnly": true + "description": "Outbound: the counterparty's name, worked out the same way. Inbound: your VASP's legal name." }, - "updated_at": { + "originator_wallet": { + "type": "string" + }, + "beneficiary_wallet": { + "type": "string" + }, + "originator_memo": { + "type": "string", + "description": "Memo or destination tag, empty on chains without one." + }, + "beneficiary_memo": { + "type": "string" + }, + "originator_wallet_type": { + "type": "string", + "enum": [ + "vasp", + "self_hosted", + "own" + ], + "description": "vasp, self_hosted (declared unhosted) or own (a wallet your address book records as yours). Your own side is always vasp." + }, + "beneficiary_wallet_type": { + "type": "string", + "enum": [ + "vasp", + "self_hosted", + "own" + ] + }, + "matched_transaction_id": { + "type": "string", + "format": "uuid", + "nullable": true, + "description": "The other side of a deposit you matched to an announced transfer by hand." + }, + "deadline_at": { "type": "string", "format": "date-time", - "readOnly": true - } - }, - "required": [ - "blueprint", - "blueprint_name", - "conditions", - "created_at", - "name", - "team_name", - "updated_at", - "uuid", - "version" - ] - }, - "CaseActionStatusEnum": { - "enum": [ - "APPLIED", - "FAILED" - ], - "type": "string", - "description": "* `APPLIED` - Applied\n* `FAILED` - Failed" - }, - "FraudCheckModuleSelectionRequest": { - "type": "object", - "properties": { - "include": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FraudExcludeEnum" - } + "nullable": true, + "description": "When the transfer expires if it is still waiting on its counterparty. Set on every transfer you create with `POST /v3/transactions/` that needs an exchange, outbound or inbound. Null below the threshold, on a Lightning payment the Travel Rule does not apply to, and on inbound transfers Didit creates when a counterparty reaches you (an inbound TRP inquiry, a network message or the INTERNAL inbound leg)." }, - "exclude": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FraudExcludeEnum" - } + "created_at": { + "type": "string", + "format": "date-time" } } } @@ -69944,6 +103880,451 @@ } } } + }, + "parameters": { + "WebhookSignature": { + "name": "X-Signature", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "description": "Lowercase hex HMAC-SHA256, keyed with the destination's secret, of the exact raw request body." + }, + "WebhookSignatureV2": { + "name": "X-Signature-V2", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "description": "Lowercase hex HMAC-SHA256, keyed with the destination's secret, of the canonical JSON of the body (keys sorted, no whitespace, non-ASCII characters unescaped). Recommended, because it survives middleware that re-encodes the body." + }, + "WebhookSignatureSimple": { + "name": "X-Signature-Simple", + "in": "header", + "required": true, + "deprecated": true, + "schema": { + "type": "string" + }, + "description": "Lowercase hex HMAC-SHA256, keyed with the destination's secret, of `{timestamp}:{session_id}:{status}:{webhook_type}` (missing fields as empty strings). Covers the envelope only, never the payload." + }, + "WebhookTimestamp": { + "name": "X-Timestamp", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "description": "Unix time in seconds, equal to the body's `timestamp`. Reject deliveries older than a few minutes." + } + } + }, + "x-webhooks": { + "status.updated": { + "post": { + "summary": "Session status changed", + "description": "A verification session changed status. `decision` is sent only for `Approved`, `Declined`, `In Review` and `Abandoned`; it has the shape of `GET /v3/session/{sessionId}/decision/` for destinations on webhook version 3 (the default), and the older reduced shape for version 1 or 2 destinations. Business sessions always receive the version 3 business decision.", + "tags": [ + "Webhook" + ], + "parameters": [ + { + "$ref": "#/components/parameters/WebhookSignature" + }, + { + "$ref": "#/components/parameters/WebhookSignatureV2" + }, + { + "$ref": "#/components/parameters/WebhookSignatureSimple" + }, + { + "$ref": "#/components/parameters/WebhookTimestamp" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionStatusUpdatedEvent" + } + } + } + }, + "responses": { + "2XX": { + "description": "Any 2xx response acknowledges the delivery. Other responses and timeouts are retried; see https://docs.didit.me/integration/webhooks." + } + } + } + }, + "data.updated": { + "post": { + "summary": "Session data changed", + "description": "Data of a verification session changed without a status change, for example after a manual review edit, a step override or an ongoing-monitoring rescreen. `decision` is sent only for `Approved`, `Declined`, `In Review` and `Abandoned`; it has the shape of `GET /v3/session/{sessionId}/decision/` for destinations on webhook version 3 (the default), and the older reduced shape for version 1 or 2 destinations. Business sessions always receive the version 3 business decision.", + "tags": [ + "Webhook" + ], + "parameters": [ + { + "$ref": "#/components/parameters/WebhookSignature" + }, + { + "$ref": "#/components/parameters/WebhookSignatureV2" + }, + { + "$ref": "#/components/parameters/WebhookSignatureSimple" + }, + { + "$ref": "#/components/parameters/WebhookTimestamp" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionDataUpdatedEvent" + } + } + } + }, + "responses": { + "2XX": { + "description": "Any 2xx response acknowledges the delivery. Other responses and timeouts are retried; see https://docs.didit.me/integration/webhooks." + } + } + } + }, + "user.status.updated": { + "post": { + "summary": "User status changed", + "description": "A user changed status. Not sent when the user is created.", + "tags": [ + "Webhook" + ], + "parameters": [ + { + "$ref": "#/components/parameters/WebhookSignature" + }, + { + "$ref": "#/components/parameters/WebhookSignatureV2" + }, + { + "$ref": "#/components/parameters/WebhookSignatureSimple" + }, + { + "$ref": "#/components/parameters/WebhookTimestamp" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserStatusUpdatedEvent" + } + } + } + }, + "responses": { + "2XX": { + "description": "Any 2xx response acknowledges the delivery. Other responses and timeouts are retried; see https://docs.didit.me/integration/webhooks." + } + } + } + }, + "user.data.updated": { + "post": { + "summary": "User data changed", + "description": "Fields of a user changed. Not sent when the user is created. One save can produce both a status and a data event.", + "tags": [ + "Webhook" + ], + "parameters": [ + { + "$ref": "#/components/parameters/WebhookSignature" + }, + { + "$ref": "#/components/parameters/WebhookSignatureV2" + }, + { + "$ref": "#/components/parameters/WebhookSignatureSimple" + }, + { + "$ref": "#/components/parameters/WebhookTimestamp" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDataUpdatedEvent" + } + } + } + }, + "responses": { + "2XX": { + "description": "Any 2xx response acknowledges the delivery. Other responses and timeouts are retried; see https://docs.didit.me/integration/webhooks." + } + } + } + }, + "business.status.updated": { + "post": { + "summary": "Business status changed", + "description": "A business changed status. Not sent when the business is created.", + "tags": [ + "Webhook" + ], + "parameters": [ + { + "$ref": "#/components/parameters/WebhookSignature" + }, + { + "$ref": "#/components/parameters/WebhookSignatureV2" + }, + { + "$ref": "#/components/parameters/WebhookSignatureSimple" + }, + { + "$ref": "#/components/parameters/WebhookTimestamp" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessStatusUpdatedEvent" + } + } + } + }, + "responses": { + "2XX": { + "description": "Any 2xx response acknowledges the delivery. Other responses and timeouts are retried; see https://docs.didit.me/integration/webhooks." + } + } + } + }, + "business.data.updated": { + "post": { + "summary": "Business data changed", + "description": "Fields of a business changed. Not sent when the business is created. One save can produce both a status and a data event.", + "tags": [ + "Webhook" + ], + "parameters": [ + { + "$ref": "#/components/parameters/WebhookSignature" + }, + { + "$ref": "#/components/parameters/WebhookSignatureV2" + }, + { + "$ref": "#/components/parameters/WebhookSignatureSimple" + }, + { + "$ref": "#/components/parameters/WebhookTimestamp" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessDataUpdatedEvent" + } + } + } + }, + "responses": { + "2XX": { + "description": "Any 2xx response acknowledges the delivery. Other responses and timeouts are retried; see https://docs.didit.me/integration/webhooks." + } + } + } + }, + "transaction.created": { + "post": { + "summary": "Transaction created", + "description": "A transaction was created and screened. Sent for live applications only.", + "tags": [ + "Webhook" + ], + "parameters": [ + { + "$ref": "#/components/parameters/WebhookSignature" + }, + { + "$ref": "#/components/parameters/WebhookSignatureV2" + }, + { + "$ref": "#/components/parameters/WebhookSignatureSimple" + }, + { + "$ref": "#/components/parameters/WebhookTimestamp" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionCreatedEvent" + } + } + } + }, + "responses": { + "2XX": { + "description": "Any 2xx response acknowledges the delivery. Other responses and timeouts are retried; see https://docs.didit.me/integration/webhooks." + } + } + } + }, + "transaction.status.updated": { + "post": { + "summary": "Transaction status changed", + "description": "A transaction changed status, from a rule, a review or an end-user action. Sent for live applications only.", + "tags": [ + "Webhook" + ], + "parameters": [ + { + "$ref": "#/components/parameters/WebhookSignature" + }, + { + "$ref": "#/components/parameters/WebhookSignatureV2" + }, + { + "$ref": "#/components/parameters/WebhookSignatureSimple" + }, + { + "$ref": "#/components/parameters/WebhookTimestamp" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransactionStatusUpdatedEvent" + } + } + } + }, + "responses": { + "2XX": { + "description": "Any 2xx response acknowledges the delivery. Other responses and timeouts are retried; see https://docs.didit.me/integration/webhooks." + } + } + } + }, + "travel_rule.status.updated": { + "post": { + "summary": "Travel Rule status changed", + "description": "A Travel Rule exchange changed status. Sent on every transition up to and including `FINISHED`, `CANCELLED` or `EXPIRED`, never after. Sent for live applications only. There is no top-level `status`.", + "tags": [ + "Webhook" + ], + "parameters": [ + { + "$ref": "#/components/parameters/WebhookSignature" + }, + { + "$ref": "#/components/parameters/WebhookSignatureV2" + }, + { + "$ref": "#/components/parameters/WebhookSignatureSimple" + }, + { + "$ref": "#/components/parameters/WebhookTimestamp" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TravelRuleStatusUpdatedEvent" + } + } + } + }, + "responses": { + "2XX": { + "description": "Any 2xx response acknowledges the delivery. Other responses and timeouts are retried; see https://docs.didit.me/integration/webhooks." + } + } + } + }, + "workflow.compliance_changed": { + "post": { + "summary": "Workflow compliance changed", + "description": "The compliance status of a published workflow changed after a re-evaluation. One event per workflow. There is no top-level `status`.", + "tags": [ + "Webhook" + ], + "parameters": [ + { + "$ref": "#/components/parameters/WebhookSignature" + }, + { + "$ref": "#/components/parameters/WebhookSignatureV2" + }, + { + "$ref": "#/components/parameters/WebhookSignatureSimple" + }, + { + "$ref": "#/components/parameters/WebhookTimestamp" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowComplianceChangedEvent" + } + } + } + }, + "responses": { + "2XX": { + "description": "Any 2xx response acknowledges the delivery. Other responses and timeouts are retried; see https://docs.didit.me/integration/webhooks." + } + } + } + }, + "kyb.registry_search.resolved": { + "post": { + "summary": "Company search resolved (unsigned callback)", + "description": "Body of the callback sent to the `webhook_url` of `POST /v3/kyb/search/` once the registry search resolves. It is not signed: the request carries `X-Didit-Unsigned-Callback: true` and none of the signature headers, so confirm it by matching `request_id` against a search you started. Uses `event_type`, not `webhook_type`.", + "tags": [ + "Webhook" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KYBRegistrySearchResolvedCallback" + } + } + } + }, + "responses": { + "2XX": { + "description": "Any 2xx response acknowledges the delivery. Other responses and timeouts are retried; see https://docs.didit.me/integration/webhooks." + } + } + } } } } diff --git a/src/didit/resources/__init__.py b/src/didit/resources/__init__.py index c4e297c..c83bd57 100644 --- a/src/didit/resources/__init__.py +++ b/src/didit/resources/__init__.py @@ -13,8 +13,10 @@ from didit.resources.age_estimation import AgeEstimation, AsyncAgeEstimation from didit.resources.aml import Aml, AsyncAml from didit.resources.aml_monitoring import AmlMonitoring, AsyncAmlMonitoring + from didit.resources.analytics import Analytics, AsyncAnalytics from didit.resources.billing import AsyncBilling, Billing from didit.resources.biometric_templates import AsyncBiometricTemplates, BiometricTemplates + from didit.resources.blocklist import AsyncBlocklist, Blocklist from didit.resources.business_sessions import AsyncBusinessSessions, BusinessSessions from didit.resources.businesses import AsyncBusinesses, Businesses from didit.resources.cases import AsyncCases, Cases @@ -27,8 +29,11 @@ from didit.resources.fraud_checks import AsyncFraudChecks, FraudChecks from didit.resources.id_verification import AsyncIdVerification, IdVerification from didit.resources.id_wallets import AsyncIdWallets, IdWallets + from didit.resources.investigator import AsyncInvestigator, Investigator from didit.resources.ip import AsyncIp, Ip from didit.resources.kyb import AsyncKyb, Kyb + from didit.resources.lists import AsyncLists, Lists + from didit.resources.monitoring_events import AsyncMonitoringEvents, MonitoringEvents from didit.resources.networks import AsyncNetworks, Networks from didit.resources.passive_liveness import AsyncPassiveLiveness, PassiveLiveness from didit.resources.phone import AsyncPhone, Phone @@ -74,6 +79,12 @@ def aml_monitoring(self) -> AmlMonitoring: return AmlMonitoring(self) + @cached_property + def analytics(self) -> Analytics: + from didit.resources.analytics import Analytics + + return Analytics(self) + @cached_property def billing(self) -> Billing: from didit.resources.billing import Billing @@ -86,6 +97,12 @@ def biometric_templates(self) -> BiometricTemplates: return BiometricTemplates(self) + @cached_property + def blocklist(self) -> Blocklist: + from didit.resources.blocklist import Blocklist + + return Blocklist(self) + @cached_property def business_sessions(self) -> BusinessSessions: from didit.resources.business_sessions import BusinessSessions @@ -158,6 +175,12 @@ def id_wallets(self) -> IdWallets: return IdWallets(self) + @cached_property + def investigator(self) -> Investigator: + from didit.resources.investigator import Investigator + + return Investigator(self) + @cached_property def ip(self) -> Ip: from didit.resources.ip import Ip @@ -170,6 +193,18 @@ def kyb(self) -> Kyb: return Kyb(self) + @cached_property + def lists(self) -> Lists: + from didit.resources.lists import Lists + + return Lists(self) + + @cached_property + def monitoring_events(self) -> MonitoringEvents: + from didit.resources.monitoring_events import MonitoringEvents + + return MonitoringEvents(self) + @cached_property def networks(self) -> Networks: from didit.resources.networks import Networks @@ -282,6 +317,12 @@ def aml_monitoring(self) -> AsyncAmlMonitoring: return AsyncAmlMonitoring(self) + @cached_property + def analytics(self) -> AsyncAnalytics: + from didit.resources.analytics import AsyncAnalytics + + return AsyncAnalytics(self) + @cached_property def billing(self) -> AsyncBilling: from didit.resources.billing import AsyncBilling @@ -294,6 +335,12 @@ def biometric_templates(self) -> AsyncBiometricTemplates: return AsyncBiometricTemplates(self) + @cached_property + def blocklist(self) -> AsyncBlocklist: + from didit.resources.blocklist import AsyncBlocklist + + return AsyncBlocklist(self) + @cached_property def business_sessions(self) -> AsyncBusinessSessions: from didit.resources.business_sessions import AsyncBusinessSessions @@ -366,6 +413,12 @@ def id_wallets(self) -> AsyncIdWallets: return AsyncIdWallets(self) + @cached_property + def investigator(self) -> AsyncInvestigator: + from didit.resources.investigator import AsyncInvestigator + + return AsyncInvestigator(self) + @cached_property def ip(self) -> AsyncIp: from didit.resources.ip import AsyncIp @@ -378,6 +431,18 @@ def kyb(self) -> AsyncKyb: return AsyncKyb(self) + @cached_property + def lists(self) -> AsyncLists: + from didit.resources.lists import AsyncLists + + return AsyncLists(self) + + @cached_property + def monitoring_events(self) -> AsyncMonitoringEvents: + from didit.resources.monitoring_events import AsyncMonitoringEvents + + return AsyncMonitoringEvents(self) + @cached_property def networks(self) -> AsyncNetworks: from didit.resources.networks import AsyncNetworks diff --git a/src/didit/resources/account/__init__.py b/src/didit/resources/account/__init__.py index 6d27a0e..d296df1 100644 --- a/src/didit/resources/account/__init__.py +++ b/src/didit/resources/account/__init__.py @@ -16,6 +16,7 @@ to_streamed_response_wrapper, ) from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.account.resend_otp_response import AccountResendOtpResponse from didit.types.components.login_response import LoginResponse from didit.types.components.register_response import RegisterResponse from didit.types.components.verify_email_response import VerifyEmailResponse @@ -207,7 +208,7 @@ def resend_otp( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> object: + ) -> AccountResendOtpResponse: """Resend the email verification code. Resend the 6-character verification code for an unverified programmatic account. Requires the account's email and password so only the owner can trigger a new code (no console Basic Auth). A fresh code replaces the previous one and is valid for 10 minutes. @@ -225,11 +226,11 @@ def resend_otp( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "object", + "AccountResendOtpResponse", self._client.request( "post", "/programmatic/resend-otp/", - cast_to=object, + cast_to=AccountResendOtpResponse, auth="none", api="account", body={"email": email, "password": password}, @@ -400,7 +401,7 @@ async def resend_otp( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> object: + ) -> AccountResendOtpResponse: """Resend the email verification code. Resend the 6-character verification code for an unverified programmatic account. Requires the account's email and password so only the owner can trigger a new code (no console Basic Auth). A fresh code replaces the previous one and is valid for 10 minutes. @@ -418,11 +419,11 @@ async def resend_otp( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "object", + "AccountResendOtpResponse", await self._client.request( "post", "/programmatic/resend-otp/", - cast_to=object, + cast_to=AccountResendOtpResponse, auth="none", api="account", body={"email": email, "password": password}, diff --git a/src/didit/resources/account/applications.py b/src/didit/resources/account/applications.py index 2c85062..47f5394 100644 --- a/src/didit/resources/account/applications.py +++ b/src/didit/resources/account/applications.py @@ -8,6 +8,7 @@ import httpx +from didit._pagination import AsyncPaginator, SyncPage from didit._resource import AsyncAPIResource, SyncAPIResource from didit._response import ( async_to_raw_response_wrapper, @@ -17,6 +18,7 @@ ) from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query from didit.types.components.application import Application +from didit.types.components.paginated_application_list import PaginatedApplicationList __all__ = [ "AccountApplications", @@ -48,33 +50,39 @@ def list( self, org_id: str, *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> object: + ) -> SyncPage[Application]: """List the organization's applications. - List every application in the organization. Use this to discover `app_id` values when recovering credentials from a new machine - follow with `GET /organizations/me/{org_id}/applications/{app_id}/` for the API key. Requires an owner or admin Bearer token. + List the organization's applications, 50 per page by default. Use this to discover `app_id` values when recovering credentials from a new machine, then call `GET /organizations/me/{org_id}/applications/{app_id}/` for the API key. Requires an owner or admin Bearer token. ``GET /organizations/me/{org_id}/applications/`` Args: org_id: UUID of the organization. Look it up with `GET /organizations/me/`. + limit: Number of applications per page. Defaults to 50. + offset: Number of applications to skip before the page starts. Defaults to 0. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "object", - self._client.request( + "SyncPage[Application]", + self._client.request_page( "get", "/organizations/me/{org_id}/applications/", - cast_to=object, + cast_to=PaginatedApplicationList, + item_type=Application, path_params={"org_id": org_id}, auth="account_token", api="account", + query={"limit": limit, "offset": offset}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -198,7 +206,7 @@ def update( ) -> Application: """Update Application. - Update application metadata (name, URLs, redirect URIs). `api_key` and `client_id` are never rotated. Requires owner/admin JWT. + Update application metadata (name, URLs, redirect URIs). `api_key` and `client_id` are never rotated. `products` is read-only: a `products` key in the body is ignored. Requires owner/admin JWT. ``PATCH /organizations/me/{org_id}/applications/{app_id}/`` @@ -257,37 +265,43 @@ def with_streaming_response(self) -> AsyncAccountApplicationsWithStreamingRespon """Every method of this resource, returning the HTTP response before reading its body.""" return AsyncAccountApplicationsWithStreamingResponse(self) - async def list( + def list( self, org_id: str, *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> object: + ) -> AsyncPaginator[Application]: """List the organization's applications. - List every application in the organization. Use this to discover `app_id` values when recovering credentials from a new machine - follow with `GET /organizations/me/{org_id}/applications/{app_id}/` for the API key. Requires an owner or admin Bearer token. + List the organization's applications, 50 per page by default. Use this to discover `app_id` values when recovering credentials from a new machine, then call `GET /organizations/me/{org_id}/applications/{app_id}/` for the API key. Requires an owner or admin Bearer token. ``GET /organizations/me/{org_id}/applications/`` Args: org_id: UUID of the organization. Look it up with `GET /organizations/me/`. + limit: Number of applications per page. Defaults to 50. + offset: Number of applications to skip before the page starts. Defaults to 0. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "object", - await self._client.request( + "AsyncPaginator[Application]", + self._client.request_page( "get", "/organizations/me/{org_id}/applications/", - cast_to=object, + cast_to=PaginatedApplicationList, + item_type=Application, path_params={"org_id": org_id}, auth="account_token", api="account", + query={"limit": limit, "offset": offset}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -411,7 +425,7 @@ async def update( ) -> Application: """Update Application. - Update application metadata (name, URLs, redirect URIs). `api_key` and `client_id` are never rotated. Requires owner/admin JWT. + Update application metadata (name, URLs, redirect URIs). `api_key` and `client_id` are never rotated. `products` is read-only: a `products` key in the body is ignored. Requires owner/admin JWT. ``PATCH /organizations/me/{org_id}/applications/{app_id}/`` diff --git a/src/didit/resources/account/organizations.py b/src/didit/resources/account/organizations.py index 8582164..cc0638f 100644 --- a/src/didit/resources/account/organizations.py +++ b/src/didit/resources/account/organizations.py @@ -3,12 +3,12 @@ from __future__ import annotations -import builtins from functools import cached_property from typing import cast import httpx +from didit._pagination import AsyncPaginator, SyncPage from didit._resource import AsyncAPIResource, SyncAPIResource from didit._response import ( async_to_raw_response_wrapper, @@ -18,6 +18,7 @@ ) from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query from didit.types.components.organization import Organization +from didit.types.components.paginated_organization_list import PaginatedOrganizationList __all__ = [ "AccountOrganizations", @@ -48,11 +49,13 @@ def with_streaming_response(self) -> AccountOrganizationsWithStreamingResponse: def list( self, *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> builtins.list[Organization]: + ) -> SyncPage[Organization]: """List My Organizations. List organizations the authenticated user belongs to. Each entry's `uuid` is the `org_id` used on the application endpoints. @@ -60,19 +63,23 @@ def list( ``GET /organizations/me/`` Args: + limit: Page size. Defaults to 50. + offset: Zero-based offset of the first result. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "builtins.list[Organization]", - self._client.request( + "SyncPage[Organization]", + self._client.request_page( "get", "/organizations/me/", - cast_to=builtins.list[Organization], + cast_to=PaginatedOrganizationList, + item_type=Organization, auth="account_token", api="account", + query={"limit": limit, "offset": offset}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -97,14 +104,16 @@ def with_streaming_response(self) -> AsyncAccountOrganizationsWithStreamingRespo """Every method of this resource, returning the HTTP response before reading its body.""" return AsyncAccountOrganizationsWithStreamingResponse(self) - async def list( + def list( self, *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> builtins.list[Organization]: + ) -> AsyncPaginator[Organization]: """List My Organizations. List organizations the authenticated user belongs to. Each entry's `uuid` is the `org_id` used on the application endpoints. @@ -112,19 +121,23 @@ async def list( ``GET /organizations/me/`` Args: + limit: Page size. Defaults to 50. + offset: Zero-based offset of the first result. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "builtins.list[Organization]", - await self._client.request( + "AsyncPaginator[Organization]", + self._client.request_page( "get", "/organizations/me/", - cast_to=builtins.list[Organization], + cast_to=PaginatedOrganizationList, + item_type=Organization, auth="account_token", api="account", + query={"limit": limit, "offset": offset}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, diff --git a/src/didit/resources/aml.py b/src/didit/resources/aml.py index e95e515..0869f03 100644 --- a/src/didit/resources/aml.py +++ b/src/didit/resources/aml.py @@ -17,6 +17,7 @@ to_streamed_response_wrapper, ) from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.aml.retrieve_dataset_freshness_response import AmlRetrieveDatasetFreshnessResponse from didit.types.aml.screen_response import AmlScreenResponse __all__ = [ @@ -32,7 +33,7 @@ class Aml(SyncAPIResource): """The ``aml`` resource of the Didit API. - Methods: ``screen``. + Methods: ``retrieve_dataset_freshness``, ``screen``. """ @cached_property @@ -149,11 +150,49 @@ def screen( ), ) + def retrieve_dataset_freshness( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AmlRetrieveDatasetFreshnessResponse: + """Get AML dataset freshness. + + Report how current the datasets behind AML screening are, per source family (`sanctions`, `pep`, `warnings`, `adverse_media`), as audit evidence for [ongoing AML monitoring](https://docs.didit.me/core-technology/aml-screening/continuous-monitoring-aml-screening#proving-that-monitoring-ran). The answer is platform-wide: no session in the path, no customer data in the body, the same answer for every application. + + Always `200` for an accepted key. Check `availability` first: when it is not `available`, `dataset_freshness` is `null`. Dates are never estimated: a family whose datasets reported no date is absent from `sources`, and a family where only some datasets reported one has `oldest_updated_at: null`. Readings are taken while ongoing monitoring runs, not on each call; `captured_at` says when. + + A scoped API key needs `sessions: read`; a key limited to some workflows or to approved sessions cannot call this endpoint. Nothing is billed. + + ``GET /v3/aml/dataset-freshness/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AmlRetrieveDatasetFreshnessResponse", + self._client.request( + "get", + "/v3/aml/dataset-freshness/", + cast_to=AmlRetrieveDatasetFreshnessResponse, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + class AsyncAml(AsyncAPIResource): """The ``aml`` resource of the Didit API. - Methods: ``screen``. + Methods: ``retrieve_dataset_freshness``, ``screen``. """ @cached_property @@ -270,26 +309,76 @@ async def screen( ), ) + async def retrieve_dataset_freshness( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AmlRetrieveDatasetFreshnessResponse: + """Get AML dataset freshness. + + Report how current the datasets behind AML screening are, per source family (`sanctions`, `pep`, `warnings`, `adverse_media`), as audit evidence for [ongoing AML monitoring](https://docs.didit.me/core-technology/aml-screening/continuous-monitoring-aml-screening#proving-that-monitoring-ran). The answer is platform-wide: no session in the path, no customer data in the body, the same answer for every application. + + Always `200` for an accepted key. Check `availability` first: when it is not `available`, `dataset_freshness` is `null`. Dates are never estimated: a family whose datasets reported no date is absent from `sources`, and a family where only some datasets reported one has `oldest_updated_at: null`. Readings are taken while ongoing monitoring runs, not on each call; `captured_at` says when. + + A scoped API key needs `sessions: read`; a key limited to some workflows or to approved sessions cannot call this endpoint. Nothing is billed. + + ``GET /v3/aml/dataset-freshness/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AmlRetrieveDatasetFreshnessResponse", + await self._client.request( + "get", + "/v3/aml/dataset-freshness/", + cast_to=AmlRetrieveDatasetFreshnessResponse, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + class AmlWithRawResponse: def __init__(self, resource: Aml) -> None: self._resource = resource self.screen = to_raw_response_wrapper(resource.screen) + self.retrieve_dataset_freshness = to_raw_response_wrapper( + resource.retrieve_dataset_freshness + ) class AsyncAmlWithRawResponse: def __init__(self, resource: AsyncAml) -> None: self._resource = resource self.screen = async_to_raw_response_wrapper(resource.screen) + self.retrieve_dataset_freshness = async_to_raw_response_wrapper( + resource.retrieve_dataset_freshness + ) class AmlWithStreamingResponse: def __init__(self, resource: Aml) -> None: self._resource = resource self.screen = to_streamed_response_wrapper(resource.screen) + self.retrieve_dataset_freshness = to_streamed_response_wrapper( + resource.retrieve_dataset_freshness + ) class AsyncAmlWithStreamingResponse: def __init__(self, resource: AsyncAml) -> None: self._resource = resource self.screen = async_to_streamed_response_wrapper(resource.screen) + self.retrieve_dataset_freshness = async_to_streamed_response_wrapper( + resource.retrieve_dataset_freshness + ) diff --git a/src/didit/resources/analytics.py b/src/didit/resources/analytics.py new file mode 100644 index 0000000..527e3cb --- /dev/null +++ b/src/didit/resources/analytics.py @@ -0,0 +1,232 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``analytics`` resource.""" + +from __future__ import annotations + +import datetime as _dt +from functools import cached_property +from typing import Literal, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.application_analytics_summary import ApplicationAnalyticsSummary +from didit.types.components.application_analytics_widget_response import ( + ApplicationAnalyticsWidgetResponse, +) + +__all__ = [ + "Analytics", + "AsyncAnalytics", + "AnalyticsWithRawResponse", + "AsyncAnalyticsWithRawResponse", + "AnalyticsWithStreamingResponse", + "AsyncAnalyticsWithStreamingResponse", +] + + +class Analytics(SyncAPIResource): + """The ``analytics`` resource of the Didit API. + + Methods: ``retrieve``. + """ + + @cached_property + def with_raw_response(self) -> AnalyticsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AnalyticsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AnalyticsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AnalyticsWithStreamingResponse(self) + + def retrieve( + self, + *, + start_date: str | _dt.date | NotGiven = NOT_GIVEN, + end_date: str | _dt.date | NotGiven = NOT_GIVEN, + scope: Literal["home", "kyc", "kyb", "transactions"] | str | NotGiven = NOT_GIVEN, + widget_keys: str | NotGiven = NOT_GIVEN, + workflow_ids: str | NotGiven = NOT_GIVEN, + versions: str | NotGiven = NOT_GIVEN, + branch_path_ids: str | NotGiven = NOT_GIVEN, + tag: str | NotGiven = NOT_GIVEN, + transaction_type: str | NotGiven = NOT_GIVEN, + action_type: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ApplicationAnalyticsSummary | ApplicationAnalyticsWidgetResponse: + """Get application analytics. + + Read the numbers behind the Business Console [analytics dashboard](https://docs.didit.me/console/analytics) for a date range. Without `scope` and `widget_keys` the response is the user verification (KYC) summary (`ApplicationAnalyticsSummary`): daily session counts, status totals, conversion rate, the same figures for the preceding period of equal length, locations, demographics, devices, workflow funnels, active verification time, resubmissions, warnings and time series. With `scope` or `widget_keys` it is the widgets of one dashboard tab (`ApplicationAnalyticsWidgetResponse`): `kyc`, `kyb`, `transactions` or `home`. + + Session counts are by session creation date (UTC) and reflect each session's current status. They come from daily rollups refreshed in the background, so the newest activity can take a short while to appear. Invalid workflow IDs and versions are ignored rather than rejected. Nothing is billed. + + Only a full-access API key can call this endpoint: every scoped key gets `403`, whatever access it holds, and so does a key of another application. See [API keys](https://docs.didit.me/console/api-keys). + + ``GET /v3/organization/{organization_id}/application/{application_id}/analytics/`` + + Args: + start_date: First day of the range, `YYYY-MM-DD`, inclusive (UTC). Send it together with `end_date`: when either is missing, both are ignored and the range runs from 30 days before today through today. + end_date: Last day of the range, `YYYY-MM-DD`, inclusive (UTC). A future date counts as today. The range may not exceed 730 days. + scope: Dashboard tab to return widgets for (case-insensitive). Omit it, together with `widget_keys`, for the KYC summary. Sending only `widget_keys` implies `kyc`. `home` accepts any KYC, KYB or transaction widget and defaults to the application's saved home layout. + widget_keys: Comma-separated widget keys, answered in this order. Keys the scope does not accept are dropped; when none is left you get the scope's defaults. KYC: `kyc_sessions_volume`, `kyc_warning_insights`, `kyc_id_locations`, `kyc_ip_locations`, `kyc_workflow_funnel`, `kyc_average_verification_time`, `kyc_conversion_rate`, `kyc_conversion_by_country`, `kyc_resubmission_insights`, `kyc_original_status_distribution`, `kyc_database_validation_coverage`, `kyc_database_validation_latency`, `kyc_demographics`, `kyc_device_details`. KYB: `kyb_sessions_volume`, `kyb_workflow_funnel`, `kyb_average_verification_time`, `kyb_ip_locations`, `kyb_business_country_distribution`, `kyb_warning_insights`, `kyb_conversion_rate`, `kyb_conversion_by_country`, `kyb_industry_breakdown`, `kyb_resubmission_insights`, `kyb_original_status_distribution`, `kyb_registry_coverage`, `kyb_registry_data_completeness`, `kyb_device_details`. Transactions: `tx_count_total`, `tx_count_daily`, `tx_volume_total_preferred_currency`, `tx_volume_daily_preferred_currency`, `tx_average_amount`, `tx_average_amount_daily`, `tx_transaction_category_breakdown`, `tx_action_type_breakdown`, `tx_status_volume_trend`, `tx_user_ip_or_country_distribution`, `tx_tag_breakdown`, `tx_top_rules_triggered`, `tx_team_member_performance`, `tx_score_histogram`. + workflow_ids: Comma-separated workflow IDs. Limits the KYC summary and the KYC and KYB widgets to those workflows. Values that are not UUIDs are ignored; when none is valid the filter is dropped. The summary also accepts `workflow_id`. + versions: Comma-separated workflow version numbers. Applies only together with `workflow_ids`; values that are not whole numbers are ignored. `version` is accepted as an alias. + branch_path_ids: Comma-separated branch path IDs. Applies only together with `workflow_ids`, and only to KYC figures. Figures are currently recorded only for `default` (all paths combined), so any other value returns empty figures. `branch_path_id` is accepted as an alias. + tag: Transaction widgets only: comma-separated tag UUIDs or tag names. + transaction_type: Transaction widgets only: comma-separated transaction categories. + action_type: Transaction widgets only: comma-separated action types (case-insensitive). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ApplicationAnalyticsSummary | ApplicationAnalyticsWidgetResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/analytics/", + cast_to=ApplicationAnalyticsSummary | ApplicationAnalyticsWidgetResponse, + scope="application", + auth="api_key", + query={ + "start_date": start_date, + "end_date": end_date, + "scope": scope, + "widget_keys": widget_keys, + "workflow_ids": workflow_ids, + "versions": versions, + "branch_path_ids": branch_path_ids, + "tag": tag, + "transaction_type": transaction_type, + "action_type": action_type, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncAnalytics(AsyncAPIResource): + """The ``analytics`` resource of the Didit API. + + Methods: ``retrieve``. + """ + + @cached_property + def with_raw_response(self) -> AsyncAnalyticsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncAnalyticsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAnalyticsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncAnalyticsWithStreamingResponse(self) + + async def retrieve( + self, + *, + start_date: str | _dt.date | NotGiven = NOT_GIVEN, + end_date: str | _dt.date | NotGiven = NOT_GIVEN, + scope: Literal["home", "kyc", "kyb", "transactions"] | str | NotGiven = NOT_GIVEN, + widget_keys: str | NotGiven = NOT_GIVEN, + workflow_ids: str | NotGiven = NOT_GIVEN, + versions: str | NotGiven = NOT_GIVEN, + branch_path_ids: str | NotGiven = NOT_GIVEN, + tag: str | NotGiven = NOT_GIVEN, + transaction_type: str | NotGiven = NOT_GIVEN, + action_type: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ApplicationAnalyticsSummary | ApplicationAnalyticsWidgetResponse: + """Get application analytics. + + Read the numbers behind the Business Console [analytics dashboard](https://docs.didit.me/console/analytics) for a date range. Without `scope` and `widget_keys` the response is the user verification (KYC) summary (`ApplicationAnalyticsSummary`): daily session counts, status totals, conversion rate, the same figures for the preceding period of equal length, locations, demographics, devices, workflow funnels, active verification time, resubmissions, warnings and time series. With `scope` or `widget_keys` it is the widgets of one dashboard tab (`ApplicationAnalyticsWidgetResponse`): `kyc`, `kyb`, `transactions` or `home`. + + Session counts are by session creation date (UTC) and reflect each session's current status. They come from daily rollups refreshed in the background, so the newest activity can take a short while to appear. Invalid workflow IDs and versions are ignored rather than rejected. Nothing is billed. + + Only a full-access API key can call this endpoint: every scoped key gets `403`, whatever access it holds, and so does a key of another application. See [API keys](https://docs.didit.me/console/api-keys). + + ``GET /v3/organization/{organization_id}/application/{application_id}/analytics/`` + + Args: + start_date: First day of the range, `YYYY-MM-DD`, inclusive (UTC). Send it together with `end_date`: when either is missing, both are ignored and the range runs from 30 days before today through today. + end_date: Last day of the range, `YYYY-MM-DD`, inclusive (UTC). A future date counts as today. The range may not exceed 730 days. + scope: Dashboard tab to return widgets for (case-insensitive). Omit it, together with `widget_keys`, for the KYC summary. Sending only `widget_keys` implies `kyc`. `home` accepts any KYC, KYB or transaction widget and defaults to the application's saved home layout. + widget_keys: Comma-separated widget keys, answered in this order. Keys the scope does not accept are dropped; when none is left you get the scope's defaults. KYC: `kyc_sessions_volume`, `kyc_warning_insights`, `kyc_id_locations`, `kyc_ip_locations`, `kyc_workflow_funnel`, `kyc_average_verification_time`, `kyc_conversion_rate`, `kyc_conversion_by_country`, `kyc_resubmission_insights`, `kyc_original_status_distribution`, `kyc_database_validation_coverage`, `kyc_database_validation_latency`, `kyc_demographics`, `kyc_device_details`. KYB: `kyb_sessions_volume`, `kyb_workflow_funnel`, `kyb_average_verification_time`, `kyb_ip_locations`, `kyb_business_country_distribution`, `kyb_warning_insights`, `kyb_conversion_rate`, `kyb_conversion_by_country`, `kyb_industry_breakdown`, `kyb_resubmission_insights`, `kyb_original_status_distribution`, `kyb_registry_coverage`, `kyb_registry_data_completeness`, `kyb_device_details`. Transactions: `tx_count_total`, `tx_count_daily`, `tx_volume_total_preferred_currency`, `tx_volume_daily_preferred_currency`, `tx_average_amount`, `tx_average_amount_daily`, `tx_transaction_category_breakdown`, `tx_action_type_breakdown`, `tx_status_volume_trend`, `tx_user_ip_or_country_distribution`, `tx_tag_breakdown`, `tx_top_rules_triggered`, `tx_team_member_performance`, `tx_score_histogram`. + workflow_ids: Comma-separated workflow IDs. Limits the KYC summary and the KYC and KYB widgets to those workflows. Values that are not UUIDs are ignored; when none is valid the filter is dropped. The summary also accepts `workflow_id`. + versions: Comma-separated workflow version numbers. Applies only together with `workflow_ids`; values that are not whole numbers are ignored. `version` is accepted as an alias. + branch_path_ids: Comma-separated branch path IDs. Applies only together with `workflow_ids`, and only to KYC figures. Figures are currently recorded only for `default` (all paths combined), so any other value returns empty figures. `branch_path_id` is accepted as an alias. + tag: Transaction widgets only: comma-separated tag UUIDs or tag names. + transaction_type: Transaction widgets only: comma-separated transaction categories. + action_type: Transaction widgets only: comma-separated action types (case-insensitive). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ApplicationAnalyticsSummary | ApplicationAnalyticsWidgetResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/analytics/", + cast_to=ApplicationAnalyticsSummary | ApplicationAnalyticsWidgetResponse, + scope="application", + auth="api_key", + query={ + "start_date": start_date, + "end_date": end_date, + "scope": scope, + "widget_keys": widget_keys, + "workflow_ids": workflow_ids, + "versions": versions, + "branch_path_ids": branch_path_ids, + "tag": tag, + "transaction_type": transaction_type, + "action_type": action_type, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AnalyticsWithRawResponse: + def __init__(self, resource: Analytics) -> None: + self._resource = resource + self.retrieve = to_raw_response_wrapper(resource.retrieve) + + +class AsyncAnalyticsWithRawResponse: + def __init__(self, resource: AsyncAnalytics) -> None: + self._resource = resource + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + + +class AnalyticsWithStreamingResponse: + def __init__(self, resource: Analytics) -> None: + self._resource = resource + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + + +class AsyncAnalyticsWithStreamingResponse: + def __init__(self, resource: AsyncAnalytics) -> None: + self._resource = resource + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) diff --git a/src/didit/resources/billing.py b/src/didit/resources/billing.py index d11223a..512c28a 100644 --- a/src/didit/resources/billing.py +++ b/src/didit/resources/billing.py @@ -92,14 +92,14 @@ def top_up( ) -> BillingTopUpResponse: """Top up credits. - Add credits via a hosted Stripe Checkout session (**$50 minimum**); not available on annual plans. Redirect the customer to the returned `checkout_session_url`. + Add credits via a hosted checkout session (**$50 minimum**); not available on annual plans. Redirect the customer to the returned `checkout_session_url`. ``POST /v3/billing/top-up/`` Args: amount_in_dollars: Amount to charge, in USD. **Must be at least 50.** Tax is added automatically at checkout based on the customer's billing address. - success_url: URL Stripe redirects the customer to after a successful payment. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. Append the literal placeholder `{CHECKOUT_SESSION_ID}` if you want Stripe to substitute the session ID. - cancel_url: URL Stripe redirects the customer to when they abort or close the Checkout window. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. + success_url: URL the hosted checkout redirects the customer to after a successful payment. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. Append the literal placeholder `{CHECKOUT_SESSION_ID}` if you want the hosted checkout to substitute the session ID. + cancel_url: URL the hosted checkout redirects the customer to when they abort or close the Checkout window. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -188,14 +188,14 @@ async def top_up( ) -> BillingTopUpResponse: """Top up credits. - Add credits via a hosted Stripe Checkout session (**$50 minimum**); not available on annual plans. Redirect the customer to the returned `checkout_session_url`. + Add credits via a hosted checkout session (**$50 minimum**); not available on annual plans. Redirect the customer to the returned `checkout_session_url`. ``POST /v3/billing/top-up/`` Args: amount_in_dollars: Amount to charge, in USD. **Must be at least 50.** Tax is added automatically at checkout based on the customer's billing address. - success_url: URL Stripe redirects the customer to after a successful payment. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. Append the literal placeholder `{CHECKOUT_SESSION_ID}` if you want Stripe to substitute the session ID. - cancel_url: URL Stripe redirects the customer to when they abort or close the Checkout window. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. + success_url: URL the hosted checkout redirects the customer to after a successful payment. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. Append the literal placeholder `{CHECKOUT_SESSION_ID}` if you want the hosted checkout to substitute the session ID. + cancel_url: URL the hosted checkout redirects the customer to when they abort or close the Checkout window. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. diff --git a/src/didit/resources/blocklist.py b/src/didit/resources/blocklist.py new file mode 100644 index 0000000..e637f76 --- /dev/null +++ b/src/didit/resources/blocklist.py @@ -0,0 +1,386 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``blocklist`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.blocklist.add_response import BlocklistAddResponse +from didit.types.blocklist.list_response import BlocklistListResponse +from didit.types.blocklist.remove_response import BlocklistRemoveResponse +from didit.types.components.blocklisted_item import BlocklistedItem + +__all__ = [ + "Blocklist", + "AsyncBlocklist", + "BlocklistWithRawResponse", + "AsyncBlocklistWithRawResponse", + "BlocklistWithStreamingResponse", + "AsyncBlocklistWithStreamingResponse", +] + + +class Blocklist(SyncAPIResource): + """The ``blocklist`` resource of the Didit API. + + Methods: ``add``, ``list``, ``remove``. + """ + + @cached_property + def with_raw_response(self) -> BlocklistWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return BlocklistWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BlocklistWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return BlocklistWithStreamingResponse(self) + + def list( + self, + *, + item_type: Literal["face", "document", "phone", "email"] | str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[BlocklistedItem]: + """List blocklisted items. + + Returns one paginated feed of the faces, identity documents, phone numbers and email addresses of the application's verification sessions that are currently blocklisted, newest `blocklisted_at` first; filter with `item_type`. Items of deleted sessions are not included. The feed covers records blocklisted from a session (with [Add session items to the blocklist](https://docs.didit.me/management-api/blocklist/add), from the console, or by a list entry created from a session); values added to a list by hand, uploaded faces, faces from a document-capture selfie and the other entry types are read with [List list entries](https://docs.didit.me/management-api/lists/list-entries). `face_image` and `front_image` are short-lived signed URLs, and a scoped API key without `media: read` receives `null`. A scoped API key needs `workflows: read`. + + ``GET /v3/blocklist/`` + + Args: + item_type: Only items of this type. When omitted, every type is returned. + limit: Page size. Defaults to 50. A value above 1000000 returns `400`; zero, a negative or a non-numeric value falls back to 50. + offset: Zero-based index of the first item to return. Prefer following `next`. A value above 1000000 returns `400`; a negative or non-numeric value falls back to 0. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[BlocklistedItem]", + self._client.request_page( + "get", + "/v3/blocklist/", + cast_to=BlocklistListResponse, + item_type=BlocklistedItem, + auth="api_key", + query={"item_type": item_type, "limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def add( + self, + *, + session_id: str, + blocklist_face: bool | NotGiven = NOT_GIVEN, + blocklist_document: bool | NotGiven = NOT_GIVEN, + blocklist_phone: bool | NotGiven = NOT_GIVEN, + blocklist_email: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BlocklistAddResponse: + """Add session items to the blocklist. + + Blocklists the face, identity document, phone number or email address of one person verification session of the application. For each selected type, Didit takes the session's most recent record, flags it as blocklisted and adds an entry linked to the session to the matching system blocklist (`added_by: "api"`), so new sessions with a matching face, document number, phone number or email address are declined. Every selected item is checked first, so if one is missing nothing is blocklisted; repeating the call for an item already listed keeps its single entry and refreshes `blocklisted_at`. The session itself and sessions that already finished are not changed. A scoped API key needs `workflows: write`. + + ``POST /v3/blocklist/add/`` + + Args: + session_id: A person verification session of your application. Deleted sessions are refused. + blocklist_face: Blocklist the session's face. + blocklist_document: Blocklist the session's identity document. The document needs its issuing state, document type, document number and full name. + blocklist_phone: Blocklist the session's phone number. + blocklist_email: Blocklist the session's email address. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BlocklistAddResponse", + self._client.request( + "post", + "/v3/blocklist/add/", + cast_to=BlocklistAddResponse, + auth="api_key", + body={ + "session_id": session_id, + "blocklist_face": blocklist_face, + "blocklist_document": blocklist_document, + "blocklist_phone": blocklist_phone, + "blocklist_email": blocklist_email, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def remove( + self, + *, + session_id: str, + unblock_face: bool | NotGiven = NOT_GIVEN, + unblock_document: bool | NotGiven = NOT_GIVEN, + unblock_phone: bool | NotGiven = NOT_GIVEN, + unblock_email: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BlocklistRemoveResponse: + """Remove session items from the blocklist. + + Lifts the block on the face, identity document, phone number or email address of one person verification session of the application. For each selected type, Didit takes the session's most recent record, clears its blocklisted flag and deletes the system blocklist entries created from that record, so it no longer declines new sessions. Entries added to a list by value are not touched: find them with [List list entries](https://docs.didit.me/management-api/lists/list-entries) and delete them with [Delete a list entry](https://docs.didit.me/management-api/lists/delete-entry). Unblocking a record that is not blocklisted succeeds and changes nothing, and sessions that already finished are not changed. A scoped API key needs `workflows: write`. + + ``POST /v3/blocklist/remove/`` + + Args: + session_id: A person verification session of your application. Deleted sessions are refused. + unblock_face: Lift the block on the session's face. + unblock_document: Lift the block on the session's identity document. + unblock_phone: Lift the block on the session's phone number. + unblock_email: Lift the block on the session's email address. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BlocklistRemoveResponse", + self._client.request( + "post", + "/v3/blocklist/remove/", + cast_to=BlocklistRemoveResponse, + auth="api_key", + body={ + "session_id": session_id, + "unblock_face": unblock_face, + "unblock_document": unblock_document, + "unblock_phone": unblock_phone, + "unblock_email": unblock_email, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncBlocklist(AsyncAPIResource): + """The ``blocklist`` resource of the Didit API. + + Methods: ``add``, ``list``, ``remove``. + """ + + @cached_property + def with_raw_response(self) -> AsyncBlocklistWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncBlocklistWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBlocklistWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncBlocklistWithStreamingResponse(self) + + def list( + self, + *, + item_type: Literal["face", "document", "phone", "email"] | str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[BlocklistedItem]: + """List blocklisted items. + + Returns one paginated feed of the faces, identity documents, phone numbers and email addresses of the application's verification sessions that are currently blocklisted, newest `blocklisted_at` first; filter with `item_type`. Items of deleted sessions are not included. The feed covers records blocklisted from a session (with [Add session items to the blocklist](https://docs.didit.me/management-api/blocklist/add), from the console, or by a list entry created from a session); values added to a list by hand, uploaded faces, faces from a document-capture selfie and the other entry types are read with [List list entries](https://docs.didit.me/management-api/lists/list-entries). `face_image` and `front_image` are short-lived signed URLs, and a scoped API key without `media: read` receives `null`. A scoped API key needs `workflows: read`. + + ``GET /v3/blocklist/`` + + Args: + item_type: Only items of this type. When omitted, every type is returned. + limit: Page size. Defaults to 50. A value above 1000000 returns `400`; zero, a negative or a non-numeric value falls back to 50. + offset: Zero-based index of the first item to return. Prefer following `next`. A value above 1000000 returns `400`; a negative or non-numeric value falls back to 0. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[BlocklistedItem]", + self._client.request_page( + "get", + "/v3/blocklist/", + cast_to=BlocklistListResponse, + item_type=BlocklistedItem, + auth="api_key", + query={"item_type": item_type, "limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def add( + self, + *, + session_id: str, + blocklist_face: bool | NotGiven = NOT_GIVEN, + blocklist_document: bool | NotGiven = NOT_GIVEN, + blocklist_phone: bool | NotGiven = NOT_GIVEN, + blocklist_email: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BlocklistAddResponse: + """Add session items to the blocklist. + + Blocklists the face, identity document, phone number or email address of one person verification session of the application. For each selected type, Didit takes the session's most recent record, flags it as blocklisted and adds an entry linked to the session to the matching system blocklist (`added_by: "api"`), so new sessions with a matching face, document number, phone number or email address are declined. Every selected item is checked first, so if one is missing nothing is blocklisted; repeating the call for an item already listed keeps its single entry and refreshes `blocklisted_at`. The session itself and sessions that already finished are not changed. A scoped API key needs `workflows: write`. + + ``POST /v3/blocklist/add/`` + + Args: + session_id: A person verification session of your application. Deleted sessions are refused. + blocklist_face: Blocklist the session's face. + blocklist_document: Blocklist the session's identity document. The document needs its issuing state, document type, document number and full name. + blocklist_phone: Blocklist the session's phone number. + blocklist_email: Blocklist the session's email address. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BlocklistAddResponse", + await self._client.request( + "post", + "/v3/blocklist/add/", + cast_to=BlocklistAddResponse, + auth="api_key", + body={ + "session_id": session_id, + "blocklist_face": blocklist_face, + "blocklist_document": blocklist_document, + "blocklist_phone": blocklist_phone, + "blocklist_email": blocklist_email, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def remove( + self, + *, + session_id: str, + unblock_face: bool | NotGiven = NOT_GIVEN, + unblock_document: bool | NotGiven = NOT_GIVEN, + unblock_phone: bool | NotGiven = NOT_GIVEN, + unblock_email: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BlocklistRemoveResponse: + """Remove session items from the blocklist. + + Lifts the block on the face, identity document, phone number or email address of one person verification session of the application. For each selected type, Didit takes the session's most recent record, clears its blocklisted flag and deletes the system blocklist entries created from that record, so it no longer declines new sessions. Entries added to a list by value are not touched: find them with [List list entries](https://docs.didit.me/management-api/lists/list-entries) and delete them with [Delete a list entry](https://docs.didit.me/management-api/lists/delete-entry). Unblocking a record that is not blocklisted succeeds and changes nothing, and sessions that already finished are not changed. A scoped API key needs `workflows: write`. + + ``POST /v3/blocklist/remove/`` + + Args: + session_id: A person verification session of your application. Deleted sessions are refused. + unblock_face: Lift the block on the session's face. + unblock_document: Lift the block on the session's identity document. + unblock_phone: Lift the block on the session's phone number. + unblock_email: Lift the block on the session's email address. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BlocklistRemoveResponse", + await self._client.request( + "post", + "/v3/blocklist/remove/", + cast_to=BlocklistRemoveResponse, + auth="api_key", + body={ + "session_id": session_id, + "unblock_face": unblock_face, + "unblock_document": unblock_document, + "unblock_phone": unblock_phone, + "unblock_email": unblock_email, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class BlocklistWithRawResponse: + def __init__(self, resource: Blocklist) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.add = to_raw_response_wrapper(resource.add) + self.remove = to_raw_response_wrapper(resource.remove) + + +class AsyncBlocklistWithRawResponse: + def __init__(self, resource: AsyncBlocklist) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.add = async_to_raw_response_wrapper(resource.add) + self.remove = async_to_raw_response_wrapper(resource.remove) + + +class BlocklistWithStreamingResponse: + def __init__(self, resource: Blocklist) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.add = to_streamed_response_wrapper(resource.add) + self.remove = to_streamed_response_wrapper(resource.remove) + + +class AsyncBlocklistWithStreamingResponse: + def __init__(self, resource: AsyncBlocklist) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.add = async_to_streamed_response_wrapper(resource.add) + self.remove = async_to_streamed_response_wrapper(resource.remove) diff --git a/src/didit/resources/business_sessions.py b/src/didit/resources/business_sessions.py index af5d6bb..7a74d8d 100644 --- a/src/didit/resources/business_sessions.py +++ b/src/didit/resources/business_sessions.py @@ -16,9 +16,7 @@ to_streamed_response_wrapper, ) from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query -from didit.types.business_sessions.list_networks_response import ( - BusinessSessionsListNetworksResponse, -) +from didit.types.components.entity_network_membership_list import EntityNetworkMembershipList __all__ = [ "BusinessSessions", @@ -54,7 +52,7 @@ def list_networks( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BusinessSessionsListNetworksResponse: + ) -> EntityNetworkMembershipList: """List networks for a business session. List the networks the business session's vendor business belongs to. @@ -69,11 +67,11 @@ def list_networks( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "BusinessSessionsListNetworksResponse", + "EntityNetworkMembershipList", self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/business-sessions/{business_session_id}/networks/", - cast_to=BusinessSessionsListNetworksResponse, + cast_to=EntityNetworkMembershipList, path_params={"business_session_id": business_session_id}, scope="application", auth="api_key", @@ -109,7 +107,7 @@ async def list_networks( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BusinessSessionsListNetworksResponse: + ) -> EntityNetworkMembershipList: """List networks for a business session. List the networks the business session's vendor business belongs to. @@ -124,11 +122,11 @@ async def list_networks( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "BusinessSessionsListNetworksResponse", + "EntityNetworkMembershipList", await self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/business-sessions/{business_session_id}/networks/", - cast_to=BusinessSessionsListNetworksResponse, + cast_to=EntityNetworkMembershipList, path_params={"business_session_id": business_session_id}, scope="application", auth="api_key", diff --git a/src/didit/resources/businesses.py b/src/didit/resources/businesses.py index 76350e9..d654763 100644 --- a/src/didit/resources/businesses.py +++ b/src/didit/resources/businesses.py @@ -19,31 +19,14 @@ ) from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query from didit.types.businesses.batch_delete_response import BusinessesBatchDeleteResponse -from didit.types.businesses.list_networks_response import BusinessesListNetworksResponse from didit.types.businesses.list_response import BusinessesListResponse -from didit.types.businesses.retrieve_ongoing_monitoring_job_response import ( - BusinessesRetrieveOngoingMonitoringJobResponse, -) -from didit.types.businesses.retrieve_session_ongoing_monitoring_job_response import ( - BusinessesRetrieveSessionOngoingMonitoringJobResponse, -) -from didit.types.businesses.set_aml_monitoring_response import ( - BusinessesSetAmlMonitoringResponse202, - BusinessesSetAmlMonitoringResponseVariant1, - BusinessesSetAmlMonitoringResponseVariant2, -) -from didit.types.businesses.set_ongoing_monitoring_response import ( - BusinessesSetOngoingMonitoringResponse202, - BusinessesSetOngoingMonitoringResponseVariant1, - BusinessesSetOngoingMonitoringResponseVariant2, -) -from didit.types.businesses.set_session_ongoing_monitoring_response import ( - BusinessesSetSessionOngoingMonitoringResponse202, - BusinessesSetSessionOngoingMonitoringResponseVariant1, - BusinessesSetSessionOngoingMonitoringResponseVariant2, -) +from didit.types.components.aml_monitoring_job import AMLMonitoringJob +from didit.types.components.aml_monitoring_toggle_result import AMLMonitoringToggleResult from didit.types.components.business_detail_item import BusinessDetailItem from didit.types.components.business_list_item import BusinessListItem +from didit.types.components.entity_network_membership_list import EntityNetworkMembershipList +from didit.types.components.ongoing_monitoring_job import OngoingMonitoringJob +from didit.types.components.ongoing_monitoring_toggle_result import OngoingMonitoringToggleResult __all__ = [ "Businesses", @@ -58,7 +41,7 @@ class Businesses(SyncAPIResource): """The ``businesses`` resource of the Didit API. - Methods: ``batch_delete``, ``create``, ``list``, ``list_networks``, ``retrieve``, ``retrieve_ongoing_monitoring_job``, ``retrieve_session_ongoing_monitoring_job``, ``set_aml_monitoring``, ``set_ongoing_monitoring``, ``set_session_ongoing_monitoring``, ``update``, ``update_status``. + Methods: ``batch_delete``, ``create``, ``generate_session_pdf``, ``list``, ``list_networks``, ``retrieve``, ``retrieve_ongoing_monitoring_job``, ``retrieve_session_ongoing_monitoring_job``, ``set_aml_monitoring``, ``set_ongoing_monitoring``, ``set_session_ongoing_monitoring``, ``update``, ``update_status``. """ @cached_property @@ -158,6 +141,7 @@ def update( country_code: str | None | NotGiven = NOT_GIVEN, status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | NotGiven = NOT_GIVEN, metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + reason: str | None | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -165,7 +149,7 @@ def update( ) -> BusinessDetailItem: """Update business. - Partial update of a business: `display_name`, `legal_name`, `registration_number`, `country_code`, `status`, `metadata`. `metadata` fully replaces. Setting `status` to `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. + Partial update of a business: `display_name`, `legal_name`, `registration_number`, `country_code`, `status`, `metadata`. `metadata` fully replaces. Setting `status` to `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. Every edit is recorded in the company's activity: a status change with the optional `reason`, and a change to `display_name`, `legal_name`, `registration_number`, `country_code` or `metadata` as a profile update. ``PATCH /v3/businesses/{vendor_data}/`` @@ -177,6 +161,7 @@ def update( country_code: Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `US`). status: Business lifecycle status. Setting `BLOCKED` automatically adds the `vendor_data` to the system blocklist; flipping away from `BLOCKED` removes that blocklist entry. metadata: Arbitrary JSON object you attach to the business. **Fully replaces** the existing metadata on update - merge client-side if you only want to add keys. + reason: Why the status changed. Recorded on the status-change entry of the company's **Activity** in the Business Console, only when the status actually changes. Webhooks do not carry it. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -197,6 +182,7 @@ def update( "country_code": country_code, "status": status, "metadata": metadata, + "reason": reason, }, extra_headers=extra_headers, extra_query=extra_query, @@ -210,6 +196,7 @@ def update_status( vendor_data: str, *, status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str, + reason: str | None | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -217,13 +204,14 @@ def update_status( ) -> BusinessDetailItem: """Update business status. - Flip only the lifecycle `status` of a business - VendorBusinessStatusChoices (`ACTIVE`/`FLAGGED`/`BLOCKED`, NOT session statuses). `BLOCKED` adds `vendor_data` to the system blocklist. + Flip only the lifecycle `status` of a business: `ACTIVE`, `FLAGGED` or `BLOCKED` (not session statuses). `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. Each change is recorded in the company's activity, with the optional `reason`. Sending the current status changes nothing and records no `reason`. The `business.status.updated` webhook does not carry `reason`. ``PATCH /v3/businesses/{vendor_data}/update-status/`` Args: vendor_data: Your unique identifier for the business (free-form string, NOT a UUID). status: New lifecycle status. `BLOCKED` also adds the `vendor_data` to the system blocklist. + reason: Why the status changed. Recorded on the status-change entry of the company's **Activity** in the Business Console, only when the status actually changes. Webhooks do not carry it. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -237,7 +225,7 @@ def update_status( cast_to=BusinessDetailItem, path_params={"vendor_data": vendor_data}, auth="api_key", - body={"status": status}, + body={"status": status, "reason": reason}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -301,11 +289,7 @@ def set_aml_monitoring( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ( - BusinessesSetAmlMonitoringResponseVariant1 - | BusinessesSetAmlMonitoringResponseVariant2 - | BusinessesSetAmlMonitoringResponse202 - ): + ) -> AMLMonitoringToggleResult | AMLMonitoringJob: """Enable or disable AML ongoing monitoring for businesses. Turns AML ongoing monitoring on or off for the selected businesses, with the same semantics as the Business Console. Exactly one monitored AML screening is kept per business, so a profile that verified several times is monitored and billed once; disabling turns monitoring off across all of the profile's screenings. Businesses without any AML screening are screened first (billed as a regular AML screening) when they have the legal or display name needed; otherwise they are reported as `skipped_no_identity_data`. @@ -325,15 +309,11 @@ def set_aml_monitoring( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "BusinessesSetAmlMonitoringResponseVariant1 | BusinessesSetAmlMonitoringResponseVariant2 | BusinessesSetAmlMonitoringResponse202", + "AMLMonitoringToggleResult | AMLMonitoringJob", self._client.request( "post", "/v3/businesses/aml-monitoring/", - cast_to={ - 200: BusinessesSetAmlMonitoringResponseVariant1 - | BusinessesSetAmlMonitoringResponseVariant2, - 202: BusinessesSetAmlMonitoringResponse202, - }, + cast_to={200: AMLMonitoringToggleResult, 202: AMLMonitoringJob}, auth="api_key", body={ "is_enabled": is_enabled, @@ -421,11 +401,7 @@ def set_ongoing_monitoring( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ( - BusinessesSetOngoingMonitoringResponseVariant1 - | BusinessesSetOngoingMonitoringResponseVariant2 - | BusinessesSetOngoingMonitoringResponse202 - ): + ) -> OngoingMonitoringToggleResult | OngoingMonitoringJob: """Toggle AML ongoing monitoring for businesses. Business-level equivalent of the user toggle: one monitored AML screening per business, with businesses lacking any AML screening screened first (company entity). Same estimate_only, inline fast-path and async job contract. @@ -448,15 +424,11 @@ def set_ongoing_monitoring( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "BusinessesSetOngoingMonitoringResponseVariant1 | BusinessesSetOngoingMonitoringResponseVariant2 | BusinessesSetOngoingMonitoringResponse202", + "OngoingMonitoringToggleResult | OngoingMonitoringJob", self._client.request( "patch", "/v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/", - cast_to={ - 200: BusinessesSetOngoingMonitoringResponseVariant1 - | BusinessesSetOngoingMonitoringResponseVariant2, - 202: BusinessesSetOngoingMonitoringResponse202, - }, + cast_to={200: OngoingMonitoringToggleResult, 202: OngoingMonitoringJob}, scope="application", auth="api_key", body={ @@ -485,7 +457,7 @@ def retrieve_ongoing_monitoring_job( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BusinessesRetrieveOngoingMonitoringJobResponse: + ) -> OngoingMonitoringJob: """Get AML ongoing monitoring job. Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING. @@ -500,11 +472,11 @@ def retrieve_ongoing_monitoring_job( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "BusinessesRetrieveOngoingMonitoringJobResponse", + "OngoingMonitoringJob", self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/{job_id}/", - cast_to=BusinessesRetrieveOngoingMonitoringJobResponse, + cast_to=OngoingMonitoringJob, path_params={"job_id": job_id}, scope="application", auth="api_key", @@ -533,11 +505,7 @@ def set_session_ongoing_monitoring( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ( - BusinessesSetSessionOngoingMonitoringResponseVariant1 - | BusinessesSetSessionOngoingMonitoringResponseVariant2 - | BusinessesSetSessionOngoingMonitoringResponse202 - ): + ) -> OngoingMonitoringToggleResult | OngoingMonitoringJob: """Toggle AML ongoing monitoring for business sessions. Session-level toggle for KYB verifications, addressed by session number. Sessions without an AML screening get one created and attached to the session itself. Same estimate_only, inline fast-path and async job contract as the user-level endpoints. Replaces the legacy synchronous behavior that skipped sessions without an AML screening. @@ -562,15 +530,11 @@ def set_session_ongoing_monitoring( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "BusinessesSetSessionOngoingMonitoringResponseVariant1 | BusinessesSetSessionOngoingMonitoringResponseVariant2 | BusinessesSetSessionOngoingMonitoringResponse202", + "OngoingMonitoringToggleResult | OngoingMonitoringJob", self._client.request( "patch", "/v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/", - cast_to={ - 200: BusinessesSetSessionOngoingMonitoringResponseVariant1 - | BusinessesSetSessionOngoingMonitoringResponseVariant2, - 202: BusinessesSetSessionOngoingMonitoringResponse202, - }, + cast_to={200: OngoingMonitoringToggleResult, 202: OngoingMonitoringJob}, scope="application", auth="api_key", body={ @@ -601,7 +565,7 @@ def retrieve_session_ongoing_monitoring_job( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BusinessesRetrieveSessionOngoingMonitoringJobResponse: + ) -> OngoingMonitoringJob: """Get AML ongoing monitoring job. Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING. @@ -616,11 +580,11 @@ def retrieve_session_ongoing_monitoring_job( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "BusinessesRetrieveSessionOngoingMonitoringJobResponse", + "OngoingMonitoringJob", self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/{job_id}/", - cast_to=BusinessesRetrieveSessionOngoingMonitoringJobResponse, + cast_to=OngoingMonitoringJob, path_params={"job_id": job_id}, scope="application", auth="api_key", @@ -639,7 +603,7 @@ def list_networks( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BusinessesListNetworksResponse: + ) -> EntityNetworkMembershipList: """List networks for a business. List the networks a vendor business belongs to. @@ -654,11 +618,11 @@ def list_networks( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "BusinessesListNetworksResponse", + "EntityNetworkMembershipList", self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/businesses/{vendor_business_uuid}/networks/", - cast_to=BusinessesListNetworksResponse, + cast_to=EntityNetworkMembershipList, path_params={"vendor_business_uuid": vendor_business_uuid}, scope="application", auth="api_key", @@ -669,11 +633,54 @@ def list_networks( ), ) + def generate_session_pdf( + self, + business_session_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download a business session PDF report. + + Render the PDF report of one Business Verification (KYB) session: company registry data, key people and beneficial owners, documents, AML results and the other workflow features, plus the session's tags and review activity. With white-label customization on, the report carries your logo and privacy policy URL. The body is the PDF itself (`application/pdf`), not JSON. + + The session must be `Approved`, `Declined` or `In Review`; any other status returns `403`, so wait for the `status.updated` [webhook](https://docs.didit.me/integration/webhooks) first. The report is rendered on every call and nothing is billed, so store the file instead of fetching it repeatedly. `GET /v3/session/{sessionId}/generate-pdf/` renders the same kind of report by session ID alone. + + A PDF report counts as media: a scoped API key needs `businesses: read` and `media: read`, and a key limited to some workflows or to approved sessions cannot call it. + + ``GET /v3/organization/{organization_id}/application/{application_id}/businesses/{business_session_id}/pdf/`` + + Args: + business_session_id: `session_id` of the Business Verification (KYB) session, as returned when the session was created. A session of another application returns `404`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/businesses/{business_session_id}/pdf/", + cast_to=bytes, + path_params={"business_session_id": business_session_id}, + scope="application", + auth="api_key", + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + class AsyncBusinesses(AsyncAPIResource): """The ``businesses`` resource of the Didit API. - Methods: ``batch_delete``, ``create``, ``list``, ``list_networks``, ``retrieve``, ``retrieve_ongoing_monitoring_job``, ``retrieve_session_ongoing_monitoring_job``, ``set_aml_monitoring``, ``set_ongoing_monitoring``, ``set_session_ongoing_monitoring``, ``update``, ``update_status``. + Methods: ``batch_delete``, ``create``, ``generate_session_pdf``, ``list``, ``list_networks``, ``retrieve``, ``retrieve_ongoing_monitoring_job``, ``retrieve_session_ongoing_monitoring_job``, ``set_aml_monitoring``, ``set_ongoing_monitoring``, ``set_session_ongoing_monitoring``, ``update``, ``update_status``. """ @cached_property @@ -773,6 +780,7 @@ async def update( country_code: str | None | NotGiven = NOT_GIVEN, status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | NotGiven = NOT_GIVEN, metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + reason: str | None | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -780,7 +788,7 @@ async def update( ) -> BusinessDetailItem: """Update business. - Partial update of a business: `display_name`, `legal_name`, `registration_number`, `country_code`, `status`, `metadata`. `metadata` fully replaces. Setting `status` to `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. + Partial update of a business: `display_name`, `legal_name`, `registration_number`, `country_code`, `status`, `metadata`. `metadata` fully replaces. Setting `status` to `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. Every edit is recorded in the company's activity: a status change with the optional `reason`, and a change to `display_name`, `legal_name`, `registration_number`, `country_code` or `metadata` as a profile update. ``PATCH /v3/businesses/{vendor_data}/`` @@ -792,6 +800,7 @@ async def update( country_code: Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `US`). status: Business lifecycle status. Setting `BLOCKED` automatically adds the `vendor_data` to the system blocklist; flipping away from `BLOCKED` removes that blocklist entry. metadata: Arbitrary JSON object you attach to the business. **Fully replaces** the existing metadata on update - merge client-side if you only want to add keys. + reason: Why the status changed. Recorded on the status-change entry of the company's **Activity** in the Business Console, only when the status actually changes. Webhooks do not carry it. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -812,6 +821,7 @@ async def update( "country_code": country_code, "status": status, "metadata": metadata, + "reason": reason, }, extra_headers=extra_headers, extra_query=extra_query, @@ -825,6 +835,7 @@ async def update_status( vendor_data: str, *, status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str, + reason: str | None | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -832,13 +843,14 @@ async def update_status( ) -> BusinessDetailItem: """Update business status. - Flip only the lifecycle `status` of a business - VendorBusinessStatusChoices (`ACTIVE`/`FLAGGED`/`BLOCKED`, NOT session statuses). `BLOCKED` adds `vendor_data` to the system blocklist. + Flip only the lifecycle `status` of a business: `ACTIVE`, `FLAGGED` or `BLOCKED` (not session statuses). `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. Each change is recorded in the company's activity, with the optional `reason`. Sending the current status changes nothing and records no `reason`. The `business.status.updated` webhook does not carry `reason`. ``PATCH /v3/businesses/{vendor_data}/update-status/`` Args: vendor_data: Your unique identifier for the business (free-form string, NOT a UUID). status: New lifecycle status. `BLOCKED` also adds the `vendor_data` to the system blocklist. + reason: Why the status changed. Recorded on the status-change entry of the company's **Activity** in the Business Console, only when the status actually changes. Webhooks do not carry it. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -852,7 +864,7 @@ async def update_status( cast_to=BusinessDetailItem, path_params={"vendor_data": vendor_data}, auth="api_key", - body={"status": status}, + body={"status": status, "reason": reason}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -916,11 +928,7 @@ async def set_aml_monitoring( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ( - BusinessesSetAmlMonitoringResponseVariant1 - | BusinessesSetAmlMonitoringResponseVariant2 - | BusinessesSetAmlMonitoringResponse202 - ): + ) -> AMLMonitoringToggleResult | AMLMonitoringJob: """Enable or disable AML ongoing monitoring for businesses. Turns AML ongoing monitoring on or off for the selected businesses, with the same semantics as the Business Console. Exactly one monitored AML screening is kept per business, so a profile that verified several times is monitored and billed once; disabling turns monitoring off across all of the profile's screenings. Businesses without any AML screening are screened first (billed as a regular AML screening) when they have the legal or display name needed; otherwise they are reported as `skipped_no_identity_data`. @@ -940,15 +948,11 @@ async def set_aml_monitoring( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "BusinessesSetAmlMonitoringResponseVariant1 | BusinessesSetAmlMonitoringResponseVariant2 | BusinessesSetAmlMonitoringResponse202", + "AMLMonitoringToggleResult | AMLMonitoringJob", await self._client.request( "post", "/v3/businesses/aml-monitoring/", - cast_to={ - 200: BusinessesSetAmlMonitoringResponseVariant1 - | BusinessesSetAmlMonitoringResponseVariant2, - 202: BusinessesSetAmlMonitoringResponse202, - }, + cast_to={200: AMLMonitoringToggleResult, 202: AMLMonitoringJob}, auth="api_key", body={ "is_enabled": is_enabled, @@ -1036,11 +1040,7 @@ async def set_ongoing_monitoring( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ( - BusinessesSetOngoingMonitoringResponseVariant1 - | BusinessesSetOngoingMonitoringResponseVariant2 - | BusinessesSetOngoingMonitoringResponse202 - ): + ) -> OngoingMonitoringToggleResult | OngoingMonitoringJob: """Toggle AML ongoing monitoring for businesses. Business-level equivalent of the user toggle: one monitored AML screening per business, with businesses lacking any AML screening screened first (company entity). Same estimate_only, inline fast-path and async job contract. @@ -1063,15 +1063,11 @@ async def set_ongoing_monitoring( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "BusinessesSetOngoingMonitoringResponseVariant1 | BusinessesSetOngoingMonitoringResponseVariant2 | BusinessesSetOngoingMonitoringResponse202", + "OngoingMonitoringToggleResult | OngoingMonitoringJob", await self._client.request( "patch", "/v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/", - cast_to={ - 200: BusinessesSetOngoingMonitoringResponseVariant1 - | BusinessesSetOngoingMonitoringResponseVariant2, - 202: BusinessesSetOngoingMonitoringResponse202, - }, + cast_to={200: OngoingMonitoringToggleResult, 202: OngoingMonitoringJob}, scope="application", auth="api_key", body={ @@ -1100,7 +1096,7 @@ async def retrieve_ongoing_monitoring_job( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BusinessesRetrieveOngoingMonitoringJobResponse: + ) -> OngoingMonitoringJob: """Get AML ongoing monitoring job. Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING. @@ -1115,11 +1111,11 @@ async def retrieve_ongoing_monitoring_job( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "BusinessesRetrieveOngoingMonitoringJobResponse", + "OngoingMonitoringJob", await self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/{job_id}/", - cast_to=BusinessesRetrieveOngoingMonitoringJobResponse, + cast_to=OngoingMonitoringJob, path_params={"job_id": job_id}, scope="application", auth="api_key", @@ -1148,11 +1144,7 @@ async def set_session_ongoing_monitoring( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ( - BusinessesSetSessionOngoingMonitoringResponseVariant1 - | BusinessesSetSessionOngoingMonitoringResponseVariant2 - | BusinessesSetSessionOngoingMonitoringResponse202 - ): + ) -> OngoingMonitoringToggleResult | OngoingMonitoringJob: """Toggle AML ongoing monitoring for business sessions. Session-level toggle for KYB verifications, addressed by session number. Sessions without an AML screening get one created and attached to the session itself. Same estimate_only, inline fast-path and async job contract as the user-level endpoints. Replaces the legacy synchronous behavior that skipped sessions without an AML screening. @@ -1177,15 +1169,11 @@ async def set_session_ongoing_monitoring( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "BusinessesSetSessionOngoingMonitoringResponseVariant1 | BusinessesSetSessionOngoingMonitoringResponseVariant2 | BusinessesSetSessionOngoingMonitoringResponse202", + "OngoingMonitoringToggleResult | OngoingMonitoringJob", await self._client.request( "patch", "/v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/", - cast_to={ - 200: BusinessesSetSessionOngoingMonitoringResponseVariant1 - | BusinessesSetSessionOngoingMonitoringResponseVariant2, - 202: BusinessesSetSessionOngoingMonitoringResponse202, - }, + cast_to={200: OngoingMonitoringToggleResult, 202: OngoingMonitoringJob}, scope="application", auth="api_key", body={ @@ -1216,7 +1204,7 @@ async def retrieve_session_ongoing_monitoring_job( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BusinessesRetrieveSessionOngoingMonitoringJobResponse: + ) -> OngoingMonitoringJob: """Get AML ongoing monitoring job. Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING. @@ -1231,11 +1219,11 @@ async def retrieve_session_ongoing_monitoring_job( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "BusinessesRetrieveSessionOngoingMonitoringJobResponse", + "OngoingMonitoringJob", await self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/{job_id}/", - cast_to=BusinessesRetrieveSessionOngoingMonitoringJobResponse, + cast_to=OngoingMonitoringJob, path_params={"job_id": job_id}, scope="application", auth="api_key", @@ -1254,7 +1242,7 @@ async def list_networks( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> BusinessesListNetworksResponse: + ) -> EntityNetworkMembershipList: """List networks for a business. List the networks a vendor business belongs to. @@ -1269,11 +1257,11 @@ async def list_networks( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "BusinessesListNetworksResponse", + "EntityNetworkMembershipList", await self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/businesses/{vendor_business_uuid}/networks/", - cast_to=BusinessesListNetworksResponse, + cast_to=EntityNetworkMembershipList, path_params={"vendor_business_uuid": vendor_business_uuid}, scope="application", auth="api_key", @@ -1284,6 +1272,49 @@ async def list_networks( ), ) + async def generate_session_pdf( + self, + business_session_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download a business session PDF report. + + Render the PDF report of one Business Verification (KYB) session: company registry data, key people and beneficial owners, documents, AML results and the other workflow features, plus the session's tags and review activity. With white-label customization on, the report carries your logo and privacy policy URL. The body is the PDF itself (`application/pdf`), not JSON. + + The session must be `Approved`, `Declined` or `In Review`; any other status returns `403`, so wait for the `status.updated` [webhook](https://docs.didit.me/integration/webhooks) first. The report is rendered on every call and nothing is billed, so store the file instead of fetching it repeatedly. `GET /v3/session/{sessionId}/generate-pdf/` renders the same kind of report by session ID alone. + + A PDF report counts as media: a scoped API key needs `businesses: read` and `media: read`, and a key limited to some workflows or to approved sessions cannot call it. + + ``GET /v3/organization/{organization_id}/application/{application_id}/businesses/{business_session_id}/pdf/`` + + Args: + business_session_id: `session_id` of the Business Verification (KYB) session, as returned when the session was created. A session of another application returns `404`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/businesses/{business_session_id}/pdf/", + cast_to=bytes, + path_params={"business_session_id": business_session_id}, + scope="application", + auth="api_key", + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + class BusinessesWithRawResponse: def __init__(self, resource: Businesses) -> None: @@ -1306,6 +1337,7 @@ def __init__(self, resource: Businesses) -> None: resource.retrieve_session_ongoing_monitoring_job ) self.list_networks = to_raw_response_wrapper(resource.list_networks) + self.generate_session_pdf = to_raw_response_wrapper(resource.generate_session_pdf) class AsyncBusinessesWithRawResponse: @@ -1329,6 +1361,7 @@ def __init__(self, resource: AsyncBusinesses) -> None: resource.retrieve_session_ongoing_monitoring_job ) self.list_networks = async_to_raw_response_wrapper(resource.list_networks) + self.generate_session_pdf = async_to_raw_response_wrapper(resource.generate_session_pdf) class BusinessesWithStreamingResponse: @@ -1352,6 +1385,7 @@ def __init__(self, resource: Businesses) -> None: resource.retrieve_session_ongoing_monitoring_job ) self.list_networks = to_streamed_response_wrapper(resource.list_networks) + self.generate_session_pdf = to_streamed_response_wrapper(resource.generate_session_pdf) class AsyncBusinessesWithStreamingResponse: @@ -1377,3 +1411,6 @@ def __init__(self, resource: AsyncBusinesses) -> None: resource.retrieve_session_ongoing_monitoring_job ) self.list_networks = async_to_streamed_response_wrapper(resource.list_networks) + self.generate_session_pdf = async_to_streamed_response_wrapper( + resource.generate_session_pdf + ) diff --git a/src/didit/resources/cases/__init__.py b/src/didit/resources/cases/__init__.py index ede07dc..14988ac 100644 --- a/src/didit/resources/cases/__init__.py +++ b/src/didit/resources/cases/__init__.py @@ -19,14 +19,13 @@ to_streamed_response_wrapper, ) from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query -from didit.types.cases.bulk_assign_response import CasesBulkAssignResponse -from didit.types.cases.bulk_update_status_response import CasesBulkUpdateStatusResponse from didit.types.cases.create_params import CasesCreateParamsSubject from didit.types.cases.list_events_response import CasesListEventsResponse from didit.types.cases.list_response import CasesListResponse from didit.types.cases.list_reviewers_response import CasesListReviewersResponse from didit.types.cases.print_response import CasesPrintResponse from didit.types.cases.retrieve_statistics_response import CasesRetrieveStatisticsResponse +from didit.types.components.aml_screening import AMLScreening from didit.types.components.case_action_action_enum import CaseActionActionEnum from didit.types.components.case_action_case_action_receipt import CaseActionCaseActionReceipt from didit.types.components.case_action_case_action_receipt_page import ( @@ -35,6 +34,7 @@ from didit.types.components.case_action_case_entity_action_result import ( CaseActionCaseEntityActionResult, ) +from didit.types.components.case_bulk_update_result import CaseBulkUpdateResult from didit.types.components.case_detail import CaseDetail from didit.types.components.case_duplicate_case_duplicate_page import CaseDuplicateCaseDuplicatePage from didit.types.components.case_duplicate_case_fraud_source_context_request import ( @@ -430,6 +430,7 @@ def update( priority: Literal["LOW", "MEDIUM", "HIGH"] | str | NotGiven = NOT_GIVEN, due_at: str | _dt.datetime | None | NotGiven = NOT_GIVEN, tags: Sequence[str] | NotGiven = NOT_GIVEN, + team: str | None | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -437,16 +438,17 @@ def update( ) -> CaseDetail: """Update case. - Update the case's title, priority, due date, or tags. Logs PRIORITY_CHANGED/DUE_DATE_CHANGED/TAG_ADDED/TAG_REMOVED events as applicable. + Update the case's title, priority, due date, tags, or owning review team. Send only the fields you want to change; fields you omit keep their value. `tags` replaces the whole tag list. `team` takes the uuid of an active review team of this application (list them with `GET .../cases/teams/`), or null to clear the team. Requires a full-access API key, or `write:cases` for a console member: a key with restricted access gets 403. Logs PRIORITY_CHANGED, DUE_DATE_CHANGED, TAG_ADDED, TAG_REMOVED and TEAM_CHANGED events as applicable; a title change is saved without a timeline event. Returns the updated case. ``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/`` Args: case_uuid: Case UUID. - title + title: New case title. priority - due_at - tags + due_at: New deadline. Send null to clear it. + tags: The complete tag list. It replaces the current tags. + team: UUID of the review team that owns the case. It must be an active team of this application. Send null to clear the team. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -461,7 +463,13 @@ def update( path_params={"case_uuid": case_uuid}, scope="application", auth="api_key", - body={"title": title, "priority": priority, "due_at": due_at, "tags": tags}, + body={ + "title": title, + "priority": priority, + "due_at": due_at, + "tags": tags, + "team": team, + }, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -522,8 +530,16 @@ def resolve( self, case_uuid: str, *, - resolution: Literal["FALSE_POSITIVE", "VALID_THREAT"] | str, + resolution: Literal[ + "FALSE_POSITIVE", + "VALID_THREAT", + "INCONCLUSIVE", + "REQUIREMENTS_NOT_MET", + "REQUIREMENTS_SATISFIED", + ] + | str, note: str | None | NotGiven = NOT_GIVEN, + checker: str | None | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -531,7 +547,7 @@ def resolve( ) -> CaseDetail: """Resolve case. - Resolve the case as false_positive or valid_threat. Blocked with 400 if the blueprint's checklist is not optional and items remain incomplete, or if the blueprint requires a resolution note and none is given. When the case's blueprint has a four_eyes_checker_blueprint configured, the resolution is staged (case.pending_resolution) and the case moves to the checker blueprint instead of resolving immediately; a different user must call approve or reject-approval. + Resolve the case with one of five resolutions: FALSE_POSITIVE (no concern found), VALID_THREAT (concern confirmed), INCONCLUSIVE, REQUIREMENTS_NOT_MET or REQUIREMENTS_SATISFIED. Blocked with 400 if the blueprint's checklist is not optional and items remain incomplete, or if the blueprint requires a resolution note and none is given. When the case's blueprint has a four-eyes checker step (four_eyes_checker_blueprint), the resolution is staged (case.pending_resolution) and the case moves to the checker blueprint instead of resolving immediately; a different officer must call approve or reject-approval. Submit a four-eyes resolution as a console member: a resolution an API key submits has no recorded maker, so approve and reject-approval return 403 for every caller. On a four-eyes blueprint you can name the member who must approve with `checker`: then only that member can approve or reject the resolution, and the response's `checker` names them. Without `checker`, the resolution waits on the checker blueprint's queue. Sending `checker` for a blueprint without a checker step returns 400. ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/resolve/`` @@ -539,6 +555,7 @@ def resolve( case_uuid: Case UUID. resolution note: Required when the blueprint sets require_resolution_note. + checker: Four-eyes blueprints only. The user uuid of the member who must approve this resolution (the same id as `assigned_to.uuid` on a case, or `members[].user_id` on a review team). The member must have case access to this application and cannot be the officer who submits the resolution. Omit it or send null to leave the resolution to the checker queue. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -553,7 +570,7 @@ def resolve( path_params={"case_uuid": case_uuid}, scope="application", auth="api_key", - body={"resolution": resolution, "note": note}, + body={"resolution": resolution, "note": note, "checker": checker}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -727,7 +744,7 @@ def approve_resolution( ) -> CaseDetail: """Approve staged resolution (4-eyes). - Apply a resolution staged by resolve on a maker blueprint. The caller must be a different user than the one who submitted it, otherwise 403. + Apply a resolution staged by resolve on a four-eyes blueprint. The caller must be a different officer than the one who submitted it, otherwise 403. When the resolution names a checker (the case's `checker`), only that member can approve it: any other caller, including an API key, gets 403. ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/approve/`` @@ -766,7 +783,7 @@ def reject_resolution( ) -> CaseDetail: """Reject staged resolution (4-eyes). - Reject a resolution staged by resolve and return the case to its maker blueprint. The caller must be a different user than the one who submitted it, otherwise 403. + Reject a resolution staged by resolve and return the case to its maker blueprint. The caller must be a different officer than the one who submitted it, otherwise 403. When the resolution names a checker (the case's `checker`), only that member can reject it: any other caller, including an API key, gets 403. ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reject-approval/`` @@ -941,7 +958,7 @@ def list_aml_hits( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> builtins.list[builtins.dict[str, object]]: + ) -> builtins.list[AMLScreening]: """List case AML hits. List AML screening hits for the case's subject. Same shape as the hits returned by POST /v3/aml/. @@ -956,11 +973,11 @@ def list_aml_hits( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "builtins.list[builtins.dict[str, object]]", + "builtins.list[AMLScreening]", self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/aml/", - cast_to=builtins.list[builtins.dict[str, object]], + cast_to=builtins.list[AMLScreening], path_params={"case_uuid": case_uuid}, scope="application", auth="api_key", @@ -1036,13 +1053,21 @@ def bulk_update_status( *, case_uuids: Sequence[str], status: Literal["OPEN", "AWAITING_USER", "RESOLVED"] | str, - resolution: Literal["FALSE_POSITIVE", "VALID_THREAT"] | str | NotGiven = NOT_GIVEN, + resolution: Literal[ + "FALSE_POSITIVE", + "VALID_THREAT", + "INCONCLUSIVE", + "REQUIREMENTS_NOT_MET", + "REQUIREMENTS_SATISFIED", + ] + | str + | NotGiven = NOT_GIVEN, note: str | None | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> CasesBulkUpdateStatusResponse: + ) -> CaseBulkUpdateResult: """Bulk update case status. Apply the same status transition to several cases in one call: OPEN reopens, AWAITING_USER requests info, RESOLVED resolves (resolution required). Each case is evaluated independently; gate failures (incomplete checklist, missing required note) are reported per case without aborting the batch. @@ -1060,11 +1085,11 @@ def bulk_update_status( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "CasesBulkUpdateStatusResponse", + "CaseBulkUpdateResult", self._client.request( "post", "/v3/organization/{organization_id}/application/{application_id}/cases/bulk/status/", - cast_to=CasesBulkUpdateStatusResponse, + cast_to=CaseBulkUpdateResult, scope="application", auth="api_key", body={ @@ -1091,7 +1116,7 @@ def bulk_assign( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> CasesBulkAssignResponse: + ) -> CaseBulkUpdateResult: """Bulk assign cases. Assign (or unassign, with assignee_user_id: null) several cases at once. All cases must share the same blueprint; mismatches are reported per case without aborting the batch. @@ -1109,11 +1134,11 @@ def bulk_assign( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "CasesBulkAssignResponse", + "CaseBulkUpdateResult", self._client.request( "post", "/v3/organization/{organization_id}/application/{application_id}/cases/bulk/assign/", - cast_to=CasesBulkAssignResponse, + cast_to=CaseBulkUpdateResult, scope="application", auth="api_key", body={ @@ -1587,6 +1612,7 @@ async def update( priority: Literal["LOW", "MEDIUM", "HIGH"] | str | NotGiven = NOT_GIVEN, due_at: str | _dt.datetime | None | NotGiven = NOT_GIVEN, tags: Sequence[str] | NotGiven = NOT_GIVEN, + team: str | None | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -1594,16 +1620,17 @@ async def update( ) -> CaseDetail: """Update case. - Update the case's title, priority, due date, or tags. Logs PRIORITY_CHANGED/DUE_DATE_CHANGED/TAG_ADDED/TAG_REMOVED events as applicable. + Update the case's title, priority, due date, tags, or owning review team. Send only the fields you want to change; fields you omit keep their value. `tags` replaces the whole tag list. `team` takes the uuid of an active review team of this application (list them with `GET .../cases/teams/`), or null to clear the team. Requires a full-access API key, or `write:cases` for a console member: a key with restricted access gets 403. Logs PRIORITY_CHANGED, DUE_DATE_CHANGED, TAG_ADDED, TAG_REMOVED and TEAM_CHANGED events as applicable; a title change is saved without a timeline event. Returns the updated case. ``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/`` Args: case_uuid: Case UUID. - title + title: New case title. priority - due_at - tags + due_at: New deadline. Send null to clear it. + tags: The complete tag list. It replaces the current tags. + team: UUID of the review team that owns the case. It must be an active team of this application. Send null to clear the team. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -1618,7 +1645,13 @@ async def update( path_params={"case_uuid": case_uuid}, scope="application", auth="api_key", - body={"title": title, "priority": priority, "due_at": due_at, "tags": tags}, + body={ + "title": title, + "priority": priority, + "due_at": due_at, + "tags": tags, + "team": team, + }, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -1679,8 +1712,16 @@ async def resolve( self, case_uuid: str, *, - resolution: Literal["FALSE_POSITIVE", "VALID_THREAT"] | str, + resolution: Literal[ + "FALSE_POSITIVE", + "VALID_THREAT", + "INCONCLUSIVE", + "REQUIREMENTS_NOT_MET", + "REQUIREMENTS_SATISFIED", + ] + | str, note: str | None | NotGiven = NOT_GIVEN, + checker: str | None | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -1688,7 +1729,7 @@ async def resolve( ) -> CaseDetail: """Resolve case. - Resolve the case as false_positive or valid_threat. Blocked with 400 if the blueprint's checklist is not optional and items remain incomplete, or if the blueprint requires a resolution note and none is given. When the case's blueprint has a four_eyes_checker_blueprint configured, the resolution is staged (case.pending_resolution) and the case moves to the checker blueprint instead of resolving immediately; a different user must call approve or reject-approval. + Resolve the case with one of five resolutions: FALSE_POSITIVE (no concern found), VALID_THREAT (concern confirmed), INCONCLUSIVE, REQUIREMENTS_NOT_MET or REQUIREMENTS_SATISFIED. Blocked with 400 if the blueprint's checklist is not optional and items remain incomplete, or if the blueprint requires a resolution note and none is given. When the case's blueprint has a four-eyes checker step (four_eyes_checker_blueprint), the resolution is staged (case.pending_resolution) and the case moves to the checker blueprint instead of resolving immediately; a different officer must call approve or reject-approval. Submit a four-eyes resolution as a console member: a resolution an API key submits has no recorded maker, so approve and reject-approval return 403 for every caller. On a four-eyes blueprint you can name the member who must approve with `checker`: then only that member can approve or reject the resolution, and the response's `checker` names them. Without `checker`, the resolution waits on the checker blueprint's queue. Sending `checker` for a blueprint without a checker step returns 400. ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/resolve/`` @@ -1696,6 +1737,7 @@ async def resolve( case_uuid: Case UUID. resolution note: Required when the blueprint sets require_resolution_note. + checker: Four-eyes blueprints only. The user uuid of the member who must approve this resolution (the same id as `assigned_to.uuid` on a case, or `members[].user_id` on a review team). The member must have case access to this application and cannot be the officer who submits the resolution. Omit it or send null to leave the resolution to the checker queue. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -1710,7 +1752,7 @@ async def resolve( path_params={"case_uuid": case_uuid}, scope="application", auth="api_key", - body={"resolution": resolution, "note": note}, + body={"resolution": resolution, "note": note, "checker": checker}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -1884,7 +1926,7 @@ async def approve_resolution( ) -> CaseDetail: """Approve staged resolution (4-eyes). - Apply a resolution staged by resolve on a maker blueprint. The caller must be a different user than the one who submitted it, otherwise 403. + Apply a resolution staged by resolve on a four-eyes blueprint. The caller must be a different officer than the one who submitted it, otherwise 403. When the resolution names a checker (the case's `checker`), only that member can approve it: any other caller, including an API key, gets 403. ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/approve/`` @@ -1923,7 +1965,7 @@ async def reject_resolution( ) -> CaseDetail: """Reject staged resolution (4-eyes). - Reject a resolution staged by resolve and return the case to its maker blueprint. The caller must be a different user than the one who submitted it, otherwise 403. + Reject a resolution staged by resolve and return the case to its maker blueprint. The caller must be a different officer than the one who submitted it, otherwise 403. When the resolution names a checker (the case's `checker`), only that member can reject it: any other caller, including an API key, gets 403. ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reject-approval/`` @@ -2098,7 +2140,7 @@ async def list_aml_hits( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> builtins.list[builtins.dict[str, object]]: + ) -> builtins.list[AMLScreening]: """List case AML hits. List AML screening hits for the case's subject. Same shape as the hits returned by POST /v3/aml/. @@ -2113,11 +2155,11 @@ async def list_aml_hits( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "builtins.list[builtins.dict[str, object]]", + "builtins.list[AMLScreening]", await self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/aml/", - cast_to=builtins.list[builtins.dict[str, object]], + cast_to=builtins.list[AMLScreening], path_params={"case_uuid": case_uuid}, scope="application", auth="api_key", @@ -2193,13 +2235,21 @@ async def bulk_update_status( *, case_uuids: Sequence[str], status: Literal["OPEN", "AWAITING_USER", "RESOLVED"] | str, - resolution: Literal["FALSE_POSITIVE", "VALID_THREAT"] | str | NotGiven = NOT_GIVEN, + resolution: Literal[ + "FALSE_POSITIVE", + "VALID_THREAT", + "INCONCLUSIVE", + "REQUIREMENTS_NOT_MET", + "REQUIREMENTS_SATISFIED", + ] + | str + | NotGiven = NOT_GIVEN, note: str | None | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> CasesBulkUpdateStatusResponse: + ) -> CaseBulkUpdateResult: """Bulk update case status. Apply the same status transition to several cases in one call: OPEN reopens, AWAITING_USER requests info, RESOLVED resolves (resolution required). Each case is evaluated independently; gate failures (incomplete checklist, missing required note) are reported per case without aborting the batch. @@ -2217,11 +2267,11 @@ async def bulk_update_status( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "CasesBulkUpdateStatusResponse", + "CaseBulkUpdateResult", await self._client.request( "post", "/v3/organization/{organization_id}/application/{application_id}/cases/bulk/status/", - cast_to=CasesBulkUpdateStatusResponse, + cast_to=CaseBulkUpdateResult, scope="application", auth="api_key", body={ @@ -2248,7 +2298,7 @@ async def bulk_assign( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> CasesBulkAssignResponse: + ) -> CaseBulkUpdateResult: """Bulk assign cases. Assign (or unassign, with assignee_user_id: null) several cases at once. All cases must share the same blueprint; mismatches are reported per case without aborting the batch. @@ -2266,11 +2316,11 @@ async def bulk_assign( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "CasesBulkAssignResponse", + "CaseBulkUpdateResult", await self._client.request( "post", "/v3/organization/{organization_id}/application/{application_id}/cases/bulk/assign/", - cast_to=CasesBulkAssignResponse, + cast_to=CaseBulkUpdateResult, scope="application", auth="api_key", body={ diff --git a/src/didit/resources/cases/blueprints.py b/src/didit/resources/cases/blueprints.py index 3613ea2..4cc9c03 100644 --- a/src/didit/resources/cases/blueprints.py +++ b/src/didit/resources/cases/blueprints.py @@ -17,13 +17,13 @@ to_streamed_response_wrapper, ) from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query -from didit.types.cases.blueprints.install_presets_response import ( - CasesBlueprintsInstallPresetsResponse, -) from didit.types.cases.blueprints.list_presets_response import CasesBlueprintsListPresetsResponse from didit.types.cases.blueprints.list_response import CasesBlueprintsListResponse from didit.types.components.case_blueprint_detail import CaseBlueprintDetail from didit.types.components.case_blueprint_list_item import CaseBlueprintListItem +from didit.types.components.case_blueprint_preset_install_result import ( + CaseBlueprintPresetInstallResult, +) from didit.types.components.case_content_config import CaseContentConfigParam from didit.types.components.case_publication_case_blueprint import CasePublicationCaseBlueprint from didit.types.components.case_publication_paginated_policy_revision_list import ( @@ -382,7 +382,7 @@ def install_presets( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> CasesBlueprintsInstallPresetsResponse: + ) -> CaseBlueprintPresetInstallResult: """Install blueprint presets. Copy one or more presets into the application as editable blueprints. Idempotent per preset_key; omit preset_keys to install every preset not yet installed. @@ -397,11 +397,11 @@ def install_presets( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "CasesBlueprintsInstallPresetsResponse", + "CaseBlueprintPresetInstallResult", self._client.request( "post", "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/install/", - cast_to=CasesBlueprintsInstallPresetsResponse, + cast_to=CaseBlueprintPresetInstallResult, scope="application", auth="api_key", body={"preset_keys": preset_keys}, @@ -949,7 +949,7 @@ async def install_presets( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> CasesBlueprintsInstallPresetsResponse: + ) -> CaseBlueprintPresetInstallResult: """Install blueprint presets. Copy one or more presets into the application as editable blueprints. Idempotent per preset_key; omit preset_keys to install every preset not yet installed. @@ -964,11 +964,11 @@ async def install_presets( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "CasesBlueprintsInstallPresetsResponse", + "CaseBlueprintPresetInstallResult", await self._client.request( "post", "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/install/", - cast_to=CasesBlueprintsInstallPresetsResponse, + cast_to=CaseBlueprintPresetInstallResult, scope="application", auth="api_key", body={"preset_keys": preset_keys}, diff --git a/src/didit/resources/cases/regulatory_reports.py b/src/didit/resources/cases/regulatory_reports.py index e23c2d2..b4e1e24 100644 --- a/src/didit/resources/cases/regulatory_reports.py +++ b/src/didit/resources/cases/regulatory_reports.py @@ -21,8 +21,8 @@ from didit.types.cases.regulatory_reports.create_params import ( CasesRegulatoryReportsCreateParamsNarrative, ) -from didit.types.cases.regulatory_reports.list_response import CasesRegulatoryReportsListResponse from didit.types.components.regulatory_report_detail import RegulatoryReportDetail +from didit.types.components.regulatory_report_list import RegulatoryReportList __all__ = [ "CasesRegulatoryReports", @@ -81,7 +81,7 @@ def list( self._client.request_page( "get", "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/regulatory-reports/", - cast_to=CasesRegulatoryReportsListResponse, + cast_to=RegulatoryReportList, item_type=RegulatoryReportDetail, path_params={"case_uuid": case_uuid}, scope="application", @@ -241,7 +241,7 @@ def list( self._client.request_page( "get", "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/regulatory-reports/", - cast_to=CasesRegulatoryReportsListResponse, + cast_to=RegulatoryReportList, item_type=RegulatoryReportDetail, path_params={"case_uuid": case_uuid}, scope="application", diff --git a/src/didit/resources/cases/requests.py b/src/didit/resources/cases/requests.py index db5cfa0..e0dc479 100644 --- a/src/didit/resources/cases/requests.py +++ b/src/didit/resources/cases/requests.py @@ -18,6 +18,7 @@ to_streamed_response_wrapper, ) from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.cases.requests.remind_response import CasesRequestsRemindResponse from didit.types.components.customer_messaging_case_request import CustomerMessagingCaseRequest from didit.types.components.customer_messaging_case_request_options import ( CustomerMessagingCaseRequestOptions, @@ -44,7 +45,7 @@ class CasesRequests(SyncAPIResource): """The ``cases.requests`` resource of the Didit API. - Methods: ``cancel``, ``create``, ``list``, ``preview``, ``retrieve_attachment``, ``retrieve_options``. + Methods: ``cancel``, ``create``, ``list``, ``preview``, ``remind``, ``retrieve_attachment``, ``retrieve_options``. """ @cached_property @@ -305,11 +306,51 @@ def preview( ), ) + def remind( + self, + case_uuid: str, + request_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesRequestsRemindResponse: + """Remind the customer of a request. + + Send a waiting customer request's email again, to the same verified recipient, with the same content. You can remind a request at most once every 24 hours. A reminder changes nothing else: the request keeps its deadline and the case keeps its status. Each reminder is logged on the case as a CUSTOMER_REQUEST event with action `reminded`. The response's `delivery_status` is how this reminder's delivery ended: SENT, SIMULATED (the email was not sent), or UNKNOWN (the email might have been accepted; it is not sent again automatically). `next_reminder_at` is when you can remind this request again. Returns 409 with a `reason` when the customer no longer has the request to answer, and 429 with `retry_at` and a `Retry-After` header when the request was reminded in the last 24 hours. Requires `write:cases`. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/remind/`` + + Args: + case_uuid: Case UUID. + request_uuid: Customer request UUID, from the case's request list. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesRequestsRemindResponse", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/remind/", + cast_to=CasesRequestsRemindResponse, + path_params={"case_uuid": case_uuid, "request_uuid": request_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + class AsyncCasesRequests(AsyncAPIResource): """The ``cases.requests`` resource of the Didit API. - Methods: ``cancel``, ``create``, ``list``, ``preview``, ``retrieve_attachment``, ``retrieve_options``. + Methods: ``cancel``, ``create``, ``list``, ``preview``, ``remind``, ``retrieve_attachment``, ``retrieve_options``. """ @cached_property @@ -570,6 +611,46 @@ async def preview( ), ) + async def remind( + self, + case_uuid: str, + request_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesRequestsRemindResponse: + """Remind the customer of a request. + + Send a waiting customer request's email again, to the same verified recipient, with the same content. You can remind a request at most once every 24 hours. A reminder changes nothing else: the request keeps its deadline and the case keeps its status. Each reminder is logged on the case as a CUSTOMER_REQUEST event with action `reminded`. The response's `delivery_status` is how this reminder's delivery ended: SENT, SIMULATED (the email was not sent), or UNKNOWN (the email might have been accepted; it is not sent again automatically). `next_reminder_at` is when you can remind this request again. Returns 409 with a `reason` when the customer no longer has the request to answer, and 429 with `retry_at` and a `Retry-After` header when the request was reminded in the last 24 hours. Requires `write:cases`. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/remind/`` + + Args: + case_uuid: Case UUID. + request_uuid: Customer request UUID, from the case's request list. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesRequestsRemindResponse", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/remind/", + cast_to=CasesRequestsRemindResponse, + path_params={"case_uuid": case_uuid, "request_uuid": request_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + class CasesRequestsWithRawResponse: def __init__(self, resource: CasesRequests) -> None: @@ -580,6 +661,7 @@ def __init__(self, resource: CasesRequests) -> None: self.retrieve_options = to_raw_response_wrapper(resource.retrieve_options) self.retrieve_attachment = to_raw_response_wrapper(resource.retrieve_attachment) self.preview = to_raw_response_wrapper(resource.preview) + self.remind = to_raw_response_wrapper(resource.remind) class AsyncCasesRequestsWithRawResponse: @@ -591,6 +673,7 @@ def __init__(self, resource: AsyncCasesRequests) -> None: self.retrieve_options = async_to_raw_response_wrapper(resource.retrieve_options) self.retrieve_attachment = async_to_raw_response_wrapper(resource.retrieve_attachment) self.preview = async_to_raw_response_wrapper(resource.preview) + self.remind = async_to_raw_response_wrapper(resource.remind) class CasesRequestsWithStreamingResponse: @@ -602,6 +685,7 @@ def __init__(self, resource: CasesRequests) -> None: self.retrieve_options = to_streamed_response_wrapper(resource.retrieve_options) self.retrieve_attachment = to_streamed_response_wrapper(resource.retrieve_attachment) self.preview = to_streamed_response_wrapper(resource.preview) + self.remind = to_streamed_response_wrapper(resource.remind) class AsyncCasesRequestsWithStreamingResponse: @@ -613,3 +697,4 @@ def __init__(self, resource: AsyncCasesRequests) -> None: self.retrieve_options = async_to_streamed_response_wrapper(resource.retrieve_options) self.retrieve_attachment = async_to_streamed_response_wrapper(resource.retrieve_attachment) self.preview = async_to_streamed_response_wrapper(resource.preview) + self.remind = async_to_streamed_response_wrapper(resource.remind) diff --git a/src/didit/resources/customization.py b/src/didit/resources/customization.py index d9eab2a..7dc6cbc 100644 --- a/src/didit/resources/customization.py +++ b/src/didit/resources/customization.py @@ -106,9 +106,11 @@ def update( callback_seconds: int | NotGiven = NOT_GIVEN, delete_logo_square: bool | NotGiven = NOT_GIVEN, delete_logo_rectangular: bool | NotGiven = NOT_GIVEN, + delete_logo_rectangular_dark: bool | NotGiven = NOT_GIVEN, delete_favicon: bool | NotGiven = NOT_GIVEN, image_square: FileTypes | NotGiven = NOT_GIVEN, image_rectangular: FileTypes | NotGiven = NOT_GIVEN, + image_rectangular_dark: FileTypes | NotGiven = NOT_GIVEN, image_favicon: FileTypes | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, @@ -147,9 +149,11 @@ def update( callback_seconds: Delay (seconds) before redirecting to the callback URL. delete_logo_square: When `true`, removes the current square logo. delete_logo_rectangular: When `true`, removes the current rectangular logo. + delete_logo_rectangular_dark: When `true`, removes the current dark-mode logo, and `logo_rectangular_dark` then reads `null`. If the same multipart request also uploads `image_rectangular_dark`, the upload wins. delete_favicon: When `true`, removes the current favicon. - image_square: Square logo (1:1 aspect ratio). Max **1 MB**. Allowed: `png`, `jpg`, `jpeg`, `webp`. - image_rectangular: Rectangular logo (2:1 aspect ratio). Max **1 MB**. Allowed: `png`, `jpg`, `jpeg`, `webp`. + image_square: Square logo (1:1 aspect ratio). PNG, JPG or WebP, at least 512 px on the long side, at most **1 MB**. + image_rectangular: Rectangular logo (2:1 aspect ratio). PNG, JPG or WebP, at least 512 px on the long side, at most **1 MB**. + image_rectangular_dark: Rectangular logo for dark mode (2:1 aspect ratio), shown on a dark background. PNG, JPG or WebP, at least 512 px on the long side, at most **1 MB**. Returned as `logo_rectangular_dark`. image_favicon: Favicon image. Max **500 KB**. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. @@ -189,15 +193,18 @@ def update( "callback_seconds": callback_seconds, "delete_logo_square": delete_logo_square, "delete_logo_rectangular": delete_logo_rectangular, + "delete_logo_rectangular_dark": delete_logo_rectangular_dark, "delete_favicon": delete_favicon, "image_square": image_square, "image_rectangular": image_rectangular, + "image_rectangular_dark": image_rectangular_dark, "image_favicon": image_favicon, }, multipart="auto", file_fields=( "image_favicon", "image_rectangular", + "image_rectangular_dark", "image_square", ), extra_headers=extra_headers, @@ -286,9 +293,11 @@ async def update( callback_seconds: int | NotGiven = NOT_GIVEN, delete_logo_square: bool | NotGiven = NOT_GIVEN, delete_logo_rectangular: bool | NotGiven = NOT_GIVEN, + delete_logo_rectangular_dark: bool | NotGiven = NOT_GIVEN, delete_favicon: bool | NotGiven = NOT_GIVEN, image_square: FileTypes | NotGiven = NOT_GIVEN, image_rectangular: FileTypes | NotGiven = NOT_GIVEN, + image_rectangular_dark: FileTypes | NotGiven = NOT_GIVEN, image_favicon: FileTypes | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, @@ -327,9 +336,11 @@ async def update( callback_seconds: Delay (seconds) before redirecting to the callback URL. delete_logo_square: When `true`, removes the current square logo. delete_logo_rectangular: When `true`, removes the current rectangular logo. + delete_logo_rectangular_dark: When `true`, removes the current dark-mode logo, and `logo_rectangular_dark` then reads `null`. If the same multipart request also uploads `image_rectangular_dark`, the upload wins. delete_favicon: When `true`, removes the current favicon. - image_square: Square logo (1:1 aspect ratio). Max **1 MB**. Allowed: `png`, `jpg`, `jpeg`, `webp`. - image_rectangular: Rectangular logo (2:1 aspect ratio). Max **1 MB**. Allowed: `png`, `jpg`, `jpeg`, `webp`. + image_square: Square logo (1:1 aspect ratio). PNG, JPG or WebP, at least 512 px on the long side, at most **1 MB**. + image_rectangular: Rectangular logo (2:1 aspect ratio). PNG, JPG or WebP, at least 512 px on the long side, at most **1 MB**. + image_rectangular_dark: Rectangular logo for dark mode (2:1 aspect ratio), shown on a dark background. PNG, JPG or WebP, at least 512 px on the long side, at most **1 MB**. Returned as `logo_rectangular_dark`. image_favicon: Favicon image. Max **500 KB**. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. @@ -369,15 +380,18 @@ async def update( "callback_seconds": callback_seconds, "delete_logo_square": delete_logo_square, "delete_logo_rectangular": delete_logo_rectangular, + "delete_logo_rectangular_dark": delete_logo_rectangular_dark, "delete_favicon": delete_favicon, "image_square": image_square, "image_rectangular": image_rectangular, + "image_rectangular_dark": image_rectangular_dark, "image_favicon": image_favicon, }, multipart="auto", file_fields=( "image_favicon", "image_rectangular", + "image_rectangular_dark", "image_square", ), extra_headers=extra_headers, diff --git a/src/didit/resources/investigator.py b/src/didit/resources/investigator.py new file mode 100644 index 0000000..3799f7e --- /dev/null +++ b/src/didit/resources/investigator.py @@ -0,0 +1,1404 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``investigator`` resource.""" + +from __future__ import annotations + +import builtins +import datetime as _dt +from functools import cached_property +from typing import Literal, Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.investigator_batch_accept_response import ( + InvestigatorBatchAcceptResponse, +) +from didit.types.components.investigator_decision import InvestigatorDecision +from didit.types.components.investigator_queue_row import InvestigatorQueueRow +from didit.types.components.investigator_run_detail import InvestigatorRunDetail +from didit.types.components.investigator_settings import InvestigatorSettings +from didit.types.investigator.list_activity_response import InvestigatorListActivityResponse +from didit.types.investigator.list_guardrails_response import InvestigatorListGuardrailsResponse +from didit.types.investigator.list_queue_response import InvestigatorListQueueResponse +from didit.types.investigator.retrieve_dashboard_response import ( + InvestigatorRetrieveDashboardResponse, +) +from didit.types.investigator.retrieve_performance_response import ( + InvestigatorRetrievePerformanceResponse, +) +from didit.types.investigator.retrieve_summary_response import InvestigatorRetrieveSummaryResponse +from didit.types.investigator.retrieve_transaction_response import ( + InvestigatorRetrieveTransactionResponse, +) + +__all__ = [ + "Investigator", + "AsyncInvestigator", + "InvestigatorWithRawResponse", + "AsyncInvestigatorWithRawResponse", + "InvestigatorWithStreamingResponse", + "AsyncInvestigatorWithStreamingResponse", +] + + +class Investigator(SyncAPIResource): + """The ``investigator`` resource of the Didit API. + + Methods: ``accept_run``, ``batch_accept_runs``, ``export_activity``, ``list_activity``, ``list_guardrails``, ``list_queue``, ``override_run``, ``retrieve_dashboard``, ``retrieve_performance``, ``retrieve_run``, ``retrieve_settings``, ``retrieve_summary``, ``retrieve_transaction``, ``update_settings``. + """ + + @cached_property + def with_raw_response(self) -> InvestigatorWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return InvestigatorWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> InvestigatorWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return InvestigatorWithStreamingResponse(self) + + def retrieve_summary( + self, + *, + scope: Literal["all", "identity", "transactions"] | str | NotGiven = NOT_GIVEN, + timezone: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorRetrieveSummaryResponse: + """Investigator summary. + + Stage, queue counts, reports prepared and decided today, 30-day agreement, prices and the autonomy level of each queue. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`. + + ``GET /v3/investigator/summary/`` + + Args: + scope: `identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened). + timezone: IANA time zone (for example `Europe/Madrid`) that "today" counts from. UTC by default. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorRetrieveSummaryResponse", + self._client.request( + "get", + "/v3/investigator/summary/", + cast_to=InvestigatorRetrieveSummaryResponse, + auth="api_key", + query={"scope": scope, "timezone": timezone}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_queue( + self, + *, + scope: Literal["all", "identity", "transactions"] | str | NotGiven = NOT_GIVEN, + kind: str | NotGiven = NOT_GIVEN, + queue: Literal["kyc", "kyb", "aml", "transactions", "cases"] | str | NotGiven = NOT_GIVEN, + recommendation: Literal["approve", "decline", "escalate", "request"] + | str + | NotGiven = NOT_GIVEN, + confidence: Literal["high", "moderate", "low"] | str | NotGiven = NOT_GIVEN, + ready: bool | NotGiven = NOT_GIVEN, + search: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[InvestigatorQueueRow]: + """List the Investigator queue. + + Open reports, **Ready to accept** first, then by deadline, risk and wait. `counts` covers every kind in scope. `count` is capped at 100. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`. + + ``GET /v3/investigator/queue/`` + + Args: + scope: `identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened). + kind: `session`, `business_session`, `transaction` or `case`, or several separated by commas. + queue + recommendation + confidence + ready + search: Name, company, case title, number, transaction ID or item ID. + limit + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[InvestigatorQueueRow]", + self._client.request_page( + "get", + "/v3/investigator/queue/", + cast_to=InvestigatorListQueueResponse, + item_type=InvestigatorQueueRow, + auth="api_key", + query={ + "scope": scope, + "kind": kind, + "queue": queue, + "recommendation": recommendation, + "confidence": confidence, + "ready": ready, + "search": search, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_run( + self, + run_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorRunDetail: + """Get an Investigator report. + + The full report of one run. 404 when the run has no completed report, or its item was deleted. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`. + + ``GET /v3/investigator/runs/{run_id}/`` + + Args: + run_id + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorRunDetail", + self._client.request( + "get", + "/v3/investigator/runs/{run_id}/", + cast_to=InvestigatorRunDetail, + path_params={"run_id": run_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def accept_run( + self, + run_id: str, + *, + narrative: str | NotGiven = NOT_GIVEN, + send_email: bool | NotGiven = NOT_GIVEN, + email_address: str | NotGiven = NOT_GIVEN, + email_language: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorDecision: + """Accept a recommendation. + + Applies the recommended action through the item's normal path, so your usual webhooks fire. A `narrative` makes it edited and accepted. Restricted keys need the `decisions` scope (write); transaction alerts also need `transactions` (write). Escalating a verification or transaction opens a case. + + ``POST /v3/investigator/runs/{run_id}/accept/`` + + Args: + run_id + narrative: A rewritten narrative. Sending one records the decision as edited and accepted. + send_email: Email the person about the decision. + email_address: Required when `send_email` is true. + email_language + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorDecision", + self._client.request( + "post", + "/v3/investigator/runs/{run_id}/accept/", + cast_to=InvestigatorDecision, + path_params={"run_id": run_id}, + auth="api_key", + body={ + "narrative": narrative, + "send_email": send_email, + "email_address": email_address, + "email_language": email_language, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def override_run( + self, + run_id: str, + *, + action: Literal["approve", "decline", "escalate", "request"] | str, + category: Literal[ + "missing_context", "needs_information", "policy_differs", "evidence_misread", "other" + ] + | str, + reason: str, + send_email: bool | NotGiven = NOT_GIVEN, + email_address: str | NotGiven = NOT_GIVEN, + email_language: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorDecision: + """Override a recommendation. + + Takes another action with a category and a reason. Also overrides a decision the Investigator took on its own while it still stands. Restricted keys need the `decisions` scope (write); transaction alerts also need `transactions` (write). Escalating a verification or transaction opens a case. + + ``POST /v3/investigator/runs/{run_id}/override/`` + + Args: + run_id + action + category + reason + send_email: Email the person about the decision. + email_address: Required when `send_email` is true. + email_language + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorDecision", + self._client.request( + "post", + "/v3/investigator/runs/{run_id}/override/", + cast_to=InvestigatorDecision, + path_params={"run_id": run_id}, + auth="api_key", + body={ + "action": action, + "category": category, + "reason": reason, + "send_email": send_email, + "email_address": email_address, + "email_language": email_language, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def batch_accept_runs( + self, + *, + run_ids: Sequence[str], + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorBatchAcceptResponse: + """Batch accept recommendations. + + Accepts up to 50 reports marked Ready to accept. Each is its own decision: one that cannot be accepted is skipped with its reason. Restricted keys need the `decisions` scope (write); transaction alerts also need `transactions` (write). Escalating a verification or transaction opens a case. + + ``POST /v3/investigator/runs/batch-accept/`` + + Args: + run_ids + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorBatchAcceptResponse", + self._client.request( + "post", + "/v3/investigator/runs/batch-accept/", + cast_to=InvestigatorBatchAcceptResponse, + auth="api_key", + body={"run_ids": run_ids}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_transaction( + self, + transaction_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorRetrieveTransactionResponse: + """Get a transaction's Investigator review. + + The transaction's latest review, or `investigator: null` when it never had one. Restricted keys need the `decisions` and `transactions` scopes (read). + + ``GET /v3/investigator/transactions/{transaction_id}/`` + + Args: + transaction_id + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorRetrieveTransactionResponse", + self._client.request( + "get", + "/v3/investigator/transactions/{transaction_id}/", + cast_to=InvestigatorRetrieveTransactionResponse, + path_params={"transaction_id": transaction_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_guardrails( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorListGuardrailsResponse: + """List the locked guardrails. + + The five guardrails that apply to every queue. They cannot be switched off. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`. + + ``GET /v3/investigator/guardrails/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorListGuardrailsResponse", + self._client.request( + "get", + "/v3/investigator/guardrails/", + cast_to=InvestigatorListGuardrailsResponse, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_dashboard( + self, + *, + scope: Literal["all", "identity", "transactions"] | str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorRetrieveDashboardResponse: + """Investigator dashboard. + + The last 30 days: reviews, decisions, automation rate, hours saved, spend, overrides, quality samples since go-live, per-queue and weekly figures. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`. + + ``GET /v3/investigator/dashboard/`` + + Args: + scope: `identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorRetrieveDashboardResponse", + self._client.request( + "get", + "/v3/investigator/dashboard/", + cast_to=InvestigatorRetrieveDashboardResponse, + auth="api_key", + query={"scope": scope}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_performance( + self, + *, + scope: Literal["all", "identity", "transactions"] | str | NotGiven = NOT_GIVEN, + period: Literal["7", "30", "90"] | str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorRetrievePerformanceResponse: + """Investigator performance. + + Agreement with your team, volume, time saved, confidence calibration, per-queue results, override reasons and disagreements for a period. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`. + + ``GET /v3/investigator/performance/`` + + Args: + scope: `identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened). + period: Days, ending today. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorRetrievePerformanceResponse", + self._client.request( + "get", + "/v3/investigator/performance/", + cast_to=InvestigatorRetrievePerformanceResponse, + auth="api_key", + query={"scope": scope, "period": period}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_activity( + self, + *, + scope: Literal["all", "identity", "transactions"] | str | NotGiven = NOT_GIVEN, + group: Literal["agent", "decisions", "overrides", "autonomy"] | str | NotGiven = NOT_GIVEN, + actor: str | NotGiven = NOT_GIVEN, + since: str | _dt.datetime | NotGiven = NOT_GIVEN, + until: str | _dt.datetime | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[builtins.dict[str, object]]: + """List Investigator activity. + + The append-only audit trail, newest first. A batch accept is one entry. Adds `counts` per group and `actors`. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`. + + ``GET /v3/investigator/activity/`` + + Args: + scope: `identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened). + group + actor: `agent` for the Investigator, or a person's email. + since + until + limit + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[builtins.dict[str, object]]", + self._client.request_page( + "get", + "/v3/investigator/activity/", + cast_to=InvestigatorListActivityResponse, + item_type=builtins.dict[str, object], + auth="api_key", + query={ + "scope": scope, + "group": group, + "actor": actor, + "since": since, + "until": until, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def export_activity( + self, + *, + scope: Literal["all", "identity", "transactions"] | str | NotGiven = NOT_GIVEN, + group: Literal["agent", "decisions", "overrides", "autonomy"] | str | NotGiven = NOT_GIVEN, + actor: str | NotGiven = NOT_GIVEN, + since: str | _dt.datetime | NotGiven = NOT_GIVEN, + until: str | _dt.datetime | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Export Investigator activity. + + The same entries as CSV, one row per decision, newest first, up to 10,000 entries. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`. + + ``GET /v3/investigator/activity/export/`` + + Args: + scope: `identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened). + group + actor: `agent` for the Investigator, or a person's email. + since + until + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + self._client.request( + "get", + "/v3/investigator/activity/export/", + cast_to=bytes, + auth="api_key", + query={ + "scope": scope, + "group": group, + "actor": actor, + "since": since, + "until": until, + }, + accept="text/csv, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_settings( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorSettings: + """Get Investigator settings. + + The stage (`none`, `shadow` or `live`) and the monthly spend limit with this month's spend. Restricted keys need the `applications` scope (read). + + ``GET /v3/investigator/settings/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorSettings", + self._client.request( + "get", + "/v3/investigator/settings/", + cast_to=InvestigatorSettings, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update_settings( + self, + *, + stage: Literal["none", "shadow", "live"] | str | NotGiven = NOT_GIVEN, + monthly_spend_limit: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorSettings: + """Update Investigator settings. + + Change the stage, the monthly spend limit, or both. Moving past `none` needs a plan that includes the Investigator (Growth or higher); `live` also needs a contract that prices it, if you have one. Restricted keys need the `applications` scope (write). + + ``PATCH /v3/investigator/settings/`` + + Args: + stage + monthly_spend_limit: Monthly limit in your billing currency, 0.01 to 1,000,000. Null turns it off. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorSettings", + self._client.request( + "patch", + "/v3/investigator/settings/", + cast_to=InvestigatorSettings, + auth="api_key", + body={"stage": stage, "monthly_spend_limit": monthly_spend_limit}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncInvestigator(AsyncAPIResource): + """The ``investigator`` resource of the Didit API. + + Methods: ``accept_run``, ``batch_accept_runs``, ``export_activity``, ``list_activity``, ``list_guardrails``, ``list_queue``, ``override_run``, ``retrieve_dashboard``, ``retrieve_performance``, ``retrieve_run``, ``retrieve_settings``, ``retrieve_summary``, ``retrieve_transaction``, ``update_settings``. + """ + + @cached_property + def with_raw_response(self) -> AsyncInvestigatorWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncInvestigatorWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncInvestigatorWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncInvestigatorWithStreamingResponse(self) + + async def retrieve_summary( + self, + *, + scope: Literal["all", "identity", "transactions"] | str | NotGiven = NOT_GIVEN, + timezone: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorRetrieveSummaryResponse: + """Investigator summary. + + Stage, queue counts, reports prepared and decided today, 30-day agreement, prices and the autonomy level of each queue. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`. + + ``GET /v3/investigator/summary/`` + + Args: + scope: `identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened). + timezone: IANA time zone (for example `Europe/Madrid`) that "today" counts from. UTC by default. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorRetrieveSummaryResponse", + await self._client.request( + "get", + "/v3/investigator/summary/", + cast_to=InvestigatorRetrieveSummaryResponse, + auth="api_key", + query={"scope": scope, "timezone": timezone}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_queue( + self, + *, + scope: Literal["all", "identity", "transactions"] | str | NotGiven = NOT_GIVEN, + kind: str | NotGiven = NOT_GIVEN, + queue: Literal["kyc", "kyb", "aml", "transactions", "cases"] | str | NotGiven = NOT_GIVEN, + recommendation: Literal["approve", "decline", "escalate", "request"] + | str + | NotGiven = NOT_GIVEN, + confidence: Literal["high", "moderate", "low"] | str | NotGiven = NOT_GIVEN, + ready: bool | NotGiven = NOT_GIVEN, + search: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[InvestigatorQueueRow]: + """List the Investigator queue. + + Open reports, **Ready to accept** first, then by deadline, risk and wait. `counts` covers every kind in scope. `count` is capped at 100. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`. + + ``GET /v3/investigator/queue/`` + + Args: + scope: `identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened). + kind: `session`, `business_session`, `transaction` or `case`, or several separated by commas. + queue + recommendation + confidence + ready + search: Name, company, case title, number, transaction ID or item ID. + limit + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[InvestigatorQueueRow]", + self._client.request_page( + "get", + "/v3/investigator/queue/", + cast_to=InvestigatorListQueueResponse, + item_type=InvestigatorQueueRow, + auth="api_key", + query={ + "scope": scope, + "kind": kind, + "queue": queue, + "recommendation": recommendation, + "confidence": confidence, + "ready": ready, + "search": search, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_run( + self, + run_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorRunDetail: + """Get an Investigator report. + + The full report of one run. 404 when the run has no completed report, or its item was deleted. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`. + + ``GET /v3/investigator/runs/{run_id}/`` + + Args: + run_id + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorRunDetail", + await self._client.request( + "get", + "/v3/investigator/runs/{run_id}/", + cast_to=InvestigatorRunDetail, + path_params={"run_id": run_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def accept_run( + self, + run_id: str, + *, + narrative: str | NotGiven = NOT_GIVEN, + send_email: bool | NotGiven = NOT_GIVEN, + email_address: str | NotGiven = NOT_GIVEN, + email_language: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorDecision: + """Accept a recommendation. + + Applies the recommended action through the item's normal path, so your usual webhooks fire. A `narrative` makes it edited and accepted. Restricted keys need the `decisions` scope (write); transaction alerts also need `transactions` (write). Escalating a verification or transaction opens a case. + + ``POST /v3/investigator/runs/{run_id}/accept/`` + + Args: + run_id + narrative: A rewritten narrative. Sending one records the decision as edited and accepted. + send_email: Email the person about the decision. + email_address: Required when `send_email` is true. + email_language + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorDecision", + await self._client.request( + "post", + "/v3/investigator/runs/{run_id}/accept/", + cast_to=InvestigatorDecision, + path_params={"run_id": run_id}, + auth="api_key", + body={ + "narrative": narrative, + "send_email": send_email, + "email_address": email_address, + "email_language": email_language, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def override_run( + self, + run_id: str, + *, + action: Literal["approve", "decline", "escalate", "request"] | str, + category: Literal[ + "missing_context", "needs_information", "policy_differs", "evidence_misread", "other" + ] + | str, + reason: str, + send_email: bool | NotGiven = NOT_GIVEN, + email_address: str | NotGiven = NOT_GIVEN, + email_language: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorDecision: + """Override a recommendation. + + Takes another action with a category and a reason. Also overrides a decision the Investigator took on its own while it still stands. Restricted keys need the `decisions` scope (write); transaction alerts also need `transactions` (write). Escalating a verification or transaction opens a case. + + ``POST /v3/investigator/runs/{run_id}/override/`` + + Args: + run_id + action + category + reason + send_email: Email the person about the decision. + email_address: Required when `send_email` is true. + email_language + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorDecision", + await self._client.request( + "post", + "/v3/investigator/runs/{run_id}/override/", + cast_to=InvestigatorDecision, + path_params={"run_id": run_id}, + auth="api_key", + body={ + "action": action, + "category": category, + "reason": reason, + "send_email": send_email, + "email_address": email_address, + "email_language": email_language, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def batch_accept_runs( + self, + *, + run_ids: Sequence[str], + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorBatchAcceptResponse: + """Batch accept recommendations. + + Accepts up to 50 reports marked Ready to accept. Each is its own decision: one that cannot be accepted is skipped with its reason. Restricted keys need the `decisions` scope (write); transaction alerts also need `transactions` (write). Escalating a verification or transaction opens a case. + + ``POST /v3/investigator/runs/batch-accept/`` + + Args: + run_ids + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorBatchAcceptResponse", + await self._client.request( + "post", + "/v3/investigator/runs/batch-accept/", + cast_to=InvestigatorBatchAcceptResponse, + auth="api_key", + body={"run_ids": run_ids}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_transaction( + self, + transaction_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorRetrieveTransactionResponse: + """Get a transaction's Investigator review. + + The transaction's latest review, or `investigator: null` when it never had one. Restricted keys need the `decisions` and `transactions` scopes (read). + + ``GET /v3/investigator/transactions/{transaction_id}/`` + + Args: + transaction_id + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorRetrieveTransactionResponse", + await self._client.request( + "get", + "/v3/investigator/transactions/{transaction_id}/", + cast_to=InvestigatorRetrieveTransactionResponse, + path_params={"transaction_id": transaction_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_guardrails( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorListGuardrailsResponse: + """List the locked guardrails. + + The five guardrails that apply to every queue. They cannot be switched off. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`. + + ``GET /v3/investigator/guardrails/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorListGuardrailsResponse", + await self._client.request( + "get", + "/v3/investigator/guardrails/", + cast_to=InvestigatorListGuardrailsResponse, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_dashboard( + self, + *, + scope: Literal["all", "identity", "transactions"] | str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorRetrieveDashboardResponse: + """Investigator dashboard. + + The last 30 days: reviews, decisions, automation rate, hours saved, spend, overrides, quality samples since go-live, per-queue and weekly figures. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`. + + ``GET /v3/investigator/dashboard/`` + + Args: + scope: `identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorRetrieveDashboardResponse", + await self._client.request( + "get", + "/v3/investigator/dashboard/", + cast_to=InvestigatorRetrieveDashboardResponse, + auth="api_key", + query={"scope": scope}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_performance( + self, + *, + scope: Literal["all", "identity", "transactions"] | str | NotGiven = NOT_GIVEN, + period: Literal["7", "30", "90"] | str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorRetrievePerformanceResponse: + """Investigator performance. + + Agreement with your team, volume, time saved, confidence calibration, per-queue results, override reasons and disagreements for a period. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`. + + ``GET /v3/investigator/performance/`` + + Args: + scope: `identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened). + period: Days, ending today. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorRetrievePerformanceResponse", + await self._client.request( + "get", + "/v3/investigator/performance/", + cast_to=InvestigatorRetrievePerformanceResponse, + auth="api_key", + query={"scope": scope, "period": period}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_activity( + self, + *, + scope: Literal["all", "identity", "transactions"] | str | NotGiven = NOT_GIVEN, + group: Literal["agent", "decisions", "overrides", "autonomy"] | str | NotGiven = NOT_GIVEN, + actor: str | NotGiven = NOT_GIVEN, + since: str | _dt.datetime | NotGiven = NOT_GIVEN, + until: str | _dt.datetime | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[builtins.dict[str, object]]: + """List Investigator activity. + + The append-only audit trail, newest first. A batch accept is one entry. Adds `counts` per group and `actors`. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`. + + ``GET /v3/investigator/activity/`` + + Args: + scope: `identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened). + group + actor: `agent` for the Investigator, or a person's email. + since + until + limit + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[builtins.dict[str, object]]", + self._client.request_page( + "get", + "/v3/investigator/activity/", + cast_to=InvestigatorListActivityResponse, + item_type=builtins.dict[str, object], + auth="api_key", + query={ + "scope": scope, + "group": group, + "actor": actor, + "since": since, + "until": until, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def export_activity( + self, + *, + scope: Literal["all", "identity", "transactions"] | str | NotGiven = NOT_GIVEN, + group: Literal["agent", "decisions", "overrides", "autonomy"] | str | NotGiven = NOT_GIVEN, + actor: str | NotGiven = NOT_GIVEN, + since: str | _dt.datetime | NotGiven = NOT_GIVEN, + until: str | _dt.datetime | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Export Investigator activity. + + The same entries as CSV, one row per decision, newest first, up to 10,000 entries. Restricted keys need the `decisions` scope (read); transaction alerts also need `transactions`. + + ``GET /v3/investigator/activity/export/`` + + Args: + scope: `identity` (sessions, and cases not opened by a transaction rule) or `transactions` (transaction alerts, and cases a transaction rule opened). + group + actor: `agent` for the Investigator, or a person's email. + since + until + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + await self._client.request( + "get", + "/v3/investigator/activity/export/", + cast_to=bytes, + auth="api_key", + query={ + "scope": scope, + "group": group, + "actor": actor, + "since": since, + "until": until, + }, + accept="text/csv, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_settings( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorSettings: + """Get Investigator settings. + + The stage (`none`, `shadow` or `live`) and the monthly spend limit with this month's spend. Restricted keys need the `applications` scope (read). + + ``GET /v3/investigator/settings/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorSettings", + await self._client.request( + "get", + "/v3/investigator/settings/", + cast_to=InvestigatorSettings, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_settings( + self, + *, + stage: Literal["none", "shadow", "live"] | str | NotGiven = NOT_GIVEN, + monthly_spend_limit: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> InvestigatorSettings: + """Update Investigator settings. + + Change the stage, the monthly spend limit, or both. Moving past `none` needs a plan that includes the Investigator (Growth or higher); `live` also needs a contract that prices it, if you have one. Restricted keys need the `applications` scope (write). + + ``PATCH /v3/investigator/settings/`` + + Args: + stage + monthly_spend_limit: Monthly limit in your billing currency, 0.01 to 1,000,000. Null turns it off. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "InvestigatorSettings", + await self._client.request( + "patch", + "/v3/investigator/settings/", + cast_to=InvestigatorSettings, + auth="api_key", + body={"stage": stage, "monthly_spend_limit": monthly_spend_limit}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class InvestigatorWithRawResponse: + def __init__(self, resource: Investigator) -> None: + self._resource = resource + self.retrieve_summary = to_raw_response_wrapper(resource.retrieve_summary) + self.list_queue = to_raw_response_wrapper(resource.list_queue) + self.retrieve_run = to_raw_response_wrapper(resource.retrieve_run) + self.accept_run = to_raw_response_wrapper(resource.accept_run) + self.override_run = to_raw_response_wrapper(resource.override_run) + self.batch_accept_runs = to_raw_response_wrapper(resource.batch_accept_runs) + self.retrieve_transaction = to_raw_response_wrapper(resource.retrieve_transaction) + self.list_guardrails = to_raw_response_wrapper(resource.list_guardrails) + self.retrieve_dashboard = to_raw_response_wrapper(resource.retrieve_dashboard) + self.retrieve_performance = to_raw_response_wrapper(resource.retrieve_performance) + self.list_activity = to_raw_response_wrapper(resource.list_activity) + self.export_activity = to_raw_response_wrapper(resource.export_activity) + self.retrieve_settings = to_raw_response_wrapper(resource.retrieve_settings) + self.update_settings = to_raw_response_wrapper(resource.update_settings) + + +class AsyncInvestigatorWithRawResponse: + def __init__(self, resource: AsyncInvestigator) -> None: + self._resource = resource + self.retrieve_summary = async_to_raw_response_wrapper(resource.retrieve_summary) + self.list_queue = async_to_raw_response_wrapper(resource.list_queue) + self.retrieve_run = async_to_raw_response_wrapper(resource.retrieve_run) + self.accept_run = async_to_raw_response_wrapper(resource.accept_run) + self.override_run = async_to_raw_response_wrapper(resource.override_run) + self.batch_accept_runs = async_to_raw_response_wrapper(resource.batch_accept_runs) + self.retrieve_transaction = async_to_raw_response_wrapper(resource.retrieve_transaction) + self.list_guardrails = async_to_raw_response_wrapper(resource.list_guardrails) + self.retrieve_dashboard = async_to_raw_response_wrapper(resource.retrieve_dashboard) + self.retrieve_performance = async_to_raw_response_wrapper(resource.retrieve_performance) + self.list_activity = async_to_raw_response_wrapper(resource.list_activity) + self.export_activity = async_to_raw_response_wrapper(resource.export_activity) + self.retrieve_settings = async_to_raw_response_wrapper(resource.retrieve_settings) + self.update_settings = async_to_raw_response_wrapper(resource.update_settings) + + +class InvestigatorWithStreamingResponse: + def __init__(self, resource: Investigator) -> None: + self._resource = resource + self.retrieve_summary = to_streamed_response_wrapper(resource.retrieve_summary) + self.list_queue = to_streamed_response_wrapper(resource.list_queue) + self.retrieve_run = to_streamed_response_wrapper(resource.retrieve_run) + self.accept_run = to_streamed_response_wrapper(resource.accept_run) + self.override_run = to_streamed_response_wrapper(resource.override_run) + self.batch_accept_runs = to_streamed_response_wrapper(resource.batch_accept_runs) + self.retrieve_transaction = to_streamed_response_wrapper(resource.retrieve_transaction) + self.list_guardrails = to_streamed_response_wrapper(resource.list_guardrails) + self.retrieve_dashboard = to_streamed_response_wrapper(resource.retrieve_dashboard) + self.retrieve_performance = to_streamed_response_wrapper(resource.retrieve_performance) + self.list_activity = to_streamed_response_wrapper(resource.list_activity) + self.export_activity = to_streamed_response_wrapper(resource.export_activity) + self.retrieve_settings = to_streamed_response_wrapper(resource.retrieve_settings) + self.update_settings = to_streamed_response_wrapper(resource.update_settings) + + +class AsyncInvestigatorWithStreamingResponse: + def __init__(self, resource: AsyncInvestigator) -> None: + self._resource = resource + self.retrieve_summary = async_to_streamed_response_wrapper(resource.retrieve_summary) + self.list_queue = async_to_streamed_response_wrapper(resource.list_queue) + self.retrieve_run = async_to_streamed_response_wrapper(resource.retrieve_run) + self.accept_run = async_to_streamed_response_wrapper(resource.accept_run) + self.override_run = async_to_streamed_response_wrapper(resource.override_run) + self.batch_accept_runs = async_to_streamed_response_wrapper(resource.batch_accept_runs) + self.retrieve_transaction = async_to_streamed_response_wrapper( + resource.retrieve_transaction + ) + self.list_guardrails = async_to_streamed_response_wrapper(resource.list_guardrails) + self.retrieve_dashboard = async_to_streamed_response_wrapper(resource.retrieve_dashboard) + self.retrieve_performance = async_to_streamed_response_wrapper( + resource.retrieve_performance + ) + self.list_activity = async_to_streamed_response_wrapper(resource.list_activity) + self.export_activity = async_to_streamed_response_wrapper(resource.export_activity) + self.retrieve_settings = async_to_streamed_response_wrapper(resource.retrieve_settings) + self.update_settings = async_to_streamed_response_wrapper(resource.update_settings) diff --git a/src/didit/resources/kyb.py b/src/didit/resources/kyb.py index fe58951..0c3a86e 100644 --- a/src/didit/resources/kyb.py +++ b/src/didit/resources/kyb.py @@ -56,7 +56,8 @@ def search( country_code: str, name: str | NotGiven = NOT_GIVEN, registration_number: str | NotGiven = NOT_GIVEN, - search_type: Literal["contains", "start_with", "fuzzy"] | str | NotGiven = NOT_GIVEN, + tier: Literal["basic", "shareholders", "ubo"] | str | NotGiven = NOT_GIVEN, + search_type: Literal["contains"] | str | NotGiven = NOT_GIVEN, vendor_data: str | NotGiven = NOT_GIVEN, metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, webhook_url: str | NotGiven = NOT_GIVEN, @@ -73,6 +74,10 @@ def search( **`kyb_response_id` is ephemeral.** It is a per-search handle, not a stable company id. Select promptly after searching and re-search to get a fresh handle instead of storing it. + **`tier` picks the provider chain.** `tier` names the registry tier the search runs on: `basic` (the default, the Lite chain), `shareholders` or `ubo`. The search uses the provider chain that serves that tier, and it is not always the same chain the Lite search reads, so an empty Lite search is not proof that the registry does not hold the company - search again at the tier you need. A country that does not offer the tier you name is searched at the highest tier it offers up to that one. Retrieve the candidate from the endpoint that sells that tier: `POST /v3/kyb/select/` (Lite), `POST /v3/kyb/shareholders/` or `POST /v3/kyb/ubo/`. + + **Authentication.** `POST /v3/kyb/search/` (and `/v2/`) takes your application's API key in the `x-api-key` header. The hosted `POST /v1/kyb/search/` is the session-authenticated route a hosted verification flow calls: it answers `403` to an API key and takes no `tier` - the workflow's KYB registry configuration picks the chain. + **Two modes.** - *Synchronous (default):* the API polls the registry until the search resolves and returns the final candidate list (`search_resolved: true`). - *Asynchronous:* pass `webhook_url` and the API returns immediately - usually with `search_status: "pending"` and an empty candidate list - then POSTs a `kyb.registry_search.resolved` callback to your URL when the registry finishes. The callback body is `{"event_id", "event_type": "kyb.registry_search.resolved", "request_id", "vendor_data", "metadata", "search_status", "search_resolved", "kyb_registry", "timestamp", "created_at"}` - note `timestamp` and `created_at` in the callback are epoch integers, unlike this endpoint's ISO `created_at` - and is sent **unsigned** (header `X-Didit-Unsigned-Callback: true`, no `X-Signature`). @@ -87,7 +92,8 @@ def search( country_code: ISO 3166-1 alpha-2 code of the country whose company registry to search. For example, `GB`. Where company registries operate at state or province level, append the ISO 3166-2 subdivision code as `XX-YY`: `US-CA` searches the California registry. United States searches always require the subdivision - a bare `US` is rejected. Case-insensitive; normalized to upper case. name: Company name to search for. Either `name` or `registration_number` is required. registration_number: Registry registration number to search for. Either `name` or `registration_number` is required. - search_type: Name-matching strategy applied by the registry search. + tier: Registry tier whose provider chain this search runs on. `basic` (default) searches the country's Lite chain; `shareholders` and `ubo` search that tier's ownership chain, which can be a different source, so an empty Lite search does not prove the registry lacks the company. A country that does not offer the named tier is searched at the highest tier it offers up to that one. Retrieve the candidate from the endpoint that sells the tier you searched: `POST /v3/kyb/select/` (Lite), `POST /v3/kyb/shareholders/` or `POST /v3/kyb/ubo/`. + search_type: Name-matching strategy applied by the registry search. `contains` is the only accepted value: no registry source behind the search offers a prefix or fuzzy mode, so `start_with` and `fuzzy` are rejected with `400` (`not a valid choice`). vendor_data: Your identifier for this lookup; echoed back and included in the webhook callback. metadata: Free-form JSON; echoed back and included in the webhook callback. webhook_url: Switches the search to asynchronous mode: the API returns immediately and POSTs an unsigned `kyb.registry_search.resolved` callback to this URL when the candidate list is ready. @@ -107,6 +113,7 @@ def search( "country_code": country_code, "name": name, "registration_number": registration_number, + "tier": tier, "search_type": search_type, "vendor_data": vendor_data, "metadata": metadata, @@ -308,7 +315,8 @@ async def search( country_code: str, name: str | NotGiven = NOT_GIVEN, registration_number: str | NotGiven = NOT_GIVEN, - search_type: Literal["contains", "start_with", "fuzzy"] | str | NotGiven = NOT_GIVEN, + tier: Literal["basic", "shareholders", "ubo"] | str | NotGiven = NOT_GIVEN, + search_type: Literal["contains"] | str | NotGiven = NOT_GIVEN, vendor_data: str | NotGiven = NOT_GIVEN, metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, webhook_url: str | NotGiven = NOT_GIVEN, @@ -325,6 +333,10 @@ async def search( **`kyb_response_id` is ephemeral.** It is a per-search handle, not a stable company id. Select promptly after searching and re-search to get a fresh handle instead of storing it. + **`tier` picks the provider chain.** `tier` names the registry tier the search runs on: `basic` (the default, the Lite chain), `shareholders` or `ubo`. The search uses the provider chain that serves that tier, and it is not always the same chain the Lite search reads, so an empty Lite search is not proof that the registry does not hold the company - search again at the tier you need. A country that does not offer the tier you name is searched at the highest tier it offers up to that one. Retrieve the candidate from the endpoint that sells that tier: `POST /v3/kyb/select/` (Lite), `POST /v3/kyb/shareholders/` or `POST /v3/kyb/ubo/`. + + **Authentication.** `POST /v3/kyb/search/` (and `/v2/`) takes your application's API key in the `x-api-key` header. The hosted `POST /v1/kyb/search/` is the session-authenticated route a hosted verification flow calls: it answers `403` to an API key and takes no `tier` - the workflow's KYB registry configuration picks the chain. + **Two modes.** - *Synchronous (default):* the API polls the registry until the search resolves and returns the final candidate list (`search_resolved: true`). - *Asynchronous:* pass `webhook_url` and the API returns immediately - usually with `search_status: "pending"` and an empty candidate list - then POSTs a `kyb.registry_search.resolved` callback to your URL when the registry finishes. The callback body is `{"event_id", "event_type": "kyb.registry_search.resolved", "request_id", "vendor_data", "metadata", "search_status", "search_resolved", "kyb_registry", "timestamp", "created_at"}` - note `timestamp` and `created_at` in the callback are epoch integers, unlike this endpoint's ISO `created_at` - and is sent **unsigned** (header `X-Didit-Unsigned-Callback: true`, no `X-Signature`). @@ -339,7 +351,8 @@ async def search( country_code: ISO 3166-1 alpha-2 code of the country whose company registry to search. For example, `GB`. Where company registries operate at state or province level, append the ISO 3166-2 subdivision code as `XX-YY`: `US-CA` searches the California registry. United States searches always require the subdivision - a bare `US` is rejected. Case-insensitive; normalized to upper case. name: Company name to search for. Either `name` or `registration_number` is required. registration_number: Registry registration number to search for. Either `name` or `registration_number` is required. - search_type: Name-matching strategy applied by the registry search. + tier: Registry tier whose provider chain this search runs on. `basic` (default) searches the country's Lite chain; `shareholders` and `ubo` search that tier's ownership chain, which can be a different source, so an empty Lite search does not prove the registry lacks the company. A country that does not offer the named tier is searched at the highest tier it offers up to that one. Retrieve the candidate from the endpoint that sells the tier you searched: `POST /v3/kyb/select/` (Lite), `POST /v3/kyb/shareholders/` or `POST /v3/kyb/ubo/`. + search_type: Name-matching strategy applied by the registry search. `contains` is the only accepted value: no registry source behind the search offers a prefix or fuzzy mode, so `start_with` and `fuzzy` are rejected with `400` (`not a valid choice`). vendor_data: Your identifier for this lookup; echoed back and included in the webhook callback. metadata: Free-form JSON; echoed back and included in the webhook callback. webhook_url: Switches the search to asynchronous mode: the API returns immediately and POSTs an unsigned `kyb.registry_search.resolved` callback to this URL when the candidate list is ready. @@ -359,6 +372,7 @@ async def search( "country_code": country_code, "name": name, "registration_number": registration_number, + "tier": tier, "search_type": search_type, "vendor_data": vendor_data, "metadata": metadata, diff --git a/src/didit/resources/lists/__init__.py b/src/didit/resources/lists/__init__.py new file mode 100644 index 0000000..d3d5057 --- /dev/null +++ b/src/didit/resources/lists/__init__.py @@ -0,0 +1,645 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``lists`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, Literal, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.compliance_list import ComplianceList +from didit.types.lists.list_response import ListsListResponse +from didit.types.lists.update_response import ListsUpdateResponse + +if TYPE_CHECKING: + from didit.resources.lists.entries import ( + AsyncListsEntries, + AsyncListsEntriesWithRawResponse, + AsyncListsEntriesWithStreamingResponse, + ListsEntries, + ListsEntriesWithRawResponse, + ListsEntriesWithStreamingResponse, + ) + +__all__ = [ + "Lists", + "AsyncLists", + "ListsWithRawResponse", + "AsyncListsWithRawResponse", + "ListsWithStreamingResponse", + "AsyncListsWithStreamingResponse", +] + + +class Lists(SyncAPIResource): + """The ``lists`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``, ``retrieve``, ``update``. + + Nested resources: ``entries``. + """ + + @cached_property + def entries(self) -> ListsEntries: + from didit.resources.lists.entries import ListsEntries + + return ListsEntries(self._client) + + @cached_property + def with_raw_response(self) -> ListsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return ListsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ListsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return ListsWithStreamingResponse(self) + + def list( + self, + *, + list_type: Literal["blocklist", "allowlist", "custom"] | str | NotGiven = NOT_GIVEN, + entry_type: Literal[ + "face", + "document", + "phone", + "email", + "ip_address", + "device_fingerprint", + "wallet_address", + "bank_account", + "country", + "user", + "business", + "key", + ] + | str + | NotGiven = NOT_GIVEN, + ordering: Literal["-created_at", "created_at"] | str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[ComplianceList]: + """List lists. + + Returns the application's lists, newest first: the system lists Didit provisions (one blocklist per entry type, the **High Risk Countries** custom list, and any system allowlist) and the allowlists and custom lists you created. Filter with `list_type` and `entry_type`; deleted lists never appear. Results are paginated with `limit` and `offset`. A scoped API key needs `workflows: read`. See [Lists](https://docs.didit.me/management-api/lists/overview) for what each list type does. + + ``GET /v3/lists/`` + + Args: + list_type: Only lists of this type. + entry_type: Only lists of this entry type. + ordering: Sort order: `-created_at` (newest first, the default) or `created_at` (oldest first). + limit: Page size. Defaults to 50. A value above 1000000 returns `400`; zero, a negative or a non-numeric value falls back to 50. + offset: Zero-based index of the first item to return. Prefer following `next`. A value above 1000000 returns `400`; a negative or non-numeric value falls back to 0. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[ComplianceList]", + self._client.request_page( + "get", + "/v3/lists/", + cast_to=ListsListResponse, + item_type=ComplianceList, + auth="api_key", + query={ + "list_type": list_type, + "entry_type": entry_type, + "ordering": ordering, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + name: str, + list_type: Literal["allowlist", "custom"] | str, + entry_type: Literal[ + "face", + "document", + "phone", + "email", + "ip_address", + "device_fingerprint", + "wallet_address", + "bank_account", + "country", + "user", + "business", + "key", + ] + | str, + description: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ComplianceList: + """Create a list. + + Creates an allowlist or a custom list in the application the API key belongs to. Blocklists cannot be created: Didit provisions one system blocklist per entry type, and you add to it with [Create a list entry](https://docs.didit.me/management-api/lists/create-entry). `entry_type` cannot change after creation and decides how entry values are validated and matched. Allowlist entries mark matching items as trusted (for example, a face allowlist skips the duplicated-face action) and never override a blocklist match; reference any list from a workflow or transaction rule condition with `value_type: "list"`. A scoped API key needs `workflows: write`. + + ``POST /v3/lists/`` + + Args: + name: List name. Must differ from the name of every other list of the application, system lists included. + list_type: `allowlist` or `custom`. `blocklist` is refused with `400`. + entry_type: What the list holds: `face`, `document` (document number), `phone`, `email`, `ip_address` (IP address or CIDR range), `device_fingerprint`, `wallet_address`, `bank_account`, `country` (ISO 3166-1 alpha-3), `user` or `business` (your `vendor_data`), or `key` (any string). It decides how entry values are validated and matched. Cannot be changed later. + description: Optional free-text description. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ComplianceList", + self._client.request( + "post", + "/v3/lists/", + cast_to=ComplianceList, + auth="api_key", + body={ + "name": name, + "list_type": list_type, + "entry_type": entry_type, + "description": description, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + list_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ComplianceList: + """Get a list. + + Returns one list of the application with its entry count and when its newest entry was added. A list of another application, or one that was deleted, returns `404`. This returns the list only: read its values with [List list entries](https://docs.didit.me/management-api/lists/list-entries). A scoped API key needs `workflows: read`. + + ``GET /v3/lists/{list_uuid}/`` + + Args: + list_uuid: UUID of the list, as returned by List lists or Create a list. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ComplianceList", + self._client.request( + "get", + "/v3/lists/{list_uuid}/", + cast_to=ComplianceList, + path_params={"list_uuid": list_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + list_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete a list. + + Deletes an allowlist or custom list together with its entries. Deletion is irreversible: the list and its entries no longer appear in any read, and workflow and transaction rule conditions that reference the list evaluate it as an empty list from then on. Sessions and transactions already evaluated keep their results, and the name can be reused for a new list. System lists cannot be deleted and return `400`; to empty one, [delete its entries](https://docs.didit.me/management-api/lists/delete-entry). A scoped API key needs `workflows: write`. + + ``DELETE /v3/lists/{list_uuid}/`` + + Args: + list_uuid: UUID of the list, as returned by List lists or Create a list. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/lists/{list_uuid}/", + cast_to=None, + path_params={"list_uuid": list_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + list_uuid: str, + *, + name: str | NotGiven = NOT_GIVEN, + description: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ListsUpdateResponse: + """Update a list. + + Renames a list or changes its description. Only `name` and `description` are read; any other field is ignored, so `list_type` and `entry_type` keep the values they were created with. System lists cannot be modified and return `400`. The response contains only `name` and `description`: call [Get a list](https://docs.didit.me/management-api/lists/get) for the full list. A scoped API key needs `workflows: write`. + + ``PATCH /v3/lists/{list_uuid}/`` + + Args: + list_uuid: UUID of the list, as returned by List lists or Create a list. + name: New name. Must differ from the name of every other list of the application. + description: New description. Send an empty string to clear it. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ListsUpdateResponse", + self._client.request( + "patch", + "/v3/lists/{list_uuid}/", + cast_to=ListsUpdateResponse, + path_params={"list_uuid": list_uuid}, + auth="api_key", + body={"name": name, "description": description}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncLists(AsyncAPIResource): + """The ``lists`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``, ``retrieve``, ``update``. + + Nested resources: ``entries``. + """ + + @cached_property + def entries(self) -> AsyncListsEntries: + from didit.resources.lists.entries import AsyncListsEntries + + return AsyncListsEntries(self._client) + + @cached_property + def with_raw_response(self) -> AsyncListsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncListsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncListsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncListsWithStreamingResponse(self) + + def list( + self, + *, + list_type: Literal["blocklist", "allowlist", "custom"] | str | NotGiven = NOT_GIVEN, + entry_type: Literal[ + "face", + "document", + "phone", + "email", + "ip_address", + "device_fingerprint", + "wallet_address", + "bank_account", + "country", + "user", + "business", + "key", + ] + | str + | NotGiven = NOT_GIVEN, + ordering: Literal["-created_at", "created_at"] | str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[ComplianceList]: + """List lists. + + Returns the application's lists, newest first: the system lists Didit provisions (one blocklist per entry type, the **High Risk Countries** custom list, and any system allowlist) and the allowlists and custom lists you created. Filter with `list_type` and `entry_type`; deleted lists never appear. Results are paginated with `limit` and `offset`. A scoped API key needs `workflows: read`. See [Lists](https://docs.didit.me/management-api/lists/overview) for what each list type does. + + ``GET /v3/lists/`` + + Args: + list_type: Only lists of this type. + entry_type: Only lists of this entry type. + ordering: Sort order: `-created_at` (newest first, the default) or `created_at` (oldest first). + limit: Page size. Defaults to 50. A value above 1000000 returns `400`; zero, a negative or a non-numeric value falls back to 50. + offset: Zero-based index of the first item to return. Prefer following `next`. A value above 1000000 returns `400`; a negative or non-numeric value falls back to 0. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[ComplianceList]", + self._client.request_page( + "get", + "/v3/lists/", + cast_to=ListsListResponse, + item_type=ComplianceList, + auth="api_key", + query={ + "list_type": list_type, + "entry_type": entry_type, + "ordering": ordering, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + name: str, + list_type: Literal["allowlist", "custom"] | str, + entry_type: Literal[ + "face", + "document", + "phone", + "email", + "ip_address", + "device_fingerprint", + "wallet_address", + "bank_account", + "country", + "user", + "business", + "key", + ] + | str, + description: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ComplianceList: + """Create a list. + + Creates an allowlist or a custom list in the application the API key belongs to. Blocklists cannot be created: Didit provisions one system blocklist per entry type, and you add to it with [Create a list entry](https://docs.didit.me/management-api/lists/create-entry). `entry_type` cannot change after creation and decides how entry values are validated and matched. Allowlist entries mark matching items as trusted (for example, a face allowlist skips the duplicated-face action) and never override a blocklist match; reference any list from a workflow or transaction rule condition with `value_type: "list"`. A scoped API key needs `workflows: write`. + + ``POST /v3/lists/`` + + Args: + name: List name. Must differ from the name of every other list of the application, system lists included. + list_type: `allowlist` or `custom`. `blocklist` is refused with `400`. + entry_type: What the list holds: `face`, `document` (document number), `phone`, `email`, `ip_address` (IP address or CIDR range), `device_fingerprint`, `wallet_address`, `bank_account`, `country` (ISO 3166-1 alpha-3), `user` or `business` (your `vendor_data`), or `key` (any string). It decides how entry values are validated and matched. Cannot be changed later. + description: Optional free-text description. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ComplianceList", + await self._client.request( + "post", + "/v3/lists/", + cast_to=ComplianceList, + auth="api_key", + body={ + "name": name, + "list_type": list_type, + "entry_type": entry_type, + "description": description, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + list_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ComplianceList: + """Get a list. + + Returns one list of the application with its entry count and when its newest entry was added. A list of another application, or one that was deleted, returns `404`. This returns the list only: read its values with [List list entries](https://docs.didit.me/management-api/lists/list-entries). A scoped API key needs `workflows: read`. + + ``GET /v3/lists/{list_uuid}/`` + + Args: + list_uuid: UUID of the list, as returned by List lists or Create a list. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ComplianceList", + await self._client.request( + "get", + "/v3/lists/{list_uuid}/", + cast_to=ComplianceList, + path_params={"list_uuid": list_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + list_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete a list. + + Deletes an allowlist or custom list together with its entries. Deletion is irreversible: the list and its entries no longer appear in any read, and workflow and transaction rule conditions that reference the list evaluate it as an empty list from then on. Sessions and transactions already evaluated keep their results, and the name can be reused for a new list. System lists cannot be deleted and return `400`; to empty one, [delete its entries](https://docs.didit.me/management-api/lists/delete-entry). A scoped API key needs `workflows: write`. + + ``DELETE /v3/lists/{list_uuid}/`` + + Args: + list_uuid: UUID of the list, as returned by List lists or Create a list. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/lists/{list_uuid}/", + cast_to=None, + path_params={"list_uuid": list_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + list_uuid: str, + *, + name: str | NotGiven = NOT_GIVEN, + description: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ListsUpdateResponse: + """Update a list. + + Renames a list or changes its description. Only `name` and `description` are read; any other field is ignored, so `list_type` and `entry_type` keep the values they were created with. System lists cannot be modified and return `400`. The response contains only `name` and `description`: call [Get a list](https://docs.didit.me/management-api/lists/get) for the full list. A scoped API key needs `workflows: write`. + + ``PATCH /v3/lists/{list_uuid}/`` + + Args: + list_uuid: UUID of the list, as returned by List lists or Create a list. + name: New name. Must differ from the name of every other list of the application. + description: New description. Send an empty string to clear it. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ListsUpdateResponse", + await self._client.request( + "patch", + "/v3/lists/{list_uuid}/", + cast_to=ListsUpdateResponse, + path_params={"list_uuid": list_uuid}, + auth="api_key", + body={"name": name, "description": description}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class ListsWithRawResponse: + def __init__(self, resource: Lists) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.delete = to_raw_response_wrapper(resource.delete) + self.update = to_raw_response_wrapper(resource.update) + + @cached_property + def entries(self) -> ListsEntriesWithRawResponse: + from didit.resources.lists.entries import ListsEntriesWithRawResponse + + return ListsEntriesWithRawResponse(self._resource.entries) + + +class AsyncListsWithRawResponse: + def __init__(self, resource: AsyncLists) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.delete = async_to_raw_response_wrapper(resource.delete) + self.update = async_to_raw_response_wrapper(resource.update) + + @cached_property + def entries(self) -> AsyncListsEntriesWithRawResponse: + from didit.resources.lists.entries import AsyncListsEntriesWithRawResponse + + return AsyncListsEntriesWithRawResponse(self._resource.entries) + + +class ListsWithStreamingResponse: + def __init__(self, resource: Lists) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.delete = to_streamed_response_wrapper(resource.delete) + self.update = to_streamed_response_wrapper(resource.update) + + @cached_property + def entries(self) -> ListsEntriesWithStreamingResponse: + from didit.resources.lists.entries import ListsEntriesWithStreamingResponse + + return ListsEntriesWithStreamingResponse(self._resource.entries) + + +class AsyncListsWithStreamingResponse: + def __init__(self, resource: AsyncLists) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.delete = async_to_streamed_response_wrapper(resource.delete) + self.update = async_to_streamed_response_wrapper(resource.update) + + @cached_property + def entries(self) -> AsyncListsEntriesWithStreamingResponse: + from didit.resources.lists.entries import AsyncListsEntriesWithStreamingResponse + + return AsyncListsEntriesWithStreamingResponse(self._resource.entries) diff --git a/src/didit/resources/lists/entries.py b/src/didit/resources/lists/entries.py new file mode 100644 index 0000000..b2343f3 --- /dev/null +++ b/src/didit/resources/lists/entries.py @@ -0,0 +1,501 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``lists.entries`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.compliance_list_entry import ComplianceListEntry +from didit.types.lists.entries.create_params import ListsEntriesCreateParamsMetadata +from didit.types.lists.entries.list_response import ListsEntriesListResponse + +__all__ = [ + "ListsEntries", + "AsyncListsEntries", + "ListsEntriesWithRawResponse", + "AsyncListsEntriesWithRawResponse", + "ListsEntriesWithStreamingResponse", + "AsyncListsEntriesWithStreamingResponse", +] + + +class ListsEntries(SyncAPIResource): + """The ``lists.entries`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``, ``upload_face``. + """ + + @cached_property + def with_raw_response(self) -> ListsEntriesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return ListsEntriesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ListsEntriesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return ListsEntriesWithStreamingResponse(self) + + def list( + self, + list_uuid: str, + *, + search: str | NotGiven = NOT_GIVEN, + reference_session_id: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[ComplianceListEntry]: + """List list entries. + + Returns the current entries of one list, newest first. `search` matches part of an entry's `value` or `display_label` without regard to case, and `reference_session_id` narrows the results to the entries taken from one person verification session. Image URLs in `metadata` are short-lived signed URLs issued again on every call; a scoped API key without `media: read` receives `null` in their place. A scoped API key needs `workflows: read`. + + ``GET /v3/lists/{list_uuid}/entries/`` + + Args: + list_uuid: UUID of the list, as returned by List lists or Create a list. + search: Case-insensitive text that the entry's `value` or `display_label` must contain. + reference_session_id: Only the entries taken from this person verification session, for example to see which of its faces are already listed. + limit: Page size. Defaults to 50. A value above 1000000 returns `400`; zero, a negative or a non-numeric value falls back to 50. + offset: Zero-based index of the first item to return. Prefer following `next`. A value above 1000000 returns `400`; a negative or non-numeric value falls back to 0. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[ComplianceListEntry]", + self._client.request_page( + "get", + "/v3/lists/{list_uuid}/entries/", + cast_to=ListsEntriesListResponse, + item_type=ComplianceListEntry, + path_params={"list_uuid": list_uuid}, + auth="api_key", + query={ + "search": search, + "reference_session_id": reference_session_id, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + list_uuid: str, + *, + value: str | NotGiven = NOT_GIVEN, + reference_session_id: str | NotGiven = NOT_GIVEN, + reference_object_uuid: str | NotGiven = NOT_GIVEN, + face_image_source: Literal[ + "liveness", "front_image_camera_front", "back_image_camera_front" + ] + | str + | NotGiven = NOT_GIVEN, + reference_node_id: str | None | NotGiven = NOT_GIVEN, + display_label: str | NotGiven = NOT_GIVEN, + comment: str | NotGiven = NOT_GIVEN, + metadata: ListsEntriesCreateParamsMetadata | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ComplianceListEntry: + """Create a list entry. + + Adds one entry to a list, in one of three ways: by `value`; from a verification session with `reference_session_id`, where Didit reads the value from the session's most recent record of the list's entry type and links the entry to it; or by `value` with a `reference_object_uuid` and `metadata.reference_type` pointing back to a transaction, user or business. On the system face, document, phone and email blocklists, the record read from the session is also flagged as blocklisted, so new matching sessions are declined; on the system user and business blocklists, matching users or businesses are set to `BLOCKED`. Sessions and transactions that already finished are not changed. A scoped API key needs `workflows: write`. See [Create a list entry](https://docs.didit.me/management-api/lists/create-entry) for each mode. + + ``POST /v3/lists/{list_uuid}/entries/`` + + Args: + list_uuid: UUID of the list to add to. For a face you only have as an image, use Upload a face to a list instead. + value: The value to list, validated against the list's `entry_type`: `email` a valid email address; `phone` a valid international number, stored in E.164; `ip_address` an IP address or CIDR range (for example `10.0.0.0/8`); `device_fingerprint` at least 8 characters among letters, digits, `-`, `_`, `:` and `.` (a blocklist refuses a fingerprint that several distinct devices have reported); `country` an ISO 3166-1 alpha-2 or alpha-3 code, stored as alpha-3; `user` and `business` the `vendor_data`; any other type a non-empty string. Stored in lower case except phone numbers and country codes, and a list holds each value once (case-insensitive). Required unless `reference_session_id` is sent; must be left empty with a document-capture `face_image_source`. + reference_session_id: A person or business verification session of your application to take the value from. Didit reads the session's most recent record of the list's entry type (`face`, `document`, `phone`, `email`, `ip_address` or `device_fingerprint`), links the entry to the session and to that record, and fills `display_label` and `metadata` for documents. For a `device_fingerprint` blocklist, a session that only has a device-class fingerprint, or was completed on more than one device, is refused: send the exact fingerprint as `value`. For other entry types, or when the session has no such record, send `value` too; your `value` is then stored and the entry is still linked to the session. + reference_object_uuid: UUID of the record the entry stands for when it does not come from a session: a transaction UUID, or the `didit_internal_id` of a user or business. Pair it with `metadata.reference_type`. Replaced by the session's record when `reference_session_id` provides one. + face_image_source: Face lists with `reference_session_id`: which of the session's images the face is taken from. `liveness` (default) is the liveness selfie. `front_image_camera_front` and `back_image_camera_front` are the selfies the front camera took while the user photographed the front or back of their document: accepted only on the system face blocklist, with `reference_session_id` and an empty `value`. Each selfie can be added once and must show one clear face. + reference_node_id: With a document-capture `face_image_source`: the ID verification node whose selfie to use, for workflows with more than one. Defaults to the most recent ID verification that has that image. Refused with `liveness`. + display_label: Human-readable label. When empty, Didit derives one from the session where it can (for example the holder's name and document number). + comment: Free-text note, such as the reason for listing. + metadata: Free-form JSON stored with the entry and returned as stored. Use `reference_type` and `reference_id` so the console links back to the source. `reference_image_url` and `front_image_url` are set by Didit only and are dropped if you send them. For an entry read from a session, Didit's keys are merged with yours. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ComplianceListEntry", + self._client.request( + "post", + "/v3/lists/{list_uuid}/entries/", + cast_to=ComplianceListEntry, + path_params={"list_uuid": list_uuid}, + auth="api_key", + body={ + "value": value, + "reference_session_id": reference_session_id, + "reference_object_uuid": reference_object_uuid, + "face_image_source": face_image_source, + "reference_node_id": reference_node_id, + "display_label": display_label, + "comment": comment, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + list_uuid: str, + entry_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete a list entry. + + Deletes one entry from a list, system lists included. The entry stops matching at once, and deletion is irreversible: to list the value again, create a new entry. On a system face, document, phone or email blocklist, the record the entry stands for is no longer flagged as blocklisted; on the system user or business blocklist, users or businesses of the application whose `vendor_data` equals the entry's value (or whose `didit_internal_id` equals its `reference_object_uuid`) and that are `BLOCKED` return to `ACTIVE`. Sessions and transactions that already finished keep their status. A scoped API key needs `workflows: write`. + + ``DELETE /v3/lists/{list_uuid}/entries/{entry_uuid}/`` + + Args: + list_uuid: UUID of the list that holds the entry. + entry_uuid: UUID of the entry, as returned by List list entries or Create a list entry. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/lists/{list_uuid}/entries/{entry_uuid}/", + cast_to=None, + path_params={"list_uuid": list_uuid, "entry_uuid": entry_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def upload_face( + self, + list_uuid: str, + *, + image: str, + comment: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ComplianceListEntry: + """Upload a face to a list. + + Adds a face to a face blocklist or face allowlist from a base64-encoded JPEG or PNG image, for when you have no verification session to reference. Didit detects the faces in the image, uses the largest one, and creates an entry whose `value` and `reference_object_uuid` are the UUID of the new face. A blocklisted face declines new sessions whose face matches; an allowlisted face skips the duplicated-face action and never overrides a blocklist match. In a sandbox application the image is not analyzed and the entry matches nothing. A scoped API key needs `workflows: write`. To list a face from an existing session, use [Create a list entry](https://docs.didit.me/management-api/lists/create-entry) with `reference_session_id` instead. + + ``POST /v3/lists/{list_uuid}/entries/face-upload/`` + + Args: + list_uuid: UUID of a list with `entry_type: face` and `list_type: blocklist` or `allowlist`. + image: Base64-encoded JPEG or PNG, at most 2 MB once decoded, showing a clearly visible face. Send the bare base64 string, without a `data:image/...;base64,` prefix. + comment: Optional free-text note, such as the reason for listing. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ComplianceListEntry", + self._client.request( + "post", + "/v3/lists/{list_uuid}/entries/face-upload/", + cast_to=ComplianceListEntry, + path_params={"list_uuid": list_uuid}, + auth="api_key", + body={"image": image, "comment": comment}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncListsEntries(AsyncAPIResource): + """The ``lists.entries`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``, ``upload_face``. + """ + + @cached_property + def with_raw_response(self) -> AsyncListsEntriesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncListsEntriesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncListsEntriesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncListsEntriesWithStreamingResponse(self) + + def list( + self, + list_uuid: str, + *, + search: str | NotGiven = NOT_GIVEN, + reference_session_id: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[ComplianceListEntry]: + """List list entries. + + Returns the current entries of one list, newest first. `search` matches part of an entry's `value` or `display_label` without regard to case, and `reference_session_id` narrows the results to the entries taken from one person verification session. Image URLs in `metadata` are short-lived signed URLs issued again on every call; a scoped API key without `media: read` receives `null` in their place. A scoped API key needs `workflows: read`. + + ``GET /v3/lists/{list_uuid}/entries/`` + + Args: + list_uuid: UUID of the list, as returned by List lists or Create a list. + search: Case-insensitive text that the entry's `value` or `display_label` must contain. + reference_session_id: Only the entries taken from this person verification session, for example to see which of its faces are already listed. + limit: Page size. Defaults to 50. A value above 1000000 returns `400`; zero, a negative or a non-numeric value falls back to 50. + offset: Zero-based index of the first item to return. Prefer following `next`. A value above 1000000 returns `400`; a negative or non-numeric value falls back to 0. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[ComplianceListEntry]", + self._client.request_page( + "get", + "/v3/lists/{list_uuid}/entries/", + cast_to=ListsEntriesListResponse, + item_type=ComplianceListEntry, + path_params={"list_uuid": list_uuid}, + auth="api_key", + query={ + "search": search, + "reference_session_id": reference_session_id, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + list_uuid: str, + *, + value: str | NotGiven = NOT_GIVEN, + reference_session_id: str | NotGiven = NOT_GIVEN, + reference_object_uuid: str | NotGiven = NOT_GIVEN, + face_image_source: Literal[ + "liveness", "front_image_camera_front", "back_image_camera_front" + ] + | str + | NotGiven = NOT_GIVEN, + reference_node_id: str | None | NotGiven = NOT_GIVEN, + display_label: str | NotGiven = NOT_GIVEN, + comment: str | NotGiven = NOT_GIVEN, + metadata: ListsEntriesCreateParamsMetadata | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ComplianceListEntry: + """Create a list entry. + + Adds one entry to a list, in one of three ways: by `value`; from a verification session with `reference_session_id`, where Didit reads the value from the session's most recent record of the list's entry type and links the entry to it; or by `value` with a `reference_object_uuid` and `metadata.reference_type` pointing back to a transaction, user or business. On the system face, document, phone and email blocklists, the record read from the session is also flagged as blocklisted, so new matching sessions are declined; on the system user and business blocklists, matching users or businesses are set to `BLOCKED`. Sessions and transactions that already finished are not changed. A scoped API key needs `workflows: write`. See [Create a list entry](https://docs.didit.me/management-api/lists/create-entry) for each mode. + + ``POST /v3/lists/{list_uuid}/entries/`` + + Args: + list_uuid: UUID of the list to add to. For a face you only have as an image, use Upload a face to a list instead. + value: The value to list, validated against the list's `entry_type`: `email` a valid email address; `phone` a valid international number, stored in E.164; `ip_address` an IP address or CIDR range (for example `10.0.0.0/8`); `device_fingerprint` at least 8 characters among letters, digits, `-`, `_`, `:` and `.` (a blocklist refuses a fingerprint that several distinct devices have reported); `country` an ISO 3166-1 alpha-2 or alpha-3 code, stored as alpha-3; `user` and `business` the `vendor_data`; any other type a non-empty string. Stored in lower case except phone numbers and country codes, and a list holds each value once (case-insensitive). Required unless `reference_session_id` is sent; must be left empty with a document-capture `face_image_source`. + reference_session_id: A person or business verification session of your application to take the value from. Didit reads the session's most recent record of the list's entry type (`face`, `document`, `phone`, `email`, `ip_address` or `device_fingerprint`), links the entry to the session and to that record, and fills `display_label` and `metadata` for documents. For a `device_fingerprint` blocklist, a session that only has a device-class fingerprint, or was completed on more than one device, is refused: send the exact fingerprint as `value`. For other entry types, or when the session has no such record, send `value` too; your `value` is then stored and the entry is still linked to the session. + reference_object_uuid: UUID of the record the entry stands for when it does not come from a session: a transaction UUID, or the `didit_internal_id` of a user or business. Pair it with `metadata.reference_type`. Replaced by the session's record when `reference_session_id` provides one. + face_image_source: Face lists with `reference_session_id`: which of the session's images the face is taken from. `liveness` (default) is the liveness selfie. `front_image_camera_front` and `back_image_camera_front` are the selfies the front camera took while the user photographed the front or back of their document: accepted only on the system face blocklist, with `reference_session_id` and an empty `value`. Each selfie can be added once and must show one clear face. + reference_node_id: With a document-capture `face_image_source`: the ID verification node whose selfie to use, for workflows with more than one. Defaults to the most recent ID verification that has that image. Refused with `liveness`. + display_label: Human-readable label. When empty, Didit derives one from the session where it can (for example the holder's name and document number). + comment: Free-text note, such as the reason for listing. + metadata: Free-form JSON stored with the entry and returned as stored. Use `reference_type` and `reference_id` so the console links back to the source. `reference_image_url` and `front_image_url` are set by Didit only and are dropped if you send them. For an entry read from a session, Didit's keys are merged with yours. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ComplianceListEntry", + await self._client.request( + "post", + "/v3/lists/{list_uuid}/entries/", + cast_to=ComplianceListEntry, + path_params={"list_uuid": list_uuid}, + auth="api_key", + body={ + "value": value, + "reference_session_id": reference_session_id, + "reference_object_uuid": reference_object_uuid, + "face_image_source": face_image_source, + "reference_node_id": reference_node_id, + "display_label": display_label, + "comment": comment, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + list_uuid: str, + entry_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete a list entry. + + Deletes one entry from a list, system lists included. The entry stops matching at once, and deletion is irreversible: to list the value again, create a new entry. On a system face, document, phone or email blocklist, the record the entry stands for is no longer flagged as blocklisted; on the system user or business blocklist, users or businesses of the application whose `vendor_data` equals the entry's value (or whose `didit_internal_id` equals its `reference_object_uuid`) and that are `BLOCKED` return to `ACTIVE`. Sessions and transactions that already finished keep their status. A scoped API key needs `workflows: write`. + + ``DELETE /v3/lists/{list_uuid}/entries/{entry_uuid}/`` + + Args: + list_uuid: UUID of the list that holds the entry. + entry_uuid: UUID of the entry, as returned by List list entries or Create a list entry. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/lists/{list_uuid}/entries/{entry_uuid}/", + cast_to=None, + path_params={"list_uuid": list_uuid, "entry_uuid": entry_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def upload_face( + self, + list_uuid: str, + *, + image: str, + comment: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ComplianceListEntry: + """Upload a face to a list. + + Adds a face to a face blocklist or face allowlist from a base64-encoded JPEG or PNG image, for when you have no verification session to reference. Didit detects the faces in the image, uses the largest one, and creates an entry whose `value` and `reference_object_uuid` are the UUID of the new face. A blocklisted face declines new sessions whose face matches; an allowlisted face skips the duplicated-face action and never overrides a blocklist match. In a sandbox application the image is not analyzed and the entry matches nothing. A scoped API key needs `workflows: write`. To list a face from an existing session, use [Create a list entry](https://docs.didit.me/management-api/lists/create-entry) with `reference_session_id` instead. + + ``POST /v3/lists/{list_uuid}/entries/face-upload/`` + + Args: + list_uuid: UUID of a list with `entry_type: face` and `list_type: blocklist` or `allowlist`. + image: Base64-encoded JPEG or PNG, at most 2 MB once decoded, showing a clearly visible face. Send the bare base64 string, without a `data:image/...;base64,` prefix. + comment: Optional free-text note, such as the reason for listing. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ComplianceListEntry", + await self._client.request( + "post", + "/v3/lists/{list_uuid}/entries/face-upload/", + cast_to=ComplianceListEntry, + path_params={"list_uuid": list_uuid}, + auth="api_key", + body={"image": image, "comment": comment}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class ListsEntriesWithRawResponse: + def __init__(self, resource: ListsEntries) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.delete = to_raw_response_wrapper(resource.delete) + self.upload_face = to_raw_response_wrapper(resource.upload_face) + + +class AsyncListsEntriesWithRawResponse: + def __init__(self, resource: AsyncListsEntries) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.delete = async_to_raw_response_wrapper(resource.delete) + self.upload_face = async_to_raw_response_wrapper(resource.upload_face) + + +class ListsEntriesWithStreamingResponse: + def __init__(self, resource: ListsEntries) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.delete = to_streamed_response_wrapper(resource.delete) + self.upload_face = to_streamed_response_wrapper(resource.upload_face) + + +class AsyncListsEntriesWithStreamingResponse: + def __init__(self, resource: AsyncListsEntries) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.delete = async_to_streamed_response_wrapper(resource.delete) + self.upload_face = async_to_streamed_response_wrapper(resource.upload_face) diff --git a/src/didit/resources/monitoring_events.py b/src/didit/resources/monitoring_events.py new file mode 100644 index 0000000..276111b --- /dev/null +++ b/src/didit/resources/monitoring_events.py @@ -0,0 +1,1226 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``monitoring_events`` resource.""" + +from __future__ import annotations + +import datetime as _dt +from functools import cached_property +from typing import Literal, Mapping, Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.monitoring_event import MonitoringEvent +from didit.types.components.monitoring_event_create_request import MonitoringEventCreateRequest +from didit.types.components.monitoring_event_detail import MonitoringEventDetail +from didit.types.components.monitoring_event_ingest_result import MonitoringEventIngestResult +from didit.types.components.monitoring_event_page import MonitoringEventPage +from didit.types.monitoring_events.create_batch_response import MonitoringEventsCreateBatchResponse +from didit.types.monitoring_events.create_params import ( + MonitoringEventsCreateParamsBusiness, + MonitoringEventsCreateParamsDeviceContext, + MonitoringEventsCreateParamsSession, + MonitoringEventsCreateParamsSubject, + MonitoringEventsCreateParamsTransactionContext, +) +from didit.types.monitoring_events.create_sdk_token_response import ( + MonitoringEventsCreateSdkTokenResponse, +) +from didit.types.monitoring_events.retrieve_enrichment_metrics_response import ( + MonitoringEventsRetrieveEnrichmentMetricsResponse, +) + +__all__ = [ + "MonitoringEvents", + "AsyncMonitoringEvents", + "MonitoringEventsWithRawResponse", + "AsyncMonitoringEventsWithRawResponse", + "MonitoringEventsWithStreamingResponse", + "AsyncMonitoringEventsWithStreamingResponse", +] + + +class MonitoringEvents(SyncAPIResource): + """The ``monitoring_events`` resource of the Didit API. + + Methods: ``create``, ``create_batch``, ``create_sdk_token``, ``delete``, ``list``, ``retrieve``, ``retrieve_enrichment_metrics``, ``retrieve_subject_timeline``. + """ + + @cached_property + def with_raw_response(self) -> MonitoringEventsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return MonitoringEventsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> MonitoringEventsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return MonitoringEventsWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + event_category: Sequence[ + Literal[ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom", + ] + | str + ] + | NotGiven = NOT_GIVEN, + action_type: Sequence[str] | NotGiven = NOT_GIVEN, + status: Sequence[Literal["ACCEPTED", "APPROVED", "IN_REVIEW", "DECLINED"] | str] + | NotGiven = NOT_GIVEN, + retention_class: Sequence[ + Literal["low_value", "standard", "risk_relevant", "monetary_or_compliance"] | str + ] + | NotGiven = NOT_GIVEN, + subject_vendor_data: str | NotGiven = NOT_GIVEN, + subject_entity_type: Literal["individual", "company"] | str | NotGiven = NOT_GIVEN, + business_vendor_data: str | NotGiven = NOT_GIVEN, + session_id: str | NotGiven = NOT_GIVEN, + device_fingerprint: str | NotGiven = NOT_GIVEN, + ip_address: str | NotGiven = NOT_GIVEN, + game_id: str | NotGiven = NOT_GIVEN, + round_id: str | NotGiven = NOT_GIVEN, + affiliate_code: str | NotGiven = NOT_GIVEN, + payment_reference_id: str | NotGiven = NOT_GIVEN, + counterparty_vendor_data: str | NotGiven = NOT_GIVEN, + matched_only: bool | NotGiven = NOT_GIVEN, + occurred_from: str | _dt.datetime | NotGiven = NOT_GIVEN, + occurred_to: str | _dt.datetime | NotGiven = NOT_GIVEN, + search: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[MonitoringEvent]: + """List monitoring events. + + List the monitoring events stored for your application, newest `occurred_at` first, with optional filters that combine with AND. Multi-value filters (`event_category`, `action_type`, `status`, `retention_class`) repeat the parameter and match any of the values; the other filters are exact matches, and unknown values simply return no events. Each result is a summary: call `GET /v3/events/{event_uuid}/` for the payload, device context, IP enrichment and rule evidence, and prefer `GET /v3/events/subjects/{vendor_data}/` for one subject's history. Reads work whether or not event ingestion is switched on. A scoped API key needs `transactions: read`. See [Event ingestion](https://docs.didit.me/transaction-monitoring/events). + + ``GET /v3/events/`` + + Args: + limit: Maximum number of events per page. Defaults to 50. Values above 1000000 return `400`; a non-numeric, zero or negative value falls back to 50. + offset: Number of events to skip. Defaults to 0. Prefer following the `next` URL from the response. + event_category: Only events in these categories. Repeat the parameter for several values (`?event_category=payment&event_category=dispute`); an event matching any of them is returned. + action_type: Only events with these action types (exact, case-sensitive). Repeat for several values. + status: Only events with these statuses. Repeat for several values. + retention_class: Only events in these retention classes. Repeat for several values. + subject_vendor_data: Exact match on the subject's `vendor_data`. The same id can name an individual and a company; add `subject_entity_type` to pick one, or use `GET /v3/events/subjects/{vendor_data}/`. + subject_entity_type: `company` returns only events of company subjects; any other non-empty value (for example `individual`) returns only events of individuals. Omit it to return both. + business_vendor_data: Exact match on the `business.vendor_data` sent with the event. + session_id: Exact match on `session.session_id`. + device_fingerprint: Exact match on `device_context.fingerprint`. + ip_address: Exact match on the event's IP address. + game_id: Exact match on the promoted `game_id`. + round_id: Exact match on the promoted `round_id`. + affiliate_code: Exact match on the promoted `affiliate_code`. + payment_reference_id: Exact match on the payment reference (from `transaction_context.payment_reference_id` or `payload.payment_reference_id`). + counterparty_vendor_data: Exact match on the promoted `counterparty_vendor_data`. + matched_only: `true` (also `1` or `yes`, case-insensitive) returns only events at least one rule matched (`rules_matched_count` above 0). Any other value is ignored. + occurred_from: Only events with `occurred_at` at or after this instant (inclusive). Send an ISO 8601 value: `2026-10-01T00:00:00Z`, an explicit offset with the `+` percent-encoded (`2026-10-01T02:00:00%2B02:00`; an unencoded `+` is decoded as a space and the value no longer parses), or a date alone (`2026-10-01`), which means 00:00:00 UTC of that day. A value without an offset is read as UTC. Values in any other format are not accepted. + occurred_to: Only events with `occurred_at` at or before this instant (inclusive). A date alone means the start of that day, so `occurred_to=2026-10-15` excludes the rest of 15 October; send `2026-10-15T23:59:59Z` to include it. Send an ISO 8601 value: `2026-10-01T00:00:00Z`, an explicit offset with the `+` percent-encoded (`2026-10-01T02:00:00%2B02:00`; an unencoded `+` is decoded as a space and the value no longer parses), or a date alone (`2026-10-01`), which means 00:00:00 UTC of that day. A value without an offset is read as UTC. Values in any other format are not accepted. + search: Case-insensitive substring match on `event_id`, `action_type`, `subject_vendor_data` or `counterparty_vendor_data`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[MonitoringEvent]", + self._client.request_page( + "get", + "/v3/events/", + cast_to=MonitoringEventPage, + item_type=MonitoringEvent, + auth="api_key", + query={ + "limit": limit, + "offset": offset, + "event_category": event_category, + "action_type": action_type, + "status": status, + "retention_class": retention_class, + "subject_vendor_data": subject_vendor_data, + "subject_entity_type": subject_entity_type, + "business_vendor_data": business_vendor_data, + "session_id": session_id, + "device_fingerprint": device_fingerprint, + "ip_address": ip_address, + "game_id": game_id, + "round_id": round_id, + "affiliate_code": affiliate_code, + "payment_reference_id": payment_reference_id, + "counterparty_vendor_data": counterparty_vendor_data, + "matched_only": matched_only, + "occurred_from": occurred_from, + "occurred_to": occurred_to, + "search": search, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + event_id: str, + event_category: Literal[ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom", + ] + | str, + action_type: str, + occurred_at: str | _dt.datetime, + subject: MonitoringEventsCreateParamsSubject, + time_zone: str | None | NotGiven = NOT_GIVEN, + business: MonitoringEventsCreateParamsBusiness | NotGiven = NOT_GIVEN, + session: MonitoringEventsCreateParamsSession | NotGiven = NOT_GIVEN, + device_context: MonitoringEventsCreateParamsDeviceContext | NotGiven = NOT_GIVEN, + transaction_context: MonitoringEventsCreateParamsTransactionContext | NotGiven = NOT_GIVEN, + payload: Mapping[str, object] | NotGiven = NOT_GIVEN, + custom_values: Mapping[str, str | float | bool | None] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> MonitoringEventIngestResult: + """Create monitoring event. + + Send one non-monetary event (login, session, gameplay, bonus, affiliate, payment, dispute, security, responsible gambling or custom) from your backend. The event is validated against its category contract, stored, IP-enriched once per unique public address, and evaluated by your real-time event rules before the response returns; cross-subject and long-window rules run asynchronously (`correlation_pending: true`), and in a sandbox application no rules run. `event_id` is the idempotency key: the same `event_id` with the same `event_category`, `action_type`, `occurred_at`, `subject.vendor_data` and `payload` returns `200` with `idempotent_replay: true` and the stored outcome (other envelope fields are not compared), while different values return `409`. Event ingestion is opt-in per application: until it is switched on this endpoint returns `403` with `code: event_monitoring_disabled` and stores nothing. A scoped API key needs `transactions: write`. See [Event ingestion](https://docs.didit.me/transaction-monitoring/events) and [Rules on events](https://docs.didit.me/transaction-monitoring/event-rules); to send events from a browser or app, use [SDK event submission](https://docs.didit.me/transaction-monitoring/sdk-event-submission). + + ``POST /v3/events/`` + + Args: + event_id: Your identifier for the event, unique per application. It is the idempotency key: resending the same `event_category`, `action_type`, `occurred_at`, `subject.vendor_data` and `payload` under it is a replay, anything different is a conflict. + event_category: Event category. + action_type: What happened within the category. Open vocabulary; the managed actions listed in [Event ingestion](https://docs.didit.me/transaction-monitoring/events) (for example `login`, `deposit_completed`, `chargeback_opened`) unlock typed validation and managed rule fields. Leading and trailing whitespace is trimmed. + occurred_at: When the event happened on your side, ISO 8601 (a value without an offset is read as UTC). Must be no more than 7 days in the past and no more than 5 minutes in the future. + subject: The user or company the event belongs to. It resolves to the user (or company) with this `vendor_data` in your application; one is created when none exists. + time_zone: Time zone of the occurrence on your side, for example `Europe/Madrid`. Stored with the event; not returned by the read endpoints. + business: The business the event belongs to, when it differs from the subject. Only `vendor_data` is read. + session: Your session context for the event. + device_context: Device and network context, stored as sent and returned by `GET /v3/events/{event_uuid}/`. On this server lane Didit does not see the end user's connection, so the IP enriched and used by rules is `network_context.ip_address` as you send it. + transaction_context: Money context. Non-empty values here also satisfy the required payload keys of the action. + payload: The category's domain fields, at most 16384 bytes once JSON-encoded. Some actions require keys (in `payload` or `transaction_context`): `gameplay.p2p_settlement` needs `game_id`, `counterparty_vendor_data`, `stake_amount`, `net_result_amount`, `role` and one of `hand_id`, `round_id`, `match_id`; `gameplay.bet_placed` and `gameplay.bet_settled` need `game_id`; `payment.deposit_initiated`, `payment.deposit_completed`, `payment.withdrawal_requested` and `payment.withdrawal_completed` need `payment_reference_id`, `amount`, `currency`, `payment_method`, `status`; `payment.withdrawal_address_changed` needs `withdrawal_address_hash`; `dispute.chargeback_opened`, `dispute.chargeback_won` and `dispute.chargeback_lost` need `payment_reference_id` and one of `dispute_id`, `chargeback_id`; `session.bot_signal_observed` needs `telemetry_schema_version` and one of `bot_score`, `actions_per_minute`, `timing_variance_ms`, `pointer_entropy`, `keystroke_entropy`, `reaction_time_ms_p50`; `responsible_gambling.self_exclusion_match` needs `registry_source`, `exclusion_status`, `identity_match_ref`. Every `security` action ending in `_changed` needs `changed_field`, plus `previous_reference_hash` or `new_reference_hash` when the field is `password`, `mfa_method`, `mfa`, `email`, `phone`, `withdrawal_address`, `payout_method` or `api_key`. Send hashes and references, never a card number, a full withdrawal address, a password hash or an MFA secret. + custom_values: Up to 100 keys of your own, addressable from rules as `custom_values.`. Values must be scalars (string, number, boolean or null); nested objects and arrays are rejected. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "MonitoringEventIngestResult", + self._client.request( + "post", + "/v3/events/", + cast_to=MonitoringEventIngestResult, + auth="api_key", + body={ + "event_id": event_id, + "event_category": event_category, + "action_type": action_type, + "occurred_at": occurred_at, + "subject": subject, + "time_zone": time_zone, + "business": business, + "session": session, + "device_context": device_context, + "transaction_context": transaction_context, + "payload": payload, + "custom_values": custom_values, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create_batch( + self, + *, + events: Sequence[MonitoringEventCreateRequest], + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> MonitoringEventsCreateBatchResponse: + """Create monitoring events in batch. + + Submit 1 to 500 events in one request, each in the same envelope as `POST /v3/events/`. Every item is validated, stored and evaluated on its own and gets its own outcome (`accepted`, `replayed` or `rejected`), so one malformed event never rolls back the others: the response is `201` when no item was rejected and `207` when at least one was, and both mean the request was processed. Send `{"events": [...]}`; a bare JSON array of events is also accepted. Event ingestion must be switched on for the application, otherwise the whole request returns `403` with `code: event_monitoring_disabled` and nothing is stored. A scoped API key needs `transactions: write`. See [Batches](https://docs.didit.me/transaction-monitoring/events#batches). + + ``POST /v3/events/batch/`` + + Args: + events: 1 to 500 events. Each item follows `MonitoringEventCreateRequest`, including the 7-day past and 5-minute future window on `occurred_at`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "MonitoringEventsCreateBatchResponse", + self._client.request( + "post", + "/v3/events/batch/", + cast_to=MonitoringEventsCreateBatchResponse, + auth="api_key", + body={"events": events}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create_sdk_token( + self, + *, + vendor_data: str, + entity_type: str | NotGiven = NOT_GIVEN, + allowed_event_categories: Sequence[ + Literal[ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom", + ] + | str + ] + | NotGiven = NOT_GIVEN, + ttl_seconds: int | NotGiven = NOT_GIVEN, + max_uses: int | None | NotGiven = NOT_GIVEN, + bind_to_origin: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> MonitoringEventsCreateSdkTokenResponse: + """Mint monitoring SDK token. + + Mint a short-lived device credential bound to one subject, so your web or mobile app can send that user's events straight to Didit without an API key. The app sends the returned `monitoring_token` in the `X-Monitoring-Token` header to the device lane (`POST /v1/events/` and `POST /v1/events/batch/`), where the subject is always overwritten with this `vendor_data`, events outside `allowed_event_categories` are rejected, each submitted event spends one use, and a request whose `Origin` does not equal `bind_to_origin` is refused. Call it from your backend only; minting creates the credential and nothing else (no user is created). Event ingestion must be switched on for the application, otherwise this endpoint returns `403` with `code: event_monitoring_disabled` instead of minting a credential that could never write. A scoped API key needs `transactions: write`. See [SDK event submission](https://docs.didit.me/transaction-monitoring/sdk-event-submission). + + ``POST /v3/events/sdk-token/`` + + Args: + vendor_data: Your id for the subject every event submitted with this credential belongs to. + entity_type: `company` binds a company subject; any other value (default `individual`) binds an individual. + allowed_event_categories: Categories the credential may submit. Omit it or send `[]` to allow every category. + ttl_seconds: Lifetime of the credential in seconds. + max_uses: Number of events the credential may submit (a batch spends one use per event). Send `null` for no cap within the lifetime. + bind_to_origin: When set, the browser `Origin` header of every device request must equal this value exactly; a request without `Origin` is refused. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "MonitoringEventsCreateSdkTokenResponse", + self._client.request( + "post", + "/v3/events/sdk-token/", + cast_to=MonitoringEventsCreateSdkTokenResponse, + auth="api_key", + body={ + "vendor_data": vendor_data, + "entity_type": entity_type, + "allowed_event_categories": allowed_event_categories, + "ttl_seconds": ttl_seconds, + "max_uses": max_uses, + "bind_to_origin": bind_to_origin, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + event_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> MonitoringEventDetail: + """Get monitoring event. + + Retrieve one monitoring event with everything the list returns plus the payload, custom values, device context, field provenance (`field_sources`), the promoted domain fields, `expires_at`, every matching rule run with its evidence, the IP enrichment and the tags rules attached. While `correlation_pending` is `true` an asynchronous rule still has to run; read the event again a few minutes later for the final `status`, `score` and `rule_runs`. Reads work whether or not event ingestion is switched on. A scoped API key needs `transactions: read`. See [Evidence](https://docs.didit.me/transaction-monitoring/event-rules#evidence) and [IP enrichment](https://docs.didit.me/transaction-monitoring/ip-enrichment). + + ``GET /v3/events/{event_uuid}/`` + + Args: + event_uuid: Didit identifier of the event (`event_uuid` from the create, batch, list or timeline response), not your own `event_id`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "MonitoringEventDetail", + self._client.request( + "get", + "/v3/events/{event_uuid}/", + cast_to=MonitoringEventDetail, + path_params={"event_uuid": event_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_subject_timeline( + self, + vendor_data: str, + *, + entity_type: Literal["individual", "company"] | str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + event_category: Sequence[ + Literal[ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom", + ] + | str + ] + | NotGiven = NOT_GIVEN, + action_type: Sequence[str] | NotGiven = NOT_GIVEN, + status: Sequence[Literal["ACCEPTED", "APPROVED", "IN_REVIEW", "DECLINED"] | str] + | NotGiven = NOT_GIVEN, + retention_class: Sequence[ + Literal["low_value", "standard", "risk_relevant", "monetary_or_compliance"] | str + ] + | NotGiven = NOT_GIVEN, + business_vendor_data: str | NotGiven = NOT_GIVEN, + session_id: str | NotGiven = NOT_GIVEN, + device_fingerprint: str | NotGiven = NOT_GIVEN, + ip_address: str | NotGiven = NOT_GIVEN, + game_id: str | NotGiven = NOT_GIVEN, + round_id: str | NotGiven = NOT_GIVEN, + affiliate_code: str | NotGiven = NOT_GIVEN, + payment_reference_id: str | NotGiven = NOT_GIVEN, + counterparty_vendor_data: str | NotGiven = NOT_GIVEN, + matched_only: bool | NotGiven = NOT_GIVEN, + occurred_from: str | _dt.datetime | NotGiven = NOT_GIVEN, + occurred_to: str | _dt.datetime | NotGiven = NOT_GIVEN, + search: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[MonitoringEvent]: + """Get subject event timeline. + + List every monitoring event of one subject, newest `occurred_at` first. A `vendor_data` is unique only within an entity type, so the same id can name an individual and a company: `entity_type` picks which timeline you get, and it defaults to `individual`, so a company's events need `entity_type=company`. The filters of `GET /v3/events/` apply on top, and a `vendor_data` with no events returns an empty page rather than `404`. A scoped API key needs `transactions: read`. See [Reading events back](https://docs.didit.me/transaction-monitoring/events#reading-events-back). + + ``GET /v3/events/subjects/{vendor_data}/`` + + Args: + vendor_data: Your id for the subject (`subject.vendor_data` of its events). URL-encode it; a value containing `/` cannot be addressed by this endpoint. + entity_type: `company` returns the company with this `vendor_data`; any other value, or none, returns the individual. `subject_entity_type` is accepted as an alias when `entity_type` is absent. + limit: Maximum number of events per page. Defaults to 50. Values above 1000000 return `400`; a non-numeric, zero or negative value falls back to 50. + offset: Number of events to skip. Defaults to 0. Prefer following the `next` URL from the response. + event_category: Only events in these categories. Repeat the parameter for several values (`?event_category=payment&event_category=dispute`); an event matching any of them is returned. + action_type: Only events with these action types (exact, case-sensitive). Repeat for several values. + status: Only events with these statuses. Repeat for several values. + retention_class: Only events in these retention classes. Repeat for several values. + business_vendor_data: Exact match on the `business.vendor_data` sent with the event. + session_id: Exact match on `session.session_id`. + device_fingerprint: Exact match on `device_context.fingerprint`. + ip_address: Exact match on the event's IP address. + game_id: Exact match on the promoted `game_id`. + round_id: Exact match on the promoted `round_id`. + affiliate_code: Exact match on the promoted `affiliate_code`. + payment_reference_id: Exact match on the payment reference (from `transaction_context.payment_reference_id` or `payload.payment_reference_id`). + counterparty_vendor_data: Exact match on the promoted `counterparty_vendor_data`. + matched_only: `true` (also `1` or `yes`, case-insensitive) returns only events at least one rule matched (`rules_matched_count` above 0). Any other value is ignored. + occurred_from: Only events with `occurred_at` at or after this instant (inclusive). Send an ISO 8601 value: `2026-10-01T00:00:00Z`, an explicit offset with the `+` percent-encoded (`2026-10-01T02:00:00%2B02:00`; an unencoded `+` is decoded as a space and the value no longer parses), or a date alone (`2026-10-01`), which means 00:00:00 UTC of that day. A value without an offset is read as UTC. Values in any other format are not accepted. + occurred_to: Only events with `occurred_at` at or before this instant (inclusive). A date alone means the start of that day, so `occurred_to=2026-10-15` excludes the rest of 15 October; send `2026-10-15T23:59:59Z` to include it. Send an ISO 8601 value: `2026-10-01T00:00:00Z`, an explicit offset with the `+` percent-encoded (`2026-10-01T02:00:00%2B02:00`; an unencoded `+` is decoded as a space and the value no longer parses), or a date alone (`2026-10-01`), which means 00:00:00 UTC of that day. A value without an offset is read as UTC. Values in any other format are not accepted. + search: Case-insensitive substring match on `event_id`, `action_type`, `subject_vendor_data` or `counterparty_vendor_data`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[MonitoringEvent]", + self._client.request_page( + "get", + "/v3/events/subjects/{vendor_data}/", + cast_to=MonitoringEventPage, + item_type=MonitoringEvent, + path_params={"vendor_data": vendor_data}, + auth="api_key", + query={ + "entity_type": entity_type, + "limit": limit, + "offset": offset, + "event_category": event_category, + "action_type": action_type, + "status": status, + "retention_class": retention_class, + "business_vendor_data": business_vendor_data, + "session_id": session_id, + "device_fingerprint": device_fingerprint, + "ip_address": ip_address, + "game_id": game_id, + "round_id": round_id, + "affiliate_code": affiliate_code, + "payment_reference_id": payment_reference_id, + "counterparty_vendor_data": counterparty_vendor_data, + "matched_only": matched_only, + "occurred_from": occurred_from, + "occurred_to": occurred_to, + "search": search, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_enrichment_metrics( + self, + *, + days: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> MonitoringEventsRetrieveEnrichmentMetricsResponse: + """Get event enrichment metrics. + + Return the event ingestion and IP-enrichment counters of your application for a period, summed over every lane (server, batch and device), so you can watch event volume and confirm that enrichment is resolved once per unique address rather than once per event. The period covers today plus the previous `days` days, counted by UTC calendar date. The three ratios are null when their denominator is zero. A scoped API key needs `transactions: read`. See [IP enrichment](https://docs.didit.me/transaction-monitoring/events#ip-enrichment). + + ``GET /v3/events/enrichment-metrics/`` + + Args: + days: Length of the period in days. Defaults to 30; values are clamped to 1 to 365, and a non-integer value falls back to 30. The value applied is returned as `period_days`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "MonitoringEventsRetrieveEnrichmentMetricsResponse", + self._client.request( + "get", + "/v3/events/enrichment-metrics/", + cast_to=MonitoringEventsRetrieveEnrichmentMetricsResponse, + auth="api_key", + query={"days": days}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + event_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete monitoring event. + + Permanently delete one monitoring event together with its rule runs, notes and tag links, for example an event sent by mistake. After the `204` the event is gone from every read (retrieve returns `404`, and it no longer appears in the list or the subject timeline), the deletion is irreversible, and its `event_id` is free to be used for a new event. Not affected: the user or company the event resolved to, cases a rule opened, list entries a rule added, and the scores and statuses already applied to other events. To erase everything about a person or a company, delete the user or the company instead, which also deletes their events. A scoped API key needs `transactions: write`. + + ``DELETE /v3/events/{event_uuid}/delete/`` + + Args: + event_uuid: Didit identifier of the event (`event_uuid` from the create, batch, list or timeline response), not your own `event_id`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/events/{event_uuid}/delete/", + cast_to=None, + path_params={"event_uuid": event_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncMonitoringEvents(AsyncAPIResource): + """The ``monitoring_events`` resource of the Didit API. + + Methods: ``create``, ``create_batch``, ``create_sdk_token``, ``delete``, ``list``, ``retrieve``, ``retrieve_enrichment_metrics``, ``retrieve_subject_timeline``. + """ + + @cached_property + def with_raw_response(self) -> AsyncMonitoringEventsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncMonitoringEventsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncMonitoringEventsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncMonitoringEventsWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + event_category: Sequence[ + Literal[ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom", + ] + | str + ] + | NotGiven = NOT_GIVEN, + action_type: Sequence[str] | NotGiven = NOT_GIVEN, + status: Sequence[Literal["ACCEPTED", "APPROVED", "IN_REVIEW", "DECLINED"] | str] + | NotGiven = NOT_GIVEN, + retention_class: Sequence[ + Literal["low_value", "standard", "risk_relevant", "monetary_or_compliance"] | str + ] + | NotGiven = NOT_GIVEN, + subject_vendor_data: str | NotGiven = NOT_GIVEN, + subject_entity_type: Literal["individual", "company"] | str | NotGiven = NOT_GIVEN, + business_vendor_data: str | NotGiven = NOT_GIVEN, + session_id: str | NotGiven = NOT_GIVEN, + device_fingerprint: str | NotGiven = NOT_GIVEN, + ip_address: str | NotGiven = NOT_GIVEN, + game_id: str | NotGiven = NOT_GIVEN, + round_id: str | NotGiven = NOT_GIVEN, + affiliate_code: str | NotGiven = NOT_GIVEN, + payment_reference_id: str | NotGiven = NOT_GIVEN, + counterparty_vendor_data: str | NotGiven = NOT_GIVEN, + matched_only: bool | NotGiven = NOT_GIVEN, + occurred_from: str | _dt.datetime | NotGiven = NOT_GIVEN, + occurred_to: str | _dt.datetime | NotGiven = NOT_GIVEN, + search: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[MonitoringEvent]: + """List monitoring events. + + List the monitoring events stored for your application, newest `occurred_at` first, with optional filters that combine with AND. Multi-value filters (`event_category`, `action_type`, `status`, `retention_class`) repeat the parameter and match any of the values; the other filters are exact matches, and unknown values simply return no events. Each result is a summary: call `GET /v3/events/{event_uuid}/` for the payload, device context, IP enrichment and rule evidence, and prefer `GET /v3/events/subjects/{vendor_data}/` for one subject's history. Reads work whether or not event ingestion is switched on. A scoped API key needs `transactions: read`. See [Event ingestion](https://docs.didit.me/transaction-monitoring/events). + + ``GET /v3/events/`` + + Args: + limit: Maximum number of events per page. Defaults to 50. Values above 1000000 return `400`; a non-numeric, zero or negative value falls back to 50. + offset: Number of events to skip. Defaults to 0. Prefer following the `next` URL from the response. + event_category: Only events in these categories. Repeat the parameter for several values (`?event_category=payment&event_category=dispute`); an event matching any of them is returned. + action_type: Only events with these action types (exact, case-sensitive). Repeat for several values. + status: Only events with these statuses. Repeat for several values. + retention_class: Only events in these retention classes. Repeat for several values. + subject_vendor_data: Exact match on the subject's `vendor_data`. The same id can name an individual and a company; add `subject_entity_type` to pick one, or use `GET /v3/events/subjects/{vendor_data}/`. + subject_entity_type: `company` returns only events of company subjects; any other non-empty value (for example `individual`) returns only events of individuals. Omit it to return both. + business_vendor_data: Exact match on the `business.vendor_data` sent with the event. + session_id: Exact match on `session.session_id`. + device_fingerprint: Exact match on `device_context.fingerprint`. + ip_address: Exact match on the event's IP address. + game_id: Exact match on the promoted `game_id`. + round_id: Exact match on the promoted `round_id`. + affiliate_code: Exact match on the promoted `affiliate_code`. + payment_reference_id: Exact match on the payment reference (from `transaction_context.payment_reference_id` or `payload.payment_reference_id`). + counterparty_vendor_data: Exact match on the promoted `counterparty_vendor_data`. + matched_only: `true` (also `1` or `yes`, case-insensitive) returns only events at least one rule matched (`rules_matched_count` above 0). Any other value is ignored. + occurred_from: Only events with `occurred_at` at or after this instant (inclusive). Send an ISO 8601 value: `2026-10-01T00:00:00Z`, an explicit offset with the `+` percent-encoded (`2026-10-01T02:00:00%2B02:00`; an unencoded `+` is decoded as a space and the value no longer parses), or a date alone (`2026-10-01`), which means 00:00:00 UTC of that day. A value without an offset is read as UTC. Values in any other format are not accepted. + occurred_to: Only events with `occurred_at` at or before this instant (inclusive). A date alone means the start of that day, so `occurred_to=2026-10-15` excludes the rest of 15 October; send `2026-10-15T23:59:59Z` to include it. Send an ISO 8601 value: `2026-10-01T00:00:00Z`, an explicit offset with the `+` percent-encoded (`2026-10-01T02:00:00%2B02:00`; an unencoded `+` is decoded as a space and the value no longer parses), or a date alone (`2026-10-01`), which means 00:00:00 UTC of that day. A value without an offset is read as UTC. Values in any other format are not accepted. + search: Case-insensitive substring match on `event_id`, `action_type`, `subject_vendor_data` or `counterparty_vendor_data`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[MonitoringEvent]", + self._client.request_page( + "get", + "/v3/events/", + cast_to=MonitoringEventPage, + item_type=MonitoringEvent, + auth="api_key", + query={ + "limit": limit, + "offset": offset, + "event_category": event_category, + "action_type": action_type, + "status": status, + "retention_class": retention_class, + "subject_vendor_data": subject_vendor_data, + "subject_entity_type": subject_entity_type, + "business_vendor_data": business_vendor_data, + "session_id": session_id, + "device_fingerprint": device_fingerprint, + "ip_address": ip_address, + "game_id": game_id, + "round_id": round_id, + "affiliate_code": affiliate_code, + "payment_reference_id": payment_reference_id, + "counterparty_vendor_data": counterparty_vendor_data, + "matched_only": matched_only, + "occurred_from": occurred_from, + "occurred_to": occurred_to, + "search": search, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + event_id: str, + event_category: Literal[ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom", + ] + | str, + action_type: str, + occurred_at: str | _dt.datetime, + subject: MonitoringEventsCreateParamsSubject, + time_zone: str | None | NotGiven = NOT_GIVEN, + business: MonitoringEventsCreateParamsBusiness | NotGiven = NOT_GIVEN, + session: MonitoringEventsCreateParamsSession | NotGiven = NOT_GIVEN, + device_context: MonitoringEventsCreateParamsDeviceContext | NotGiven = NOT_GIVEN, + transaction_context: MonitoringEventsCreateParamsTransactionContext | NotGiven = NOT_GIVEN, + payload: Mapping[str, object] | NotGiven = NOT_GIVEN, + custom_values: Mapping[str, str | float | bool | None] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> MonitoringEventIngestResult: + """Create monitoring event. + + Send one non-monetary event (login, session, gameplay, bonus, affiliate, payment, dispute, security, responsible gambling or custom) from your backend. The event is validated against its category contract, stored, IP-enriched once per unique public address, and evaluated by your real-time event rules before the response returns; cross-subject and long-window rules run asynchronously (`correlation_pending: true`), and in a sandbox application no rules run. `event_id` is the idempotency key: the same `event_id` with the same `event_category`, `action_type`, `occurred_at`, `subject.vendor_data` and `payload` returns `200` with `idempotent_replay: true` and the stored outcome (other envelope fields are not compared), while different values return `409`. Event ingestion is opt-in per application: until it is switched on this endpoint returns `403` with `code: event_monitoring_disabled` and stores nothing. A scoped API key needs `transactions: write`. See [Event ingestion](https://docs.didit.me/transaction-monitoring/events) and [Rules on events](https://docs.didit.me/transaction-monitoring/event-rules); to send events from a browser or app, use [SDK event submission](https://docs.didit.me/transaction-monitoring/sdk-event-submission). + + ``POST /v3/events/`` + + Args: + event_id: Your identifier for the event, unique per application. It is the idempotency key: resending the same `event_category`, `action_type`, `occurred_at`, `subject.vendor_data` and `payload` under it is a replay, anything different is a conflict. + event_category: Event category. + action_type: What happened within the category. Open vocabulary; the managed actions listed in [Event ingestion](https://docs.didit.me/transaction-monitoring/events) (for example `login`, `deposit_completed`, `chargeback_opened`) unlock typed validation and managed rule fields. Leading and trailing whitespace is trimmed. + occurred_at: When the event happened on your side, ISO 8601 (a value without an offset is read as UTC). Must be no more than 7 days in the past and no more than 5 minutes in the future. + subject: The user or company the event belongs to. It resolves to the user (or company) with this `vendor_data` in your application; one is created when none exists. + time_zone: Time zone of the occurrence on your side, for example `Europe/Madrid`. Stored with the event; not returned by the read endpoints. + business: The business the event belongs to, when it differs from the subject. Only `vendor_data` is read. + session: Your session context for the event. + device_context: Device and network context, stored as sent and returned by `GET /v3/events/{event_uuid}/`. On this server lane Didit does not see the end user's connection, so the IP enriched and used by rules is `network_context.ip_address` as you send it. + transaction_context: Money context. Non-empty values here also satisfy the required payload keys of the action. + payload: The category's domain fields, at most 16384 bytes once JSON-encoded. Some actions require keys (in `payload` or `transaction_context`): `gameplay.p2p_settlement` needs `game_id`, `counterparty_vendor_data`, `stake_amount`, `net_result_amount`, `role` and one of `hand_id`, `round_id`, `match_id`; `gameplay.bet_placed` and `gameplay.bet_settled` need `game_id`; `payment.deposit_initiated`, `payment.deposit_completed`, `payment.withdrawal_requested` and `payment.withdrawal_completed` need `payment_reference_id`, `amount`, `currency`, `payment_method`, `status`; `payment.withdrawal_address_changed` needs `withdrawal_address_hash`; `dispute.chargeback_opened`, `dispute.chargeback_won` and `dispute.chargeback_lost` need `payment_reference_id` and one of `dispute_id`, `chargeback_id`; `session.bot_signal_observed` needs `telemetry_schema_version` and one of `bot_score`, `actions_per_minute`, `timing_variance_ms`, `pointer_entropy`, `keystroke_entropy`, `reaction_time_ms_p50`; `responsible_gambling.self_exclusion_match` needs `registry_source`, `exclusion_status`, `identity_match_ref`. Every `security` action ending in `_changed` needs `changed_field`, plus `previous_reference_hash` or `new_reference_hash` when the field is `password`, `mfa_method`, `mfa`, `email`, `phone`, `withdrawal_address`, `payout_method` or `api_key`. Send hashes and references, never a card number, a full withdrawal address, a password hash or an MFA secret. + custom_values: Up to 100 keys of your own, addressable from rules as `custom_values.`. Values must be scalars (string, number, boolean or null); nested objects and arrays are rejected. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "MonitoringEventIngestResult", + await self._client.request( + "post", + "/v3/events/", + cast_to=MonitoringEventIngestResult, + auth="api_key", + body={ + "event_id": event_id, + "event_category": event_category, + "action_type": action_type, + "occurred_at": occurred_at, + "subject": subject, + "time_zone": time_zone, + "business": business, + "session": session, + "device_context": device_context, + "transaction_context": transaction_context, + "payload": payload, + "custom_values": custom_values, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create_batch( + self, + *, + events: Sequence[MonitoringEventCreateRequest], + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> MonitoringEventsCreateBatchResponse: + """Create monitoring events in batch. + + Submit 1 to 500 events in one request, each in the same envelope as `POST /v3/events/`. Every item is validated, stored and evaluated on its own and gets its own outcome (`accepted`, `replayed` or `rejected`), so one malformed event never rolls back the others: the response is `201` when no item was rejected and `207` when at least one was, and both mean the request was processed. Send `{"events": [...]}`; a bare JSON array of events is also accepted. Event ingestion must be switched on for the application, otherwise the whole request returns `403` with `code: event_monitoring_disabled` and nothing is stored. A scoped API key needs `transactions: write`. See [Batches](https://docs.didit.me/transaction-monitoring/events#batches). + + ``POST /v3/events/batch/`` + + Args: + events: 1 to 500 events. Each item follows `MonitoringEventCreateRequest`, including the 7-day past and 5-minute future window on `occurred_at`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "MonitoringEventsCreateBatchResponse", + await self._client.request( + "post", + "/v3/events/batch/", + cast_to=MonitoringEventsCreateBatchResponse, + auth="api_key", + body={"events": events}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create_sdk_token( + self, + *, + vendor_data: str, + entity_type: str | NotGiven = NOT_GIVEN, + allowed_event_categories: Sequence[ + Literal[ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom", + ] + | str + ] + | NotGiven = NOT_GIVEN, + ttl_seconds: int | NotGiven = NOT_GIVEN, + max_uses: int | None | NotGiven = NOT_GIVEN, + bind_to_origin: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> MonitoringEventsCreateSdkTokenResponse: + """Mint monitoring SDK token. + + Mint a short-lived device credential bound to one subject, so your web or mobile app can send that user's events straight to Didit without an API key. The app sends the returned `monitoring_token` in the `X-Monitoring-Token` header to the device lane (`POST /v1/events/` and `POST /v1/events/batch/`), where the subject is always overwritten with this `vendor_data`, events outside `allowed_event_categories` are rejected, each submitted event spends one use, and a request whose `Origin` does not equal `bind_to_origin` is refused. Call it from your backend only; minting creates the credential and nothing else (no user is created). Event ingestion must be switched on for the application, otherwise this endpoint returns `403` with `code: event_monitoring_disabled` instead of minting a credential that could never write. A scoped API key needs `transactions: write`. See [SDK event submission](https://docs.didit.me/transaction-monitoring/sdk-event-submission). + + ``POST /v3/events/sdk-token/`` + + Args: + vendor_data: Your id for the subject every event submitted with this credential belongs to. + entity_type: `company` binds a company subject; any other value (default `individual`) binds an individual. + allowed_event_categories: Categories the credential may submit. Omit it or send `[]` to allow every category. + ttl_seconds: Lifetime of the credential in seconds. + max_uses: Number of events the credential may submit (a batch spends one use per event). Send `null` for no cap within the lifetime. + bind_to_origin: When set, the browser `Origin` header of every device request must equal this value exactly; a request without `Origin` is refused. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "MonitoringEventsCreateSdkTokenResponse", + await self._client.request( + "post", + "/v3/events/sdk-token/", + cast_to=MonitoringEventsCreateSdkTokenResponse, + auth="api_key", + body={ + "vendor_data": vendor_data, + "entity_type": entity_type, + "allowed_event_categories": allowed_event_categories, + "ttl_seconds": ttl_seconds, + "max_uses": max_uses, + "bind_to_origin": bind_to_origin, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + event_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> MonitoringEventDetail: + """Get monitoring event. + + Retrieve one monitoring event with everything the list returns plus the payload, custom values, device context, field provenance (`field_sources`), the promoted domain fields, `expires_at`, every matching rule run with its evidence, the IP enrichment and the tags rules attached. While `correlation_pending` is `true` an asynchronous rule still has to run; read the event again a few minutes later for the final `status`, `score` and `rule_runs`. Reads work whether or not event ingestion is switched on. A scoped API key needs `transactions: read`. See [Evidence](https://docs.didit.me/transaction-monitoring/event-rules#evidence) and [IP enrichment](https://docs.didit.me/transaction-monitoring/ip-enrichment). + + ``GET /v3/events/{event_uuid}/`` + + Args: + event_uuid: Didit identifier of the event (`event_uuid` from the create, batch, list or timeline response), not your own `event_id`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "MonitoringEventDetail", + await self._client.request( + "get", + "/v3/events/{event_uuid}/", + cast_to=MonitoringEventDetail, + path_params={"event_uuid": event_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_subject_timeline( + self, + vendor_data: str, + *, + entity_type: Literal["individual", "company"] | str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + event_category: Sequence[ + Literal[ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom", + ] + | str + ] + | NotGiven = NOT_GIVEN, + action_type: Sequence[str] | NotGiven = NOT_GIVEN, + status: Sequence[Literal["ACCEPTED", "APPROVED", "IN_REVIEW", "DECLINED"] | str] + | NotGiven = NOT_GIVEN, + retention_class: Sequence[ + Literal["low_value", "standard", "risk_relevant", "monetary_or_compliance"] | str + ] + | NotGiven = NOT_GIVEN, + business_vendor_data: str | NotGiven = NOT_GIVEN, + session_id: str | NotGiven = NOT_GIVEN, + device_fingerprint: str | NotGiven = NOT_GIVEN, + ip_address: str | NotGiven = NOT_GIVEN, + game_id: str | NotGiven = NOT_GIVEN, + round_id: str | NotGiven = NOT_GIVEN, + affiliate_code: str | NotGiven = NOT_GIVEN, + payment_reference_id: str | NotGiven = NOT_GIVEN, + counterparty_vendor_data: str | NotGiven = NOT_GIVEN, + matched_only: bool | NotGiven = NOT_GIVEN, + occurred_from: str | _dt.datetime | NotGiven = NOT_GIVEN, + occurred_to: str | _dt.datetime | NotGiven = NOT_GIVEN, + search: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[MonitoringEvent]: + """Get subject event timeline. + + List every monitoring event of one subject, newest `occurred_at` first. A `vendor_data` is unique only within an entity type, so the same id can name an individual and a company: `entity_type` picks which timeline you get, and it defaults to `individual`, so a company's events need `entity_type=company`. The filters of `GET /v3/events/` apply on top, and a `vendor_data` with no events returns an empty page rather than `404`. A scoped API key needs `transactions: read`. See [Reading events back](https://docs.didit.me/transaction-monitoring/events#reading-events-back). + + ``GET /v3/events/subjects/{vendor_data}/`` + + Args: + vendor_data: Your id for the subject (`subject.vendor_data` of its events). URL-encode it; a value containing `/` cannot be addressed by this endpoint. + entity_type: `company` returns the company with this `vendor_data`; any other value, or none, returns the individual. `subject_entity_type` is accepted as an alias when `entity_type` is absent. + limit: Maximum number of events per page. Defaults to 50. Values above 1000000 return `400`; a non-numeric, zero or negative value falls back to 50. + offset: Number of events to skip. Defaults to 0. Prefer following the `next` URL from the response. + event_category: Only events in these categories. Repeat the parameter for several values (`?event_category=payment&event_category=dispute`); an event matching any of them is returned. + action_type: Only events with these action types (exact, case-sensitive). Repeat for several values. + status: Only events with these statuses. Repeat for several values. + retention_class: Only events in these retention classes. Repeat for several values. + business_vendor_data: Exact match on the `business.vendor_data` sent with the event. + session_id: Exact match on `session.session_id`. + device_fingerprint: Exact match on `device_context.fingerprint`. + ip_address: Exact match on the event's IP address. + game_id: Exact match on the promoted `game_id`. + round_id: Exact match on the promoted `round_id`. + affiliate_code: Exact match on the promoted `affiliate_code`. + payment_reference_id: Exact match on the payment reference (from `transaction_context.payment_reference_id` or `payload.payment_reference_id`). + counterparty_vendor_data: Exact match on the promoted `counterparty_vendor_data`. + matched_only: `true` (also `1` or `yes`, case-insensitive) returns only events at least one rule matched (`rules_matched_count` above 0). Any other value is ignored. + occurred_from: Only events with `occurred_at` at or after this instant (inclusive). Send an ISO 8601 value: `2026-10-01T00:00:00Z`, an explicit offset with the `+` percent-encoded (`2026-10-01T02:00:00%2B02:00`; an unencoded `+` is decoded as a space and the value no longer parses), or a date alone (`2026-10-01`), which means 00:00:00 UTC of that day. A value without an offset is read as UTC. Values in any other format are not accepted. + occurred_to: Only events with `occurred_at` at or before this instant (inclusive). A date alone means the start of that day, so `occurred_to=2026-10-15` excludes the rest of 15 October; send `2026-10-15T23:59:59Z` to include it. Send an ISO 8601 value: `2026-10-01T00:00:00Z`, an explicit offset with the `+` percent-encoded (`2026-10-01T02:00:00%2B02:00`; an unencoded `+` is decoded as a space and the value no longer parses), or a date alone (`2026-10-01`), which means 00:00:00 UTC of that day. A value without an offset is read as UTC. Values in any other format are not accepted. + search: Case-insensitive substring match on `event_id`, `action_type`, `subject_vendor_data` or `counterparty_vendor_data`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[MonitoringEvent]", + self._client.request_page( + "get", + "/v3/events/subjects/{vendor_data}/", + cast_to=MonitoringEventPage, + item_type=MonitoringEvent, + path_params={"vendor_data": vendor_data}, + auth="api_key", + query={ + "entity_type": entity_type, + "limit": limit, + "offset": offset, + "event_category": event_category, + "action_type": action_type, + "status": status, + "retention_class": retention_class, + "business_vendor_data": business_vendor_data, + "session_id": session_id, + "device_fingerprint": device_fingerprint, + "ip_address": ip_address, + "game_id": game_id, + "round_id": round_id, + "affiliate_code": affiliate_code, + "payment_reference_id": payment_reference_id, + "counterparty_vendor_data": counterparty_vendor_data, + "matched_only": matched_only, + "occurred_from": occurred_from, + "occurred_to": occurred_to, + "search": search, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_enrichment_metrics( + self, + *, + days: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> MonitoringEventsRetrieveEnrichmentMetricsResponse: + """Get event enrichment metrics. + + Return the event ingestion and IP-enrichment counters of your application for a period, summed over every lane (server, batch and device), so you can watch event volume and confirm that enrichment is resolved once per unique address rather than once per event. The period covers today plus the previous `days` days, counted by UTC calendar date. The three ratios are null when their denominator is zero. A scoped API key needs `transactions: read`. See [IP enrichment](https://docs.didit.me/transaction-monitoring/events#ip-enrichment). + + ``GET /v3/events/enrichment-metrics/`` + + Args: + days: Length of the period in days. Defaults to 30; values are clamped to 1 to 365, and a non-integer value falls back to 30. The value applied is returned as `period_days`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "MonitoringEventsRetrieveEnrichmentMetricsResponse", + await self._client.request( + "get", + "/v3/events/enrichment-metrics/", + cast_to=MonitoringEventsRetrieveEnrichmentMetricsResponse, + auth="api_key", + query={"days": days}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + event_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete monitoring event. + + Permanently delete one monitoring event together with its rule runs, notes and tag links, for example an event sent by mistake. After the `204` the event is gone from every read (retrieve returns `404`, and it no longer appears in the list or the subject timeline), the deletion is irreversible, and its `event_id` is free to be used for a new event. Not affected: the user or company the event resolved to, cases a rule opened, list entries a rule added, and the scores and statuses already applied to other events. To erase everything about a person or a company, delete the user or the company instead, which also deletes their events. A scoped API key needs `transactions: write`. + + ``DELETE /v3/events/{event_uuid}/delete/`` + + Args: + event_uuid: Didit identifier of the event (`event_uuid` from the create, batch, list or timeline response), not your own `event_id`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/events/{event_uuid}/delete/", + cast_to=None, + path_params={"event_uuid": event_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class MonitoringEventsWithRawResponse: + def __init__(self, resource: MonitoringEvents) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.create_batch = to_raw_response_wrapper(resource.create_batch) + self.create_sdk_token = to_raw_response_wrapper(resource.create_sdk_token) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.retrieve_subject_timeline = to_raw_response_wrapper(resource.retrieve_subject_timeline) + self.retrieve_enrichment_metrics = to_raw_response_wrapper( + resource.retrieve_enrichment_metrics + ) + self.delete = to_raw_response_wrapper(resource.delete) + + +class AsyncMonitoringEventsWithRawResponse: + def __init__(self, resource: AsyncMonitoringEvents) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.create_batch = async_to_raw_response_wrapper(resource.create_batch) + self.create_sdk_token = async_to_raw_response_wrapper(resource.create_sdk_token) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.retrieve_subject_timeline = async_to_raw_response_wrapper( + resource.retrieve_subject_timeline + ) + self.retrieve_enrichment_metrics = async_to_raw_response_wrapper( + resource.retrieve_enrichment_metrics + ) + self.delete = async_to_raw_response_wrapper(resource.delete) + + +class MonitoringEventsWithStreamingResponse: + def __init__(self, resource: MonitoringEvents) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.create_batch = to_streamed_response_wrapper(resource.create_batch) + self.create_sdk_token = to_streamed_response_wrapper(resource.create_sdk_token) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.retrieve_subject_timeline = to_streamed_response_wrapper( + resource.retrieve_subject_timeline + ) + self.retrieve_enrichment_metrics = to_streamed_response_wrapper( + resource.retrieve_enrichment_metrics + ) + self.delete = to_streamed_response_wrapper(resource.delete) + + +class AsyncMonitoringEventsWithStreamingResponse: + def __init__(self, resource: AsyncMonitoringEvents) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.create_batch = async_to_streamed_response_wrapper(resource.create_batch) + self.create_sdk_token = async_to_streamed_response_wrapper(resource.create_sdk_token) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.retrieve_subject_timeline = async_to_streamed_response_wrapper( + resource.retrieve_subject_timeline + ) + self.retrieve_enrichment_metrics = async_to_streamed_response_wrapper( + resource.retrieve_enrichment_metrics + ) + self.delete = async_to_streamed_response_wrapper(resource.delete) diff --git a/src/didit/resources/networks/__init__.py b/src/didit/resources/networks/__init__.py index 254c156..d7e848f 100644 --- a/src/didit/resources/networks/__init__.py +++ b/src/didit/resources/networks/__init__.py @@ -5,7 +5,7 @@ import datetime as _dt from functools import cached_property -from typing import TYPE_CHECKING, Literal, cast +from typing import TYPE_CHECKING, Literal, Sequence, cast import httpx @@ -18,14 +18,15 @@ to_streamed_response_wrapper, ) from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query -from didit.types.components.network_detail import NetworkDetail +from didit.types.components.network_detail_response import NetworkDetailResponse from didit.types.components.network_graph import NetworkGraph from didit.types.components.network_list_item import NetworkListItem from didit.types.components.network_map import NetworkMap +from didit.types.components.network_timeline import NetworkTimeline +from didit.types.components.network_timeline_page import NetworkTimelinePage from didit.types.networks.list_members_response import NetworksListMembersResponse from didit.types.networks.list_response import NetworksListResponse from didit.types.networks.list_signals_response import NetworksListSignalsResponse -from didit.types.networks.retrieve_timeline_response import NetworksRetrieveTimelineResponse if TYPE_CHECKING: from didit.resources.networks.notes import ( @@ -50,7 +51,7 @@ class Networks(SyncAPIResource): """The ``networks`` resource of the Didit API. - Methods: ``download_report``, ``list``, ``list_members``, ``list_signals``, ``retrieve``, ``retrieve_graph``, ``retrieve_map``, ``retrieve_timeline``. + Methods: ``add_tag``, ``download_report``, ``list``, ``list_members``, ``list_signals``, ``remove_tag``, ``retrieve``, ``retrieve_graph``, ``retrieve_map``, ``retrieve_timeline``. Nested resources: ``notes``. """ @@ -75,17 +76,23 @@ def list( self, *, status: str | NotGiven = NOT_GIVEN, + scope: Literal["identity", "transactions"] | str | NotGiven = NOT_GIVEN, signal_type: str | NotGiven = NOT_GIVEN, pattern_type: str | NotGiven = NOT_GIVEN, risk_band: str | NotGiven = NOT_GIVEN, min_size: int | NotGiven = NOT_GIVEN, max_size: int | NotGiven = NOT_GIVEN, + assignee: str | NotGiven = NOT_GIVEN, + priority: str | NotGiven = NOT_GIVEN, + risk_min: int | NotGiven = NOT_GIVEN, + has_case: bool | NotGiven = NOT_GIVEN, date_from: str | _dt.date | NotGiven = NOT_GIVEN, date_to: str | _dt.date | NotGiven = NOT_GIVEN, q: str | NotGiven = NOT_GIVEN, ordering: str | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, + summary: bool | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -93,23 +100,29 @@ def list( ) -> SyncPage[NetworkListItem]: """List networks. - List fraud networks detected within this application's organization, newest activity first by default. By default dismissed networks are excluded; pass status to include them. Sandbox applications never have networks - clustering excludes sandbox data entirely. + List fraud networks detected within this application's organization, highest risk first by default. By default dismissed networks are excluded; pass status to include them. Sandbox applications never have networks: clustering excludes sandbox data entirely. Every response carries the organization's `sharing_mode` and whether cross-organization signals apply to it (`cross_org_available`, `cross_org_unavailable_reason`). `kpis` aggregate the networks that match your filters and `counts` aggregate every network the application can see; pass `summary=false` to skip both when you only need the page. Requires a full-access API key, or `list:networks` for a console member. ``GET /v3/organization/{organization_id}/application/{application_id}/networks/`` Args: status: Comma-separated statuses to include: active,in_review,resolved,dismissed. Omit to exclude dismissed. + scope: `transactions` keeps networks whose members are all transaction parties; `identity` keeps every other network. Omit for both. It also narrows `counts.views` and `counts.facets`. Any other value returns 400. signal_type: Comma-separated signal types: ip_address,device,face,document_number,phone,email,address. pattern_type: Comma-separated pattern types, e.g. exact_same_device,same_email. risk_band: Comma-separated bands: low,medium,high. min_size: Minimum member count. max_size: Maximum member count. + assignee: Comma-separated assignees: `unassigned`, member emails (matched case-insensitively), or `me` for the calling console member. A network matches any value. `me` needs a console member token: an API key gets 400 `{"assignee": ["\\"me\\" needs a user token; an API key has no assignee identity."]}`. + priority: Comma-separated priorities: `low`, `medium`, `high`, or `none` for networks without a priority. Any other value returns 400 `{"priority": "Unknown priority: ."}`. + risk_min: Minimum risk score, a whole number from 0 to 100. A value that is not a whole number returns 400 `{"risk_min": "Use a whole number from 0 to 100."}`. + has_case: `true` keeps the networks your application opened a case for, `false` the others. Any other value is ignored. date_from: Lower bound on last_activity_at (ISO 8601 date or datetime). date_to: Upper bound on first_activity_at (ISO 8601 date or datetime). q: Free-text search across network name, member names, the network_id (N-0001), and exact pattern_type tokens. ordering: One of risk_score, -risk_score, last_activity_at, -last_activity_at, first_activity_at, -first_activity_at, network_number, -network_number. Defaults to -risk_score,-last_activity_at. limit: Page size, 1-500. Defaults to 100. offset: Zero-based offset of the first record. + summary: `false` returns the page and its `count` only, without `kpis` and `counts`. Defaults to `true`. Any value other than `true` or `false` returns 400. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -126,17 +139,23 @@ def list( auth="api_key", query={ "status": status, + "scope": scope, "signal_type": signal_type, "pattern_type": pattern_type, "risk_band": risk_band, "min_size": min_size, "max_size": max_size, + "assignee": assignee, + "priority": priority, + "risk_min": risk_min, + "has_case": has_case, "date_from": date_from, "date_to": date_to, "q": q, "ordering": ordering, "limit": limit, "offset": offset, + "summary": summary, }, extra_headers=extra_headers, extra_query=extra_query, @@ -153,10 +172,10 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> NetworkDetail: + ) -> NetworkDetailResponse: """Get network. - Retrieve a single network's full detail, including stats, fraud tags, and risk signals. + Retrieve a single network's full detail: stats, fraud tags and risk signals, plus the organization's `sharing_mode` and cross-organization availability, your application's `tags` on the network, member `outcomes` and pattern scores (`patterns`). Requires a full-access API key, or `read:networks` for a console member. ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/`` @@ -168,11 +187,11 @@ def retrieve( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "NetworkDetail", + "NetworkDetailResponse", self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/", - cast_to=NetworkDetail, + cast_to=NetworkDetailResponse, path_params={"network_uuid": network_uuid}, scope="application", auth="api_key", @@ -310,33 +329,64 @@ def retrieve_timeline( self, network_uuid: str, *, + contract: Literal["2"] | str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + cursor: str | NotGiven = NOT_GIVEN, + event_type: Sequence[ + Literal[ + "network_detected", + "signal_observed", + "network_active", + "network_in_review", + "network_resolved", + "network_dismissed", + "verification_attempt", + "transaction", + "login", + "rule_hit", + "case_linked", + "note_added", + ] + | str + ] + | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> NetworksRetrieveTimelineResponse: + ) -> NetworkTimeline | NetworkTimelinePage: """Get network timeline. - Chronological events for this network: detection, each signal's observation, and lifecycle status changes. + Without `contract`: the network's own events (detection, each signal's latest observation and its review), oldest first and not paginated. `limit`, `cursor` and `event_type` are ignored. With `contract=2`: the typed timeline, newest first and paginated by cursor. It adds what the member accounts did (verification attempts, transactions, sign-ins you post through the events API and the live rules they hit) and the cases opened for the network. `note_added` events are returned to console members only, never to an API key. Requires a full-access API key, or `read:networks` for a console member. ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/timeline/`` Args: network_uuid: Network UUID. + contract: `2` returns the typed timeline. Omit it for the default timeline. Any other value returns 400. + limit: Typed timeline only. Events per page, 1 to 500. Defaults to 100. + cursor: Typed timeline only. The previous page's `next_cursor`. An invalid or empty cursor returns 400. + event_type: Typed timeline only. Event types to keep, repeated or comma-separated, at most 20 values. Defaults to every type. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "NetworksRetrieveTimelineResponse", + "NetworkTimeline | NetworkTimelinePage", self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/timeline/", - cast_to=NetworksRetrieveTimelineResponse, + cast_to=NetworkTimeline | NetworkTimelinePage, path_params={"network_uuid": network_uuid}, scope="application", auth="api_key", + query={ + "contract": contract, + "limit": limit, + "cursor": cursor, + "event_type": event_type, + }, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -421,11 +471,93 @@ def download_report( ), ) + def add_tag( + self, + network_uuid: str, + *, + tag_uuid: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworkDetailResponse: + """Add a tag to a network. + + Put one of your application's tags on the network. Returns the network detail, whose `tags` lists your application's tags on the network, with `201` when the tag was added and `200` when it was already on the network. A tag that belongs to another application, or does not exist, returns 404 `Tag not found.`. Requires a full-access API key, or `write:networks` for a console member. + + ``POST /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/tags/add/`` + + Args: + network_uuid: Network UUID. + tag_uuid: UUID of one of your application's tags. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworkDetailResponse", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/tags/add/", + cast_to=NetworkDetailResponse, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + body={"tag_uuid": tag_uuid}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def remove_tag( + self, + network_uuid: str, + *, + tag_uuid: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworkDetailResponse: + """Remove a tag from a network. + + Remove one of your application's tags from the network. Returns the network detail. 404 when the tag is not on the network, or does not belong to your application. Requires a full-access API key, or `write:networks` for a console member. + + ``POST /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/tags/remove/`` + + Args: + network_uuid: Network UUID. + tag_uuid: UUID of one of your application's tags. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworkDetailResponse", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/tags/remove/", + cast_to=NetworkDetailResponse, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + body={"tag_uuid": tag_uuid}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + class AsyncNetworks(AsyncAPIResource): """The ``networks`` resource of the Didit API. - Methods: ``download_report``, ``list``, ``list_members``, ``list_signals``, ``retrieve``, ``retrieve_graph``, ``retrieve_map``, ``retrieve_timeline``. + Methods: ``add_tag``, ``download_report``, ``list``, ``list_members``, ``list_signals``, ``remove_tag``, ``retrieve``, ``retrieve_graph``, ``retrieve_map``, ``retrieve_timeline``. Nested resources: ``notes``. """ @@ -450,17 +582,23 @@ def list( self, *, status: str | NotGiven = NOT_GIVEN, + scope: Literal["identity", "transactions"] | str | NotGiven = NOT_GIVEN, signal_type: str | NotGiven = NOT_GIVEN, pattern_type: str | NotGiven = NOT_GIVEN, risk_band: str | NotGiven = NOT_GIVEN, min_size: int | NotGiven = NOT_GIVEN, max_size: int | NotGiven = NOT_GIVEN, + assignee: str | NotGiven = NOT_GIVEN, + priority: str | NotGiven = NOT_GIVEN, + risk_min: int | NotGiven = NOT_GIVEN, + has_case: bool | NotGiven = NOT_GIVEN, date_from: str | _dt.date | NotGiven = NOT_GIVEN, date_to: str | _dt.date | NotGiven = NOT_GIVEN, q: str | NotGiven = NOT_GIVEN, ordering: str | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, + summary: bool | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -468,23 +606,29 @@ def list( ) -> AsyncPaginator[NetworkListItem]: """List networks. - List fraud networks detected within this application's organization, newest activity first by default. By default dismissed networks are excluded; pass status to include them. Sandbox applications never have networks - clustering excludes sandbox data entirely. + List fraud networks detected within this application's organization, highest risk first by default. By default dismissed networks are excluded; pass status to include them. Sandbox applications never have networks: clustering excludes sandbox data entirely. Every response carries the organization's `sharing_mode` and whether cross-organization signals apply to it (`cross_org_available`, `cross_org_unavailable_reason`). `kpis` aggregate the networks that match your filters and `counts` aggregate every network the application can see; pass `summary=false` to skip both when you only need the page. Requires a full-access API key, or `list:networks` for a console member. ``GET /v3/organization/{organization_id}/application/{application_id}/networks/`` Args: status: Comma-separated statuses to include: active,in_review,resolved,dismissed. Omit to exclude dismissed. + scope: `transactions` keeps networks whose members are all transaction parties; `identity` keeps every other network. Omit for both. It also narrows `counts.views` and `counts.facets`. Any other value returns 400. signal_type: Comma-separated signal types: ip_address,device,face,document_number,phone,email,address. pattern_type: Comma-separated pattern types, e.g. exact_same_device,same_email. risk_band: Comma-separated bands: low,medium,high. min_size: Minimum member count. max_size: Maximum member count. + assignee: Comma-separated assignees: `unassigned`, member emails (matched case-insensitively), or `me` for the calling console member. A network matches any value. `me` needs a console member token: an API key gets 400 `{"assignee": ["\\"me\\" needs a user token; an API key has no assignee identity."]}`. + priority: Comma-separated priorities: `low`, `medium`, `high`, or `none` for networks without a priority. Any other value returns 400 `{"priority": "Unknown priority: ."}`. + risk_min: Minimum risk score, a whole number from 0 to 100. A value that is not a whole number returns 400 `{"risk_min": "Use a whole number from 0 to 100."}`. + has_case: `true` keeps the networks your application opened a case for, `false` the others. Any other value is ignored. date_from: Lower bound on last_activity_at (ISO 8601 date or datetime). date_to: Upper bound on first_activity_at (ISO 8601 date or datetime). q: Free-text search across network name, member names, the network_id (N-0001), and exact pattern_type tokens. ordering: One of risk_score, -risk_score, last_activity_at, -last_activity_at, first_activity_at, -first_activity_at, network_number, -network_number. Defaults to -risk_score,-last_activity_at. limit: Page size, 1-500. Defaults to 100. offset: Zero-based offset of the first record. + summary: `false` returns the page and its `count` only, without `kpis` and `counts`. Defaults to `true`. Any value other than `true` or `false` returns 400. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -501,17 +645,23 @@ def list( auth="api_key", query={ "status": status, + "scope": scope, "signal_type": signal_type, "pattern_type": pattern_type, "risk_band": risk_band, "min_size": min_size, "max_size": max_size, + "assignee": assignee, + "priority": priority, + "risk_min": risk_min, + "has_case": has_case, "date_from": date_from, "date_to": date_to, "q": q, "ordering": ordering, "limit": limit, "offset": offset, + "summary": summary, }, extra_headers=extra_headers, extra_query=extra_query, @@ -528,10 +678,10 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> NetworkDetail: + ) -> NetworkDetailResponse: """Get network. - Retrieve a single network's full detail, including stats, fraud tags, and risk signals. + Retrieve a single network's full detail: stats, fraud tags and risk signals, plus the organization's `sharing_mode` and cross-organization availability, your application's `tags` on the network, member `outcomes` and pattern scores (`patterns`). Requires a full-access API key, or `read:networks` for a console member. ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/`` @@ -543,11 +693,11 @@ async def retrieve( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "NetworkDetail", + "NetworkDetailResponse", await self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/", - cast_to=NetworkDetail, + cast_to=NetworkDetailResponse, path_params={"network_uuid": network_uuid}, scope="application", auth="api_key", @@ -685,33 +835,64 @@ async def retrieve_timeline( self, network_uuid: str, *, + contract: Literal["2"] | str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + cursor: str | NotGiven = NOT_GIVEN, + event_type: Sequence[ + Literal[ + "network_detected", + "signal_observed", + "network_active", + "network_in_review", + "network_resolved", + "network_dismissed", + "verification_attempt", + "transaction", + "login", + "rule_hit", + "case_linked", + "note_added", + ] + | str + ] + | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> NetworksRetrieveTimelineResponse: + ) -> NetworkTimeline | NetworkTimelinePage: """Get network timeline. - Chronological events for this network: detection, each signal's observation, and lifecycle status changes. + Without `contract`: the network's own events (detection, each signal's latest observation and its review), oldest first and not paginated. `limit`, `cursor` and `event_type` are ignored. With `contract=2`: the typed timeline, newest first and paginated by cursor. It adds what the member accounts did (verification attempts, transactions, sign-ins you post through the events API and the live rules they hit) and the cases opened for the network. `note_added` events are returned to console members only, never to an API key. Requires a full-access API key, or `read:networks` for a console member. ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/timeline/`` Args: network_uuid: Network UUID. + contract: `2` returns the typed timeline. Omit it for the default timeline. Any other value returns 400. + limit: Typed timeline only. Events per page, 1 to 500. Defaults to 100. + cursor: Typed timeline only. The previous page's `next_cursor`. An invalid or empty cursor returns 400. + event_type: Typed timeline only. Event types to keep, repeated or comma-separated, at most 20 values. Defaults to every type. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "NetworksRetrieveTimelineResponse", + "NetworkTimeline | NetworkTimelinePage", await self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/timeline/", - cast_to=NetworksRetrieveTimelineResponse, + cast_to=NetworkTimeline | NetworkTimelinePage, path_params={"network_uuid": network_uuid}, scope="application", auth="api_key", + query={ + "contract": contract, + "limit": limit, + "cursor": cursor, + "event_type": event_type, + }, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -796,6 +977,88 @@ async def download_report( ), ) + async def add_tag( + self, + network_uuid: str, + *, + tag_uuid: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworkDetailResponse: + """Add a tag to a network. + + Put one of your application's tags on the network. Returns the network detail, whose `tags` lists your application's tags on the network, with `201` when the tag was added and `200` when it was already on the network. A tag that belongs to another application, or does not exist, returns 404 `Tag not found.`. Requires a full-access API key, or `write:networks` for a console member. + + ``POST /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/tags/add/`` + + Args: + network_uuid: Network UUID. + tag_uuid: UUID of one of your application's tags. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworkDetailResponse", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/tags/add/", + cast_to=NetworkDetailResponse, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + body={"tag_uuid": tag_uuid}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def remove_tag( + self, + network_uuid: str, + *, + tag_uuid: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworkDetailResponse: + """Remove a tag from a network. + + Remove one of your application's tags from the network. Returns the network detail. 404 when the tag is not on the network, or does not belong to your application. Requires a full-access API key, or `write:networks` for a console member. + + ``POST /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/tags/remove/`` + + Args: + network_uuid: Network UUID. + tag_uuid: UUID of one of your application's tags. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworkDetailResponse", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/tags/remove/", + cast_to=NetworkDetailResponse, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + body={"tag_uuid": tag_uuid}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + class NetworksWithRawResponse: def __init__(self, resource: Networks) -> None: @@ -808,6 +1071,8 @@ def __init__(self, resource: Networks) -> None: self.retrieve_timeline = to_raw_response_wrapper(resource.retrieve_timeline) self.retrieve_map = to_raw_response_wrapper(resource.retrieve_map) self.download_report = to_raw_response_wrapper(resource.download_report) + self.add_tag = to_raw_response_wrapper(resource.add_tag) + self.remove_tag = to_raw_response_wrapper(resource.remove_tag) @cached_property def notes(self) -> NetworksNotesWithRawResponse: @@ -827,6 +1092,8 @@ def __init__(self, resource: AsyncNetworks) -> None: self.retrieve_timeline = async_to_raw_response_wrapper(resource.retrieve_timeline) self.retrieve_map = async_to_raw_response_wrapper(resource.retrieve_map) self.download_report = async_to_raw_response_wrapper(resource.download_report) + self.add_tag = async_to_raw_response_wrapper(resource.add_tag) + self.remove_tag = async_to_raw_response_wrapper(resource.remove_tag) @cached_property def notes(self) -> AsyncNetworksNotesWithRawResponse: @@ -846,6 +1113,8 @@ def __init__(self, resource: Networks) -> None: self.retrieve_timeline = to_streamed_response_wrapper(resource.retrieve_timeline) self.retrieve_map = to_streamed_response_wrapper(resource.retrieve_map) self.download_report = to_streamed_response_wrapper(resource.download_report) + self.add_tag = to_streamed_response_wrapper(resource.add_tag) + self.remove_tag = to_streamed_response_wrapper(resource.remove_tag) @cached_property def notes(self) -> NetworksNotesWithStreamingResponse: @@ -865,6 +1134,8 @@ def __init__(self, resource: AsyncNetworks) -> None: self.retrieve_timeline = async_to_streamed_response_wrapper(resource.retrieve_timeline) self.retrieve_map = async_to_streamed_response_wrapper(resource.retrieve_map) self.download_report = async_to_streamed_response_wrapper(resource.download_report) + self.add_tag = async_to_streamed_response_wrapper(resource.add_tag) + self.remove_tag = async_to_streamed_response_wrapper(resource.remove_tag) @cached_property def notes(self) -> AsyncNetworksNotesWithStreamingResponse: diff --git a/src/didit/resources/phone.py b/src/didit/resources/phone.py index d6c2bc6..6121460 100644 --- a/src/didit/resources/phone.py +++ b/src/didit/resources/phone.py @@ -140,7 +140,7 @@ def send_code( **Delivery channels.** `options.preferred_channel` selects the channel (`whatsapp` by default; `sms`, `telegram`, `voice`, `rcs`, `viber`, and `zalo` are also supported). When the preferred channel is unavailable for the destination country or number, delivery automatically falls back to SMS - the send still reports `Success`, and the channel actually used is reported by the check response (`phone.verification_method` and the `lifecycle` events). - **Branding.** SMS codes carry your application's SMS sender name, set in the Business Console, at no extra cost. Pass `options.sms_sender_name` to sign one code with another name, or `options.use_white_label_customization: true` to fall back to your white-label public name when no sender name is set. WhatsApp codes use a fixed message and are not branded. + **Branding.** SMS codes carry your application's SMS sender name, set in the Business Console, at no extra cost. Pass `options.sms_sender_name` to sign one code with another name, or `options.use_white_label_customization: true` to fall back to your white-label public name when no sender name is set. WhatsApp codes use a fixed message; connect your own WhatsApp Business number in the Business Console to send them from your business name and number, billed at the Didit fee only. **Send statuses.** `Success` - OTP sent for a new verification; `Retry` - OTP re-sent for the pending verification; `Blocked` - the anti-fraud layer refused to send (see `reason`, e.g. `repeated_attempts`, `suspicious`, `spam`). A `Blocked` send immediately finalizes the verification as `Declined` with a high-risk warning, so a follow-up check returns `Expired or Not Found`. @@ -375,7 +375,7 @@ async def send_code( **Delivery channels.** `options.preferred_channel` selects the channel (`whatsapp` by default; `sms`, `telegram`, `voice`, `rcs`, `viber`, and `zalo` are also supported). When the preferred channel is unavailable for the destination country or number, delivery automatically falls back to SMS - the send still reports `Success`, and the channel actually used is reported by the check response (`phone.verification_method` and the `lifecycle` events). - **Branding.** SMS codes carry your application's SMS sender name, set in the Business Console, at no extra cost. Pass `options.sms_sender_name` to sign one code with another name, or `options.use_white_label_customization: true` to fall back to your white-label public name when no sender name is set. WhatsApp codes use a fixed message and are not branded. + **Branding.** SMS codes carry your application's SMS sender name, set in the Business Console, at no extra cost. Pass `options.sms_sender_name` to sign one code with another name, or `options.use_white_label_customization: true` to fall back to your white-label public name when no sender name is set. WhatsApp codes use a fixed message; connect your own WhatsApp Business number in the Business Console to send them from your business name and number, billed at the Didit fee only. **Send statuses.** `Success` - OTP sent for a new verification; `Retry` - OTP re-sent for the pending verification; `Blocked` - the anti-fraud layer refused to send (see `reason`, e.g. `repeated_attempts`, `suspicious`, `spam`). A `Blocked` send immediately finalizes the verification as `Declined` with a high-risk warning, so a follow-up check returns `Expired or Not Found`. diff --git a/src/didit/resources/regulatory_reports.py b/src/didit/resources/regulatory_reports.py index 15b0062..af5240f 100644 --- a/src/didit/resources/regulatory_reports.py +++ b/src/didit/resources/regulatory_reports.py @@ -19,8 +19,8 @@ ) from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query from didit.types.components.regulatory_report_detail import RegulatoryReportDetail +from didit.types.components.regulatory_report_list import RegulatoryReportList from didit.types.regulatory_reports.create_params import RegulatoryReportsCreateParamsNarrative -from didit.types.regulatory_reports.list_response import RegulatoryReportsListResponse from didit.types.regulatory_reports.retrieve_download_url_response import ( RegulatoryReportsRetrieveDownloadUrlResponse, ) @@ -90,7 +90,7 @@ def list( self._client.request_page( "get", "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/", - cast_to=RegulatoryReportsListResponse, + cast_to=RegulatoryReportList, item_type=RegulatoryReportDetail, scope="application", auth="api_key", @@ -542,7 +542,7 @@ def list( self._client.request_page( "get", "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/", - cast_to=RegulatoryReportsListResponse, + cast_to=RegulatoryReportList, item_type=RegulatoryReportDetail, scope="application", auth="api_key", diff --git a/src/didit/resources/sessions/__init__.py b/src/didit/resources/sessions/__init__.py index 76d7075..7242bb0 100644 --- a/src/didit/resources/sessions/__init__.py +++ b/src/didit/resources/sessions/__init__.py @@ -17,35 +17,55 @@ to_raw_response_wrapper, to_streamed_response_wrapper, ) -from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query -from didit.types.sessions.batch_delete_response import ( - SessionsBatchDeleteResponse, - SessionsBatchDeleteResponse202, -) +from didit._types import NOT_GIVEN, Body, FileTypes, Headers, NotGiven, Query +from didit.types.components.aml_screening_run import AMLScreeningRun +from didit.types.components.business_session_list_item import BusinessSessionListItem +from didit.types.components.entity_network_membership_list import EntityNetworkMembershipList +from didit.types.components.feature_status_override import FeatureStatusOverride +from didit.types.components.kyb_key_people_party import KYBKeyPeopleParty +from didit.types.components.sandbox_simulate_response import SandboxSimulateResponse +from didit.types.components.session_decision import SessionDecision +from didit.types.components.session_deletion_job import SessionDeletionJob +from didit.types.components.session_event import SessionEvent +from didit.types.components.user_session_list_item import UserSessionListItem +from didit.types.sessions.add_images_response import SessionsAddImagesResponse +from didit.types.sessions.batch_delete_response import SessionsBatchDeleteResponse from didit.types.sessions.bulk_update_aml_hit_status_params import ( SessionsBulkUpdateAmlHitStatusParamsHitUpdates, ) +from didit.types.sessions.bulk_update_aml_hit_status_response import ( + SessionsBulkUpdateAmlHitStatusResponse, +) from didit.types.sessions.create_params import ( SessionsCreateParamsContactDetails, SessionsCreateParamsExpectedDetails, + SessionsCreateParamsSign, ) from didit.types.sessions.create_response import SessionsCreateResponse from didit.types.sessions.delete_response import SessionsDeleteResponse -from didit.types.sessions.list_networks_response import SessionsListNetworksResponse -from didit.types.sessions.list_response import ( - SessionsListResponse, - SessionsListResponseResultsKybSessionRowSessionKindBusiness, - SessionsListResponseResultsKycSessionRowSessionKindUser, +from didit.types.sessions.list_aml_screening_history_response import ( + SessionsListAmlScreeningHistoryResponse, +) +from didit.types.sessions.list_events_response import SessionsListEventsResponse +from didit.types.sessions.list_feature_overrides_response import ( + SessionsListFeatureOverridesResponse, +) +from didit.types.sessions.list_response import SessionsListResponse +from didit.types.sessions.preview_decision_email_response import ( + SessionsPreviewDecisionEmailResponse, ) from didit.types.sessions.preview_resubmission_params import ( SessionsPreviewResubmissionParamsNodesToResubmit, ) from didit.types.sessions.preview_resubmission_response import SessionsPreviewResubmissionResponse -from didit.types.sessions.retrieve_decision_response import SessionsRetrieveDecisionResponse -from didit.types.sessions.retrieve_deletion_job_response import SessionsRetrieveDeletionJobResponse from didit.types.sessions.share_response import SessionsShareResponse +from didit.types.sessions.update_aml_hit_status_response import SessionsUpdateAmlHitStatusResponse +from didit.types.sessions.update_aml_status_response import SessionsUpdateAmlStatusResponse from didit.types.sessions.update_data_params import SessionsUpdateDataParamsParsedAddress from didit.types.sessions.update_data_response import SessionsUpdateDataResponse +from didit.types.sessions.update_document_ai_data_response import ( + SessionsUpdateDocumentAiDataResponse, +) from didit.types.sessions.update_feature_status_response import SessionsUpdateFeatureStatusResponse from didit.types.sessions.update_kyb_company_data_response import ( SessionsUpdateKybCompanyDataResponse, @@ -73,6 +93,22 @@ SessionsReviewsWithRawResponse, SessionsReviewsWithStreamingResponse, ) + from didit.resources.sessions.signatures import ( + AsyncSessionsSignatures, + AsyncSessionsSignaturesWithRawResponse, + AsyncSessionsSignaturesWithStreamingResponse, + SessionsSignatures, + SessionsSignaturesWithRawResponse, + SessionsSignaturesWithStreamingResponse, + ) + from didit.resources.sessions.webhooks import ( + AsyncSessionsWebhooks, + AsyncSessionsWebhooksWithRawResponse, + AsyncSessionsWebhooksWithStreamingResponse, + SessionsWebhooks, + SessionsWebhooksWithRawResponse, + SessionsWebhooksWithStreamingResponse, + ) __all__ = [ "Sessions", @@ -87,9 +123,9 @@ class Sessions(SyncAPIResource): """The ``sessions`` resource of the Didit API. - Methods: ``batch_delete``, ``bulk_update_aml_hit_status``, ``create``, ``delete``, ``generate_pdf``, ``import_shared``, ``list``, ``list_networks``, ``preview_resubmission``, ``retrieve_decision``, ``retrieve_deletion_job``, ``share``, ``update_aml_hit_status``, ``update_data``, ``update_feature_status``, ``update_kyb_company_data``, ``update_kyb_party``, ``update_poa_data``, ``update_status``. + Methods: ``add_images``, ``batch_delete``, ``bulk_update_aml_hit_status``, ``cancel_feature_override``, ``create``, ``delete``, ``export_aml_screening_history``, ``generate_pdf``, ``import_shared``, ``list``, ``list_aml_screening_history``, ``list_events``, ``list_feature_overrides``, ``list_networks``, ``preview_decision_email``, ``preview_resubmission``, ``regenerate_kyb_party``, ``retrieve_decision``, ``retrieve_deletion_job``, ``share``, ``simulate``, ``update_aml_hit_status``, ``update_aml_status``, ``update_data``, ``update_document_ai_data``, ``update_feature_status``, ``update_kyb_company_data``, ``update_kyb_party``, ``update_poa_data``, ``update_status``. - Nested resources: ``imports``, ``reviews``. + Nested resources: ``imports``, ``reviews``, ``signatures``, ``webhooks``. """ @cached_property @@ -104,6 +140,18 @@ def reviews(self) -> SessionsReviews: return SessionsReviews(self._client) + @cached_property + def signatures(self) -> SessionsSignatures: + from didit.resources.sessions.signatures import SessionsSignatures + + return SessionsSignatures(self._client) + + @cached_property + def webhooks(self) -> SessionsWebhooks: + from didit.resources.sessions.webhooks import SessionsWebhooks + + return SessionsWebhooks(self._client) + @cached_property def with_raw_response(self) -> SessionsWithRawResponse: """Every method of this resource, returning the raw HTTP response.""" @@ -184,6 +232,7 @@ def create( expected_details: SessionsCreateParamsExpectedDetails | NotGiven = NOT_GIVEN, portrait_image: str | NotGiven = NOT_GIVEN, sandbox_scenario: str | NotGiven = NOT_GIVEN, + sign: SessionsCreateParamsSign | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -204,7 +253,7 @@ def create( Args: workflow_id: Stable identifier of the workflow that defines which verification steps the session will run. Workflows are created and managed in the [Workflows](/console/workflows) page of the Console. The `workflow_id` also implicitly selects whether the session is KYC or KYB. vendor_data: A unique identifier for the user being verified, such as a UUID, email, or internal user ID. This field is used for: (1) **User grouping** - sessions with the same vendor_data are linked to the same user profile in the Users tab. (2) **Cross-session duplicate detection** - when checking for duplicated faces, documents, phone numbers, emails, IP addresses, or device fingerprints, sessions with the same vendor_data are treated as the same user and excluded from matches. Without vendor_data, every session is treated as a unique user and all potential duplicates are surfaced. We strongly recommend always providing a vendor_data to reduce noise in duplicate detection. - callback: URL to redirect the user to after verification completes. Didit automatically appends `verificationSessionId` and `status` (Approved, Declined, In Review) as query parameters. Custom URL schemes (e.g. `myapp://`) are supported for mobile callbacks. If omitted, the workflow's default `callback_url` is used. + callback: URL to redirect the user to after verification completes. Didit automatically appends `verificationSessionId` and `status` (Approved, Declined, In Review) as query parameters. Custom URL schemes (e.g. `myapp://`) are supported for mobile callbacks. If omitted, the workflow's default `callback_url` is used. When the application has allowed domains (`allowed_domains` on `PATCH /v3/webhook/`), an `http` or `https` callback must be on one of those origins (same scheme, host and port), otherwise the request fails with 400 on `callback`: `callback must be on one of the application's allowed domains: https://app.example.com`. Custom URL schemes are always accepted. callback_method: Determines which device should handle the redirect to the provided callback URL. Use `initiator` to redirect only the device that started the flow, `completer` for the device that finishes it, or `both` to allow either device to trigger the callback. If you ever notice the callback not triggering reliably, we recommend setting this value to `both`. metadata: Arbitrary JSON stored with the session and echoed back in the response and webhooks. Any JSON value is accepted (object, string, number, array), but a JSON object of key/value pairs is recommended. Not shown to the end user. Use it to pass your own correlation ids, A/B variants, or business context. language: Language code (ISO 639-1) for the verification process interface. Controls the language displayed to the end user during verification. If not provided, the browser's language will be automatically detected and used. Check all the supported languages [here](/integration/supported-languages). @@ -212,6 +261,7 @@ def create( expected_details: Expected user details used to cross-validate against the data extracted from the user's ID document, Proof of Address, and other verification steps. Mismatches are surfaced as warnings on the decision; some fields (e.g. `id_country`, `expected_document_types`) also alter the user-facing flow. For Business Verification (KYB) workflows only the business fields (`company_name`, `registry_country`, `registration_number`) are used - they pre-fill the company registry search for the user - and all person-level fields are ignored. portrait_image: Base64-encoded portrait image of the end user's face (max 2MB; JPEG, PNG, WebP, or TIFF). Used as the reference image to match against the liveness capture when the workflow is a `Biometric Authentication` workflow with Face Match enabled, or any graph workflow where Face Match runs before ID Verification (OCR). Optional when the `vendor_data` user already has a stored face image: Didit reuses it automatically, resolved in priority order: approved liveness face, ePassport chip photo, ID document portrait, manually enrolled profile face (only faces from approved sessions or faces you enrolled yourself are reused). When provided, `portrait_image` always takes precedence over the stored face. If omitted and no stored face exists (or `vendor_data` is not sent), session creation fails with `400`. Ignored for other workflow types. sandbox_scenario: Sandbox-only scenario slug. Only accepted when the API key belongs to a sandbox-mode application; rejected on live applications. When set, the create flow expands the scenario's bundled magic values onto the session's input fields (contact_details, expected_details) so the mocked providers reproduce the scenario's outcome through the real workflow pipeline. See the scenario catalog at GET /v1/sandbox/scenarios/ or [Sandbox & Test Data](/integration/sandbox-testing). You can also arm (or re-arm) a scenario after create via POST /v3/session/{sessionId}/sandbox/arm/, as long as the session is still `Not Started`. For non-doc lookup, use `lookup_partial_match`, `lookup_no_match`, or `lookup_provider_error` to test the configured attempt budget and fallback. For identity wallets, use `wallet_cancelled`, `wallet_timeout`, or `wallet_provider_error`; `approve` simulates a verified wallet. All catalog wallets can be tested in sandbox, including schemes unavailable on live applications. Simulated attempts are not charged. + sign: Electronic Signature input for this session. `documents` are the documents to sign, for a workflow whose Electronic Signature step takes documents from the session (`signature_documents_source` `session` or `both`): up to 20 documents and 50 MB in total. `field_values` fills the fields whose `prefill` is `api.`, on workflow and session documents alike. Creating a session again for the same `workflow_id` and `vendor_data` returns the pending session; if that call sends `sign`, its documents, fields and values must be identical to the first ones, and anything different returns `400` with `{"sign": "signature_document_changed"}`. See [Electronic signature documents](/sessions-api/create-session#electronic-signature-documents). extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -235,6 +285,7 @@ def create( "expected_details": expected_details, "portrait_image": portrait_image, "sandbox_scenario": sandbox_scenario, + "sign": sign, }, extra_headers=extra_headers, extra_query=extra_query, @@ -254,12 +305,12 @@ def retrieve_decision( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsRetrieveDecisionResponse: + ) -> SessionDecision: """Retrieve the full decision for a verification session. Retrieve the complete decision report for a verification session - the rolled-up session `status` plus one report per verification feature the workflow executed. This is the canonical read endpoint for both User Verification (KYC) and Business Verification (KYB) sessions: the same path serves both, and the top-level `session_kind` discriminator tells you which shape you received (`"user"` or `"business"`). - **Every feature report is a plural array.** V3 workflows are graphs that can run the same feature more than once, so each block (`id_verifications`, `nfc_verifications`, `liveness_checks`, `face_matches`, `poa_verifications`, `document_ai_documents`, `phone_verifications`, `email_verifications`, `aml_screenings`, `ip_analyses`, `database_validations`, `questionnaire_responses`) is a JSON array whose items each carry a `node_id` identifying the workflow graph node that produced them. Read NFC as `response.nfc_verifications[0]` - never as a singular `nfc` field. A block is `null` until the workflow has run that feature at least once; features the workflow does not include never appear with data. + **Every feature report is a plural array.** V3 workflows are graphs that can run the same feature more than once, so each block (`id_verifications`, `nfc_verifications`, `liveness_checks`, `face_matches`, `poa_verifications`, `document_ai_documents`, `phone_verifications`, `email_verifications`, `aml_screenings`, `ip_analyses`, `database_validations`, `questionnaire_responses`) is a JSON array whose items each carry a `node_id` identifying the workflow graph node that produced them. Read NFC as `response.nfc_verifications[0]` - never as a singular `nfc` field. A block is `null` until the workflow has run that feature at least once; features the workflow does not include never appear with data. Electronic Signature results are in `signatures`, which is present only when the workflow has an Electronic Signature step. **When to call.** The recommended pattern is event-driven: wait for the `status.updated` webhook that fires when the session reaches a decision (`Approved`, `Declined`, `In Review`), then call this endpoint once to fetch the full report. Polling also works - the endpoint can be called at any point in the session lifecycle and returns the data produced so far - but webhook-then-fetch is cheaper and faster. Media URLs in the response (document images, videos, PDFs) are short-lived presigned links: fetch them promptly or re-request the decision to get fresh ones, and do not persist them as long-term references. @@ -280,11 +331,11 @@ def retrieve_decision( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsRetrieveDecisionResponse", + "SessionDecision", self._client.request( "get", "/v3/session/{sessionId}/decision/", - cast_to=SessionsRetrieveDecisionResponse, + cast_to=SessionDecision, path_params={"sessionId": session_id}, auth="api_key", query={"include": include}, @@ -317,7 +368,7 @@ def delete( **What is deleted:** - The session and every verification attempt it holds, including results that a resubmission replaced: the decision, extracted data, all related feature records (ID verification, face, liveness, face match, AML, registry checks, IP analysis, and so on), its reviews and its activity log. - - All stored files owned by the session: document front/back photos (full, cropped, and privacy-blurred variants), document-capture videos, portrait crops, NFC chip portrait and signature images, face reference images, liveness videos, face-match source/target images, Proof of Address documents, and any extra uploaded files (KYC); uploaded company documents and extra files (KYB). Previously issued media URLs (`https:///...`) stop resolving. + - All stored files owned by the session: document front/back photos (full, cropped, and privacy-blurred variants), document-capture videos, portrait crops, NFC chip portrait and signature images, face reference images, liveness videos, face-match source/target images, Proof of Address documents, and any extra uploaded files (KYC); uploaded company documents and extra files (KYB). Electronic Signature documents, signed PDFs, evidence PDFs and signature images (KYC and KYB): their download paths stop working. Previously issued media URLs (`https:///...`) stop resolving. - The session's face data. Its face embedding is deleted too, unless you choose to keep an image-free template (see **Face biometric data**). - Webhook deliveries still queued for the session, and their retries. Webhooks already delivered are not affected. - For a Business Verification (KYB) session: the verifications it created for its key people (KYC) and its corporate owners (KYB), recursively, reported as `deleted_child_verifications`. A verification it only links to, such as an existing approved KYC it reused, is not deleted. @@ -459,13 +510,13 @@ def update_status( Manually override the final decision of a KYC or KYB verification session. Set `new_status` to `Approved` or `Declined` to record a manual decision, or to `Resubmitted` to clear specific verification steps and let the user redo them through the same verification URL. The endpoint accepts both user (KYC) and business (KYB) session IDs. - Call it once a session has reached a reviewable state: the current status must be `Approved`, `Declined`, `In Review`, `Kyc Expired`, `Abandoned`, or `Resubmitted`. Sessions that are still `Not Started`, `In Progress`, or `Awaiting User`, or that ended as `Expired`, return `400`. The new status must differ from the current one, and your API key's organization needs the `write:sessions` permission. The eligible-state check runs before body validation, so an ineligible session returns the `Wrong Current Status` error even when the payload is also invalid. + Call it once a session has reached a reviewable state: the current status must be `Approved`, `Declined`, `In Review`, `Kyc Expired`, `Abandoned`, or `Resubmitted`. Sessions that are still `Not Started`, `In Progress`, or `Awaiting User`, or that ended as `Expired`, return `400`. Your API key needs the `write:sessions` permission (a scoped API key needs write access to decisions). The eligible-state check runs before body validation, so an ineligible session returns the `Wrong Current Status` error even when the payload is also invalid. - For `Resubmitted`, pass `nodes_to_resubmit` to choose which workflow steps the user must redo, or omit it to auto-select existing feature attempts in a resubmittable state (`Declined`, `In Review`, `Not Finished`, `Expired`). Never-attempted features are not auto-selected - list them explicitly in `nodes_to_resubmit`, or the request returns `400` when no recorded attempt needs resubmission. Captured data for the selected steps is deleted, the steps are reordered to match the workflow graph, the session's expiration window restarts, and the original verification URL becomes usable again. Backend-only steps (`AML`, `DATABASE_VALIDATION`, `IP_ANALYSIS`) at the head of the list run immediately without user interaction; if every resubmitted step is backend-only, the session re-finalizes within the same request. `KYB_REGISTRY` and `KYB_KEY_PEOPLE` cannot be resubmitted directly - resubmit the relevant child KYC sessions instead and the parent business session recomputes automatically. + For `Resubmitted`, pass `nodes_to_resubmit` to choose which workflow steps the user must redo, or omit it to auto-select existing feature attempts in a resubmittable state (`Declined`, `In Review`, `Not Finished`, `Expired`). Never-attempted features are not auto-selected: list them explicitly in `nodes_to_resubmit`, or the request returns `400` when no recorded attempt needs resubmission. The selected steps' previous results move to the resubmission's history (returned by Get Decision with `include=resubmissions`), the steps are reordered to match the workflow graph, the session's expiration window restarts, and the original verification URL becomes usable again. Backend-only steps (`AML`, `DATABASE_VALIDATION`, `IP_ANALYSIS`) at the head of the list run immediately without user interaction; if every resubmitted step is backend-only, the session re-finalizes within the same request. `KYB_REGISTRY` and `KYB_KEY_PEOPLE` cannot be resubmitted directly: resubmit the relevant child KYC sessions instead and the parent business session recomputes automatically. - Every successful call fires the `status.updated` [webhook](/integration/webhooks) once the change commits and appends an activity entry - with actor attribution and your `comment` - to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and in the console activity timeline. The user-profile aggregates linked to the session's `vendor_data` update as well. Declining disables ongoing AML monitoring for the session; re-approving re-enables it (from `Resubmitted`, `Declined`, or `Kyc Expired`) when the workflow has monitoring turned on. Approving or declining a child KYC that belongs to a KYB key-people check recomputes the parent business session's status. Set `send_email: true` (with `email_address`) to notify the user: a status notice for `Approved`/`Declined`, or a resubmission email containing the verification link and per-step reasons for `Resubmitted`. + Every call that changes the status fires the `status.updated` [webhook](/integration/webhooks) once the change commits and appends an activity entry (with actor attribution and your `comment`) to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and in the console activity timeline. The user-profile aggregates linked to the session's `vendor_data` update as well. Declining disables ongoing AML monitoring for the session; re-approving re-enables it (from `Resubmitted`, `Declined`, or `Kyc Expired`) when the workflow has monitoring turned on. Approving or declining a child KYC that belongs to a KYB key-people check recomputes the parent business session's status. Set `send_email: true` (with `email_address`) to notify the user: a status notice for `Approved`/`Declined`, or a resubmission email containing the verification link and per-step reasons for `Resubmitted`. To check the email first, preview it with [Preview Decision Email](/sessions-api/preview-decision-email) (`Approved`, `Declined`) or [Preview Resubmission Email](/sessions-api/preview-resubmission) (`Resubmitted`) and pass its `preview_token` as `email_preview_token`: the update then sends exactly the previewed email, or returns `409` before changing anything when the email or the session changed since the preview or the receipt is older than 15 minutes. - The operation is intentionally not repeatable with the same value: re-sending the current status returns `400` (`The new status is the same as the current status.`), so accidental double-submissions are harmless. Authentication and permission failures both return `403` - this API never responds `401`. + Repeating `Approved` or `Declined` on a session that already has that status changes nothing and returns `200`: no webhook, no activity entry and no email. Repeating `Resubmitted` returns `400` (`The new status is the same as the current status.`). Missing or invalid credentials and missing permissions return `403`; an expired scoped API key returns `401`. ``PATCH /v3/session/{sessionId}/update-status/`` @@ -477,7 +528,7 @@ def update_status( send_email: Whether to email the user about the change. Requires `email_address`. For `Approved`/`Declined` the user receives a status notice; for `Resubmitted` the email includes the verification link and the per-step resubmission reasons. email_address: Recipient for the notification email. **Required when `send_email` is `true`** - omitting it returns `400`. email_language: Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`). - email_preview_token: Optional signed receipt from Preview Resubmission Email. When supplied with send_email=true and new_status=Resubmitted, the recipient, language, checks, current session state, sender and message must still match the preview. A changed or expired receipt returns 409 before changing the session or sending email. Existing integrations may omit this field. + email_preview_token: Optional receipt from [Preview Decision Email](/sessions-api/preview-decision-email) (for `Approved` and `Declined`) or [Preview Resubmission Email](/sessions-api/preview-resubmission) (for `Resubmitted`): its `preview_token`. Valid only with `send_email: true`; without it the request returns `400`. The recipient, language, session status, sender and email (and, for a resubmission, the selected steps) must still match the preview, and the receipt expires after 15 minutes. Otherwise the request returns `409` before the session changes or an email is sent. Existing integrations may omit this field. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -550,10 +601,7 @@ def list( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SyncPage[ - SessionsListResponseResultsKycSessionRowSessionKindUser - | SessionsListResponseResultsKybSessionRowSessionKindBusiness - ]: + ) -> SyncPage[UserSessionListItem | BusinessSessionListItem]: """List verification sessions (KYC and/or KYB) with filters and pagination. Returns a paginated list of verification sessions for the application identified by your API key, newest first (descending `session_number`). @@ -608,13 +656,12 @@ def list( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SyncPage[SessionsListResponseResultsKycSessionRowSessionKindUser | SessionsListResponseResultsKybSessionRowSessionKindBusiness]", + "SyncPage[UserSessionListItem | BusinessSessionListItem]", self._client.request_page( "get", "/v3/sessions/", cast_to=SessionsListResponse, - item_type=SessionsListResponseResultsKycSessionRowSessionKindUser - | SessionsListResponseResultsKybSessionRowSessionKindBusiness, + item_type=UserSessionListItem | BusinessSessionListItem, auth="api_key", query={ "session_kind": session_kind, @@ -700,7 +747,7 @@ def import_shared( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> object: + ) -> SessionDecision: """Redeem a share token to clone a verification session into your application. Redeem a share token to clone a KYC or KYB session into the calling application with a fresh `session_id`. Not idempotent - a token redeems once per receiver. @@ -718,11 +765,11 @@ def import_shared( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "object", + "SessionDecision", self._client.request( "post", "/v3/session/import-shared/", - cast_to=object, + cast_to=SessionDecision, auth="api_key", body={ "share_token": share_token, @@ -749,11 +796,30 @@ def batch_delete( | str | NotGiven = NOT_GIVEN, instruction_id: str | NotGiven = NOT_GIVEN, + filter_status: str | Sequence[str] | NotGiven = NOT_GIVEN, + filter_vendor_data: str | NotGiven = NOT_GIVEN, + filter_search: str | NotGiven = NOT_GIVEN, + filter_workflow_id: str | Sequence[str] | NotGiven = NOT_GIVEN, + filter_date_from: str | _dt.date | NotGiven = NOT_GIVEN, + filter_date_to: str | _dt.date | NotGiven = NOT_GIVEN, + filter_country: str | Sequence[str] | NotGiven = NOT_GIVEN, + filter_document_type: str | Sequence[str] | NotGiven = NOT_GIVEN, + filter_document_name: str | Sequence[str] | NotGiven = NOT_GIVEN, + filter_region: str | Sequence[str] | NotGiven = NOT_GIVEN, + filter_didit_internal_id: str | NotGiven = NOT_GIVEN, + filter_session_type: str | Sequence[str] | NotGiven = NOT_GIVEN, + filter_api_service: str | NotGiven = NOT_GIVEN, + filter_screened_full_name: str | NotGiven = NOT_GIVEN, + filter_warning: str | Sequence[str] | NotGiven = NOT_GIVEN, + filter_status_initialized_as_in_review: bool | NotGiven = NOT_GIVEN, + filter_is_kyc_to_be_reviewed: bool | NotGiven = NOT_GIVEN, + filter_euuid: str | NotGiven = NOT_GIVEN, + filter_organization_name: str | Sequence[str] | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsBatchDeleteResponse | SessionsBatchDeleteResponse202: + ) -> SessionsBatchDeleteResponse | SessionDeletionJob: """Bulk delete KYC sessions by session_number. Bulk delete User Verification (KYC) sessions by their numeric `session_number`, or every KYC session in your application with `delete_all: true`. @@ -767,6 +833,8 @@ def batch_delete( Each deleted session gets exactly the same treatment as the single-session delete endpoint: the session, every verification attempt it holds, its decision and extracted data, its related feature records (ID verification, face, liveness, face match, and so on) and all of its stored files are deleted, it disappears from every read endpoint, and previously issued media URLs stop resolving. Webhook deliveries still queued for a deleted session, and their retries, are dropped; webhooks already delivered are not affected. Blocklist entries are **not** removed, credits already consumed are **not** refunded, and the parent User entities are **not** deleted (use `POST /v3/users/delete/` for those); no webhook is emitted for the deletion. Deletion is permanent and irreversible. **Selection semantics:** + - With `delete_all: true` you can narrow the selection with the filters of `GET /v3/sessions/`, each prefixed with `filter_` (for example `filter_status`, `filter_workflow_id`, `filter_date_from`). Filters that take several values in List Sessions (`status`, `workflow_id`, `country`, `region`, `document_type`, `document_name`, `session_type`, `warning`, `organization_name`) accept a comma-separated string or an array; the others take one value. An empty value is ignored. Filters apply only together with `delete_all`, and a deletion job applies the same filters. + - An unknown `filter_*` key, or a filter value that cannot be read, rejects the whole request with `400` before anything is deleted, so a mistyped filter never widens the deletion to every session. - Entries in `session_numbers` that do not match a live KYC session in your application (unknown numbers, already-deleted sessions, or sessions owned by another application) are **silently skipped**: they do not appear in `results` and are not counted in a job's `total`. - Validation failures (a non-numeric entry, an empty list, neither `session_numbers` nor `delete_all` provided, or a template retention the application cannot honour) reject the **whole request** with `400` and delete nothing. - Sessions are deleted in groups. If a group cannot be deleted (for example because a storage service is unavailable), every session in that group is reported as `face_retention_outcome: failed_retryable` and stays in place; the other groups are unaffected. Repeat the request for those sessions. A job retries such a group by itself. @@ -790,23 +858,42 @@ def batch_delete( Args: session_numbers: Numeric `session_number` values of the KYC sessions to delete, as digit-only strings (JSON numbers are also accepted and coerced). Required unless `delete_all` is `true`; an empty list returns `400`. Any non-digit entry rejects the whole request with `400` (unless `delete_all` is `true`, in which case validation is skipped and the entries are ignored). Numbers that do not match a live session in your application are silently skipped. - delete_all: When `true`, every remaining KYC session in the calling application is deleted and `session_numbers` is ignored. More than 20 matching sessions run as a deletion job (`202`) that covers only the sessions created before the request. Defaults to `false`. Irreversible; there is no confirmation step. + delete_all: When `true`, every remaining KYC session in the calling application is deleted and `session_numbers` is ignored, unless you narrow the selection with `filter_*` keys. More than 20 matching sessions run as a deletion job (`202`) that covers only the sessions created before the request. Defaults to `false`. Irreversible; there is no confirmation step. retain_face_embeddings: Override the application's `face_retention_policy` for this deletion. `true` keeps one image-free face biometric template anchored to the session's User after the session is deleted; `false` deletes the face embedding with the session; omit or `null` to follow the application policy. Ignored for KYB sessions and for sessions without a face embedding. face_retention_days: Finite retention duration, in days, for a retained template. Required when a template is retained unless the application already sets `face_retention_days`. The template's `expires_at` is the earliest of this duration, the application's general data-retention window, and `face_retention_deadline`. face_retention_deadline: Optional hard expiry for a retained template. Must be in the future. deletion_instruction: The class of instruction you are giving. `operational_session_delete` deletes the session and retains a template only when the policy or `retain_face_embeddings` says so. `privacy_erasure` purges every retained biometric template anchored to the session's User before deleting the session, cannot be overridden by the application policy, and cannot be combined with `retain_face_embeddings: true` (`400`). instruction_id: Your durable reference for this deletion instruction (for example an erasure-ticket id). Recorded on the audit trail and on any retained template. Generated by Didit when omitted. + filter_status: Narrows a `delete_all` to the sessions that match the `status` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_vendor_data: Narrows a `delete_all` to the sessions that match the `vendor_data` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated text, so several values are not matched as alternatives. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_search: Narrows a `delete_all` to the sessions that match the `search` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated text, so several values are not matched as alternatives. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_workflow_id: Narrows a `delete_all` to the sessions that match the `workflow_id` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_date_from: Narrows a `delete_all` to the sessions that match the `date_from` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_date_to: Narrows a `delete_all` to the sessions that match the `date_to` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_country: Narrows a `delete_all` to the sessions that match the `country` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_document_type: Narrows a `delete_all` to the sessions that match the `document_type` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_document_name: Narrows a `delete_all` to the sessions that match the `document_name` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_region: Narrows a `delete_all` to the sessions that match the `region` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_didit_internal_id: Narrows a `delete_all` to the sessions that match the `didit_internal_id` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_session_type: Narrows a `delete_all` to the sessions that match the `session_type` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_api_service: Narrows a `delete_all` to the sessions that match the `api_service` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_screened_full_name: Narrows a `delete_all` to the sessions that match the `screened_full_name` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated text, so several values are not matched as alternatives. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_warning: Narrows a `delete_all` to the sessions that match the `warning` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_status_initialized_as_in_review: Narrows a `delete_all` to the sessions that match the `status_initialized_as_in_review` filter of [List Sessions](/sessions-api/list-sessions), with the same values. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_is_kyc_to_be_reviewed: Narrows a `delete_all` to the sessions that match the `is_kyc_to_be_reviewed` filter of [List Sessions](/sessions-api/list-sessions), with the same values. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_euuid: Narrows a `delete_all` to the sessions that match the `euuid` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_organization_name: Narrows a `delete_all` to the sessions that match the `organization_name` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. With an API key it can only match your own organization's name, so it selects all of your sessions or none. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsBatchDeleteResponse | SessionsBatchDeleteResponse202", + "SessionsBatchDeleteResponse | SessionDeletionJob", self._client.request( "post", "/v3/sessions/delete/", - cast_to={200: SessionsBatchDeleteResponse, 202: SessionsBatchDeleteResponse202}, + cast_to={200: SessionsBatchDeleteResponse, 202: SessionDeletionJob}, auth="api_key", body={ "session_numbers": session_numbers, @@ -816,6 +903,25 @@ def batch_delete( "face_retention_deadline": face_retention_deadline, "deletion_instruction": deletion_instruction, "instruction_id": instruction_id, + "filter_status": filter_status, + "filter_vendor_data": filter_vendor_data, + "filter_search": filter_search, + "filter_workflow_id": filter_workflow_id, + "filter_date_from": filter_date_from, + "filter_date_to": filter_date_to, + "filter_country": filter_country, + "filter_document_type": filter_document_type, + "filter_document_name": filter_document_name, + "filter_region": filter_region, + "filter_didit_internal_id": filter_didit_internal_id, + "filter_session_type": filter_session_type, + "filter_api_service": filter_api_service, + "filter_screened_full_name": filter_screened_full_name, + "filter_warning": filter_warning, + "filter_status_initialized_as_in_review": filter_status_initialized_as_in_review, + "filter_is_kyc_to_be_reviewed": filter_is_kyc_to_be_reviewed, + "filter_euuid": filter_euuid, + "filter_organization_name": filter_organization_name, }, extra_headers=extra_headers, extra_query=extra_query, @@ -832,7 +938,7 @@ def retrieve_deletion_job( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsRetrieveDeletionJobResponse: + ) -> SessionDeletionJob: """Get a bulk session deletion job. Progress of a bulk session deletion that `POST /v3/sessions/delete/` answered with `202` (more than 20 matching sessions, selected by `session_numbers` or with `delete_all`). Poll every few seconds while `status` is `PENDING` or `PROCESSING`; stop when it is `COMPLETED` or `FAILED`. @@ -851,11 +957,11 @@ def retrieve_deletion_job( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsRetrieveDeletionJobResponse", + "SessionDeletionJob", self._client.request( "get", "/v3/sessions/delete/{job_id}/", - cast_to=SessionsRetrieveDeletionJobResponse, + cast_to=SessionDeletionJob, path_params={"job_id": job_id}, auth="api_key", extra_headers=extra_headers, @@ -980,6 +1086,38 @@ def update_feature_status( *, new_status: Literal["Approved", "Declined", "In Review"] | str, comment: str | NotGiven = NOT_GIVEN, + feature_type: Literal[ + "ID_VERIFICATION", + "OCR", + "OCR_BACK", + "NFC", + "LIVENESS", + "FACE", + "AGE_ESTIMATION", + "AGE_VERIFICATION", + "FACE_SEARCH", + "FACE_MATCH", + "IP_ANALYSIS", + "AML", + "PHONE", + "PHONE_VERIFICATION", + "EMAIL", + "EMAIL_VERIFICATION", + "POA", + "PROOF_OF_ADDRESS", + "DATABASE_VALIDATION", + "DATA_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + "BANK_VERIFICATION", + "GEOLOCATION", + ] + | str + | NotGiven = NOT_GIVEN, + defer_until_flow_finishes: bool | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -987,13 +1125,19 @@ def update_feature_status( ) -> SessionsUpdateFeatureStatusResponse: """Update an individual feature (step) status. - Set the status of a single verification feature (step) within a session **without changing the overall session decision**. Target the feature by its workflow graph `nodeId` (use `default` for non-graph workflows where the step has no node). + Set the status of a single verification feature (step) within a session. Target the feature by its workflow graph `nodeId`. On non-graph workflows, whose steps have no node, use `default` and also pass `feature_type` to say which step you mean. - Works for both User Verification (KYC) and Business Verification (KYB) sessions and every step type - ID/OCR, Liveness, Face Match, NFC, AML, Proof of Address, Phone, Email, Database Validation, Questionnaire, and the KYB registry / documents / key-people checks. + Works for both User Verification (KYC) and Business Verification (KYB) sessions and every step type: ID/OCR, Liveness, Face Match, NFC, AML, Proof of Address, Phone, Email, Database Validation, Questionnaire, and the KYB registry / documents / key-people checks. This endpoint is **not** available for standalone API sessions (`session_type = API`). - `new_status` must be one of `Approved`, `Declined`, or `In Review`, and must differ from the feature's current status. A successful call fires a `data.updated` webhook and appends an activity entry (with actor attribution) to the session's review trail. This endpoint is **not** available for standalone API sessions (`session_type = API`). + `new_status` must be one of `Approved`, `Declined`, or `In Review`, and must differ from the feature's current status. - Authenticate with your API key (`x-api-key`) or a user authorization header - either must carry the `write:sessions` privilege. + **When the override applies.** Without `defer_until_flow_finishes` (or with `false`) the override applies at once, as it always did; when the person is still in the flow it is recorded as `forced`. With `defer_until_flow_finishes: true` while the person is still in the flow (the session is `Not Started`, `In Progress` or `Resubmitted` and has not finished), the override is stored as `pending_until_flow_finishes` and applies when the flow finishes, after the flow reaches its own decision. On a session whose flow already ended it applies at once either way. A waiting override is cancelled instead when the person redoes its step before finishing, when a newer override on the same check replaces it, when the session leaves the flow unfinished (abandoned, expired, or decided mid-flow), or when you cancel it with `POST /v3/session/{sessionId}/features/overrides/{overrideId}/cancel/`. List a session's overrides with `GET /v3/session/{sessionId}/features/overrides/`. + + **Effect on the session.** A session that already has a decision keeps it: only the check changes. When the session has no decision yet and its workflow is waiting on exactly this step, applying the override completes the step: the workflow moves on, later steps run, and a status node can set the session decision and fire `status.updated`. + + **Webhooks and activity.** A `data.updated` webhook fires when the override applies (at once, or when the flow finishes), never while it waits or when it is cancelled. Every override is recorded in the session's activity as `FEATURE_OVERRIDE_PENDING`, `FEATURE_OVERRIDE_APPLIED` or `FEATURE_OVERRIDE_CANCELLED`, with who made it; the request's entry carries your `comment`. + + Authenticate with your API key (`x-api-key`) or a user authorization header carrying the `write:sessions` privilege; a scoped API key needs write access to decisions. ``PATCH /v3/session/{sessionId}/features/{nodeId}/update-status/`` @@ -1002,6 +1146,8 @@ def update_feature_status( node_id: Workflow graph node ID of the feature to update (for example `feature_ocr_1`). Use the literal value `default` for non-graph (single-step) workflows where the feature has no node ID. new_status: Target status for this feature. Must differ from the current value. comment: Optional free-text note recorded on the session's review trail for this feature change. + feature_type: Which kind of step to target. Needed on non-graph workflows (`nodeId` = `default`), where every step shares no node; optional otherwise. An unknown value returns `400`. + defer_until_flow_finishes: While the person is still in the flow, store the override as `pending_until_flow_finishes` and apply it when the flow finishes, instead of at once. Omitted or `false`, the override applies at once. On a session whose flow already ended it applies at once either way. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -1015,7 +1161,12 @@ def update_feature_status( cast_to=SessionsUpdateFeatureStatusResponse, path_params={"sessionId": session_id, "nodeId": node_id}, auth="api_key", - body={"new_status": new_status, "comment": comment}, + body={ + "new_status": new_status, + "comment": comment, + "feature_type": feature_type, + "defer_until_flow_finishes": defer_until_flow_finishes, + }, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -1036,7 +1187,7 @@ def update_aml_hit_status( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> object: + ) -> SessionsUpdateAmlHitStatusResponse: """Update an AML hit review status. Set the review status of a **single AML hit** (a potential sanction / PEP / watchlist / adverse-media match) returned by AML screening, without changing the overall session decision. Identify the hit by its `hit_id` (the `id` of an entry in the AML check's `hits` array - see the session decision). @@ -1061,11 +1212,11 @@ def update_aml_hit_status( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "object", + "SessionsUpdateAmlHitStatusResponse", self._client.request( "patch", "/v3/session/{sessionId}/update-aml-hit-status/", - cast_to=object, + cast_to=SessionsUpdateAmlHitStatusResponse, path_params={"sessionId": session_id}, auth="api_key", query={"node_id": node_id_query}, @@ -1088,7 +1239,7 @@ def bulk_update_aml_hit_status( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> object: + ) -> SessionsBulkUpdateAmlHitStatusResponse: """Bulk update AML hit review statuses. Update the review status of **multiple AML hits** in one call. Provide `hit_updates`, a list of `{ hit_id, review_status }` pairs. `review_status` is one of `Unreviewed`, `Confirmed Match`, `False Positive`, or `Inconclusive`. @@ -1110,11 +1261,11 @@ def bulk_update_aml_hit_status( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "object", + "SessionsBulkUpdateAmlHitStatusResponse", self._client.request( "patch", "/v3/session/{sessionId}/bulk-update-aml-hit-status/", - cast_to=object, + cast_to=SessionsBulkUpdateAmlHitStatusResponse, path_params={"sessionId": session_id}, auth="api_key", query={"node_id": node_id_query}, @@ -1412,7 +1563,7 @@ def list_networks( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsListNetworksResponse: + ) -> EntityNetworkMembershipList: """List networks for a session. List the networks the session's vendor user belongs to. Resolves through the session's vendor_data-linked user. @@ -1427,11 +1578,11 @@ def list_networks( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsListNetworksResponse", + "EntityNetworkMembershipList", self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/sessions/{session_id}/networks/", - cast_to=SessionsListNetworksResponse, + cast_to=EntityNetworkMembershipList, path_params={"session_id": session_id}, scope="application", auth="api_key", @@ -1446,9 +1597,9 @@ def preview_resubmission( self, session_id: str, *, - email_address: str, nodes_to_resubmit: Sequence[SessionsPreviewResubmissionParamsNodesToResubmit] | NotGiven = NOT_GIVEN, + email_address: str | NotGiven = NOT_GIVEN, email_language: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, @@ -1457,18 +1608,20 @@ def preview_resubmission( ) -> SessionsPreviewResubmissionResponse: """Preview a resubmission email. - Render the email that would accompany a KYC or KYB resubmission without changing the session, resetting checks, extending its expiry, running checks or sending mail. Requires write:sessions and an application-scoped session in Approved, Declined, In Review, Kyc Expired or Abandoned status. The server validates the selected workflow steps and renders its fixed translated template with the applicable verified white-label sender and branding. The request cannot set a subject, message body or action URL. + Render the email that would accompany a KYC or KYB resubmission without changing the session, resetting checks, extending its expiry, running checks or sending mail. Requires the `write:sessions` privilege (a scoped API key needs write access to decisions) and a session in `Approved`, `Declined`, `In Review`, `Kyc Expired` or `Abandoned` status. The server validates the selected workflow steps and renders its fixed translated template with the applicable verified white-label sender and branding. The request cannot set a subject, message body or action URL. + + `email_address` is optional. Without it, the email goes to the person's own address: the one they verified in an email verification step of the flow, else the one given in `contact_details` when the session was created. An address you pass wins (`recipient_source: request`). When no address is known and you pass none, `recipient` and `preview_token` are empty strings and no email can be sent. - The response includes the recipient, sender, subject, HTML, resolved steps and a signed preview_token. requires_user_action describes whether the selected steps include an interactive check. Email delivery remains conditional: if automatic checks finalize the verification during the subsequent resubmission, no action-request email is sent. + The response includes the recipient and where it comes from, the sender, subject, HTML, language, resolved steps, whether confirming sends the email (`will_be_sent`, `not_sent_reason`) and a signed `preview_token`. `requires_user_action` is `false` when every selected step runs without the person: the resubmission then finishes on its own and no email goes out (`not_sent_reason: no_user_action`). - This preview does not reserve session state or authorize delivery. After reviewing it, call Update Session Status with new_status=Resubmitted, the same selection, recipient and language, and email_preview_token set to the returned preview_token. The receipt expires after 15 minutes. When a receipt is supplied, changed inputs, message content, sender or session state return 409 before any mutation or send. Existing integrations may omit the receipt. Regenerate the preview after a conflict. Treat the response as sensitive verification content; responses use Cache-Control: no-store. Render HTML in a sandboxed frame, never directly in your application DOM. + This preview does not reserve session state or authorize delivery. After reviewing it, call [Update Session Status](/sessions-api/update-status) with `new_status: Resubmitted`, the same `nodes_to_resubmit` and `email_language` you previewed, `send_email: true`, `email_address` set to `recipient` and `email_preview_token` set to `preview_token`. The receipt expires after 15 minutes. When a receipt is supplied, changed inputs, message content, sender or session state return `409` before any mutation or send. Existing integrations may omit the receipt. Regenerate the preview after a conflict. Treat the response as sensitive verification content; responses use `Cache-Control: no-store`. Render HTML in a sandboxed frame, never directly in your application DOM. ``POST /v3/session/{sessionId}/resubmission-preview/`` Args: session_id: UUID of the verification session to update. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. - email_address: Recipient shown in the preview. No email is sent. nodes_to_resubmit: Steps to preview, using the same validation, selection and workflow ordering as a resubmission. Omit to auto-select recorded attempts needing resubmission. + email_address: Optional. Send the email to this address instead of the person's own. Without it, the recipient is the address the person verified in the flow, else the one given in `contact_details`. No email is sent by this call. email_language: Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`). extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. @@ -1484,8 +1637,8 @@ def preview_resubmission( path_params={"sessionId": session_id}, auth="api_key", body={ - "email_address": email_address, "nodes_to_resubmit": nodes_to_resubmit, + "email_address": email_address, "email_language": email_language, }, extra_headers=extra_headers, @@ -1495,159 +1648,45 @@ def preview_resubmission( ), ) - -class AsyncSessions(AsyncAPIResource): - """The ``sessions`` resource of the Didit API. - - Methods: ``batch_delete``, ``bulk_update_aml_hit_status``, ``create``, ``delete``, ``generate_pdf``, ``import_shared``, ``list``, ``list_networks``, ``preview_resubmission``, ``retrieve_decision``, ``retrieve_deletion_job``, ``share``, ``update_aml_hit_status``, ``update_data``, ``update_feature_status``, ``update_kyb_company_data``, ``update_kyb_party``, ``update_poa_data``, ``update_status``. - - Nested resources: ``imports``, ``reviews``. - """ - - @cached_property - def imports(self) -> AsyncSessionsImports: - from didit.resources.sessions.imports import AsyncSessionsImports - - return AsyncSessionsImports(self._client) - - @cached_property - def reviews(self) -> AsyncSessionsReviews: - from didit.resources.sessions.reviews import AsyncSessionsReviews - - return AsyncSessionsReviews(self._client) - - @cached_property - def with_raw_response(self) -> AsyncSessionsWithRawResponse: - """Every method of this resource, returning the raw HTTP response.""" - return AsyncSessionsWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncSessionsWithStreamingResponse: - """Every method of this resource, returning the HTTP response before reading its body.""" - return AsyncSessionsWithStreamingResponse(self) - - async def create( + def update_document_ai_data( self, + session_id: str, + document_uuid: str, *, - workflow_id: str, - vendor_data: str | NotGiven = NOT_GIVEN, - callback: str | NotGiven = NOT_GIVEN, - callback_method: Literal["initiator", "completer", "both"] | str | NotGiven = NOT_GIVEN, - metadata: object | NotGiven = NOT_GIVEN, - language: Literal[ - "en", - "ar", - "bg", - "bn", - "bs", - "ca", - "cnr", - "cs", - "da", - "de", - "el", - "es", - "et", - "fa", - "fi", - "fr", - "he", - "hi", - "hr", - "hu", - "hy", - "id", - "it", - "ja", - "ka", - "kk", - "ko", - "ky", - "lt", - "lv", - "mk", - "mn", - "ms", - "nl", - "no", - "pl", - "pt-BR", - "pt", - "ro", - "ru", - "sk", - "sl", - "so", - "sq", - "sr", - "sv", - "th", - "tr", - "uk", - "uz", - "vi", - "zh-CN", - "zh-TW", - "zh", - ] + extracted_data: Mapping[str, object] | NotGiven = NOT_GIVEN, + status: Literal["Approved", "Declined", "In Review", "Not Finished"] | str | NotGiven = NOT_GIVEN, - contact_details: SessionsCreateParamsContactDetails | NotGiven = NOT_GIVEN, - expected_details: SessionsCreateParamsExpectedDetails | NotGiven = NOT_GIVEN, - portrait_image: str | NotGiven = NOT_GIVEN, - sandbox_scenario: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsCreateResponse: - """Create a verification session. + ) -> SessionsUpdateDocumentAiDataResponse: + """Update a Document AI document. - Create a User Verification (KYC) or Business Verification (KYB) session and receive a hosted verification `url` plus a `session_token` to redirect or embed for your end user. The `workflow_id` selects which verification steps run and whether the session is KYC or KYB. - - Call this from your backend (never the browser - the API key is a secret) whenever a user or business needs to be verified: at signup, before a sensitive action, or when re-verification is required. Prerequisites: a published workflow (created in the Console [Workflows](https://docs.didit.me/console/workflows) page) and an application API key with available credits. Sandbox applications bypass the credit check. - - **Idempotency:** when `vendor_data` is provided and an unfinished session (`Not Started`, `In Progress`, `Resubmitted`, or `Awaiting User`) with the same `vendor_data` already exists on the workflow's **current latest published version** (and that session already has a hosted verification URL), the existing session is returned (still `201`) instead of creating a duplicate - with its `callback` and `metadata` updated to the newly provided values. An unfinished session created on an older version of the workflow (one that has since been republished) is **not** reused; a new session pinned to the latest published version is created instead. Sessions in `Approved`, `Declined`, `In Review`, `Expired`, `Abandoned`, or `Kyc Expired` are never reused. - - **Side effects:** the hosted verification URL is generated and stored; if `contact_details.send_notification_emails` is `true` and an email is provided, a verification invite email is sent. The session expires after the workflow's configured session expiration time. For KYB workflows, `contact_details.phone` and `expected_details` are ignored. + Correct the fields Document AI extracted from one uploaded document, set its review status, or both, on a KYC or KYB session. `extracted_data` must be a JSON object and is merged key by key: a key you send replaces the stored value (a key sent as `null` is stored as `null`) and keys you leave out keep their values. The document status feeds the Document AI result of its workflow node in the session decision; the session status itself is not recomputed. Every successful call, including one that changes nothing, sends a `data.updated` [webhook](https://docs.didit.me/integration/webhooks) with trigger `manual_review`. A scoped API key needs `decisions: write`; see [Document AI](https://docs.didit.me/core-technology/document-ai/overview). - ``POST /v3/session/`` + ``PATCH /v3/document-ai/{sessionId}/documents/{documentUuid}/`` Args: - workflow_id: Stable identifier of the workflow that defines which verification steps the session will run. Workflows are created and managed in the [Workflows](/console/workflows) page of the Console. The `workflow_id` also implicitly selects whether the session is KYC or KYB. - vendor_data: A unique identifier for the user being verified, such as a UUID, email, or internal user ID. This field is used for: (1) **User grouping** - sessions with the same vendor_data are linked to the same user profile in the Users tab. (2) **Cross-session duplicate detection** - when checking for duplicated faces, documents, phone numbers, emails, IP addresses, or device fingerprints, sessions with the same vendor_data are treated as the same user and excluded from matches. Without vendor_data, every session is treated as a unique user and all potential duplicates are surfaced. We strongly recommend always providing a vendor_data to reduce noise in duplicate detection. - callback: URL to redirect the user to after verification completes. Didit automatically appends `verificationSessionId` and `status` (Approved, Declined, In Review) as query parameters. Custom URL schemes (e.g. `myapp://`) are supported for mobile callbacks. If omitted, the workflow's default `callback_url` is used. - callback_method: Determines which device should handle the redirect to the provided callback URL. Use `initiator` to redirect only the device that started the flow, `completer` for the device that finishes it, or `both` to allow either device to trigger the callback. If you ever notice the callback not triggering reliably, we recommend setting this value to `both`. - metadata: Arbitrary JSON stored with the session and echoed back in the response and webhooks. Any JSON value is accepted (object, string, number, array), but a JSON object of key/value pairs is recommended. Not shown to the end user. Use it to pass your own correlation ids, A/B variants, or business context. - language: Language code (ISO 639-1) for the verification process interface. Controls the language displayed to the end user during verification. If not provided, the browser's language will be automatically detected and used. Check all the supported languages [here](/integration/supported-languages). - contact_details: User contact information that can be used for notifications, prefilling verification forms, and phone verification. This includes email address, preferred language for communications, and phone number. - expected_details: Expected user details used to cross-validate against the data extracted from the user's ID document, Proof of Address, and other verification steps. Mismatches are surfaced as warnings on the decision; some fields (e.g. `id_country`, `expected_document_types`) also alter the user-facing flow. For Business Verification (KYB) workflows only the business fields (`company_name`, `registry_country`, `registration_number`) are used - they pre-fill the company registry search for the user - and all person-level fields are ignored. - portrait_image: Base64-encoded portrait image of the end user's face (max 2MB; JPEG, PNG, WebP, or TIFF). Used as the reference image to match against the liveness capture when the workflow is a `Biometric Authentication` workflow with Face Match enabled, or any graph workflow where Face Match runs before ID Verification (OCR). Optional when the `vendor_data` user already has a stored face image: Didit reuses it automatically, resolved in priority order: approved liveness face, ePassport chip photo, ID document portrait, manually enrolled profile face (only faces from approved sessions or faces you enrolled yourself are reused). When provided, `portrait_image` always takes precedence over the stored face. If omitted and no stored face exists (or `vendor_data` is not sent), session creation fails with `400`. Ignored for other workflow types. - sandbox_scenario: Sandbox-only scenario slug. Only accepted when the API key belongs to a sandbox-mode application; rejected on live applications. When set, the create flow expands the scenario's bundled magic values onto the session's input fields (contact_details, expected_details) so the mocked providers reproduce the scenario's outcome through the real workflow pipeline. See the scenario catalog at GET /v1/sandbox/scenarios/ or [Sandbox & Test Data](/integration/sandbox-testing). You can also arm (or re-arm) a scenario after create via POST /v3/session/{sessionId}/sandbox/arm/, as long as the session is still `Not Started`. For non-doc lookup, use `lookup_partial_match`, `lookup_no_match`, or `lookup_provider_error` to test the configured attempt budget and fallback. For identity wallets, use `wallet_cancelled`, `wallet_timeout`, or `wallet_provider_error`; `approve` simulates a verified wallet. All catalog wallets can be tested in sandbox, including schemes unavailable on live applications. Simulated attempts are not charged. + session_id: UUID of the verification session (the `session_id` returned by `POST /v3/session/`). Accepts both User Verification (KYC) and Business Verification (KYB) session IDs: the server resolves the ID across both session types. Must be a well-formed UUID; a malformed value does not match the route and returns an HTML `404`. + document_uuid: UUID of the Document AI document (`document_ai_documents[].items[].uuid` in `GET /v3/session/{sessionId}/decision/`). It must belong to the session in the path. + extracted_data: Field keys (the keys you configured for the document) and their corrected values. Must be a JSON object; merged key by key into the stored data. Leave it out rather than sending `null`, which returns `400`. + status: New review status of the document. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsCreateResponse", - await self._client.request( - "post", - "/v3/session/", - cast_to=SessionsCreateResponse, + "SessionsUpdateDocumentAiDataResponse", + self._client.request( + "patch", + "/v3/document-ai/{sessionId}/documents/{documentUuid}/", + cast_to=SessionsUpdateDocumentAiDataResponse, + path_params={"sessionId": session_id, "documentUuid": document_uuid}, auth="api_key", - body={ - "workflow_id": workflow_id, - "vendor_data": vendor_data, - "callback": callback, - "callback_method": callback_method, - "metadata": metadata, - "language": language, - "contact_details": contact_details, - "expected_details": expected_details, - "portrait_image": portrait_image, - "sandbox_scenario": sandbox_scenario, - }, + body={"extracted_data": extracted_data, "status": status}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -1655,51 +1694,38 @@ async def create( ), ) - async def retrieve_decision( + def regenerate_kyb_party( self, session_id: str, + party_uuid: str, *, - include: Literal["events", "resubmissions", "events,resubmissions"] - | str - | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsRetrieveDecisionResponse: - """Retrieve the full decision for a verification session. - - Retrieve the complete decision report for a verification session - the rolled-up session `status` plus one report per verification feature the workflow executed. This is the canonical read endpoint for both User Verification (KYC) and Business Verification (KYB) sessions: the same path serves both, and the top-level `session_kind` discriminator tells you which shape you received (`"user"` or `"business"`). - - **Every feature report is a plural array.** V3 workflows are graphs that can run the same feature more than once, so each block (`id_verifications`, `nfc_verifications`, `liveness_checks`, `face_matches`, `poa_verifications`, `document_ai_documents`, `phone_verifications`, `email_verifications`, `aml_screenings`, `ip_analyses`, `database_validations`, `questionnaire_responses`) is a JSON array whose items each carry a `node_id` identifying the workflow graph node that produced them. Read NFC as `response.nfc_verifications[0]` - never as a singular `nfc` field. A block is `null` until the workflow has run that feature at least once; features the workflow does not include never appear with data. - - **When to call.** The recommended pattern is event-driven: wait for the `status.updated` webhook that fires when the session reaches a decision (`Approved`, `Declined`, `In Review`), then call this endpoint once to fetch the full report. Polling also works - the endpoint can be called at any point in the session lifecycle and returns the data produced so far - but webhook-then-fetch is cheaper and faster. Media URLs in the response (document images, videos, PDFs) are short-lived presigned links: fetch them promptly or re-request the decision to get fresh ones, and do not persist them as long-term references. - - **Statuses.** The top-level `status` is the session lifecycle status (`Not Started`, `In Progress`, `Awaiting User`, `In Review`, `Approved`, `Declined`, `Resubmitted`, `Expired`, `Kyc Expired`, `Abandoned`). Each feature item additionally carries its own feature-level `status` (`Not Finished`, `Approved`, `Declined`, `In Review`, ...). A session can be `Approved` overall while an individual feature is `In Review`, depending on the workflow's decision rules. - - **Authentication and visibility.** Authenticate with the application API key in the `x-api-key` header (a console user Bearer token with the `read:sessions` permission also works). Only sessions owned by the authenticated application are visible. If your workflow restricts response attributes, non-allowed fields inside feature items are returned as `null` while `status`, `warnings`, and `node_id` are always preserved. + ) -> KYBKeyPeopleParty: + """Regenerate a Key People party's KYC session. - **Rate limits.** GET requests are limited to 600 per minute per API key. Exceeding the limit returns `429` with `Retry-After` and `X-RateLimit-*` headers. + Give a Key People party of a Business Verification (KYB) session a new individual KYC session after its previous one was deleted, so the Key People check can finish again; send no request body. It works only when the party's KYC session was deleted (`kyc_session_deleted: true`): a party whose KYC session still exists (in any status), a party that never had one, or a company party returns `409`. Didit creates a new hosted KYC session with the workflow the deleted one ran (its latest published version) or, when that is no longer known, the workflow your Key People settings assign to the party's role, emails the link to the party when an email is on file, clears `is_skipped`, and recomputes the Key People check and the business session status. It then sends a `data.updated` [webhook](https://docs.didit.me/integration/webhooks) for the business session with trigger `console_key_people_regenerate`; to exempt the party instead, use `PATCH /v3/session/{sessionId}/kyb/parties/{partyUuid}/`. A scoped API key needs `decisions: write`, and without `sessions: write` it receives `kyc_session_url` as `null`. - ``GET /v3/session/{sessionId}/decision/`` + ``POST /v3/session/{sessionId}/kyb/parties/{partyUuid}/regenerate/`` Args: - session_id: UUID of the verification session whose decision you want to retrieve. This is the `session_id` returned by `POST /v3/session/`. The same path works for both User Verification (KYC) and Business Verification (KYB) sessions - the server resolves the id against both session types. **Must be a valid UUID.** The route only matches well-formed UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{"detail": ...}` envelope shown below. Validate the UUID format client-side so you can distinguish an input error from a genuine missing-session `404`. - include: Comma-separated list of extra payload sections. Supported values: `events` and `resubmissions`, alone or together (`events,resubmissions`). When `include=events` is passed, the response additionally contains the session event timeline (`events`, including the `WORKFLOW_BRANCH_EVALUATED` / `WORKFLOW_STATUS_NODE_REACHED` routing events described on the Retrieve Session page), the full activity log (`activity_reviews`), blocklist flags (`blocklisted`), a per-session `cost_breakdown`, workflow metadata (`workflow_type`, `workflow_version`, `workflow_version_id`, and `workflow_terminal_node_id` - the last recorded workflow status node that set a session status, or `null` if none has been recorded; historical provenance that is not cleared by manual approvals, declines or resubmissions, and is replaced by a later workflow status node), and session metadata (`api_service` - string or `null`, set only for standalone-API sessions; `session_type` - `API`, `HOSTED`, or `MIGRATED`; `has_device_nfc_support` - boolean). Two fields change relative to the plain shape: the top-level `session_kind` discriminator is **omitted** (the events shape only exists for user sessions), and the `features` array switches from plain strings to `{feature, node_id}` objects - where the email feature is reported as `EMAIL` instead of `EMAIL_VERIFICATION`. This expanded shape is primarily used by the Didit Console; most API integrations should omit the parameter. **KYB limitation:** `include=events` only applies to user (KYC) sessions. Business sessions (`session_kind = "business"`) always return the standard business decision shape - `events`, `activity_reviews`, `blocklisted`, `cost_breakdown` and the extra `workflow_*`/session metadata are never returned for business sessions, and their `features` array stays a plain string array. With `include=resubmissions` (user and business sessions alike) the response also contains `resubmissions`: one entry per resubmission request, oldest first, each with `number`, `created_at`, `previous_status`, `nodes`, `reasons`, `requested_by` and `results` (what the resubmitted steps returned before the request, under the same keys and in the same shape as the current results). Each `cost_breakdown` item carries `usage_type`, `price`, `details` and `created_at`: a resubmitted step that runs again is billed as a new item. + session_id: UUID of the parent Business Verification (KYB) session. + party_uuid: UUID of the Key People party (`key_people_checks[].submitted.parties[].uuid` on the business session decision). Must belong to the session referenced by `sessionId`. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsRetrieveDecisionResponse", - await self._client.request( - "get", - "/v3/session/{sessionId}/decision/", - cast_to=SessionsRetrieveDecisionResponse, - path_params={"sessionId": session_id}, + "KYBKeyPeopleParty", + self._client.request( + "post", + "/v3/session/{sessionId}/kyb/parties/{partyUuid}/regenerate/", + cast_to=KYBKeyPeopleParty, + path_params={"sessionId": session_id, "partyUuid": party_uuid}, auth="api_key", - query={"include": include}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -1707,82 +1733,44 @@ async def retrieve_decision( ), ) - async def delete( + def list_events( self, session_id: str, *, - retain_face_embeddings: bool | None | NotGiven = NOT_GIVEN, - face_retention_days: int | NotGiven = NOT_GIVEN, - face_retention_deadline: str | _dt.datetime | NotGiven = NOT_GIVEN, - deletion_instruction: Literal["operational_session_delete", "privacy_erasure"] - | str - | NotGiven = NOT_GIVEN, - instruction_id: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + ordering: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsDeleteResponse: - """Delete a verification session (KYC or KYB). - - Delete a single verification session, User Verification (KYC) or Business Verification (KYB), by its `session_id`. The id is resolved against KYC sessions first, then KYB sessions, so both kinds are deleted through this one URL. - - **What is deleted:** - - The session and every verification attempt it holds, including results that a resubmission replaced: the decision, extracted data, all related feature records (ID verification, face, liveness, face match, AML, registry checks, IP analysis, and so on), its reviews and its activity log. - - All stored files owned by the session: document front/back photos (full, cropped, and privacy-blurred variants), document-capture videos, portrait crops, NFC chip portrait and signature images, face reference images, liveness videos, face-match source/target images, Proof of Address documents, and any extra uploaded files (KYC); uploaded company documents and extra files (KYB). Previously issued media URLs (`https:///...`) stop resolving. - - The session's face data. Its face embedding is deleted too, unless you choose to keep an image-free template (see **Face biometric data**). - - Webhook deliveries still queued for the session, and their retries. Webhooks already delivered are not affected. - - For a Business Verification (KYB) session: the verifications it created for its key people (KYC) and its corporate owners (KYB), recursively, reported as `deleted_child_verifications`. A verification it only links to, such as an existing approved KYC it reused, is not deleted. - - The session disappears from every read endpoint immediately: `GET /v3/session/{sessionId}/decision/` returns `404` and `GET /v3/sessions/` stops listing it. Ongoing AML monitoring and KYB registry monitoring started by the deleted verifications stop. - - **What is not affected:** blocklist entries created from this session (face or document) are **not** removed; manage those with the blocklist endpoints. Share tokens issued for the session are not revoked, but there is nothing left for them to import: an import with one returns `400` with `{"share_token": ["Original session does not exist."]}`. Credits already consumed are **not** refunded. The parent User or Business entity is **not** deleted; use `POST /v3/users/delete/` or `POST /v3/businesses/delete/` for those. If the User's profile portrait came from this session, the portrait is removed with it. + ) -> SyncPage[SessionEvent]: + """List session events. - **Face biometric data:** by default the session's face embedding is deleted with the session (`face_retention_outcome: deleted`). An application that opted in to biometric-template retention (`face_retention_policy: retain_until_user_deleted` on `PATCH /v3/webhook/`), or a request with `retain_face_embeddings: true`, keeps one separately managed, image-free face biometric template anchored to the session's User (`retained_with_user`) so that duplicate detection, Face Search, and biometric authentication keep working. The template never contains the face image, media, identity fields, the decision, or the deleted session's id; it is biometric data deleted at `biometric_template_expires_at`, or earlier on User deletion, privacy erasure, or an explicit purge through `/v3/biometric-templates/`. A session without a linked User cannot retain a template (`ineligible_no_vendor_user`). Send `deletion_instruction: privacy_erasure` for data-subject requests: it purges every retained template for that User and never retains a new one. - - **Irreversible:** deletion is permanent and there is no restore endpoint. Export anything you need (for example a decision PDF) before you call it. - - **Side effects:** no webhook is emitted for deletions. - - **Response:** `200 OK` once the deletion has completed, with a JSON body reporting the `face_retention_outcome`, the retained template's expiry and, for a KYB session, `deleted_child_verifications`. Earlier versions of this endpoint returned `204 No Content`; update clients that assert on `204`. Keep your `instruction_id` in your own erasure log. - - **Idempotency:** not idempotent at the HTTP level: the first call returns `200`; repeating it returns `404` because the session no longer resolves. After a `503` the session is still in place; repeat the same request to finish deleting it. - - **Authentication:** send your application's API key in the `x-api-key` header; the session must belong to that application. Console user access tokens (`Authorization: Bearer ...`) may also call this endpoint when they carry the `delete:sessions` permission. Authentication and permission failures both return `403`; this API never returns `401`. - - **Rate limit:** shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. - - To bulk-delete KYC sessions by their numeric `session_number`, use `POST /v3/sessions/delete/` instead. + Read the event timeline of one verification session, newest first by `sent_at`: the screens the user saw, buttons and uploads, failed attempts, status and feature-step changes, and the workflow routing trace. Works for User Verification (KYC) and Business Verification (KYB) session IDs. Events are stored asynchronously, so a new event can take a few seconds to appear. Results are paginated with `limit` and `offset`; follow `next` until it is `null`. A scoped API key needs `sessions: read` and `decisions: read`, and without `media: read` it receives `file_url` as `null`. The routing events are described in [Retrieve session](https://docs.didit.me/sessions-api/retrieve-session). - ``DELETE /v3/session/{sessionId}/delete/`` + ``GET /v3/session/{sessionId}/events/`` Args: - session_id: UUID (`session_id`) of the User Verification (KYC) or Business Verification (KYB) session to delete, as returned when the session was created. Must be a canonical hyphenated UUID - a non-UUID value does not match the route and returns `404`. - retain_face_embeddings: Override the application's `face_retention_policy` for this deletion. `true` keeps one image-free face biometric template anchored to the session's User after the session is deleted; `false` deletes the face embedding with the session; omit or `null` to follow the application policy. Ignored for KYB sessions and for sessions without a face embedding. - face_retention_days: Finite retention duration, in days, for a retained template. Required when a template is retained unless the application already sets `face_retention_days`. The template's `expires_at` is the earliest of this duration, the application's general data-retention window, and `face_retention_deadline`. - face_retention_deadline: Optional hard expiry for a retained template. Must be in the future. - deletion_instruction: The class of instruction you are giving. `operational_session_delete` deletes the session and retains a template only when the policy or `retain_face_embeddings` says so. `privacy_erasure` purges every retained biometric template anchored to the session's User before deleting the session, cannot be overridden by the application policy, and cannot be combined with `retain_face_embeddings: true` (`400`). - instruction_id: Your durable reference for this deletion instruction (for example an erasure-ticket id). Recorded on the audit trail and on any retained template. Generated by Didit when omitted. + session_id: UUID of the verification session (the `session_id` returned by `POST /v3/session/`). Accepts both User Verification (KYC) and Business Verification (KYB) session IDs: the server resolves the ID across both session types. Must be a well-formed UUID; a malformed value does not match the route and returns an HTML `404`. + limit: Maximum number of events per page. Defaults to 50. A value below 1 or a non-numeric value falls back to 50; a value above 1,000,000 returns `400`. + offset: Number of results to skip before the first returned one. Defaults to 0; a negative or non-numeric value falls back to 0, a value above 1,000,000 returns `400`. Prefer following the `next` and `previous` URLs. + ordering: Optional sort override. The default order is `-sent_at`. Pass a result field name (for example `sent_at`, `created_at`, `event_type`, `feature_step` or `step`) to sort ascending, prefix it with `-` to sort descending, and separate several fields with commas. Unknown fields are ignored. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsDeleteResponse", - await self._client.request( - "delete", - "/v3/session/{sessionId}/delete/", - cast_to=SessionsDeleteResponse, + "SyncPage[SessionEvent]", + self._client.request_page( + "get", + "/v3/session/{sessionId}/events/", + cast_to=SessionsListEventsResponse, + item_type=SessionEvent, path_params={"sessionId": session_id}, auth="api_key", - body={ - "retain_face_embeddings": retain_face_embeddings, - "face_retention_days": face_retention_days, - "face_retention_deadline": face_retention_deadline, - "deletion_instruction": deletion_instruction, - "instruction_id": instruction_id, - }, + query={"limit": limit, "offset": offset, "ordering": ordering}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -1790,59 +1778,46 @@ async def delete( ), ) - async def generate_pdf( + def list_aml_screening_history( self, session_id: str, *, + node_id: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + ordering: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> bytes: - """Download a PDF report for a verification session. - - Render and download a compliance-ready PDF report for a finished verification session. The same path serves both User Verification (KYC) and Business Verification (KYB) sessions - the server resolves the `sessionId` against both session types and picks the matching report template. - - **Eligible statuses.** The session must already carry a final or reviewable status: - - - KYC sessions: `Approved`, `Declined`, `In Review`, or `Kyc Expired`. - - KYB sessions: `Approved`, `Declined`, or `In Review`. - - Any other status (`Not Started`, `In Progress`, `Expired`, `Abandoned`, …) returns `403` with an explanatory `detail` string - see the `403` response below. - - **What the report contains.** The PDF mirrors the console session view at the moment of the request: - - - **KYC** - rolled-up session status, the warnings list, and one section per executed feature: ID Verification (document images plus extracted fields), NFC, Liveness, Face Match, Email Verification, Phone Verification, Proof of Address, Questionnaire answers, Database Validation, AML Screening, and IP Analysis - followed by session tags, console review activity (comments and status changes), and the session event timeline. - - **KYB** - registry checks (company data), AML screenings, business document verifications, key-people checks, questionnaire responses, phone/email verifications, IP analyses, and the session event timeline, followed by tags and review activity. - - **Branding.** If your application has white-label customization, the report is rendered with your logo and links to your privacy-policy URL; otherwise it carries Didit branding. Reports are rendered in English - there is no language parameter. - - **No caching.** Every call re-renders the PDF from the current session data, so a report generated after a manual review reflects the reviewer’s decision. Two calls for the same session can produce byte-different files - archive the downloaded file if you need an immutable copy. - - **Latency.** Rendering downloads every stored image (document sides, selfies, extra files) before composing the PDF, so media-heavy sessions can take several seconds. Use a generous client read timeout (60 s recommended) and stream the body to disk. - - **Trailing slash.** The canonical route ends with a trailing slash (`…/generate-pdf/`). A request without it is served directly with the same response - there is no `301` redirect, so no client has to follow redirects and `curl` does not need `-L`. Use the slashed URL as in the samples. + ) -> SyncPage[AMLScreeningRun]: + """List AML screening history. - If you need the underlying data as JSON instead of a rendered report, use `GET /v3/session/{sessionId}/decision/`. For every reportable session of one user in a single document, use `GET /v3/users/{vendor_data}/generate-pdf/`. + List every completed ongoing-monitoring re-screen of a session's AML check, newest first by `completed_at`, including the runs where nothing changed: alerts, activity entries and webhooks fire only when a result moves, so this history is the evidence that a clean subject was still being screened. Works for KYC and KYB session IDs; pass `node_id` when the session ran more than one AML step. Only ongoing-monitoring re-screens are listed (the first screening is part of the session decision) and runs from before this history existed are not reconstructed, so a session without ongoing monitoring returns an empty list. `count` is exact up to 100 and capped at 100 above that, so follow `next` until it is `null`. A scoped API key needs `decisions: read`; see [Continuous AML monitoring](https://docs.didit.me/core-technology/aml-screening/continuous-monitoring-aml-screening). - ``GET /v3/session/{sessionId}/generate-pdf/`` + ``GET /v3/session/{sessionId}/aml-screening-history/`` Args: - session_id: UUID of the User Verification (KYC) or Business Verification (KYB) session to render - the `session_id` returned by `POST /v3/session/`. The same path works for both session kinds; use the `Content-Disposition` filename on the response to tell which report type you received. **Must be a well-formed, lowercase UUID.** The route only matches valid UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{"detail": ...}` envelope shown below. The route's UUID converter only matches the canonical lowercase form - uppercase-hex UUIDs are rejected with the HTML 404. + session_id: UUID of the verification session (the `session_id` returned by `POST /v3/session/`). Accepts both User Verification (KYC) and Business Verification (KYB) session IDs: the server resolves the ID across both session types. Must be a well-formed UUID; a malformed value does not match the route and returns an HTML `404`. + node_id: Workflow graph node ID of the AML check. Needed only when the session ran more than one AML step; without it the most recent AML check is used. An unknown node returns `404`. + limit: Maximum number of screening runs per page. Defaults to 50. A value below 1 or a non-numeric value falls back to 50; a value above 1,000,000 returns `400`. + offset: Number of results to skip before the first returned one. Defaults to 0; a negative or non-numeric value falls back to 0, a value above 1,000,000 returns `400`. Prefer following the `next` and `previous` URLs. + ordering: Optional sort override. The default order is `-completed_at`. Pass a result field name (for example `completed_at`, `started_at`, `outcome` or `score`) to sort ascending, prefix it with `-` to sort descending, and separate several fields with commas. Unknown fields are ignored. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "bytes", - await self._client.request( + "SyncPage[AMLScreeningRun]", + self._client.request_page( "get", - "/v3/session/{sessionId}/generate-pdf/", - cast_to=bytes, + "/v3/session/{sessionId}/aml-screening-history/", + cast_to=SessionsListAmlScreeningHistoryResponse, + item_type=AMLScreeningRun, path_params={"sessionId": session_id}, auth="api_key", - accept="application/pdf, application/json", + query={"node_id": node_id, "limit": limit, "offset": offset, "ordering": ordering}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -1850,68 +1825,40 @@ async def generate_pdf( ), ) - async def update_status( + def export_aml_screening_history( self, session_id: str, *, - new_status: Literal["Approved", "Declined", "Resubmitted"] | str, - comment: str | NotGiven = NOT_GIVEN, - nodes_to_resubmit: Sequence[SessionsUpdateStatusParamsNodesToResubmit] - | NotGiven = NOT_GIVEN, - send_email: bool | NotGiven = NOT_GIVEN, - email_address: str | NotGiven = NOT_GIVEN, - email_language: str | NotGiven = NOT_GIVEN, - email_preview_token: str | NotGiven = NOT_GIVEN, + node_id: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsUpdateStatusResponse: - """Approve, decline, or request resubmission for a verification session. - - Manually override the final decision of a KYC or KYB verification session. Set `new_status` to `Approved` or `Declined` to record a manual decision, or to `Resubmitted` to clear specific verification steps and let the user redo them through the same verification URL. The endpoint accepts both user (KYC) and business (KYB) session IDs. - - Call it once a session has reached a reviewable state: the current status must be `Approved`, `Declined`, `In Review`, `Kyc Expired`, `Abandoned`, or `Resubmitted`. Sessions that are still `Not Started`, `In Progress`, or `Awaiting User`, or that ended as `Expired`, return `400`. The new status must differ from the current one, and your API key's organization needs the `write:sessions` permission. The eligible-state check runs before body validation, so an ineligible session returns the `Wrong Current Status` error even when the payload is also invalid. - - For `Resubmitted`, pass `nodes_to_resubmit` to choose which workflow steps the user must redo, or omit it to auto-select existing feature attempts in a resubmittable state (`Declined`, `In Review`, `Not Finished`, `Expired`). Never-attempted features are not auto-selected - list them explicitly in `nodes_to_resubmit`, or the request returns `400` when no recorded attempt needs resubmission. Captured data for the selected steps is deleted, the steps are reordered to match the workflow graph, the session's expiration window restarts, and the original verification URL becomes usable again. Backend-only steps (`AML`, `DATABASE_VALIDATION`, `IP_ANALYSIS`) at the head of the list run immediately without user interaction; if every resubmitted step is backend-only, the session re-finalizes within the same request. `KYB_REGISTRY` and `KYB_KEY_PEOPLE` cannot be resubmitted directly - resubmit the relevant child KYC sessions instead and the parent business session recomputes automatically. - - Every successful call fires the `status.updated` [webhook](/integration/webhooks) once the change commits and appends an activity entry - with actor attribution and your `comment` - to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and in the console activity timeline. The user-profile aggregates linked to the session's `vendor_data` update as well. Declining disables ongoing AML monitoring for the session; re-approving re-enables it (from `Resubmitted`, `Declined`, or `Kyc Expired`) when the workflow has monitoring turned on. Approving or declining a child KYC that belongs to a KYB key-people check recomputes the parent business session's status. Set `send_email: true` (with `email_address`) to notify the user: a status notice for `Approved`/`Declined`, or a resubmission email containing the verification link and per-step reasons for `Resubmitted`. + ) -> bytes: + """Export AML screening history as CSV. - The operation is intentionally not repeatable with the same value: re-sending the current status returns `400` (`The new status is the same as the current status.`), so accidental double-submissions are harmless. Authentication and permission failures both return `403` - this API never responds `401`. + Download the same ongoing-monitoring history as `GET /v3/session/{sessionId}/aml-screening-history/` as a CSV file, one line per completed re-screen, newest first, ready to hand to an auditor. The file is streamed whole (no pagination) and starts with a header line; an empty cell means unknown, dataset dates are never estimated, and a value that would start a spreadsheet formula is prefixed with an apostrophe. Works for KYC and KYB session IDs; pass `node_id` when the session ran more than one AML step. Errors are returned as JSON, not CSV. A scoped API key needs `decisions: read` and `media: read`: the response is a file, so a key without `media: read` gets `403`. - ``PATCH /v3/session/{sessionId}/update-status/`` + ``GET /v3/session/{sessionId}/aml-screening-history/export/`` Args: - session_id: UUID of the verification session to update. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. - new_status: Target status. `Approved` and `Declined` record a final manual decision (each can also overturn the other). `Resubmitted` clears the selected steps and sends the session back to the user. Any other value returns `400`. - comment: Free-text reason for the change, stored on the session's review trail and returned in the `reviews` array of [Get Decision](#get-/v3/session/-sessionId-/decision/). For example `Duplicated user`. - nodes_to_resubmit: Workflow steps the user must redo. Only acted on when `new_status` is `Resubmitted`. For `Approved`/`Declined` the entries are still schema-validated (an invalid `feature` value returns `400`) but schema-valid entries are semantically ignored. If omitted, the server auto-selects existing OCR, Liveness, Face Match, POA, Phone, Email, AML, Database Validation, and Questionnaire attempts whose status is `Declined`, `In Review`, `Not Finished`, or `Expired` (NFC, IP analysis, age estimation, face search, and KYB document attempts are never auto-selected; non-face-match face attempts are selected as `LIVENESS`) - features the user never attempted (no recorded attempt) are NOT selected, so a session with zero attempts returns `400` ("No features found that need resubmission") even though nothing was approved; pass `nodes_to_resubmit` explicitly in that case. Steps are executed in workflow-graph order regardless of the order you send them. `KYB_REGISTRY`, `KYB_KEY_PEOPLE`, and the `KYB` alias are rejected with `400` - those parent checks recompute from their child KYC sessions. - send_email: Whether to email the user about the change. Requires `email_address`. For `Approved`/`Declined` the user receives a status notice; for `Resubmitted` the email includes the verification link and the per-step resubmission reasons. - email_address: Recipient for the notification email. **Required when `send_email` is `true`** - omitting it returns `400`. - email_language: Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`). - email_preview_token: Optional signed receipt from Preview Resubmission Email. When supplied with send_email=true and new_status=Resubmitted, the recipient, language, checks, current session state, sender and message must still match the preview. A changed or expired receipt returns 409 before changing the session or sending email. Existing integrations may omit this field. + session_id: UUID of the verification session (the `session_id` returned by `POST /v3/session/`). Accepts both User Verification (KYC) and Business Verification (KYB) session IDs: the server resolves the ID across both session types. Must be a well-formed UUID; a malformed value does not match the route and returns an HTML `404`. + node_id: Workflow graph node ID of the AML check. Needed only when the session ran more than one AML step; without it the most recent AML check is used. An unknown node returns `404`. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsUpdateStatusResponse", - await self._client.request( - "patch", - "/v3/session/{sessionId}/update-status/", - cast_to=SessionsUpdateStatusResponse, + "bytes", + self._client.request( + "get", + "/v3/session/{sessionId}/aml-screening-history/export/", + cast_to=bytes, path_params={"sessionId": session_id}, auth="api_key", - body={ - "new_status": new_status, - "comment": comment, - "nodes_to_resubmit": nodes_to_resubmit, - "send_email": send_email, - "email_address": email_address, - "email_language": email_language, - "email_preview_token": email_preview_token, - }, + query={"node_id": node_id}, + accept="text/csv, application/json", extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -1919,139 +1866,45 @@ async def update_status( ), ) - def list( + def add_images( self, + session_id: str, *, - session_kind: Literal["user", "business", "all"] | str | NotGiven = NOT_GIVEN, - limit: int | NotGiven = NOT_GIVEN, - offset: int | NotGiven = NOT_GIVEN, - status: str | NotGiven = NOT_GIVEN, - vendor_data: str | NotGiven = NOT_GIVEN, - search: str | NotGiven = NOT_GIVEN, - workflow_id: str | NotGiven = NOT_GIVEN, - date_from: str | _dt.date | NotGiven = NOT_GIVEN, - date_to: str | _dt.date | NotGiven = NOT_GIVEN, - country: str | NotGiven = NOT_GIVEN, - document_type: str | NotGiven = NOT_GIVEN, - document_name: str | NotGiven = NOT_GIVEN, - region: str | NotGiven = NOT_GIVEN, - didit_internal_id: str | NotGiven = NOT_GIVEN, - session_type: str | NotGiven = NOT_GIVEN, - api_service: Literal[ - "ID_VERIFICATION", - "FACE_MATCH", - "AGE_ESTIMATION", - "FACE_SEARCH", - "POA", - "AML", - "KYB_REGISTRY", - "PASSIVE_LIVENESS", - "DATABASE_VALIDATION", - "PHONE_VERIFICATION", - "EMAIL_VERIFICATION", - ] - | str - | NotGiven = NOT_GIVEN, - screened_full_name: str | NotGiven = NOT_GIVEN, - warning: str | NotGiven = NOT_GIVEN, - status_initialized_as_in_review: bool | NotGiven = NOT_GIVEN, - is_kyc_to_be_reviewed: bool | NotGiven = NOT_GIVEN, - euuid: str | NotGiven = NOT_GIVEN, - organization_name: str | NotGiven = NOT_GIVEN, + file: FileTypes, + model_type: Literal["kyc", "poa"] | str, + file_password: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncPaginator[ - SessionsListResponseResultsKycSessionRowSessionKindUser - | SessionsListResponseResultsKybSessionRowSessionKindBusiness - ]: - """List verification sessions (KYC and/or KYB) with filters and pagination. - - Returns a paginated list of verification sessions for the application identified by your API key, newest first (descending `session_number`). - - **Canonical URL:** the route is `/v3/sessions/` (trailing slash). Requests to `/v3/sessions` without the slash receive a `301` redirect to the slashed URL with the query string preserved; most HTTP clients follow it transparently on GET. - - **When to use this endpoint vs webhooks.** Use this endpoint for dashboards, back-office search, reconciliation, and batch exports. Do not poll it to track the progress of individual verifications - configure [webhooks](/integration/webhooks) instead, which push `status.updated` events the moment a session changes. For one session's full result, call `GET /v3/session/{sessionId}/decision/`. - - **KYC and KYB sessions.** `session_kind` selects which kinds are listed: - - `user` (default) - User Verification (KYC) sessions only. Backward-compatible behavior. - - `business` - Business Verification (KYB) sessions only. Rows have a different, smaller shape (see the response schema). - - `all` - both kinds in one response: the KYC page and the KYB page for the same `limit`/`offset` are concatenated in `results` (KYC rows first), so a page can contain up to `2 x limit` rows. `count` is the sum of both kinds and `next`/`previous` reflect the KYC-side pagination. For strict pagination of mixed data, call `user` and `business` separately. - - Every row carries a `session_kind` discriminator (`"user"` or `"business"`). - - **Pagination.** Standard limit/offset: `limit` (default 50) and `offset` (default 0), with `next`/`previous` ready-made page URLs. For KYC rows `count` is exact. For KYB rows `count` is computed with a bounded scan and is capped at 100 - when there are more than 100 matches it stays at 100, so iterate using `next` (or by checking whether a full page came back) rather than computing total pages from `count`. - - **Filter semantics.** All filters combine with AND and apply to KYC rows. When `session_kind` is `business` or `all`, the KYB side honors the shared parameters `vendor_data`, `status`, `workflow_id`, `country` (registry-company country code), `date_from`, `date_to`, `search` (company name, registration number - type registration numbers without separators, e.g. `DPC001` not `DPC-001` - vendor_data, session number, or session UUID), `warning`, and `organization_name`; the remaining KYC-only filters are ignored for KYB rows. Comma-separated parameters (`status`, `country`, `document_type`, `region`, `workflow_id`, `document_name`, `warning`, `organization_name`, `session_type`) match any of the listed values (OR within the parameter). `vendor_data` is an exact match - use `search` for partial matching. Unknown values in list filters simply return no rows (no error); malformed typed values (`date_from`, `date_to`, `didit_internal_id`, `api_service`, `session_kind`) return `400` with a per-field error object. - - **Search.** `search` adapts to the input: a full UUID looks up `session_id`; digits (with optional `+ - . space`) match session-number prefix, document/personal numbers, phone-number prefix, or `vendor_data`; values containing `@` match email prefixes; any other text matches names (accent-insensitive) and `vendor_data`. + ) -> SessionsAddImagesResponse: + """Add an image to a session. - No fixed per-endpoint rate limit is enforced, but keep list polling infrequent and rely on webhooks for real-time updates. + Attach one extra image or PDF to the ID verification (`model_type=kyc`) or Proof of Address (`model_type=poa`) result of a finished User Verification (KYC) session, for example evidence collected outside the flow. The session must be `Approved`, `Declined`, `In Review` or `Kyc Expired` and carry the result named by `model_type`; Business Verification (KYB) sessions are not supported. Each call appends one file (to the most recent result when the step ran more than once), keeps the existing files and records a `FILE_ADDED` entry in the session activity log; the upload sends no webhook and does not change the session status. The file is compressed on upload and is returned in `extra_files` of the session decision. This endpoint is limited to 10 requests per minute per API key. A scoped API key needs `sessions: write`, and without `media: read` it receives the `extra_files` links as `null`. - ``GET /v3/sessions/`` + ``PATCH /v3/session/{sessionId}/add-images/`` Args: - session_kind: Which kind of sessions to list: `user` (KYC only, default), `business` (KYB only), or `all` (both, concatenated - see the description for merged-pagination semantics). Matching is case-insensitive (`BUSINESS` works); any value other than a case variant of `user`/`business`/`all` returns `400`. - limit: Maximum number of rows per page (per kind when `session_kind=all`). Defaults to 50. - offset: Number of rows to skip before the first returned row. Defaults to 0. Prefer following the `next`/`previous` URLs from the response. - status: Comma-separated list of session statuses to include, e.g. `Approved,In Review`. Values are case-sensitive; see [Verification Statuses](/integration/verification-statuses). Unknown values match nothing. - vendor_data: Exact-match filter on the `vendor_data` you sent at session creation (your own user/business identifier). For partial matches use `search` instead. - search: Smart free-text search. A full UUID matches `session_id`; numeric input matches session-number prefix, document/personal numbers, phone prefix, or `vendor_data`; input containing `@` matches email prefixes; other text matches names (accent-insensitive) and `vendor_data`. Free text containing digits additionally matches document/personal-number search fields and session-number prefixes. - workflow_id: Comma-separated workflow UUIDs (find them on the Workflows page in the Console). Malformed UUIDs in the list are ignored; if none are valid the result is empty. - date_from: Only sessions created on or after this date (UTC), format `YYYY-MM-DD`. Malformed dates return `400`. - date_to: Only sessions created up to this date (UTC), format `YYYY-MM-DD`. For KYC rows the entire day is included; for KYB rows the comparison is `created_at <= date_to` (midnight), so use the next day's date to include a full day of KYB sessions. - country: Comma-separated country filter. For KYC rows: ISO 3166-1 alpha-3 codes of the ID document's issuing state (e.g. `ESP,USA`), which may differ from nationality. For KYB rows: the registry company's country code. - document_type: Comma-separated ID-document type codes verified in the session (KYC rows only): `P` (Passport), `ID` (Identity Card), `DL` (Driver's License), `RP` (Residence Permit), `SSC` (Social Security Card), `HIC` (Health Insurance Card), `WP` (Work Permit), `TC` (Tax Card), `VISA`, `PSC` (Public Service Card), `BC` (Birth Certificate), `OTHER`. Note the response field `document_type` contains the display name, not the code. - document_name: Exact document names; multiple names can be passed comma-separated, but the value is only split on commas when it contains more than one ` - ` separator (a lone name with an embedded comma is matched whole). Names are as recognized by document verification (front or back side), e.g. `Spain - Id Card (2021)`. KYC rows only. - region: Comma-separated document region/state values (for documents issued per region, such as US driver's licenses). KYC rows only. - didit_internal_id: Filter by Didit's internal user identifier (the `didit_internal_id` response field) to list all sessions of one verified user. Must be a valid UUID; malformed values return `400`. KYC rows only. - session_type: Comma-separated session creation channels: `API`, `HOSTED`, `MIGRATED`. The special value `ALL` (or omitting the parameter) disables the filter. KYC rows only. - api_service: Filter standalone API sessions by service. Invalid values return `400`. KYC rows only. - screened_full_name: Accent-insensitive name search inside AML and database-validation screened data. When combined with `api_service=AML` or `api_service=DATABASE_VALIDATION`, the search is restricted to that feature's screened data. KYC rows only. - warning: Comma-separated warning risk codes; returns sessions that raised any of them (e.g. `POSSIBLE_FRAUD`). Risk codes are listed per feature in the [warnings documentation](/integration/webhooks). Applies to KYC rows and, with business warning codes, to KYB rows. - status_initialized_as_in_review: When `true`, only sessions whose decision initially landed in `In Review` (useful to audit manual-review volume). KYC rows only. - is_kyc_to_be_reviewed: `true` returns sessions whose document verification is flagged for manual review; `false` returns sessions with document verification explicitly not flagged. Omit to disable. KYC rows only. - euuid: Filter by the end user's euuid (UUID assigned to the verifying user, used by Console deep links). Must be a valid UUID; malformed values return a 400 field error. KYC rows only. - organization_name: Comma-separated organization names. Console-oriented; with an API key (already scoped to one application) it only matches your own organization's name. + session_id: UUID of the User Verification (KYC) session to add the file to (the `session_id` returned by `POST /v3/session/`). A Business Verification (KYB) session ID returns `404`. + file: The image or PDF. Allowed extensions: `jpg`, `jpeg`, `png`, `webp`, `tiff`, `pdf`. At most 15 MB as uploaded; the file is then compressed and rejected with `400` if it is still larger than 2 MB (2,097,152 bytes). Empty files are rejected. + model_type: `kyc` adds the file to the ID verification result, `poa` to the Proof of Address result. + file_password: Password of an encrypted PDF. Without it, or with a wrong one, an encrypted PDF returns `400`. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "AsyncPaginator[SessionsListResponseResultsKycSessionRowSessionKindUser | SessionsListResponseResultsKybSessionRowSessionKindBusiness]", - self._client.request_page( - "get", - "/v3/sessions/", - cast_to=SessionsListResponse, - item_type=SessionsListResponseResultsKycSessionRowSessionKindUser - | SessionsListResponseResultsKybSessionRowSessionKindBusiness, + "SessionsAddImagesResponse", + self._client.request( + "patch", + "/v3/session/{sessionId}/add-images/", + cast_to=SessionsAddImagesResponse, + path_params={"sessionId": session_id}, auth="api_key", - query={ - "session_kind": session_kind, - "limit": limit, - "offset": offset, - "status": status, - "vendor_data": vendor_data, - "search": search, - "workflow_id": workflow_id, - "date_from": date_from, - "date_to": date_to, - "country": country, - "document_type": document_type, - "document_name": document_name, - "region": region, - "didit_internal_id": didit_internal_id, - "session_type": session_type, - "api_service": api_service, - "screened_full_name": screened_full_name, - "warning": warning, - "status_initialized_as_in_review": status_initialized_as_in_review, - "is_kyc_to_be_reviewed": is_kyc_to_be_reviewed, - "euuid": euuid, - "organization_name": organization_name, - }, + body={"file": file, "model_type": model_type, "file_password": file_password}, + multipart=True, + file_fields=("file",), extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -2059,41 +1912,45 @@ def list( ), ) - async def share( + def update_aml_status( self, session_id: str, *, - for_application_id: str, - ttl_in_seconds: int | NotGiven = NOT_GIVEN, + status: Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] + | str, + node_id: str | NotGiven = NOT_GIVEN, + node_id_query: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsShareResponse: - """Mint a share token for a finished verification session. + ) -> SessionsUpdateAmlStatusResponse: + """Update an AML check status. - Mint a short-lived JWT that lets a specific Didit application import this finished session. Pair with `POST /v3/session/import-shared/`. + Set the status of a session's AML screening check as a whole (the `status` of an `aml_screenings[]` entry in the session decision), for example `Approved` after your team cleared every hit. Works for KYC and KYB session IDs; pass `node_id` (query parameter or body field) when the session ran more than one AML step, otherwise the most recent AML check is updated. When the status actually changes, an `AML_STATUS_UPDATED` activity entry is recorded and a `data.updated` [webhook](https://docs.didit.me/integration/webhooks) is sent; sending the current status returns `200` and changes nothing. The session status is not recomputed: use `PATCH /v3/session/{sessionId}/update-status/` for the session decision and `PATCH /v3/session/{sessionId}/update-aml-hit-status/` for single hits. A scoped API key needs `decisions: write`. - ``POST /v3/session/{sessionId}/share/`` + ``PATCH /v3/session/{sessionId}/update-aml-status/`` Args: - session_id: UUID of the source verification session to mint a token for. - for_application_id: UUID of the Didit application that will redeem the token. Must exist, must not be deleted, and must differ from the calling application. Find it in the Business Console under **Settings → Application**. - ttl_in_seconds: Token lifetime, in seconds. Minimum `60`, maximum `86400` (24 h). Defaults to `3600` (1 h). + session_id: UUID of the verification session (the `session_id` returned by `POST /v3/session/`). Accepts both User Verification (KYC) and Business Verification (KYB) session IDs: the server resolves the ID across both session types. Must be a well-formed UUID; a malformed value does not match the route and returns an HTML `404`. + status: New status of the AML check. + node_id: Workflow graph node ID of the AML check, as an alternative to the query parameter. + node_id_query: Workflow graph node ID of the AML check to update. Needed only when the session ran more than one AML step. Takes precedence over `node_id` in the body. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsShareResponse", - await self._client.request( - "post", - "/v3/session/{sessionId}/share/", - cast_to=SessionsShareResponse, + "SessionsUpdateAmlStatusResponse", + self._client.request( + "patch", + "/v3/session/{sessionId}/update-aml-status/", + cast_to=SessionsUpdateAmlStatusResponse, path_params={"sessionId": session_id}, auth="api_key", - body={"for_application_id": for_application_id, "ttl_in_seconds": ttl_in_seconds}, + query={"node_id": node_id_query}, + body={"status": status, "node_id": node_id}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -2101,46 +1958,63 @@ async def share( ), ) - async def import_shared( + def simulate( self, + session_id: str, *, - share_token: str, - trust_review: bool, - workflow_id: str, - vendor_data: str | None | NotGiven = NOT_GIVEN, + new_status: Literal["Approved", "Declined", "In Review", "Expired", "Abandoned"] + | str + | NotGiven = NOT_GIVEN, + comment: str | NotGiven = NOT_GIVEN, + scenario: str | NotGiven = NOT_GIVEN, + risk: str | NotGiven = NOT_GIVEN, + node_id: str | NotGiven = NOT_GIVEN, + send_webhooks: bool | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> object: - """Redeem a share token to clone a verification session into your application. + ) -> SandboxSimulateResponse: + """Simulate a sandbox session's status. - Redeem a share token to clone a KYC or KYB session into the calling application with a fresh `session_id`. Not idempotent - a token redeems once per receiver. + 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. - ``POST /v3/session/import-shared/`` + Set `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. + + `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. + + Requires 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. + + ``POST /v3/session/{sessionId}/simulate/`` Args: - share_token: JWT share token issued by [`POST /v3/session/{sessionId}/share/`](/sessions-api/share-session). - trust_review: If `true`, the cloned session keeps the source's final `status`. If `false`, it is forced into `In Review`. - workflow_id: UUID of a workflow in the calling application. Cross-application IDs are rejected with `404`. - vendor_data: Optional override for the cloned session's `vendor_data`. + session_id: UUID of the sandbox verification session. Accepts both user (KYC) and business (KYB) session IDs. + new_status: The status to set. When you leave it out, it defaults to the default status of the risk case (`risk`, or the first risk the scenario's magic values carry), else to the scenario's default status. + comment: Note added to the activity entry the status change records. + scenario: A scenario slug from the catalog (`GET /v1/sandbox/scenarios/`). It supplies the default status and, without `risk`, the warning its magic values carry. An unknown slug returns `400`. + risk: A risk case from the catalog. Its warning is added to the session, with a log type that follows `new_status`. An unknown value returns `400`. + node_id: Workflow node the risk warning is attached to. Defaults to the session's current node. + send_webhooks: `true` sends exactly the webhooks a real status change sends to the sandbox application's destinations: `status.updated`, plus the `user.status.updated` / `user.data.updated` (or `business.status.updated` / `business.data.updated`) events the change causes, for destinations subscribed to them. `false` changes the status without logging or sending any webhook: the missed-webhook recovery never re-sends that decision, the console still updates live, and the next status change of the session sends webhooks as usual. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "object", - await self._client.request( + "SandboxSimulateResponse", + self._client.request( "post", - "/v3/session/import-shared/", - cast_to=object, + "/v3/session/{sessionId}/simulate/", + cast_to=SandboxSimulateResponse, + path_params={"sessionId": session_id}, auth="api_key", body={ - "share_token": share_token, - "trust_review": trust_review, - "workflow_id": workflow_id, - "vendor_data": vendor_data, + "new_status": new_status, + "comment": comment, + "scenario": scenario, + "risk": risk, + "node_id": node_id, + "send_webhooks": send_webhooks, }, extra_headers=extra_headers, extra_query=extra_query, @@ -2149,85 +2023,1659 @@ async def import_shared( ), ) - async def batch_delete( + def list_feature_overrides( self, + session_id: str, *, - session_numbers: Sequence[str] | NotGiven = NOT_GIVEN, - delete_all: bool | NotGiven = NOT_GIVEN, - retain_face_embeddings: bool | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsListFeatureOverridesResponse: + """List a session's check overrides. + + Every override of an individual feature (step) status on a User Verification (KYC) or Business Verification (KYB) session, newest first and not paginated: applied ones, ones waiting for the person to finish the flow (`pending_until_flow_finishes`) and cancelled ones, each with who asked, their reason, and whether it applied while the person was still in the flow (`forced`). + + `person_in_flow` tells you whether a new override with `defer_until_flow_finishes: true` would wait. `flow_finished_at` is when the person last finished the flow; it is `null` while they are still in it (a resubmission included) and when the session closed before they finished. + + Requires the `read:sessions` privilege; a scoped API key needs read access to decisions. + + ``GET /v3/session/{sessionId}/features/overrides/`` + + Args: + session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsListFeatureOverridesResponse", + self._client.request( + "get", + "/v3/session/{sessionId}/features/overrides/", + cast_to=SessionsListFeatureOverridesResponse, + path_params={"sessionId": session_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def cancel_feature_override( + self, + session_id: str, + override_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FeatureStatusOverride: + """Cancel a check override that waits for the flow to finish. + + Cancel an override of a feature (step) status that is still `pending_until_flow_finishes`, so it never applies. The check keeps its status, the cancellation is recorded in the session's activity as `FEATURE_OVERRIDE_CANCELLED`, and no webhook is sent. The response is the override with `state: cancelled` and `cancel_reason: cancelled_by_reviewer`. + + An override that already applied or was cancelled returns `409` with the code `feature_override_not_pending`. Find override IDs with `GET /v3/session/{sessionId}/features/overrides/` or in the `override` returned by the feature status update. + + Requires the `write:sessions` privilege; a scoped API key needs write access to decisions. + + ``POST /v3/session/{sessionId}/features/overrides/{overrideId}/cancel/`` + + Args: + session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs. + override_id: The `id` of the override to cancel. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "FeatureStatusOverride", + self._client.request( + "post", + "/v3/session/{sessionId}/features/overrides/{overrideId}/cancel/", + cast_to=FeatureStatusOverride, + path_params={"sessionId": session_id, "overrideId": override_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def preview_decision_email( + self, + session_id: str, + *, + new_status: Literal["Approved", "Declined"] | str, + email_language: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsPreviewDecisionEmailResponse: + """Preview a session's decision email. + + Render the email the person receives when you set a session to `Approved` or `Declined` with `send_email: true`, without changing the session or sending anything. The response shows the subject and HTML exactly as they are sent, the recipient, the sender, the language, and whether confirming the change sends the email at all (`will_be_sent`, `not_sent_reason`). + + The recipient is the person's own address: the one they verified in an email verification step of the flow, else the one given in `contact_details` when the session was created. The sender is your application's verified sender domain when white label applies (`sender_kind: customer`), else Didit's sender (`sender_kind: didit`). + + To send exactly what you previewed, call [Update Session Status](/sessions-api/update-status) with the same `new_status`, `send_email: true`, `email_address` set to `recipient`, `email_language` set to `language` and `email_preview_token` set to `preview_token`. The receipt expires after 15 minutes. When the recipient, language, session status, sender or email changed since the preview, the update returns `409` before anything changes; preview again and confirm with the new receipt. + + Works for User Verification (KYC) and Business Verification (KYB) sessions in `Approved`, `Declined`, `In Review`, `Kyc Expired`, `Abandoned` or `Resubmitted` status. The status check runs before body validation. Requires the `write:sessions` privilege; a scoped API key needs write access to decisions. Responses use `Cache-Control: no-store`: treat them as sensitive verification content. + + ``POST /v3/session/{sessionId}/decision-email-preview/`` + + Args: + session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs. + new_status: The decision whose email you want to preview. + email_language: Language of the email, the same default as Update Session Status. A language that is not offered falls back to English; `language` in the response says which one is used. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsPreviewDecisionEmailResponse", + self._client.request( + "post", + "/v3/session/{sessionId}/decision-email-preview/", + cast_to=SessionsPreviewDecisionEmailResponse, + path_params={"sessionId": session_id}, + auth="api_key", + body={"new_status": new_status, "email_language": email_language}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncSessions(AsyncAPIResource): + """The ``sessions`` resource of the Didit API. + + Methods: ``add_images``, ``batch_delete``, ``bulk_update_aml_hit_status``, ``cancel_feature_override``, ``create``, ``delete``, ``export_aml_screening_history``, ``generate_pdf``, ``import_shared``, ``list``, ``list_aml_screening_history``, ``list_events``, ``list_feature_overrides``, ``list_networks``, ``preview_decision_email``, ``preview_resubmission``, ``regenerate_kyb_party``, ``retrieve_decision``, ``retrieve_deletion_job``, ``share``, ``simulate``, ``update_aml_hit_status``, ``update_aml_status``, ``update_data``, ``update_document_ai_data``, ``update_feature_status``, ``update_kyb_company_data``, ``update_kyb_party``, ``update_poa_data``, ``update_status``. + + Nested resources: ``imports``, ``reviews``, ``signatures``, ``webhooks``. + """ + + @cached_property + def imports(self) -> AsyncSessionsImports: + from didit.resources.sessions.imports import AsyncSessionsImports + + return AsyncSessionsImports(self._client) + + @cached_property + def reviews(self) -> AsyncSessionsReviews: + from didit.resources.sessions.reviews import AsyncSessionsReviews + + return AsyncSessionsReviews(self._client) + + @cached_property + def signatures(self) -> AsyncSessionsSignatures: + from didit.resources.sessions.signatures import AsyncSessionsSignatures + + return AsyncSessionsSignatures(self._client) + + @cached_property + def webhooks(self) -> AsyncSessionsWebhooks: + from didit.resources.sessions.webhooks import AsyncSessionsWebhooks + + return AsyncSessionsWebhooks(self._client) + + @cached_property + def with_raw_response(self) -> AsyncSessionsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncSessionsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSessionsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncSessionsWithStreamingResponse(self) + + async def create( + self, + *, + workflow_id: str, + vendor_data: str | NotGiven = NOT_GIVEN, + callback: str | NotGiven = NOT_GIVEN, + callback_method: Literal["initiator", "completer", "both"] | str | NotGiven = NOT_GIVEN, + metadata: object | NotGiven = NOT_GIVEN, + language: Literal[ + "en", + "ar", + "bg", + "bn", + "bs", + "ca", + "cnr", + "cs", + "da", + "de", + "el", + "es", + "et", + "fa", + "fi", + "fr", + "he", + "hi", + "hr", + "hu", + "hy", + "id", + "it", + "ja", + "ka", + "kk", + "ko", + "ky", + "lt", + "lv", + "mk", + "mn", + "ms", + "nl", + "no", + "pl", + "pt-BR", + "pt", + "ro", + "ru", + "sk", + "sl", + "so", + "sq", + "sr", + "sv", + "th", + "tr", + "uk", + "uz", + "vi", + "zh-CN", + "zh-TW", + "zh", + ] + | str + | NotGiven = NOT_GIVEN, + contact_details: SessionsCreateParamsContactDetails | NotGiven = NOT_GIVEN, + expected_details: SessionsCreateParamsExpectedDetails | NotGiven = NOT_GIVEN, + portrait_image: str | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + sign: SessionsCreateParamsSign | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsCreateResponse: + """Create a verification session. + + Create a User Verification (KYC) or Business Verification (KYB) session and receive a hosted verification `url` plus a `session_token` to redirect or embed for your end user. The `workflow_id` selects which verification steps run and whether the session is KYC or KYB. + + Call this from your backend (never the browser - the API key is a secret) whenever a user or business needs to be verified: at signup, before a sensitive action, or when re-verification is required. Prerequisites: a published workflow (created in the Console [Workflows](https://docs.didit.me/console/workflows) page) and an application API key with available credits. Sandbox applications bypass the credit check. + + **Idempotency:** when `vendor_data` is provided and an unfinished session (`Not Started`, `In Progress`, `Resubmitted`, or `Awaiting User`) with the same `vendor_data` already exists on the workflow's **current latest published version** (and that session already has a hosted verification URL), the existing session is returned (still `201`) instead of creating a duplicate - with its `callback` and `metadata` updated to the newly provided values. An unfinished session created on an older version of the workflow (one that has since been republished) is **not** reused; a new session pinned to the latest published version is created instead. Sessions in `Approved`, `Declined`, `In Review`, `Expired`, `Abandoned`, or `Kyc Expired` are never reused. + + **Side effects:** the hosted verification URL is generated and stored; if `contact_details.send_notification_emails` is `true` and an email is provided, a verification invite email is sent. The session expires after the workflow's configured session expiration time. For KYB workflows, `contact_details.phone` and `expected_details` are ignored. + + ``POST /v3/session/`` + + Args: + workflow_id: Stable identifier of the workflow that defines which verification steps the session will run. Workflows are created and managed in the [Workflows](/console/workflows) page of the Console. The `workflow_id` also implicitly selects whether the session is KYC or KYB. + vendor_data: A unique identifier for the user being verified, such as a UUID, email, or internal user ID. This field is used for: (1) **User grouping** - sessions with the same vendor_data are linked to the same user profile in the Users tab. (2) **Cross-session duplicate detection** - when checking for duplicated faces, documents, phone numbers, emails, IP addresses, or device fingerprints, sessions with the same vendor_data are treated as the same user and excluded from matches. Without vendor_data, every session is treated as a unique user and all potential duplicates are surfaced. We strongly recommend always providing a vendor_data to reduce noise in duplicate detection. + callback: URL to redirect the user to after verification completes. Didit automatically appends `verificationSessionId` and `status` (Approved, Declined, In Review) as query parameters. Custom URL schemes (e.g. `myapp://`) are supported for mobile callbacks. If omitted, the workflow's default `callback_url` is used. When the application has allowed domains (`allowed_domains` on `PATCH /v3/webhook/`), an `http` or `https` callback must be on one of those origins (same scheme, host and port), otherwise the request fails with 400 on `callback`: `callback must be on one of the application's allowed domains: https://app.example.com`. Custom URL schemes are always accepted. + callback_method: Determines which device should handle the redirect to the provided callback URL. Use `initiator` to redirect only the device that started the flow, `completer` for the device that finishes it, or `both` to allow either device to trigger the callback. If you ever notice the callback not triggering reliably, we recommend setting this value to `both`. + metadata: Arbitrary JSON stored with the session and echoed back in the response and webhooks. Any JSON value is accepted (object, string, number, array), but a JSON object of key/value pairs is recommended. Not shown to the end user. Use it to pass your own correlation ids, A/B variants, or business context. + language: Language code (ISO 639-1) for the verification process interface. Controls the language displayed to the end user during verification. If not provided, the browser's language will be automatically detected and used. Check all the supported languages [here](/integration/supported-languages). + contact_details: User contact information that can be used for notifications, prefilling verification forms, and phone verification. This includes email address, preferred language for communications, and phone number. + expected_details: Expected user details used to cross-validate against the data extracted from the user's ID document, Proof of Address, and other verification steps. Mismatches are surfaced as warnings on the decision; some fields (e.g. `id_country`, `expected_document_types`) also alter the user-facing flow. For Business Verification (KYB) workflows only the business fields (`company_name`, `registry_country`, `registration_number`) are used - they pre-fill the company registry search for the user - and all person-level fields are ignored. + portrait_image: Base64-encoded portrait image of the end user's face (max 2MB; JPEG, PNG, WebP, or TIFF). Used as the reference image to match against the liveness capture when the workflow is a `Biometric Authentication` workflow with Face Match enabled, or any graph workflow where Face Match runs before ID Verification (OCR). Optional when the `vendor_data` user already has a stored face image: Didit reuses it automatically, resolved in priority order: approved liveness face, ePassport chip photo, ID document portrait, manually enrolled profile face (only faces from approved sessions or faces you enrolled yourself are reused). When provided, `portrait_image` always takes precedence over the stored face. If omitted and no stored face exists (or `vendor_data` is not sent), session creation fails with `400`. Ignored for other workflow types. + sandbox_scenario: Sandbox-only scenario slug. Only accepted when the API key belongs to a sandbox-mode application; rejected on live applications. When set, the create flow expands the scenario's bundled magic values onto the session's input fields (contact_details, expected_details) so the mocked providers reproduce the scenario's outcome through the real workflow pipeline. See the scenario catalog at GET /v1/sandbox/scenarios/ or [Sandbox & Test Data](/integration/sandbox-testing). You can also arm (or re-arm) a scenario after create via POST /v3/session/{sessionId}/sandbox/arm/, as long as the session is still `Not Started`. For non-doc lookup, use `lookup_partial_match`, `lookup_no_match`, or `lookup_provider_error` to test the configured attempt budget and fallback. For identity wallets, use `wallet_cancelled`, `wallet_timeout`, or `wallet_provider_error`; `approve` simulates a verified wallet. All catalog wallets can be tested in sandbox, including schemes unavailable on live applications. Simulated attempts are not charged. + sign: Electronic Signature input for this session. `documents` are the documents to sign, for a workflow whose Electronic Signature step takes documents from the session (`signature_documents_source` `session` or `both`): up to 20 documents and 50 MB in total. `field_values` fills the fields whose `prefill` is `api.`, on workflow and session documents alike. Creating a session again for the same `workflow_id` and `vendor_data` returns the pending session; if that call sends `sign`, its documents, fields and values must be identical to the first ones, and anything different returns `400` with `{"sign": "signature_document_changed"}`. See [Electronic signature documents](/sessions-api/create-session#electronic-signature-documents). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsCreateResponse", + await self._client.request( + "post", + "/v3/session/", + cast_to=SessionsCreateResponse, + auth="api_key", + body={ + "workflow_id": workflow_id, + "vendor_data": vendor_data, + "callback": callback, + "callback_method": callback_method, + "metadata": metadata, + "language": language, + "contact_details": contact_details, + "expected_details": expected_details, + "portrait_image": portrait_image, + "sandbox_scenario": sandbox_scenario, + "sign": sign, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_decision( + self, + session_id: str, + *, + include: Literal["events", "resubmissions", "events,resubmissions"] + | str + | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionDecision: + """Retrieve the full decision for a verification session. + + Retrieve the complete decision report for a verification session - the rolled-up session `status` plus one report per verification feature the workflow executed. This is the canonical read endpoint for both User Verification (KYC) and Business Verification (KYB) sessions: the same path serves both, and the top-level `session_kind` discriminator tells you which shape you received (`"user"` or `"business"`). + + **Every feature report is a plural array.** V3 workflows are graphs that can run the same feature more than once, so each block (`id_verifications`, `nfc_verifications`, `liveness_checks`, `face_matches`, `poa_verifications`, `document_ai_documents`, `phone_verifications`, `email_verifications`, `aml_screenings`, `ip_analyses`, `database_validations`, `questionnaire_responses`) is a JSON array whose items each carry a `node_id` identifying the workflow graph node that produced them. Read NFC as `response.nfc_verifications[0]` - never as a singular `nfc` field. A block is `null` until the workflow has run that feature at least once; features the workflow does not include never appear with data. Electronic Signature results are in `signatures`, which is present only when the workflow has an Electronic Signature step. + + **When to call.** The recommended pattern is event-driven: wait for the `status.updated` webhook that fires when the session reaches a decision (`Approved`, `Declined`, `In Review`), then call this endpoint once to fetch the full report. Polling also works - the endpoint can be called at any point in the session lifecycle and returns the data produced so far - but webhook-then-fetch is cheaper and faster. Media URLs in the response (document images, videos, PDFs) are short-lived presigned links: fetch them promptly or re-request the decision to get fresh ones, and do not persist them as long-term references. + + **Statuses.** The top-level `status` is the session lifecycle status (`Not Started`, `In Progress`, `Awaiting User`, `In Review`, `Approved`, `Declined`, `Resubmitted`, `Expired`, `Kyc Expired`, `Abandoned`). Each feature item additionally carries its own feature-level `status` (`Not Finished`, `Approved`, `Declined`, `In Review`, ...). A session can be `Approved` overall while an individual feature is `In Review`, depending on the workflow's decision rules. + + **Authentication and visibility.** Authenticate with the application API key in the `x-api-key` header (a console user Bearer token with the `read:sessions` permission also works). Only sessions owned by the authenticated application are visible. If your workflow restricts response attributes, non-allowed fields inside feature items are returned as `null` while `status`, `warnings`, and `node_id` are always preserved. + + **Rate limits.** GET requests are limited to 600 per minute per API key. Exceeding the limit returns `429` with `Retry-After` and `X-RateLimit-*` headers. + + ``GET /v3/session/{sessionId}/decision/`` + + Args: + session_id: UUID of the verification session whose decision you want to retrieve. This is the `session_id` returned by `POST /v3/session/`. The same path works for both User Verification (KYC) and Business Verification (KYB) sessions - the server resolves the id against both session types. **Must be a valid UUID.** The route only matches well-formed UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{"detail": ...}` envelope shown below. Validate the UUID format client-side so you can distinguish an input error from a genuine missing-session `404`. + include: Comma-separated list of extra payload sections. Supported values: `events` and `resubmissions`, alone or together (`events,resubmissions`). When `include=events` is passed, the response additionally contains the session event timeline (`events`, including the `WORKFLOW_BRANCH_EVALUATED` / `WORKFLOW_STATUS_NODE_REACHED` routing events described on the Retrieve Session page), the full activity log (`activity_reviews`), blocklist flags (`blocklisted`), a per-session `cost_breakdown`, workflow metadata (`workflow_type`, `workflow_version`, `workflow_version_id`, and `workflow_terminal_node_id` - the last recorded workflow status node that set a session status, or `null` if none has been recorded; historical provenance that is not cleared by manual approvals, declines or resubmissions, and is replaced by a later workflow status node), and session metadata (`api_service` - string or `null`, set only for standalone-API sessions; `session_type` - `API`, `HOSTED`, or `MIGRATED`; `has_device_nfc_support` - boolean). Two fields change relative to the plain shape: the top-level `session_kind` discriminator is **omitted** (the events shape only exists for user sessions), and the `features` array switches from plain strings to `{feature, node_id}` objects - where the email feature is reported as `EMAIL` instead of `EMAIL_VERIFICATION`. This expanded shape is primarily used by the Didit Console; most API integrations should omit the parameter. **KYB limitation:** `include=events` only applies to user (KYC) sessions. Business sessions (`session_kind = "business"`) always return the standard business decision shape - `events`, `activity_reviews`, `blocklisted`, `cost_breakdown` and the extra `workflow_*`/session metadata are never returned for business sessions, and their `features` array stays a plain string array. With `include=resubmissions` (user and business sessions alike) the response also contains `resubmissions`: one entry per resubmission request, oldest first, each with `number`, `created_at`, `previous_status`, `nodes`, `reasons`, `requested_by` and `results` (what the resubmitted steps returned before the request, under the same keys and in the same shape as the current results). Each `cost_breakdown` item carries `usage_type`, `price`, `details` and `created_at`: a resubmitted step that runs again is billed as a new item. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionDecision", + await self._client.request( + "get", + "/v3/session/{sessionId}/decision/", + cast_to=SessionDecision, + path_params={"sessionId": session_id}, + auth="api_key", + query={"include": include}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + session_id: str, + *, + retain_face_embeddings: bool | None | NotGiven = NOT_GIVEN, + face_retention_days: int | NotGiven = NOT_GIVEN, + face_retention_deadline: str | _dt.datetime | NotGiven = NOT_GIVEN, + deletion_instruction: Literal["operational_session_delete", "privacy_erasure"] + | str + | NotGiven = NOT_GIVEN, + instruction_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsDeleteResponse: + """Delete a verification session (KYC or KYB). + + Delete a single verification session, User Verification (KYC) or Business Verification (KYB), by its `session_id`. The id is resolved against KYC sessions first, then KYB sessions, so both kinds are deleted through this one URL. + + **What is deleted:** + - The session and every verification attempt it holds, including results that a resubmission replaced: the decision, extracted data, all related feature records (ID verification, face, liveness, face match, AML, registry checks, IP analysis, and so on), its reviews and its activity log. + - All stored files owned by the session: document front/back photos (full, cropped, and privacy-blurred variants), document-capture videos, portrait crops, NFC chip portrait and signature images, face reference images, liveness videos, face-match source/target images, Proof of Address documents, and any extra uploaded files (KYC); uploaded company documents and extra files (KYB). Electronic Signature documents, signed PDFs, evidence PDFs and signature images (KYC and KYB): their download paths stop working. Previously issued media URLs (`https:///...`) stop resolving. + - The session's face data. Its face embedding is deleted too, unless you choose to keep an image-free template (see **Face biometric data**). + - Webhook deliveries still queued for the session, and their retries. Webhooks already delivered are not affected. + - For a Business Verification (KYB) session: the verifications it created for its key people (KYC) and its corporate owners (KYB), recursively, reported as `deleted_child_verifications`. A verification it only links to, such as an existing approved KYC it reused, is not deleted. + + The session disappears from every read endpoint immediately: `GET /v3/session/{sessionId}/decision/` returns `404` and `GET /v3/sessions/` stops listing it. Ongoing AML monitoring and KYB registry monitoring started by the deleted verifications stop. + + **What is not affected:** blocklist entries created from this session (face or document) are **not** removed; manage those with the blocklist endpoints. Share tokens issued for the session are not revoked, but there is nothing left for them to import: an import with one returns `400` with `{"share_token": ["Original session does not exist."]}`. Credits already consumed are **not** refunded. The parent User or Business entity is **not** deleted; use `POST /v3/users/delete/` or `POST /v3/businesses/delete/` for those. If the User's profile portrait came from this session, the portrait is removed with it. + + **Face biometric data:** by default the session's face embedding is deleted with the session (`face_retention_outcome: deleted`). An application that opted in to biometric-template retention (`face_retention_policy: retain_until_user_deleted` on `PATCH /v3/webhook/`), or a request with `retain_face_embeddings: true`, keeps one separately managed, image-free face biometric template anchored to the session's User (`retained_with_user`) so that duplicate detection, Face Search, and biometric authentication keep working. The template never contains the face image, media, identity fields, the decision, or the deleted session's id; it is biometric data deleted at `biometric_template_expires_at`, or earlier on User deletion, privacy erasure, or an explicit purge through `/v3/biometric-templates/`. A session without a linked User cannot retain a template (`ineligible_no_vendor_user`). Send `deletion_instruction: privacy_erasure` for data-subject requests: it purges every retained template for that User and never retains a new one. + + **Irreversible:** deletion is permanent and there is no restore endpoint. Export anything you need (for example a decision PDF) before you call it. + + **Side effects:** no webhook is emitted for deletions. + + **Response:** `200 OK` once the deletion has completed, with a JSON body reporting the `face_retention_outcome`, the retained template's expiry and, for a KYB session, `deleted_child_verifications`. Earlier versions of this endpoint returned `204 No Content`; update clients that assert on `204`. Keep your `instruction_id` in your own erasure log. + + **Idempotency:** not idempotent at the HTTP level: the first call returns `200`; repeating it returns `404` because the session no longer resolves. After a `503` the session is still in place; repeat the same request to finish deleting it. + + **Authentication:** send your application's API key in the `x-api-key` header; the session must belong to that application. Console user access tokens (`Authorization: Bearer ...`) may also call this endpoint when they carry the `delete:sessions` permission. Authentication and permission failures both return `403`; this API never returns `401`. + + **Rate limit:** shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + To bulk-delete KYC sessions by their numeric `session_number`, use `POST /v3/sessions/delete/` instead. + + ``DELETE /v3/session/{sessionId}/delete/`` + + Args: + session_id: UUID (`session_id`) of the User Verification (KYC) or Business Verification (KYB) session to delete, as returned when the session was created. Must be a canonical hyphenated UUID - a non-UUID value does not match the route and returns `404`. + retain_face_embeddings: Override the application's `face_retention_policy` for this deletion. `true` keeps one image-free face biometric template anchored to the session's User after the session is deleted; `false` deletes the face embedding with the session; omit or `null` to follow the application policy. Ignored for KYB sessions and for sessions without a face embedding. + face_retention_days: Finite retention duration, in days, for a retained template. Required when a template is retained unless the application already sets `face_retention_days`. The template's `expires_at` is the earliest of this duration, the application's general data-retention window, and `face_retention_deadline`. + face_retention_deadline: Optional hard expiry for a retained template. Must be in the future. + deletion_instruction: The class of instruction you are giving. `operational_session_delete` deletes the session and retains a template only when the policy or `retain_face_embeddings` says so. `privacy_erasure` purges every retained biometric template anchored to the session's User before deleting the session, cannot be overridden by the application policy, and cannot be combined with `retain_face_embeddings: true` (`400`). + instruction_id: Your durable reference for this deletion instruction (for example an erasure-ticket id). Recorded on the audit trail and on any retained template. Generated by Didit when omitted. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsDeleteResponse", + await self._client.request( + "delete", + "/v3/session/{sessionId}/delete/", + cast_to=SessionsDeleteResponse, + path_params={"sessionId": session_id}, + auth="api_key", + body={ + "retain_face_embeddings": retain_face_embeddings, + "face_retention_days": face_retention_days, + "face_retention_deadline": face_retention_deadline, + "deletion_instruction": deletion_instruction, + "instruction_id": instruction_id, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def generate_pdf( + self, + session_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download a PDF report for a verification session. + + Render and download a compliance-ready PDF report for a finished verification session. The same path serves both User Verification (KYC) and Business Verification (KYB) sessions - the server resolves the `sessionId` against both session types and picks the matching report template. + + **Eligible statuses.** The session must already carry a final or reviewable status: + + - KYC sessions: `Approved`, `Declined`, `In Review`, or `Kyc Expired`. + - KYB sessions: `Approved`, `Declined`, or `In Review`. + + Any other status (`Not Started`, `In Progress`, `Expired`, `Abandoned`, …) returns `403` with an explanatory `detail` string - see the `403` response below. + + **What the report contains.** The PDF mirrors the console session view at the moment of the request: + + - **KYC** - rolled-up session status, the warnings list, and one section per executed feature: ID Verification (document images plus extracted fields), NFC, Liveness, Face Match, Email Verification, Phone Verification, Proof of Address, Questionnaire answers, Database Validation, AML Screening, and IP Analysis - followed by session tags, console review activity (comments and status changes), and the session event timeline. + - **KYB** - registry checks (company data), AML screenings, business document verifications, key-people checks, questionnaire responses, phone/email verifications, IP analyses, and the session event timeline, followed by tags and review activity. + + **Branding.** If your application has white-label customization, the report is rendered with your logo and links to your privacy-policy URL; otherwise it carries Didit branding. Reports are rendered in English - there is no language parameter. + + **No caching.** Every call re-renders the PDF from the current session data, so a report generated after a manual review reflects the reviewer’s decision. Two calls for the same session can produce byte-different files - archive the downloaded file if you need an immutable copy. + + **Latency.** Rendering downloads every stored image (document sides, selfies, extra files) before composing the PDF, so media-heavy sessions can take several seconds. Use a generous client read timeout (60 s recommended) and stream the body to disk. + + **Trailing slash.** The canonical route ends with a trailing slash (`…/generate-pdf/`). A request without it is served directly with the same response - there is no `301` redirect, so no client has to follow redirects and `curl` does not need `-L`. Use the slashed URL as in the samples. + + If you need the underlying data as JSON instead of a rendered report, use `GET /v3/session/{sessionId}/decision/`. For every reportable session of one user in a single document, use `GET /v3/users/{vendor_data}/generate-pdf/`. + + ``GET /v3/session/{sessionId}/generate-pdf/`` + + Args: + session_id: UUID of the User Verification (KYC) or Business Verification (KYB) session to render - the `session_id` returned by `POST /v3/session/`. The same path works for both session kinds; use the `Content-Disposition` filename on the response to tell which report type you received. **Must be a well-formed, lowercase UUID.** The route only matches valid UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{"detail": ...}` envelope shown below. The route's UUID converter only matches the canonical lowercase form - uppercase-hex UUIDs are rejected with the HTML 404. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + await self._client.request( + "get", + "/v3/session/{sessionId}/generate-pdf/", + cast_to=bytes, + path_params={"sessionId": session_id}, + auth="api_key", + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_status( + self, + session_id: str, + *, + new_status: Literal["Approved", "Declined", "Resubmitted"] | str, + comment: str | NotGiven = NOT_GIVEN, + nodes_to_resubmit: Sequence[SessionsUpdateStatusParamsNodesToResubmit] + | NotGiven = NOT_GIVEN, + send_email: bool | NotGiven = NOT_GIVEN, + email_address: str | NotGiven = NOT_GIVEN, + email_language: str | NotGiven = NOT_GIVEN, + email_preview_token: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdateStatusResponse: + """Approve, decline, or request resubmission for a verification session. + + Manually override the final decision of a KYC or KYB verification session. Set `new_status` to `Approved` or `Declined` to record a manual decision, or to `Resubmitted` to clear specific verification steps and let the user redo them through the same verification URL. The endpoint accepts both user (KYC) and business (KYB) session IDs. + + Call it once a session has reached a reviewable state: the current status must be `Approved`, `Declined`, `In Review`, `Kyc Expired`, `Abandoned`, or `Resubmitted`. Sessions that are still `Not Started`, `In Progress`, or `Awaiting User`, or that ended as `Expired`, return `400`. Your API key needs the `write:sessions` permission (a scoped API key needs write access to decisions). The eligible-state check runs before body validation, so an ineligible session returns the `Wrong Current Status` error even when the payload is also invalid. + + For `Resubmitted`, pass `nodes_to_resubmit` to choose which workflow steps the user must redo, or omit it to auto-select existing feature attempts in a resubmittable state (`Declined`, `In Review`, `Not Finished`, `Expired`). Never-attempted features are not auto-selected: list them explicitly in `nodes_to_resubmit`, or the request returns `400` when no recorded attempt needs resubmission. The selected steps' previous results move to the resubmission's history (returned by Get Decision with `include=resubmissions`), the steps are reordered to match the workflow graph, the session's expiration window restarts, and the original verification URL becomes usable again. Backend-only steps (`AML`, `DATABASE_VALIDATION`, `IP_ANALYSIS`) at the head of the list run immediately without user interaction; if every resubmitted step is backend-only, the session re-finalizes within the same request. `KYB_REGISTRY` and `KYB_KEY_PEOPLE` cannot be resubmitted directly: resubmit the relevant child KYC sessions instead and the parent business session recomputes automatically. + + Every call that changes the status fires the `status.updated` [webhook](/integration/webhooks) once the change commits and appends an activity entry (with actor attribution and your `comment`) to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and in the console activity timeline. The user-profile aggregates linked to the session's `vendor_data` update as well. Declining disables ongoing AML monitoring for the session; re-approving re-enables it (from `Resubmitted`, `Declined`, or `Kyc Expired`) when the workflow has monitoring turned on. Approving or declining a child KYC that belongs to a KYB key-people check recomputes the parent business session's status. Set `send_email: true` (with `email_address`) to notify the user: a status notice for `Approved`/`Declined`, or a resubmission email containing the verification link and per-step reasons for `Resubmitted`. To check the email first, preview it with [Preview Decision Email](/sessions-api/preview-decision-email) (`Approved`, `Declined`) or [Preview Resubmission Email](/sessions-api/preview-resubmission) (`Resubmitted`) and pass its `preview_token` as `email_preview_token`: the update then sends exactly the previewed email, or returns `409` before changing anything when the email or the session changed since the preview or the receipt is older than 15 minutes. + + Repeating `Approved` or `Declined` on a session that already has that status changes nothing and returns `200`: no webhook, no activity entry and no email. Repeating `Resubmitted` returns `400` (`The new status is the same as the current status.`). Missing or invalid credentials and missing permissions return `403`; an expired scoped API key returns `401`. + + ``PATCH /v3/session/{sessionId}/update-status/`` + + Args: + session_id: UUID of the verification session to update. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. + new_status: Target status. `Approved` and `Declined` record a final manual decision (each can also overturn the other). `Resubmitted` clears the selected steps and sends the session back to the user. Any other value returns `400`. + comment: Free-text reason for the change, stored on the session's review trail and returned in the `reviews` array of [Get Decision](#get-/v3/session/-sessionId-/decision/). For example `Duplicated user`. + nodes_to_resubmit: Workflow steps the user must redo. Only acted on when `new_status` is `Resubmitted`. For `Approved`/`Declined` the entries are still schema-validated (an invalid `feature` value returns `400`) but schema-valid entries are semantically ignored. If omitted, the server auto-selects existing OCR, Liveness, Face Match, POA, Phone, Email, AML, Database Validation, and Questionnaire attempts whose status is `Declined`, `In Review`, `Not Finished`, or `Expired` (NFC, IP analysis, age estimation, face search, and KYB document attempts are never auto-selected; non-face-match face attempts are selected as `LIVENESS`) - features the user never attempted (no recorded attempt) are NOT selected, so a session with zero attempts returns `400` ("No features found that need resubmission") even though nothing was approved; pass `nodes_to_resubmit` explicitly in that case. Steps are executed in workflow-graph order regardless of the order you send them. `KYB_REGISTRY`, `KYB_KEY_PEOPLE`, and the `KYB` alias are rejected with `400` - those parent checks recompute from their child KYC sessions. + send_email: Whether to email the user about the change. Requires `email_address`. For `Approved`/`Declined` the user receives a status notice; for `Resubmitted` the email includes the verification link and the per-step resubmission reasons. + email_address: Recipient for the notification email. **Required when `send_email` is `true`** - omitting it returns `400`. + email_language: Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`). + email_preview_token: Optional receipt from [Preview Decision Email](/sessions-api/preview-decision-email) (for `Approved` and `Declined`) or [Preview Resubmission Email](/sessions-api/preview-resubmission) (for `Resubmitted`): its `preview_token`. Valid only with `send_email: true`; without it the request returns `400`. The recipient, language, session status, sender and email (and, for a resubmission, the selected steps) must still match the preview, and the receipt expires after 15 minutes. Otherwise the request returns `409` before the session changes or an email is sent. Existing integrations may omit this field. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdateStatusResponse", + await self._client.request( + "patch", + "/v3/session/{sessionId}/update-status/", + cast_to=SessionsUpdateStatusResponse, + path_params={"sessionId": session_id}, + auth="api_key", + body={ + "new_status": new_status, + "comment": comment, + "nodes_to_resubmit": nodes_to_resubmit, + "send_email": send_email, + "email_address": email_address, + "email_language": email_language, + "email_preview_token": email_preview_token, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list( + self, + *, + session_kind: Literal["user", "business", "all"] | str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + status: str | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + search: str | NotGiven = NOT_GIVEN, + workflow_id: str | NotGiven = NOT_GIVEN, + date_from: str | _dt.date | NotGiven = NOT_GIVEN, + date_to: str | _dt.date | NotGiven = NOT_GIVEN, + country: str | NotGiven = NOT_GIVEN, + document_type: str | NotGiven = NOT_GIVEN, + document_name: str | NotGiven = NOT_GIVEN, + region: str | NotGiven = NOT_GIVEN, + didit_internal_id: str | NotGiven = NOT_GIVEN, + session_type: str | NotGiven = NOT_GIVEN, + api_service: Literal[ + "ID_VERIFICATION", + "FACE_MATCH", + "AGE_ESTIMATION", + "FACE_SEARCH", + "POA", + "AML", + "KYB_REGISTRY", + "PASSIVE_LIVENESS", + "DATABASE_VALIDATION", + "PHONE_VERIFICATION", + "EMAIL_VERIFICATION", + ] + | str + | NotGiven = NOT_GIVEN, + screened_full_name: str | NotGiven = NOT_GIVEN, + warning: str | NotGiven = NOT_GIVEN, + status_initialized_as_in_review: bool | NotGiven = NOT_GIVEN, + is_kyc_to_be_reviewed: bool | NotGiven = NOT_GIVEN, + euuid: str | NotGiven = NOT_GIVEN, + organization_name: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[UserSessionListItem | BusinessSessionListItem]: + """List verification sessions (KYC and/or KYB) with filters and pagination. + + Returns a paginated list of verification sessions for the application identified by your API key, newest first (descending `session_number`). + + **Canonical URL:** the route is `/v3/sessions/` (trailing slash). Requests to `/v3/sessions` without the slash receive a `301` redirect to the slashed URL with the query string preserved; most HTTP clients follow it transparently on GET. + + **When to use this endpoint vs webhooks.** Use this endpoint for dashboards, back-office search, reconciliation, and batch exports. Do not poll it to track the progress of individual verifications - configure [webhooks](/integration/webhooks) instead, which push `status.updated` events the moment a session changes. For one session's full result, call `GET /v3/session/{sessionId}/decision/`. + + **KYC and KYB sessions.** `session_kind` selects which kinds are listed: + - `user` (default) - User Verification (KYC) sessions only. Backward-compatible behavior. + - `business` - Business Verification (KYB) sessions only. Rows have a different, smaller shape (see the response schema). + - `all` - both kinds in one response: the KYC page and the KYB page for the same `limit`/`offset` are concatenated in `results` (KYC rows first), so a page can contain up to `2 x limit` rows. `count` is the sum of both kinds and `next`/`previous` reflect the KYC-side pagination. For strict pagination of mixed data, call `user` and `business` separately. + + Every row carries a `session_kind` discriminator (`"user"` or `"business"`). + + **Pagination.** Standard limit/offset: `limit` (default 50) and `offset` (default 0), with `next`/`previous` ready-made page URLs. For KYC rows `count` is exact. For KYB rows `count` is computed with a bounded scan and is capped at 100 - when there are more than 100 matches it stays at 100, so iterate using `next` (or by checking whether a full page came back) rather than computing total pages from `count`. + + **Filter semantics.** All filters combine with AND and apply to KYC rows. When `session_kind` is `business` or `all`, the KYB side honors the shared parameters `vendor_data`, `status`, `workflow_id`, `country` (registry-company country code), `date_from`, `date_to`, `search` (company name, registration number - type registration numbers without separators, e.g. `DPC001` not `DPC-001` - vendor_data, session number, or session UUID), `warning`, and `organization_name`; the remaining KYC-only filters are ignored for KYB rows. Comma-separated parameters (`status`, `country`, `document_type`, `region`, `workflow_id`, `document_name`, `warning`, `organization_name`, `session_type`) match any of the listed values (OR within the parameter). `vendor_data` is an exact match - use `search` for partial matching. Unknown values in list filters simply return no rows (no error); malformed typed values (`date_from`, `date_to`, `didit_internal_id`, `api_service`, `session_kind`) return `400` with a per-field error object. + + **Search.** `search` adapts to the input: a full UUID looks up `session_id`; digits (with optional `+ - . space`) match session-number prefix, document/personal numbers, phone-number prefix, or `vendor_data`; values containing `@` match email prefixes; any other text matches names (accent-insensitive) and `vendor_data`. + + No fixed per-endpoint rate limit is enforced, but keep list polling infrequent and rely on webhooks for real-time updates. + + ``GET /v3/sessions/`` + + Args: + session_kind: Which kind of sessions to list: `user` (KYC only, default), `business` (KYB only), or `all` (both, concatenated - see the description for merged-pagination semantics). Matching is case-insensitive (`BUSINESS` works); any value other than a case variant of `user`/`business`/`all` returns `400`. + limit: Maximum number of rows per page (per kind when `session_kind=all`). Defaults to 50. + offset: Number of rows to skip before the first returned row. Defaults to 0. Prefer following the `next`/`previous` URLs from the response. + status: Comma-separated list of session statuses to include, e.g. `Approved,In Review`. Values are case-sensitive; see [Verification Statuses](/integration/verification-statuses). Unknown values match nothing. + vendor_data: Exact-match filter on the `vendor_data` you sent at session creation (your own user/business identifier). For partial matches use `search` instead. + search: Smart free-text search. A full UUID matches `session_id`; numeric input matches session-number prefix, document/personal numbers, phone prefix, or `vendor_data`; input containing `@` matches email prefixes; other text matches names (accent-insensitive) and `vendor_data`. Free text containing digits additionally matches document/personal-number search fields and session-number prefixes. + workflow_id: Comma-separated workflow UUIDs (find them on the Workflows page in the Console). Malformed UUIDs in the list are ignored; if none are valid the result is empty. + date_from: Only sessions created on or after this date (UTC), format `YYYY-MM-DD`. Malformed dates return `400`. + date_to: Only sessions created up to this date (UTC), format `YYYY-MM-DD`. For KYC rows the entire day is included; for KYB rows the comparison is `created_at <= date_to` (midnight), so use the next day's date to include a full day of KYB sessions. + country: Comma-separated country filter. For KYC rows: ISO 3166-1 alpha-3 codes of the ID document's issuing state (e.g. `ESP,USA`), which may differ from nationality. For KYB rows: the registry company's country code. + document_type: Comma-separated ID-document type codes verified in the session (KYC rows only): `P` (Passport), `ID` (Identity Card), `DL` (Driver's License), `RP` (Residence Permit), `SSC` (Social Security Card), `HIC` (Health Insurance Card), `WP` (Work Permit), `TC` (Tax Card), `VISA`, `PSC` (Public Service Card), `BC` (Birth Certificate), `OTHER`. Note the response field `document_type` contains the display name, not the code. + document_name: Exact document names; multiple names can be passed comma-separated, but the value is only split on commas when it contains more than one ` - ` separator (a lone name with an embedded comma is matched whole). Names are as recognized by document verification (front or back side), e.g. `Spain - Id Card (2021)`. KYC rows only. + region: Comma-separated document region/state values (for documents issued per region, such as US driver's licenses). KYC rows only. + didit_internal_id: Filter by Didit's internal user identifier (the `didit_internal_id` response field) to list all sessions of one verified user. Must be a valid UUID; malformed values return `400`. KYC rows only. + session_type: Comma-separated session creation channels: `API`, `HOSTED`, `MIGRATED`. The special value `ALL` (or omitting the parameter) disables the filter. KYC rows only. + api_service: Filter standalone API sessions by service. Invalid values return `400`. KYC rows only. + screened_full_name: Accent-insensitive name search inside AML and database-validation screened data. When combined with `api_service=AML` or `api_service=DATABASE_VALIDATION`, the search is restricted to that feature's screened data. KYC rows only. + warning: Comma-separated warning risk codes; returns sessions that raised any of them (e.g. `POSSIBLE_FRAUD`). Risk codes are listed per feature in the [warnings documentation](/integration/webhooks). Applies to KYC rows and, with business warning codes, to KYB rows. + status_initialized_as_in_review: When `true`, only sessions whose decision initially landed in `In Review` (useful to audit manual-review volume). KYC rows only. + is_kyc_to_be_reviewed: `true` returns sessions whose document verification is flagged for manual review; `false` returns sessions with document verification explicitly not flagged. Omit to disable. KYC rows only. + euuid: Filter by the end user's euuid (UUID assigned to the verifying user, used by Console deep links). Must be a valid UUID; malformed values return a 400 field error. KYC rows only. + organization_name: Comma-separated organization names. Console-oriented; with an API key (already scoped to one application) it only matches your own organization's name. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[UserSessionListItem | BusinessSessionListItem]", + self._client.request_page( + "get", + "/v3/sessions/", + cast_to=SessionsListResponse, + item_type=UserSessionListItem | BusinessSessionListItem, + auth="api_key", + query={ + "session_kind": session_kind, + "limit": limit, + "offset": offset, + "status": status, + "vendor_data": vendor_data, + "search": search, + "workflow_id": workflow_id, + "date_from": date_from, + "date_to": date_to, + "country": country, + "document_type": document_type, + "document_name": document_name, + "region": region, + "didit_internal_id": didit_internal_id, + "session_type": session_type, + "api_service": api_service, + "screened_full_name": screened_full_name, + "warning": warning, + "status_initialized_as_in_review": status_initialized_as_in_review, + "is_kyc_to_be_reviewed": is_kyc_to_be_reviewed, + "euuid": euuid, + "organization_name": organization_name, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def share( + self, + session_id: str, + *, + for_application_id: str, + ttl_in_seconds: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsShareResponse: + """Mint a share token for a finished verification session. + + Mint a short-lived JWT that lets a specific Didit application import this finished session. Pair with `POST /v3/session/import-shared/`. + + ``POST /v3/session/{sessionId}/share/`` + + Args: + session_id: UUID of the source verification session to mint a token for. + for_application_id: UUID of the Didit application that will redeem the token. Must exist, must not be deleted, and must differ from the calling application. Find it in the Business Console under **Settings → Application**. + ttl_in_seconds: Token lifetime, in seconds. Minimum `60`, maximum `86400` (24 h). Defaults to `3600` (1 h). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsShareResponse", + await self._client.request( + "post", + "/v3/session/{sessionId}/share/", + cast_to=SessionsShareResponse, + path_params={"sessionId": session_id}, + auth="api_key", + body={"for_application_id": for_application_id, "ttl_in_seconds": ttl_in_seconds}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def import_shared( + self, + *, + share_token: str, + trust_review: bool, + workflow_id: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionDecision: + """Redeem a share token to clone a verification session into your application. + + Redeem a share token to clone a KYC or KYB session into the calling application with a fresh `session_id`. Not idempotent - a token redeems once per receiver. + + ``POST /v3/session/import-shared/`` + + Args: + share_token: JWT share token issued by [`POST /v3/session/{sessionId}/share/`](/sessions-api/share-session). + trust_review: If `true`, the cloned session keeps the source's final `status`. If `false`, it is forced into `In Review`. + workflow_id: UUID of a workflow in the calling application. Cross-application IDs are rejected with `404`. + vendor_data: Optional override for the cloned session's `vendor_data`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionDecision", + await self._client.request( + "post", + "/v3/session/import-shared/", + cast_to=SessionDecision, + auth="api_key", + body={ + "share_token": share_token, + "trust_review": trust_review, + "workflow_id": workflow_id, + "vendor_data": vendor_data, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def batch_delete( + self, + *, + session_numbers: Sequence[str] | NotGiven = NOT_GIVEN, + delete_all: bool | NotGiven = NOT_GIVEN, + retain_face_embeddings: bool | None | NotGiven = NOT_GIVEN, face_retention_days: int | NotGiven = NOT_GIVEN, face_retention_deadline: str | _dt.datetime | NotGiven = NOT_GIVEN, deletion_instruction: Literal["operational_session_delete", "privacy_erasure"] | str | NotGiven = NOT_GIVEN, - instruction_id: str | NotGiven = NOT_GIVEN, + instruction_id: str | NotGiven = NOT_GIVEN, + filter_status: str | Sequence[str] | NotGiven = NOT_GIVEN, + filter_vendor_data: str | NotGiven = NOT_GIVEN, + filter_search: str | NotGiven = NOT_GIVEN, + filter_workflow_id: str | Sequence[str] | NotGiven = NOT_GIVEN, + filter_date_from: str | _dt.date | NotGiven = NOT_GIVEN, + filter_date_to: str | _dt.date | NotGiven = NOT_GIVEN, + filter_country: str | Sequence[str] | NotGiven = NOT_GIVEN, + filter_document_type: str | Sequence[str] | NotGiven = NOT_GIVEN, + filter_document_name: str | Sequence[str] | NotGiven = NOT_GIVEN, + filter_region: str | Sequence[str] | NotGiven = NOT_GIVEN, + filter_didit_internal_id: str | NotGiven = NOT_GIVEN, + filter_session_type: str | Sequence[str] | NotGiven = NOT_GIVEN, + filter_api_service: str | NotGiven = NOT_GIVEN, + filter_screened_full_name: str | NotGiven = NOT_GIVEN, + filter_warning: str | Sequence[str] | NotGiven = NOT_GIVEN, + filter_status_initialized_as_in_review: bool | NotGiven = NOT_GIVEN, + filter_is_kyc_to_be_reviewed: bool | NotGiven = NOT_GIVEN, + filter_euuid: str | NotGiven = NOT_GIVEN, + filter_organization_name: str | Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsBatchDeleteResponse | SessionDeletionJob: + """Bulk delete KYC sessions by session_number. + + Bulk delete User Verification (KYC) sessions by their numeric `session_number`, or every KYC session in your application with `delete_all: true`. + + `session_number` is the human-friendly incrementing counter shown in the Console session list and returned as `session_number` by the create-session and decision endpoints; it is **not** the `session_id` UUID. To delete a session by UUID, or to delete a Business Verification (KYB) session, which this endpoint never touches, use `DELETE /v3/session/{sessionId}/delete/` instead. + + **Up to 20 sessions are deleted in the request, more run as a job.** The number of matching sessions decides, whether they are selected by `session_numbers` or with `delete_all`: + - Up to 20 matching sessions: they are deleted before the response, which is `200` with one entry per session in `results[]`. + - More than 20: the response is `202` with a deletion job, and the sessions are deleted in the background. Poll `GET /v3/sessions/delete/{job_id}/` until `status` is `COMPLETED` or `FAILED`. A job reports counts per outcome, never which sessions it deleted. A `delete_all` job deletes only the sessions created before the request; sessions started while it runs are not deleted. + + Each deleted session gets exactly the same treatment as the single-session delete endpoint: the session, every verification attempt it holds, its decision and extracted data, its related feature records (ID verification, face, liveness, face match, and so on) and all of its stored files are deleted, it disappears from every read endpoint, and previously issued media URLs stop resolving. Webhook deliveries still queued for a deleted session, and their retries, are dropped; webhooks already delivered are not affected. Blocklist entries are **not** removed, credits already consumed are **not** refunded, and the parent User entities are **not** deleted (use `POST /v3/users/delete/` for those); no webhook is emitted for the deletion. Deletion is permanent and irreversible. + + **Selection semantics:** + - With `delete_all: true` you can narrow the selection with the filters of `GET /v3/sessions/`, each prefixed with `filter_` (for example `filter_status`, `filter_workflow_id`, `filter_date_from`). Filters that take several values in List Sessions (`status`, `workflow_id`, `country`, `region`, `document_type`, `document_name`, `session_type`, `warning`, `organization_name`) accept a comma-separated string or an array; the others take one value. An empty value is ignored. Filters apply only together with `delete_all`, and a deletion job applies the same filters. + - An unknown `filter_*` key, or a filter value that cannot be read, rejects the whole request with `400` before anything is deleted, so a mistyped filter never widens the deletion to every session. + - Entries in `session_numbers` that do not match a live KYC session in your application (unknown numbers, already-deleted sessions, or sessions owned by another application) are **silently skipped**: they do not appear in `results` and are not counted in a job's `total`. + - Validation failures (a non-numeric entry, an empty list, neither `session_numbers` nor `delete_all` provided, or a template retention the application cannot honour) reject the **whole request** with `400` and delete nothing. + - Sessions are deleted in groups. If a group cannot be deleted (for example because a storage service is unavailable), every session in that group is reported as `face_retention_outcome: failed_retryable` and stays in place; the other groups are unaffected. Repeat the request for those sessions. A job retries such a group by itself. + - There is no documented cap on the list length; the shared write rate limit (below) is the practical bound. + + **Face biometric data:** the optional `retain_face_embeddings`, `face_retention_days`, `face_retention_deadline`, `deletion_instruction`, and `instruction_id` fields have the same meaning as on `DELETE /v3/session/{sessionId}/delete/` and apply to every matched session. Each result reports its own `face_retention_outcome`. + + **Response:** `200 OK` with `results[]` for up to 20 matching sessions, or `202 Accepted` with a deletion job for more. Every result confirms deletion except `face_retention_outcome: failed_retryable`, which reports a session that was NOT deleted and should be retried. Earlier versions of this endpoint returned `204 No Content`; update clients that assert on `204`. + + **Idempotency:** repeating the same request deletes nothing more: numbers that no longer match a session are skipped. + + **`delete_all: true` (destructive):** deletes **every** remaining KYC session in the application; `session_numbers` is ignored when set. There is no confirmation step and no undo. + + **Authentication:** API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`); this API never returns `401`. + + **Rate limit:** shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + This endpoint accepts API keys only. Business Console user Bearer tokens are rejected with `403` (unlike the single-session delete, which accepts both). + + ``POST /v3/sessions/delete/`` + + Args: + session_numbers: Numeric `session_number` values of the KYC sessions to delete, as digit-only strings (JSON numbers are also accepted and coerced). Required unless `delete_all` is `true`; an empty list returns `400`. Any non-digit entry rejects the whole request with `400` (unless `delete_all` is `true`, in which case validation is skipped and the entries are ignored). Numbers that do not match a live session in your application are silently skipped. + delete_all: When `true`, every remaining KYC session in the calling application is deleted and `session_numbers` is ignored, unless you narrow the selection with `filter_*` keys. More than 20 matching sessions run as a deletion job (`202`) that covers only the sessions created before the request. Defaults to `false`. Irreversible; there is no confirmation step. + retain_face_embeddings: Override the application's `face_retention_policy` for this deletion. `true` keeps one image-free face biometric template anchored to the session's User after the session is deleted; `false` deletes the face embedding with the session; omit or `null` to follow the application policy. Ignored for KYB sessions and for sessions without a face embedding. + face_retention_days: Finite retention duration, in days, for a retained template. Required when a template is retained unless the application already sets `face_retention_days`. The template's `expires_at` is the earliest of this duration, the application's general data-retention window, and `face_retention_deadline`. + face_retention_deadline: Optional hard expiry for a retained template. Must be in the future. + deletion_instruction: The class of instruction you are giving. `operational_session_delete` deletes the session and retains a template only when the policy or `retain_face_embeddings` says so. `privacy_erasure` purges every retained biometric template anchored to the session's User before deleting the session, cannot be overridden by the application policy, and cannot be combined with `retain_face_embeddings: true` (`400`). + instruction_id: Your durable reference for this deletion instruction (for example an erasure-ticket id). Recorded on the audit trail and on any retained template. Generated by Didit when omitted. + filter_status: Narrows a `delete_all` to the sessions that match the `status` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_vendor_data: Narrows a `delete_all` to the sessions that match the `vendor_data` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated text, so several values are not matched as alternatives. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_search: Narrows a `delete_all` to the sessions that match the `search` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated text, so several values are not matched as alternatives. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_workflow_id: Narrows a `delete_all` to the sessions that match the `workflow_id` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_date_from: Narrows a `delete_all` to the sessions that match the `date_from` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_date_to: Narrows a `delete_all` to the sessions that match the `date_to` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_country: Narrows a `delete_all` to the sessions that match the `country` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_document_type: Narrows a `delete_all` to the sessions that match the `document_type` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_document_name: Narrows a `delete_all` to the sessions that match the `document_name` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_region: Narrows a `delete_all` to the sessions that match the `region` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_didit_internal_id: Narrows a `delete_all` to the sessions that match the `didit_internal_id` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_session_type: Narrows a `delete_all` to the sessions that match the `session_type` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_api_service: Narrows a `delete_all` to the sessions that match the `api_service` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_screened_full_name: Narrows a `delete_all` to the sessions that match the `screened_full_name` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated text, so several values are not matched as alternatives. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_warning: Narrows a `delete_all` to the sessions that match the `warning` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_status_initialized_as_in_review: Narrows a `delete_all` to the sessions that match the `status_initialized_as_in_review` filter of [List Sessions](/sessions-api/list-sessions), with the same values. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_is_kyc_to_be_reviewed: Narrows a `delete_all` to the sessions that match the `is_kyc_to_be_reviewed` filter of [List Sessions](/sessions-api/list-sessions), with the same values. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_euuid: Narrows a `delete_all` to the sessions that match the `euuid` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + filter_organization_name: Narrows a `delete_all` to the sessions that match the `organization_name` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. With an API key it can only match your own organization's name, so it selects all of your sessions or none. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsBatchDeleteResponse | SessionDeletionJob", + await self._client.request( + "post", + "/v3/sessions/delete/", + cast_to={200: SessionsBatchDeleteResponse, 202: SessionDeletionJob}, + auth="api_key", + body={ + "session_numbers": session_numbers, + "delete_all": delete_all, + "retain_face_embeddings": retain_face_embeddings, + "face_retention_days": face_retention_days, + "face_retention_deadline": face_retention_deadline, + "deletion_instruction": deletion_instruction, + "instruction_id": instruction_id, + "filter_status": filter_status, + "filter_vendor_data": filter_vendor_data, + "filter_search": filter_search, + "filter_workflow_id": filter_workflow_id, + "filter_date_from": filter_date_from, + "filter_date_to": filter_date_to, + "filter_country": filter_country, + "filter_document_type": filter_document_type, + "filter_document_name": filter_document_name, + "filter_region": filter_region, + "filter_didit_internal_id": filter_didit_internal_id, + "filter_session_type": filter_session_type, + "filter_api_service": filter_api_service, + "filter_screened_full_name": filter_screened_full_name, + "filter_warning": filter_warning, + "filter_status_initialized_as_in_review": filter_status_initialized_as_in_review, + "filter_is_kyc_to_be_reviewed": filter_is_kyc_to_be_reviewed, + "filter_euuid": filter_euuid, + "filter_organization_name": filter_organization_name, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_deletion_job( + self, + job_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionDeletionJob: + """Get a bulk session deletion job. + + Progress of a bulk session deletion that `POST /v3/sessions/delete/` answered with `202` (more than 20 matching sessions, selected by `session_numbers` or with `delete_all`). Poll every few seconds while `status` is `PENDING` or `PROCESSING`; stop when it is `COMPLETED` or `FAILED`. + + The job reports how many sessions ended in each outcome and how many it has processed, never which sessions it deleted. A `FAILED` job carries the reason in `error`; the sessions it had not reached are still in place, so repeat the deletion request to delete them. + + **Authentication:** API key in the `x-api-key` header. A job is only visible to the application that created it; any other `job_id` returns `404`. Missing or invalid credentials return `403`; this API never returns `401`. + + ``GET /v3/sessions/delete/{job_id}/`` + + Args: + job_id: The `job_id` returned with the `202` from `POST /v3/sessions/delete/`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionDeletionJob", + await self._client.request( + "get", + "/v3/sessions/delete/{job_id}/", + cast_to=SessionDeletionJob, + path_params={"job_id": job_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_data( + self, + session_id: str, + *, + document_type: Literal[ + "P", "DL", "ID", "RP", "SSC", "HIC", "WP", "TC", "VISA", "PSC", "BC", "OTHER" + ] + | str + | None + | NotGiven = NOT_GIVEN, + document_subtype: str | None | NotGiven = NOT_GIVEN, + document_number: str | None | NotGiven = NOT_GIVEN, + personal_number: str | None | NotGiven = NOT_GIVEN, + date_of_birth: str | _dt.date | None | NotGiven = NOT_GIVEN, + date_of_issue: str | _dt.date | None | NotGiven = NOT_GIVEN, + expiration_date: str | _dt.date | None | NotGiven = NOT_GIVEN, + issuing_state: str | None | NotGiven = NOT_GIVEN, + first_name: str | None | NotGiven = NOT_GIVEN, + last_name: str | None | NotGiven = NOT_GIVEN, + gender: Literal["M", "F", "U"] | str | None | NotGiven = NOT_GIVEN, + address: str | None | NotGiven = NOT_GIVEN, + place_of_birth: str | None | NotGiven = NOT_GIVEN, + nationality: str | None | NotGiven = NOT_GIVEN, + marital_status: Literal["SINGLE", "MARRIED", "DIVORCED", "WIDOWED", "UNKNOWN"] + | str + | NotGiven = NOT_GIVEN, + extra_fields: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + parsed_address: SessionsUpdateDataParamsParsedAddress | None | NotGiven = NOT_GIVEN, + node_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdateDataResponse: + """Update KYC data extracted from the ID document on a verification session. + + Correct the identity fields that were extracted (OCR / NFC) from the user's ID document - misread names, dates, document numbers, and so on - after the verification has finished. Typical callers are a compliance analyst working from your own back office, or an automated reconciliation job. Every field in the body is optional: only the keys you send are written, everything else is left untouched (`PATCH` semantics). + + The session must belong to your application and be in `Approved`, `Declined`, `In Review`, or `Kyc Expired` status. Sessions in any other state (e.g. still `In Progress`) return `404` with `"No Session matches the given query."` - distinct from the `"Not found."` returned for an unknown or deleted `sessionId`. The session must also carry an ID-verification (KYC) record: sessions whose workflow never ran an ID-verification step return `404` (`"The session does not have a KYC."`). For graph workflows with multiple ID-verification steps, target a specific step with `node_id` (query parameter or body field); when omitted, the most recent KYC record is patched. + + Two derived fields are recomputed on every call: `full_name` (from `first_name` + `last_name`) and `issuing_state_name` (from `issuing_state`). If the free-text `address` changes, the server may also re-geocode it and refresh `parsed_address` (skipped for sandbox sessions). `extra_fields` and `parsed_address` are merged key-by-key rather than replaced - see the per-field descriptions for the `null`-to-delete semantics. + + Submitting at least one valid field fires the `data.updated` [webhook](/integration/webhooks) (trigger `manual_review`) - even when the submitted values equal the stored ones. When values actually change, a `KYC_DATA_UPDATED` activity entry recording the changed fields with previous and new values is appended to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and the console activity timeline. The session's status and decision are **not** re-evaluated - use [Update Status](#patch-/v3/session/-sessionId-/update-status/) to change the outcome. An empty JSON body (`{}`) is accepted and simply returns the current KYC data without firing the webhook. + + Accepts an API key or a console user token; either way the credential needs the `write:sessions` permission. Authentication and permission failures both return `403` - this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. This endpoint has a dedicated rate limit of 10 requests per minute per API key (invalid requests count too); the shared write budget of 300 requests per minute also applies. + + ``PATCH /v3/session/{sessionId}/update-data/`` + + Args: + session_id: UUID of the User Verification (KYC) session whose extracted document data you are correcting. Business (KYB) session IDs are not valid here - for company data use [Update KYB Company Data](#patch-/v3/session/-sessionId-/kyb/-companyUuid-/update-data/). + document_type: Document type code. Accepts the short codes listed in the enum, plus six long-form aliases normalized server-side before validation: `Passport` → `P`, `Identity Card` → `ID`, `Driver's License` → `DL`, `Residence Permit` → `RP`, `Health Insurance Card` → `HIC`, `Tax Card` → `TC`. Any other value (including `PASSPORT` in caps) returns `400`. + document_subtype: Document subtype from the document registry, including the region prefix when the registry distinguishes regional variants. + document_number: Document number as printed on the ID. + personal_number: Personal / national identification number where the document carries one (e.g. Spanish DNI on a passport). + date_of_birth: Date of birth in `YYYY-MM-DD`. Any other format returns `400`. + date_of_issue: Issue date of the document in `YYYY-MM-DD`. + expiration_date: Expiration date of the document in `YYYY-MM-DD`. + issuing_state: Issuing country as an ISO 3166-1 alpha-3 code. Case-insensitive (`esp` is stored as `ESP`); the human-readable `issuing_state_name` shown in [Get Decision](#get-/v3/session/-sessionId-/decision/) is recomputed from this value. Invalid codes return `400`. + first_name: Given name(s). The stored `full_name` is recomputed as `first_name + ' ' + last_name` on every call to this endpoint. + last_name: Family name(s). Also feeds the recomputed `full_name`. + gender: Sex as printed on the document: `M` (male), `F` (female), or `U` (unknown). Any other value - including `X` - returns `400`. + address: Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it synchronously before responding - the 200 body already reflects the refreshed `parsed_address`. The re-geocode also requires `issuing_state` to be present on the KYC record. + place_of_birth: Place of birth as printed on the document. + nationality: Nationality as printed on the document. Free text up to 32 characters - typically an ISO 3166-1 alpha-3 code (e.g. `ESP`), but not validated as one. + marital_status: Marital status where the document carries it. Any other value returns `400`. + extra_fields: Per-key overrides for document-specific extra fields (e.g. `blood_group`, `dl_categories`, `mrz_string`). Keys are merged into the existing override map: send a key with a value to set it, or with `null` / `""` to remove that key from the merged view. Send the whole field as `null` to clear all previously applied overrides. Must be a JSON object, otherwise `400`. The response always returns the merged view (OCR-extracted values with your overrides applied). + parsed_address: Structured address. Only these nested keys are accepted: `address_type`, `city`, `region`, `street_1`, `street_2`, `postal_code`, `country` - any other key returns `400`. Per-key semantics: a value sets the key, `null` removes it. `formatted_address` is recomputed server-side by joining `street_1`, `street_2`, `city`, `region`, `postal_code`, and `country` and is returned in the response - you cannot set it directly. + node_id: Workflow graph node whose KYC record to patch, for graph workflows with multiple ID-verification steps (also accepted as a `node_id` body field). When omitted, the most recent KYC record on the session is patched. If no KYC exists for the given node, the call returns `404` (`"The session does not have a KYC for node ''."`). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdateDataResponse", + await self._client.request( + "patch", + "/v3/session/{sessionId}/update-data/", + cast_to=SessionsUpdateDataResponse, + path_params={"sessionId": session_id}, + auth="api_key", + query={"node_id": node_id}, + body={ + "document_type": document_type, + "document_subtype": document_subtype, + "document_number": document_number, + "personal_number": personal_number, + "date_of_birth": date_of_birth, + "date_of_issue": date_of_issue, + "expiration_date": expiration_date, + "issuing_state": issuing_state, + "first_name": first_name, + "last_name": last_name, + "gender": gender, + "address": address, + "place_of_birth": place_of_birth, + "nationality": nationality, + "marital_status": marital_status, + "extra_fields": extra_fields, + "parsed_address": parsed_address, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_feature_status( + self, + session_id: str, + node_id: str, + *, + new_status: Literal["Approved", "Declined", "In Review"] | str, + comment: str | NotGiven = NOT_GIVEN, + feature_type: Literal[ + "ID_VERIFICATION", + "OCR", + "OCR_BACK", + "NFC", + "LIVENESS", + "FACE", + "AGE_ESTIMATION", + "AGE_VERIFICATION", + "FACE_SEARCH", + "FACE_MATCH", + "IP_ANALYSIS", + "AML", + "PHONE", + "PHONE_VERIFICATION", + "EMAIL", + "EMAIL_VERIFICATION", + "POA", + "PROOF_OF_ADDRESS", + "DATABASE_VALIDATION", + "DATA_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + "BANK_VERIFICATION", + "GEOLOCATION", + ] + | str + | NotGiven = NOT_GIVEN, + defer_until_flow_finishes: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdateFeatureStatusResponse: + """Update an individual feature (step) status. + + Set the status of a single verification feature (step) within a session. Target the feature by its workflow graph `nodeId`. On non-graph workflows, whose steps have no node, use `default` and also pass `feature_type` to say which step you mean. + + Works for both User Verification (KYC) and Business Verification (KYB) sessions and every step type: ID/OCR, Liveness, Face Match, NFC, AML, Proof of Address, Phone, Email, Database Validation, Questionnaire, and the KYB registry / documents / key-people checks. This endpoint is **not** available for standalone API sessions (`session_type = API`). + + `new_status` must be one of `Approved`, `Declined`, or `In Review`, and must differ from the feature's current status. + + **When the override applies.** Without `defer_until_flow_finishes` (or with `false`) the override applies at once, as it always did; when the person is still in the flow it is recorded as `forced`. With `defer_until_flow_finishes: true` while the person is still in the flow (the session is `Not Started`, `In Progress` or `Resubmitted` and has not finished), the override is stored as `pending_until_flow_finishes` and applies when the flow finishes, after the flow reaches its own decision. On a session whose flow already ended it applies at once either way. A waiting override is cancelled instead when the person redoes its step before finishing, when a newer override on the same check replaces it, when the session leaves the flow unfinished (abandoned, expired, or decided mid-flow), or when you cancel it with `POST /v3/session/{sessionId}/features/overrides/{overrideId}/cancel/`. List a session's overrides with `GET /v3/session/{sessionId}/features/overrides/`. + + **Effect on the session.** A session that already has a decision keeps it: only the check changes. When the session has no decision yet and its workflow is waiting on exactly this step, applying the override completes the step: the workflow moves on, later steps run, and a status node can set the session decision and fire `status.updated`. + + **Webhooks and activity.** A `data.updated` webhook fires when the override applies (at once, or when the flow finishes), never while it waits or when it is cancelled. Every override is recorded in the session's activity as `FEATURE_OVERRIDE_PENDING`, `FEATURE_OVERRIDE_APPLIED` or `FEATURE_OVERRIDE_CANCELLED`, with who made it; the request's entry carries your `comment`. + + Authenticate with your API key (`x-api-key`) or a user authorization header carrying the `write:sessions` privilege; a scoped API key needs write access to decisions. + + ``PATCH /v3/session/{sessionId}/features/{nodeId}/update-status/`` + + Args: + session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. + node_id: Workflow graph node ID of the feature to update (for example `feature_ocr_1`). Use the literal value `default` for non-graph (single-step) workflows where the feature has no node ID. + new_status: Target status for this feature. Must differ from the current value. + comment: Optional free-text note recorded on the session's review trail for this feature change. + feature_type: Which kind of step to target. Needed on non-graph workflows (`nodeId` = `default`), where every step shares no node; optional otherwise. An unknown value returns `400`. + defer_until_flow_finishes: While the person is still in the flow, store the override as `pending_until_flow_finishes` and apply it when the flow finishes, instead of at once. Omitted or `false`, the override applies at once. On a session whose flow already ended it applies at once either way. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdateFeatureStatusResponse", + await self._client.request( + "patch", + "/v3/session/{sessionId}/features/{nodeId}/update-status/", + cast_to=SessionsUpdateFeatureStatusResponse, + path_params={"sessionId": session_id, "nodeId": node_id}, + auth="api_key", + body={ + "new_status": new_status, + "comment": comment, + "feature_type": feature_type, + "defer_until_flow_finishes": defer_until_flow_finishes, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_aml_hit_status( + self, + session_id: str, + *, + hit_id: str, + review_status: Literal["Unreviewed", "Confirmed Match", "False Positive", "Inconclusive"] + | str, + node_id: str | NotGiven = NOT_GIVEN, + node_id_query: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdateAmlHitStatusResponse: + """Update an AML hit review status. + + Set the review status of a **single AML hit** (a potential sanction / PEP / watchlist / adverse-media match) returned by AML screening, without changing the overall session decision. Identify the hit by its `hit_id` (the `id` of an entry in the AML check's `hits` array - see the session decision). + + `review_status` is one of `Unreviewed`, `Confirmed Match`, `False Positive`, or `Inconclusive`. Reviewer decisions are preserved across ongoing-monitoring updates, so a hit you mark `False Positive` stays that way when the provider re-screens. + + A successful call fires a `data.updated` webhook and appends an activity entry (with actor attribution) to the session's review trail. For graph workflows with multiple AML checks, pass `node_id` to target a specific check. + + Authenticate with your API key (`x-api-key`) or a user authorization header - either must carry the `write:sessions` privilege. + + ``PATCH /v3/session/{sessionId}/update-aml-hit-status/`` + + Args: + session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. + hit_id: The `id` of the hit in the AML check's `hits` array. + review_status: The new review status for the hit. + node_id: Optional graph node ID. Use it (here or as a query parameter) when the session has multiple AML checks. + node_id_query: Workflow graph node ID. Required only when the session has multiple AML checks (one per node); omit it for single-AML sessions. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdateAmlHitStatusResponse", + await self._client.request( + "patch", + "/v3/session/{sessionId}/update-aml-hit-status/", + cast_to=SessionsUpdateAmlHitStatusResponse, + path_params={"sessionId": session_id}, + auth="api_key", + query={"node_id": node_id_query}, + body={"hit_id": hit_id, "review_status": review_status, "node_id": node_id}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def bulk_update_aml_hit_status( + self, + session_id: str, + *, + hit_updates: Sequence[SessionsBulkUpdateAmlHitStatusParamsHitUpdates], + node_id: str | NotGiven = NOT_GIVEN, + node_id_query: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsBulkUpdateAmlHitStatusResponse: + """Bulk update AML hit review statuses. + + Update the review status of **multiple AML hits** in one call. Provide `hit_updates`, a list of `{ hit_id, review_status }` pairs. `review_status` is one of `Unreviewed`, `Confirmed Match`, `False Positive`, or `Inconclusive`. + + The update is all-or-nothing: if any `hit_id` is not found in the AML check, the request returns `404` and no hit is changed. A successful call fires a single `data.updated` webhook and records one activity entry covering all changes. For graph workflows with multiple AML checks, pass `node_id` to target a specific check. + + Authenticate with your API key (`x-api-key`) or a user authorization header - either must carry the `write:sessions` privilege. + + ``PATCH /v3/session/{sessionId}/bulk-update-aml-hit-status/`` + + Args: + session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. + hit_updates: Non-empty list of hit updates. + node_id: Optional graph node ID. Use it (here or as a query parameter) when the session has multiple AML checks. + node_id_query: Workflow graph node ID. Required only when the session has multiple AML checks (one per node); omit it for single-AML sessions. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsBulkUpdateAmlHitStatusResponse", + await self._client.request( + "patch", + "/v3/session/{sessionId}/bulk-update-aml-hit-status/", + cast_to=SessionsBulkUpdateAmlHitStatusResponse, + path_params={"sessionId": session_id}, + auth="api_key", + query={"node_id": node_id_query}, + body={"hit_updates": hit_updates, "node_id": node_id}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_poa_data( + self, + session_id: str, + *, + issuing_state: str | None | NotGiven = NOT_GIVEN, + document_type: Literal[ + "UTILITY_BILL", + "BANK_STATEMENT", + "GOVERNMENT_ISSUED_DOCUMENT", + "OTHER_POA_DOCUMENT", + "UNKNOWN", + ] + | str + | NotGiven = NOT_GIVEN, + document_language: str | None | NotGiven = NOT_GIVEN, + issuer: str | None | NotGiven = NOT_GIVEN, + issue_date: str | _dt.date | None | NotGiven = NOT_GIVEN, + poa_address: str | None | NotGiven = NOT_GIVEN, + name_on_document: str | None | NotGiven = NOT_GIVEN, + extra_fields: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + poa_parsed_address: SessionsUpdatePoaDataParamsPoaParsedAddress + | None + | NotGiven = NOT_GIVEN, + node_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdatePoaDataResponse: + """Update Proof of Address data extracted from the POA document on a verification session. + + Correct the fields extracted from the user's Proof of Address document - utility bill, bank statement, or other address evidence - after the verification has finished: issuer, dates, the printed name, the address itself, and banking details. Every field in the body is optional: only the keys you send are written, everything else is left untouched (`PATCH` semantics). + + The session must belong to your application and be in `Approved`, `Declined`, `In Review`, or `Kyc Expired` status. Sessions in any other state (e.g. still `In Progress`) return `404` with `"No Session matches the given query."` - distinct from the `"Not found."` returned for an unknown or deleted `sessionId`. The session must also carry a POA record: sessions whose workflow never ran a Proof of Address step return `404` (`"The session does not have a POA."`). For graph workflows with multiple POA steps, target a specific step with `node_id` (query parameter or body field); when omitted, the most recent POA record is patched. + + `extra_fields` is merged key-by-key: nine known keys (`bank_iban`, `additional_names`, …) write through to dedicated POA columns, any other key is kept as a custom extra, and `null` / `""` deletes a key. `poa_parsed_address` accepts only the seven documented keys and recomputes `formatted_address` / `poa_formatted_address` after every edit. If the free-text `poa_address` changes, the server may also re-geocode it and refresh the structured address (skipped for sandbox sessions). + + Unlike the KYC variant of this endpoint, the `data.updated` [webhook](/integration/webhooks) (trigger `manual_review`) fires only when at least one stored value **actually changed** - re-sending identical values is a silent no-op. When values change, a `POA_DATA_UPDATED` activity entry recording the changed fields with previous and new values is appended to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and the console activity timeline. The session's status and decision are **not** re-evaluated - use [Update Status](#patch-/v3/session/-sessionId-/update-status/) to change the outcome. + + Accepts an API key or a console user token; either way the credential needs the `write:sessions` permission. Authentication and permission failures both return `403` - this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. This endpoint has a dedicated rate limit of 10 requests per minute per API key (invalid requests count too); the shared write budget of 300 requests per minute also applies. + + An empty JSON body `{}` is a no-op: it returns `200` echoing the current POA record without firing the webhook (`has_changes` stays false). + + ``PATCH /v3/session/{sessionId}/update-poa-data/`` + + Args: + session_id: UUID of the User Verification (KYC) session whose attached POA record you are correcting. + issuing_state: Country that issued the POA document, ISO 3166-1 alpha-3. Case-insensitive (normalized to uppercase); invalid codes return `400`. + document_type: POA document category. Values are uppercase - lowercase variants like `utility_bill` return `400`. + document_language: Language of the document, typically an ISO 639-1 code (e.g. `es`). Free text - not validated as a language code. + issuer: Organization that issued the document (utility company, bank, government agency). + issue_date: Issue date of the POA document in `YYYY-MM-DD`. Any other format returns `400`. + poa_address: Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it in the background of the same request and may overwrite `poa_parsed_address` / `poa_formatted_address` with the geocoder result. The re-geocode also requires `issuing_state` to be present on the POA record. + name_on_document: Recipient name as printed on the document. + extra_fields: Document-specific extras, merged key-by-key. Nine keys map to dedicated POA columns: `bank_account_number`, `bank_iban`, `bank_sort_code`, `bank_routing_number`, `bank_swift_bic`, `bank_branch_name`, `bank_branch_address`, `document_phone_number`, and `additional_names` (must be a list of strings or `null`, otherwise `400`). Any other key is stored as a custom extra field. Per-key semantics: a value sets the key; `null` or `""` deletes it. Must be a JSON object, otherwise `400`. + poa_parsed_address: Structured address. Only these nested keys are accepted: `address_type`, `city`, `region`, `street_1`, `street_2`, `postal_code`, `country` - any other key returns `400`. Per-key semantics: a value sets the key, `null` removes it. `formatted_address` (and the top-level `poa_formatted_address`) is recomputed server-side by joining `street_1`, `street_2`, `city`, `region`, `postal_code`, and `country` - you cannot set it directly. + node_id: Workflow graph node whose POA record to patch, for graph workflows with multiple Proof of Address steps (also accepted as a `node_id` body field). When omitted, the most recent POA record on the session is patched. If no POA exists for the given node, the call returns `404` (`"The session does not have a POA for node ''."`). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdatePoaDataResponse", + await self._client.request( + "patch", + "/v3/session/{sessionId}/update-poa-data/", + cast_to=SessionsUpdatePoaDataResponse, + path_params={"sessionId": session_id}, + auth="api_key", + query={"node_id": node_id}, + body={ + "issuing_state": issuing_state, + "document_type": document_type, + "document_language": document_language, + "issuer": issuer, + "issue_date": issue_date, + "poa_address": poa_address, + "name_on_document": name_on_document, + "extra_fields": extra_fields, + "poa_parsed_address": poa_parsed_address, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_kyb_company_data( + self, + session_id: str, + company_uuid: str, + *, + company_name: str | NotGiven = NOT_GIVEN, + registration_number: str | NotGiven = NOT_GIVEN, + country_code: str | NotGiven = NOT_GIVEN, + region: str | None | NotGiven = NOT_GIVEN, + incorporation_date: str | _dt.date | NotGiven = NOT_GIVEN, + registration_date: str | _dt.date | None | NotGiven = NOT_GIVEN, + dissolution_date: str | _dt.date | None | NotGiven = NOT_GIVEN, + tax_number: str | NotGiven = NOT_GIVEN, + legal_address: str | NotGiven = NOT_GIVEN, + company_type: str | NotGiven = NOT_GIVEN, + registry_status: Literal[ + "active", + "dissolved", + "deregistered", + "see full details", + "authorised", + "appointed representative", + "unauthorised", + "inactive", + "no longer authorised", + "closed", + "struck off", + ] + | str + | None + | NotGiven = NOT_GIVEN, + verification_status: Literal["verified", "failed", "unknown"] + | str + | None + | NotGiven = NOT_GIVEN, + alternative_names: str | NotGiven = NOT_GIVEN, + nature_of_business: str | NotGiven = NOT_GIVEN, + registered_capital: str | NotGiven = NOT_GIVEN, + registered_capital_amount: float | None | NotGiven = NOT_GIVEN, + registered_capital_currency: str | None | NotGiven = NOT_GIVEN, + website: str | NotGiven = NOT_GIVEN, + email: str | NotGiven = NOT_GIVEN, + phone: str | NotGiven = NOT_GIVEN, + legal_entity_identifier: str | NotGiven = NOT_GIVEN, + location_of_registration: str | NotGiven = NOT_GIVEN, + vat_number: str | NotGiven = NOT_GIVEN, + control_scheme: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsBatchDeleteResponse | SessionsBatchDeleteResponse202: - """Bulk delete KYC sessions by session_number. + ) -> SessionsUpdateKybCompanyDataResponse: + """Update registry-extracted company data on a Business Verification (KYB) session. - Bulk delete User Verification (KYC) sessions by their numeric `session_number`, or every KYC session in your application with `delete_all: true`. + Correct the registry-extracted company data on a Business Verification (KYB) session - company name, registration number, addresses, status, contact details - identified by `companyUuid`. Typical callers are a compliance analyst fixing a registry misread, or your back office syncing corrected company data. Every field in the body is optional (`PATCH` semantics); an empty JSON body (`{}`) is a no-op that simply echoes the current company record. - `session_number` is the human-friendly incrementing counter shown in the Console session list and returned as `session_number` by the create-session and decision endpoints; it is **not** the `session_id` UUID. To delete a session by UUID, or to delete a Business Verification (KYB) session, which this endpoint never touches, use `DELETE /v3/session/{sessionId}/delete/` instead. + Edits are written in two places at once: fields with a canonical company column (name, registration number, country, incorporation date, tax number, type, statuses, capital fields (`registered_capital`, `registered_capital_amount`, `registered_capital_currency`), alternative names, nature of business, website, email, phone, LEI, location, and `legal_address` → `registered_address`) update that column, and **every** submitted key is also merged into the `registry_data` snapshot (dates as ISO strings) so the "extracted from registry" view reflects the correction. Fields without a column - `region`, `registration_date`, `dissolution_date`, `control_scheme` - land in `registry_data` only. `user_provided_data` (what the end user confirmed in the hosted flow) is never touched, and `last_console_edit_at` is stamped on every non-empty PATCH. - **Up to 20 sessions are deleted in the request, more run as a job.** The number of matching sessions decides, whether they are selected by `session_numbers` or with `delete_all`: - - Up to 20 matching sessions: they are deleted before the response, which is `200` with one entry per session in `results[]`. - - More than 20: the response is `202` with a deletion job, and the sessions are deleted in the background. Poll `GET /v3/sessions/delete/{job_id}/` until `status` is `COMPLETED` or `FAILED`. A job reports counts per outcome, never which sessions it deleted. A `delete_all` job deletes only the sessions created before the request; sessions started while it runs are not deleted. + Unlike the KYC/POA update endpoints, there is **no session-status gate**: the company can be edited at any point in the business session's lifecycle. The company must belong to the session in the path (`404` with `"KYB company does not belong to the given session."` otherwise) and to your application. Every non-empty PATCH fires the `data.updated` [webhook](/integration/webhooks) on the business session (trigger `console_registry_edit`) - even when the submitted values equal the stored ones. The session's status and decision are **not** re-evaluated, and no review/audit entry is recorded. - Each deleted session gets exactly the same treatment as the single-session delete endpoint: the session, every verification attempt it holds, its decision and extracted data, its related feature records (ID verification, face, liveness, face match, and so on) and all of its stored files are deleted, it disappears from every read endpoint, and previously issued media URLs stop resolving. Webhook deliveries still queued for a deleted session, and their retries, are dropped; webhooks already delivered are not affected. Blocklist entries are **not** removed, credits already consumed are **not** refunded, and the parent User entities are **not** deleted (use `POST /v3/users/delete/` for those); no webhook is emitted for the deletion. Deletion is permanent and irreversible. + Accepts an API key or a console user token; either way the credential needs the `write:businesses` permission. Authentication and permission failures both return `403` - this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. The shared write budget of 300 requests per minute per API key applies. - **Selection semantics:** - - Entries in `session_numbers` that do not match a live KYC session in your application (unknown numbers, already-deleted sessions, or sessions owned by another application) are **silently skipped**: they do not appear in `results` and are not counted in a job's `total`. - - Validation failures (a non-numeric entry, an empty list, neither `session_numbers` nor `delete_all` provided, or a template retention the application cannot honour) reject the **whole request** with `400` and delete nothing. - - Sessions are deleted in groups. If a group cannot be deleted (for example because a storage service is unavailable), every session in that group is reported as `face_retention_outcome: failed_retryable` and stays in place; the other groups are unaffected. Repeat the request for those sessions. A job retries such a group by itself. - - There is no documented cap on the list length; the shared write rate limit (below) is the practical bound. + For User Verification sessions, the KYC counterpart is `PATCH /v3/session/{sessionId}/update-data/`; to change the session decision itself use `PATCH /v3/session/{sessionId}/update-status/`. - **Face biometric data:** the optional `retain_face_embeddings`, `face_retention_days`, `face_retention_deadline`, `deletion_instruction`, and `instruction_id` fields have the same meaning as on `DELETE /v3/session/{sessionId}/delete/` and apply to every matched session. Each result reports its own `face_retention_outcome`. + ``PATCH /v3/session/{sessionId}/kyb/{companyUuid}/update-data/`` - **Response:** `200 OK` with `results[]` for up to 20 matching sessions, or `202 Accepted` with a deletion job for more. Every result confirms deletion except `face_retention_outcome: failed_retryable`, which reports a session that was NOT deleted and should be retried. Earlier versions of this endpoint returned `204 No Content`; update clients that assert on `204`. + Args: + session_id: UUID of the parent Business Verification (KYB) session. + company_uuid: UUID of the company record to update (`registry_checks[].company.uuid` on the business session decision). Must belong to the session referenced by `sessionId`. + company_name: Legal name of the company. Written to the canonical column and the registry snapshot. + registration_number: Official registration number with the company registry. + country_code: Country of incorporation, ISO 3166-1 alpha-2. Case-insensitive (`us` is stored as `US`). Alpha-3 codes fail the 2-character limit with `400`. + region: Subdivision / region tag where the registry has one (e.g. US state). Stored in the registry snapshot only - it has no canonical column. + incorporation_date: Incorporation date in `YYYY-MM-DD`. Optional like every other field - the `PATCH` is partial. Any other date format returns `400`. + registration_date: Registration date where the registry distinguishes it from incorporation. Stored in the registry snapshot only. + dissolution_date: Dissolution date for companies that are no longer active. Stored in the registry snapshot only. + tax_number: Tax identification number. + legal_address: Legal / registered address. Aliased server-side to the canonical `registered_address` column - the response returns it under `registered_address`, while the registry snapshot keeps the `legal_address` key you sent. + company_type: Legal entity type (e.g. `Limited Liability Company`, `PLC`, `GmbH`). + registry_status: Registry-reported company status. Lowercase, some values contain spaces (e.g. `struck off`, `no longer authorised`). Any other value returns `400`. + verification_status: Outcome of the registry verification. Any other value returns `400`. + alternative_names: Trade names, former legal names, DBA names (free text). + nature_of_business: Short description of the company's activity. + registered_capital: Human-readable registered capital (e.g. `USD 50000`). Use `registered_capital_amount` + `registered_capital_currency` for structured values, or this field for free-form text. + registered_capital_amount: Numeric registered-capital amount (up to 20 digits, 2 decimal places). Sets the `registered_capital_amount` column and is mirrored into `registry_data` as a decimal string; the response echoes it as a decimal string (e.g. `"1500000.50"`). + registered_capital_currency: ISO 4217 currency code for `registered_capital_amount`. Case-insensitive (`usd` is stored as `USD`); values that are not exactly 3 letters return `400` with the ISO 4217 message. Sets the canonical `registered_capital_currency` column and is mirrored into `registry_data`. + website: Company website. Bare domains are accepted and normalized with an `https://` prefix (`docsprobe.example.com` is stored as `https://docsprobe.example.com`); values that still fail URL validation return `400`. + email: Contact email on file with the registry. + phone: Contact phone. Must match `+`, digits, spaces, dashes, dots, or parentheses, with 7-15 digits total - otherwise `400`. + legal_entity_identifier: LEI, where the company has one. + location_of_registration: City / subdivision of the registry of record. + vat_number: EU VAT number. Merged into the `registry_data` snapshot only - editing it here does not re-run VAT and tax ID validation, so `vat_validation_status` and the other `vat_*` result fields are unchanged. + control_scheme: Control / governance scheme when the registry exposes it. Stored in the registry snapshot only. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdateKybCompanyDataResponse", + await self._client.request( + "patch", + "/v3/session/{sessionId}/kyb/{companyUuid}/update-data/", + cast_to=SessionsUpdateKybCompanyDataResponse, + path_params={"sessionId": session_id, "companyUuid": company_uuid}, + auth="api_key", + body={ + "company_name": company_name, + "registration_number": registration_number, + "country_code": country_code, + "region": region, + "incorporation_date": incorporation_date, + "registration_date": registration_date, + "dissolution_date": dissolution_date, + "tax_number": tax_number, + "legal_address": legal_address, + "company_type": company_type, + "registry_status": registry_status, + "verification_status": verification_status, + "alternative_names": alternative_names, + "nature_of_business": nature_of_business, + "registered_capital": registered_capital, + "registered_capital_amount": registered_capital_amount, + "registered_capital_currency": registered_capital_currency, + "website": website, + "email": email, + "phone": phone, + "legal_entity_identifier": legal_entity_identifier, + "location_of_registration": location_of_registration, + "vat_number": vat_number, + "control_scheme": control_scheme, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) - **Idempotency:** repeating the same request deletes nothing more: numbers that no longer match a session are skipped. + async def update_kyb_party( + self, + session_id: str, + party_uuid: str, + *, + is_skipped: bool, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdateKybPartyResponse: + """Skip (or re-require) a Key People party on a Business Verification (KYB) session. + + Exempt a Key People party from verification, or put it back in scope. This is the operator escape hatch for a party that can never finish its own check - most often because the individual KYC session handed to a UBO or director was deleted, which freezes that child's status and leaves the KYB parked in `Awaiting User` with nothing left that could move it. + + Setting `is_skipped: true` drops the party out of the Key People aggregation and immediately recomputes the `KYBKeyPeopleCheck` and the parent session status, so the business session can settle and then be approved or declined. Sending `is_skipped: false` puts the party back in scope and the check goes back to waiting on it. + + Unlike the applicant-facing skip in the hosted Key People screen, this endpoint is **not** gated by the role's `allow_skip` setting - that flag limits what the applicant may decide, not what your analyst may override. `requires_verification` is deliberately left untouched, so an overridden party reads as `requires_verification: true` **and** `is_skipped: true` and the record still shows what the rules asked for. + + The party must belong to the session in the path (`404` with `"Party does not belong to the given session."` otherwise) and to your application. A PATCH that actually changes the flag fires the `data.updated` [webhook](/integration/webhooks) on the business session with trigger `console_key_people_skip`; a PATCH that sets the flag to its current value is a no-op that echoes the party back. + + Accepts an API key or a console user token; either way the credential needs the `write:businesses` permission. + + ``PATCH /v3/session/{sessionId}/kyb/parties/{partyUuid}/`` + + Args: + session_id: UUID of the parent Business Verification (KYB) session. + party_uuid: UUID of the Key People party to update (`key_people_checks[].submitted.parties[].uuid` on the business session decision). Must belong to the session referenced by `sessionId`. + is_skipped: `true` exempts the party from verification so the Key People check can settle without it; `false` puts it back in scope. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdateKybPartyResponse", + await self._client.request( + "patch", + "/v3/session/{sessionId}/kyb/parties/{partyUuid}/", + cast_to=SessionsUpdateKybPartyResponse, + path_params={"sessionId": session_id, "partyUuid": party_uuid}, + auth="api_key", + body={"is_skipped": is_skipped}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_networks( + self, + session_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> EntityNetworkMembershipList: + """List networks for a session. + + List the networks the session's vendor user belongs to. Resolves through the session's vendor_data-linked user. + + ``GET /v3/organization/{organization_id}/application/{application_id}/sessions/{session_id}/networks/`` + + Args: + session_id: Session UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "EntityNetworkMembershipList", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/sessions/{session_id}/networks/", + cast_to=EntityNetworkMembershipList, + path_params={"session_id": session_id}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def preview_resubmission( + self, + session_id: str, + *, + nodes_to_resubmit: Sequence[SessionsPreviewResubmissionParamsNodesToResubmit] + | NotGiven = NOT_GIVEN, + email_address: str | NotGiven = NOT_GIVEN, + email_language: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsPreviewResubmissionResponse: + """Preview a resubmission email. - **`delete_all: true` (destructive):** deletes **every** remaining KYC session in the application; `session_numbers` is ignored when set. There is no confirmation step and no undo. + Render the email that would accompany a KYC or KYB resubmission without changing the session, resetting checks, extending its expiry, running checks or sending mail. Requires the `write:sessions` privilege (a scoped API key needs write access to decisions) and a session in `Approved`, `Declined`, `In Review`, `Kyc Expired` or `Abandoned` status. The server validates the selected workflow steps and renders its fixed translated template with the applicable verified white-label sender and branding. The request cannot set a subject, message body or action URL. - **Authentication:** API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`); this API never returns `401`. + `email_address` is optional. Without it, the email goes to the person's own address: the one they verified in an email verification step of the flow, else the one given in `contact_details` when the session was created. An address you pass wins (`recipient_source: request`). When no address is known and you pass none, `recipient` and `preview_token` are empty strings and no email can be sent. - **Rate limit:** shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + The response includes the recipient and where it comes from, the sender, subject, HTML, language, resolved steps, whether confirming sends the email (`will_be_sent`, `not_sent_reason`) and a signed `preview_token`. `requires_user_action` is `false` when every selected step runs without the person: the resubmission then finishes on its own and no email goes out (`not_sent_reason: no_user_action`). - This endpoint accepts API keys only. Business Console user Bearer tokens are rejected with `403` (unlike the single-session delete, which accepts both). + This preview does not reserve session state or authorize delivery. After reviewing it, call [Update Session Status](/sessions-api/update-status) with `new_status: Resubmitted`, the same `nodes_to_resubmit` and `email_language` you previewed, `send_email: true`, `email_address` set to `recipient` and `email_preview_token` set to `preview_token`. The receipt expires after 15 minutes. When a receipt is supplied, changed inputs, message content, sender or session state return `409` before any mutation or send. Existing integrations may omit the receipt. Regenerate the preview after a conflict. Treat the response as sensitive verification content; responses use `Cache-Control: no-store`. Render HTML in a sandboxed frame, never directly in your application DOM. - ``POST /v3/sessions/delete/`` + ``POST /v3/session/{sessionId}/resubmission-preview/`` Args: - session_numbers: Numeric `session_number` values of the KYC sessions to delete, as digit-only strings (JSON numbers are also accepted and coerced). Required unless `delete_all` is `true`; an empty list returns `400`. Any non-digit entry rejects the whole request with `400` (unless `delete_all` is `true`, in which case validation is skipped and the entries are ignored). Numbers that do not match a live session in your application are silently skipped. - delete_all: When `true`, every remaining KYC session in the calling application is deleted and `session_numbers` is ignored. More than 20 matching sessions run as a deletion job (`202`) that covers only the sessions created before the request. Defaults to `false`. Irreversible; there is no confirmation step. - retain_face_embeddings: Override the application's `face_retention_policy` for this deletion. `true` keeps one image-free face biometric template anchored to the session's User after the session is deleted; `false` deletes the face embedding with the session; omit or `null` to follow the application policy. Ignored for KYB sessions and for sessions without a face embedding. - face_retention_days: Finite retention duration, in days, for a retained template. Required when a template is retained unless the application already sets `face_retention_days`. The template's `expires_at` is the earliest of this duration, the application's general data-retention window, and `face_retention_deadline`. - face_retention_deadline: Optional hard expiry for a retained template. Must be in the future. - deletion_instruction: The class of instruction you are giving. `operational_session_delete` deletes the session and retains a template only when the policy or `retain_face_embeddings` says so. `privacy_erasure` purges every retained biometric template anchored to the session's User before deleting the session, cannot be overridden by the application policy, and cannot be combined with `retain_face_embeddings: true` (`400`). - instruction_id: Your durable reference for this deletion instruction (for example an erasure-ticket id). Recorded on the audit trail and on any retained template. Generated by Didit when omitted. + session_id: UUID of the verification session to update. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. + nodes_to_resubmit: Steps to preview, using the same validation, selection and workflow ordering as a resubmission. Omit to auto-select recorded attempts needing resubmission. + email_address: Optional. Send the email to this address instead of the person's own. Without it, the recipient is the address the person verified in the flow, else the one given in `contact_details`. No email is sent by this call. + email_language: Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`). extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsBatchDeleteResponse | SessionsBatchDeleteResponse202", + "SessionsPreviewResubmissionResponse", await self._client.request( "post", - "/v3/sessions/delete/", - cast_to={200: SessionsBatchDeleteResponse, 202: SessionsBatchDeleteResponse202}, + "/v3/session/{sessionId}/resubmission-preview/", + cast_to=SessionsPreviewResubmissionResponse, + path_params={"sessionId": session_id}, auth="api_key", body={ - "session_numbers": session_numbers, - "delete_all": delete_all, - "retain_face_embeddings": retain_face_embeddings, - "face_retention_days": face_retention_days, - "face_retention_deadline": face_retention_deadline, - "deletion_instruction": deletion_instruction, - "instruction_id": instruction_id, + "nodes_to_resubmit": nodes_to_resubmit, + "email_address": email_address, + "email_language": email_language, }, extra_headers=extra_headers, extra_query=extra_query, @@ -2236,40 +3684,45 @@ async def batch_delete( ), ) - async def retrieve_deletion_job( + async def update_document_ai_data( self, - job_id: str, + session_id: str, + document_uuid: str, *, + extracted_data: Mapping[str, object] | NotGiven = NOT_GIVEN, + status: Literal["Approved", "Declined", "In Review", "Not Finished"] + | str + | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsRetrieveDeletionJobResponse: - """Get a bulk session deletion job. + ) -> SessionsUpdateDocumentAiDataResponse: + """Update a Document AI document. - Progress of a bulk session deletion that `POST /v3/sessions/delete/` answered with `202` (more than 20 matching sessions, selected by `session_numbers` or with `delete_all`). Poll every few seconds while `status` is `PENDING` or `PROCESSING`; stop when it is `COMPLETED` or `FAILED`. - - The job reports how many sessions ended in each outcome and how many it has processed, never which sessions it deleted. A `FAILED` job carries the reason in `error`; the sessions it had not reached are still in place, so repeat the deletion request to delete them. - - **Authentication:** API key in the `x-api-key` header. A job is only visible to the application that created it; any other `job_id` returns `404`. Missing or invalid credentials return `403`; this API never returns `401`. + Correct the fields Document AI extracted from one uploaded document, set its review status, or both, on a KYC or KYB session. `extracted_data` must be a JSON object and is merged key by key: a key you send replaces the stored value (a key sent as `null` is stored as `null`) and keys you leave out keep their values. The document status feeds the Document AI result of its workflow node in the session decision; the session status itself is not recomputed. Every successful call, including one that changes nothing, sends a `data.updated` [webhook](https://docs.didit.me/integration/webhooks) with trigger `manual_review`. A scoped API key needs `decisions: write`; see [Document AI](https://docs.didit.me/core-technology/document-ai/overview). - ``GET /v3/sessions/delete/{job_id}/`` + ``PATCH /v3/document-ai/{sessionId}/documents/{documentUuid}/`` Args: - job_id: The `job_id` returned with the `202` from `POST /v3/sessions/delete/`. + session_id: UUID of the verification session (the `session_id` returned by `POST /v3/session/`). Accepts both User Verification (KYC) and Business Verification (KYB) session IDs: the server resolves the ID across both session types. Must be a well-formed UUID; a malformed value does not match the route and returns an HTML `404`. + document_uuid: UUID of the Document AI document (`document_ai_documents[].items[].uuid` in `GET /v3/session/{sessionId}/decision/`). It must belong to the session in the path. + extracted_data: Field keys (the keys you configured for the document) and their corrected values. Must be a JSON object; merged key by key into the stored data. Leave it out rather than sending `null`, which returns `400`. + status: New review status of the document. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsRetrieveDeletionJobResponse", + "SessionsUpdateDocumentAiDataResponse", await self._client.request( - "get", - "/v3/sessions/delete/{job_id}/", - cast_to=SessionsRetrieveDeletionJobResponse, - path_params={"job_id": job_id}, + "patch", + "/v3/document-ai/{sessionId}/documents/{documentUuid}/", + cast_to=SessionsUpdateDocumentAiDataResponse, + path_params={"sessionId": session_id, "documentUuid": document_uuid}, auth="api_key", + body={"extracted_data": extracted_data, "status": status}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -2277,107 +3730,38 @@ async def retrieve_deletion_job( ), ) - async def update_data( + async def regenerate_kyb_party( self, session_id: str, + party_uuid: str, *, - document_type: Literal[ - "P", "DL", "ID", "RP", "SSC", "HIC", "WP", "TC", "VISA", "PSC", "BC", "OTHER" - ] - | str - | None - | NotGiven = NOT_GIVEN, - document_subtype: str | None | NotGiven = NOT_GIVEN, - document_number: str | None | NotGiven = NOT_GIVEN, - personal_number: str | None | NotGiven = NOT_GIVEN, - date_of_birth: str | _dt.date | None | NotGiven = NOT_GIVEN, - date_of_issue: str | _dt.date | None | NotGiven = NOT_GIVEN, - expiration_date: str | _dt.date | None | NotGiven = NOT_GIVEN, - issuing_state: str | None | NotGiven = NOT_GIVEN, - first_name: str | None | NotGiven = NOT_GIVEN, - last_name: str | None | NotGiven = NOT_GIVEN, - gender: Literal["M", "F", "U"] | str | None | NotGiven = NOT_GIVEN, - address: str | None | NotGiven = NOT_GIVEN, - place_of_birth: str | None | NotGiven = NOT_GIVEN, - nationality: str | None | NotGiven = NOT_GIVEN, - marital_status: Literal["SINGLE", "MARRIED", "DIVORCED", "WIDOWED", "UNKNOWN"] - | str - | NotGiven = NOT_GIVEN, - extra_fields: Mapping[str, object] | None | NotGiven = NOT_GIVEN, - parsed_address: SessionsUpdateDataParamsParsedAddress | None | NotGiven = NOT_GIVEN, - node_id: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsUpdateDataResponse: - """Update KYC data extracted from the ID document on a verification session. + ) -> KYBKeyPeopleParty: + """Regenerate a Key People party's KYC session. - Correct the identity fields that were extracted (OCR / NFC) from the user's ID document - misread names, dates, document numbers, and so on - after the verification has finished. Typical callers are a compliance analyst working from your own back office, or an automated reconciliation job. Every field in the body is optional: only the keys you send are written, everything else is left untouched (`PATCH` semantics). - - The session must belong to your application and be in `Approved`, `Declined`, `In Review`, or `Kyc Expired` status. Sessions in any other state (e.g. still `In Progress`) return `404` with `"No Session matches the given query."` - distinct from the `"Not found."` returned for an unknown or deleted `sessionId`. The session must also carry an ID-verification (KYC) record: sessions whose workflow never ran an ID-verification step return `404` (`"The session does not have a KYC."`). For graph workflows with multiple ID-verification steps, target a specific step with `node_id` (query parameter or body field); when omitted, the most recent KYC record is patched. - - Two derived fields are recomputed on every call: `full_name` (from `first_name` + `last_name`) and `issuing_state_name` (from `issuing_state`). If the free-text `address` changes, the server may also re-geocode it and refresh `parsed_address` (skipped for sandbox sessions). `extra_fields` and `parsed_address` are merged key-by-key rather than replaced - see the per-field descriptions for the `null`-to-delete semantics. - - Submitting at least one valid field fires the `data.updated` [webhook](/integration/webhooks) (trigger `manual_review`) - even when the submitted values equal the stored ones. When values actually change, a `KYC_DATA_UPDATED` activity entry recording the changed fields with previous and new values is appended to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and the console activity timeline. The session's status and decision are **not** re-evaluated - use [Update Status](#patch-/v3/session/-sessionId-/update-status/) to change the outcome. An empty JSON body (`{}`) is accepted and simply returns the current KYC data without firing the webhook. - - Accepts an API key or a console user token; either way the credential needs the `write:sessions` permission. Authentication and permission failures both return `403` - this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. This endpoint has a dedicated rate limit of 10 requests per minute per API key (invalid requests count too); the shared write budget of 300 requests per minute also applies. + Give a Key People party of a Business Verification (KYB) session a new individual KYC session after its previous one was deleted, so the Key People check can finish again; send no request body. It works only when the party's KYC session was deleted (`kyc_session_deleted: true`): a party whose KYC session still exists (in any status), a party that never had one, or a company party returns `409`. Didit creates a new hosted KYC session with the workflow the deleted one ran (its latest published version) or, when that is no longer known, the workflow your Key People settings assign to the party's role, emails the link to the party when an email is on file, clears `is_skipped`, and recomputes the Key People check and the business session status. It then sends a `data.updated` [webhook](https://docs.didit.me/integration/webhooks) for the business session with trigger `console_key_people_regenerate`; to exempt the party instead, use `PATCH /v3/session/{sessionId}/kyb/parties/{partyUuid}/`. A scoped API key needs `decisions: write`, and without `sessions: write` it receives `kyc_session_url` as `null`. - ``PATCH /v3/session/{sessionId}/update-data/`` + ``POST /v3/session/{sessionId}/kyb/parties/{partyUuid}/regenerate/`` Args: - session_id: UUID of the User Verification (KYC) session whose extracted document data you are correcting. Business (KYB) session IDs are not valid here - for company data use [Update KYB Company Data](#patch-/v3/session/-sessionId-/kyb/-companyUuid-/update-data/). - document_type: Document type code. Accepts the short codes listed in the enum, plus six long-form aliases normalized server-side before validation: `Passport` → `P`, `Identity Card` → `ID`, `Driver's License` → `DL`, `Residence Permit` → `RP`, `Health Insurance Card` → `HIC`, `Tax Card` → `TC`. Any other value (including `PASSPORT` in caps) returns `400`. - document_subtype: Document subtype from the document registry, including the region prefix when the registry distinguishes regional variants. - document_number: Document number as printed on the ID. - personal_number: Personal / national identification number where the document carries one (e.g. Spanish DNI on a passport). - date_of_birth: Date of birth in `YYYY-MM-DD`. Any other format returns `400`. - date_of_issue: Issue date of the document in `YYYY-MM-DD`. - expiration_date: Expiration date of the document in `YYYY-MM-DD`. - issuing_state: Issuing country as an ISO 3166-1 alpha-3 code. Case-insensitive (`esp` is stored as `ESP`); the human-readable `issuing_state_name` shown in [Get Decision](#get-/v3/session/-sessionId-/decision/) is recomputed from this value. Invalid codes return `400`. - first_name: Given name(s). The stored `full_name` is recomputed as `first_name + ' ' + last_name` on every call to this endpoint. - last_name: Family name(s). Also feeds the recomputed `full_name`. - gender: Sex as printed on the document: `M` (male), `F` (female), or `U` (unknown). Any other value - including `X` - returns `400`. - address: Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it synchronously before responding - the 200 body already reflects the refreshed `parsed_address`. The re-geocode also requires `issuing_state` to be present on the KYC record. - place_of_birth: Place of birth as printed on the document. - nationality: Nationality as printed on the document. Free text up to 32 characters - typically an ISO 3166-1 alpha-3 code (e.g. `ESP`), but not validated as one. - marital_status: Marital status where the document carries it. Any other value returns `400`. - extra_fields: Per-key overrides for document-specific extra fields (e.g. `blood_group`, `dl_categories`, `mrz_string`). Keys are merged into the existing override map: send a key with a value to set it, or with `null` / `""` to remove that key from the merged view. Send the whole field as `null` to clear all previously applied overrides. Must be a JSON object, otherwise `400`. The response always returns the merged view (OCR-extracted values with your overrides applied). - parsed_address: Structured address. Only these nested keys are accepted: `address_type`, `city`, `region`, `street_1`, `street_2`, `postal_code`, `country` - any other key returns `400`. Per-key semantics: a value sets the key, `null` removes it. `formatted_address` is recomputed server-side by joining `street_1`, `street_2`, `city`, `region`, `postal_code`, and `country` and is returned in the response - you cannot set it directly. - node_id: Workflow graph node whose KYC record to patch, for graph workflows with multiple ID-verification steps (also accepted as a `node_id` body field). When omitted, the most recent KYC record on the session is patched. If no KYC exists for the given node, the call returns `404` (`"The session does not have a KYC for node ''."`). + session_id: UUID of the parent Business Verification (KYB) session. + party_uuid: UUID of the Key People party (`key_people_checks[].submitted.parties[].uuid` on the business session decision). Must belong to the session referenced by `sessionId`. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsUpdateDataResponse", + "KYBKeyPeopleParty", await self._client.request( - "patch", - "/v3/session/{sessionId}/update-data/", - cast_to=SessionsUpdateDataResponse, - path_params={"sessionId": session_id}, + "post", + "/v3/session/{sessionId}/kyb/parties/{partyUuid}/regenerate/", + cast_to=KYBKeyPeopleParty, + path_params={"sessionId": session_id, "partyUuid": party_uuid}, auth="api_key", - query={"node_id": node_id}, - body={ - "document_type": document_type, - "document_subtype": document_subtype, - "document_number": document_number, - "personal_number": personal_number, - "date_of_birth": date_of_birth, - "date_of_issue": date_of_issue, - "expiration_date": expiration_date, - "issuing_state": issuing_state, - "first_name": first_name, - "last_name": last_name, - "gender": gender, - "address": address, - "place_of_birth": place_of_birth, - "nationality": nationality, - "marital_status": marital_status, - "extra_fields": extra_fields, - "parsed_address": parsed_address, - }, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -2385,49 +3769,44 @@ async def update_data( ), ) - async def update_feature_status( + def list_events( self, session_id: str, - node_id: str, *, - new_status: Literal["Approved", "Declined", "In Review"] | str, - comment: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + ordering: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsUpdateFeatureStatusResponse: - """Update an individual feature (step) status. - - Set the status of a single verification feature (step) within a session **without changing the overall session decision**. Target the feature by its workflow graph `nodeId` (use `default` for non-graph workflows where the step has no node). - - Works for both User Verification (KYC) and Business Verification (KYB) sessions and every step type - ID/OCR, Liveness, Face Match, NFC, AML, Proof of Address, Phone, Email, Database Validation, Questionnaire, and the KYB registry / documents / key-people checks. - - `new_status` must be one of `Approved`, `Declined`, or `In Review`, and must differ from the feature's current status. A successful call fires a `data.updated` webhook and appends an activity entry (with actor attribution) to the session's review trail. This endpoint is **not** available for standalone API sessions (`session_type = API`). + ) -> AsyncPaginator[SessionEvent]: + """List session events. - Authenticate with your API key (`x-api-key`) or a user authorization header - either must carry the `write:sessions` privilege. + Read the event timeline of one verification session, newest first by `sent_at`: the screens the user saw, buttons and uploads, failed attempts, status and feature-step changes, and the workflow routing trace. Works for User Verification (KYC) and Business Verification (KYB) session IDs. Events are stored asynchronously, so a new event can take a few seconds to appear. Results are paginated with `limit` and `offset`; follow `next` until it is `null`. A scoped API key needs `sessions: read` and `decisions: read`, and without `media: read` it receives `file_url` as `null`. The routing events are described in [Retrieve session](https://docs.didit.me/sessions-api/retrieve-session). - ``PATCH /v3/session/{sessionId}/features/{nodeId}/update-status/`` + ``GET /v3/session/{sessionId}/events/`` Args: - session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. - node_id: Workflow graph node ID of the feature to update (for example `feature_ocr_1`). Use the literal value `default` for non-graph (single-step) workflows where the feature has no node ID. - new_status: Target status for this feature. Must differ from the current value. - comment: Optional free-text note recorded on the session's review trail for this feature change. + session_id: UUID of the verification session (the `session_id` returned by `POST /v3/session/`). Accepts both User Verification (KYC) and Business Verification (KYB) session IDs: the server resolves the ID across both session types. Must be a well-formed UUID; a malformed value does not match the route and returns an HTML `404`. + limit: Maximum number of events per page. Defaults to 50. A value below 1 or a non-numeric value falls back to 50; a value above 1,000,000 returns `400`. + offset: Number of results to skip before the first returned one. Defaults to 0; a negative or non-numeric value falls back to 0, a value above 1,000,000 returns `400`. Prefer following the `next` and `previous` URLs. + ordering: Optional sort override. The default order is `-sent_at`. Pass a result field name (for example `sent_at`, `created_at`, `event_type`, `feature_step` or `step`) to sort ascending, prefix it with `-` to sort descending, and separate several fields with commas. Unknown fields are ignored. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsUpdateFeatureStatusResponse", - await self._client.request( - "patch", - "/v3/session/{sessionId}/features/{nodeId}/update-status/", - cast_to=SessionsUpdateFeatureStatusResponse, - path_params={"sessionId": session_id, "nodeId": node_id}, + "AsyncPaginator[SessionEvent]", + self._client.request_page( + "get", + "/v3/session/{sessionId}/events/", + cast_to=SessionsListEventsResponse, + item_type=SessionEvent, + path_params={"sessionId": session_id}, auth="api_key", - body={"new_status": new_status, "comment": comment}, + query={"limit": limit, "offset": offset, "ordering": ordering}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -2435,53 +3814,46 @@ async def update_feature_status( ), ) - async def update_aml_hit_status( + def list_aml_screening_history( self, session_id: str, *, - hit_id: str, - review_status: Literal["Unreviewed", "Confirmed Match", "False Positive", "Inconclusive"] - | str, node_id: str | NotGiven = NOT_GIVEN, - node_id_query: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + ordering: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> object: - """Update an AML hit review status. - - Set the review status of a **single AML hit** (a potential sanction / PEP / watchlist / adverse-media match) returned by AML screening, without changing the overall session decision. Identify the hit by its `hit_id` (the `id` of an entry in the AML check's `hits` array - see the session decision). - - `review_status` is one of `Unreviewed`, `Confirmed Match`, `False Positive`, or `Inconclusive`. Reviewer decisions are preserved across ongoing-monitoring updates, so a hit you mark `False Positive` stays that way when the provider re-screens. - - A successful call fires a `data.updated` webhook and appends an activity entry (with actor attribution) to the session's review trail. For graph workflows with multiple AML checks, pass `node_id` to target a specific check. + ) -> AsyncPaginator[AMLScreeningRun]: + """List AML screening history. - Authenticate with your API key (`x-api-key`) or a user authorization header - either must carry the `write:sessions` privilege. + List every completed ongoing-monitoring re-screen of a session's AML check, newest first by `completed_at`, including the runs where nothing changed: alerts, activity entries and webhooks fire only when a result moves, so this history is the evidence that a clean subject was still being screened. Works for KYC and KYB session IDs; pass `node_id` when the session ran more than one AML step. Only ongoing-monitoring re-screens are listed (the first screening is part of the session decision) and runs from before this history existed are not reconstructed, so a session without ongoing monitoring returns an empty list. `count` is exact up to 100 and capped at 100 above that, so follow `next` until it is `null`. A scoped API key needs `decisions: read`; see [Continuous AML monitoring](https://docs.didit.me/core-technology/aml-screening/continuous-monitoring-aml-screening). - ``PATCH /v3/session/{sessionId}/update-aml-hit-status/`` + ``GET /v3/session/{sessionId}/aml-screening-history/`` Args: - session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. - hit_id: The `id` of the hit in the AML check's `hits` array. - review_status: The new review status for the hit. - node_id: Optional graph node ID. Use it (here or as a query parameter) when the session has multiple AML checks. - node_id_query: Workflow graph node ID. Required only when the session has multiple AML checks (one per node); omit it for single-AML sessions. + session_id: UUID of the verification session (the `session_id` returned by `POST /v3/session/`). Accepts both User Verification (KYC) and Business Verification (KYB) session IDs: the server resolves the ID across both session types. Must be a well-formed UUID; a malformed value does not match the route and returns an HTML `404`. + node_id: Workflow graph node ID of the AML check. Needed only when the session ran more than one AML step; without it the most recent AML check is used. An unknown node returns `404`. + limit: Maximum number of screening runs per page. Defaults to 50. A value below 1 or a non-numeric value falls back to 50; a value above 1,000,000 returns `400`. + offset: Number of results to skip before the first returned one. Defaults to 0; a negative or non-numeric value falls back to 0, a value above 1,000,000 returns `400`. Prefer following the `next` and `previous` URLs. + ordering: Optional sort override. The default order is `-completed_at`. Pass a result field name (for example `completed_at`, `started_at`, `outcome` or `score`) to sort ascending, prefix it with `-` to sort descending, and separate several fields with commas. Unknown fields are ignored. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "object", - await self._client.request( - "patch", - "/v3/session/{sessionId}/update-aml-hit-status/", - cast_to=object, + "AsyncPaginator[AMLScreeningRun]", + self._client.request_page( + "get", + "/v3/session/{sessionId}/aml-screening-history/", + cast_to=SessionsListAmlScreeningHistoryResponse, + item_type=AMLScreeningRun, path_params={"sessionId": session_id}, auth="api_key", - query={"node_id": node_id_query}, - body={"hit_id": hit_id, "review_status": review_status, "node_id": node_id}, + query={"node_id": node_id, "limit": limit, "offset": offset, "ordering": ordering}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -2489,48 +3861,40 @@ async def update_aml_hit_status( ), ) - async def bulk_update_aml_hit_status( + async def export_aml_screening_history( self, session_id: str, *, - hit_updates: Sequence[SessionsBulkUpdateAmlHitStatusParamsHitUpdates], node_id: str | NotGiven = NOT_GIVEN, - node_id_query: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> object: - """Bulk update AML hit review statuses. - - Update the review status of **multiple AML hits** in one call. Provide `hit_updates`, a list of `{ hit_id, review_status }` pairs. `review_status` is one of `Unreviewed`, `Confirmed Match`, `False Positive`, or `Inconclusive`. - - The update is all-or-nothing: if any `hit_id` is not found in the AML check, the request returns `404` and no hit is changed. A successful call fires a single `data.updated` webhook and records one activity entry covering all changes. For graph workflows with multiple AML checks, pass `node_id` to target a specific check. + ) -> bytes: + """Export AML screening history as CSV. - Authenticate with your API key (`x-api-key`) or a user authorization header - either must carry the `write:sessions` privilege. + Download the same ongoing-monitoring history as `GET /v3/session/{sessionId}/aml-screening-history/` as a CSV file, one line per completed re-screen, newest first, ready to hand to an auditor. The file is streamed whole (no pagination) and starts with a header line; an empty cell means unknown, dataset dates are never estimated, and a value that would start a spreadsheet formula is prefixed with an apostrophe. Works for KYC and KYB session IDs; pass `node_id` when the session ran more than one AML step. Errors are returned as JSON, not CSV. A scoped API key needs `decisions: read` and `media: read`: the response is a file, so a key without `media: read` gets `403`. - ``PATCH /v3/session/{sessionId}/bulk-update-aml-hit-status/`` + ``GET /v3/session/{sessionId}/aml-screening-history/export/`` Args: - session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. - hit_updates: Non-empty list of hit updates. - node_id: Optional graph node ID. Use it (here or as a query parameter) when the session has multiple AML checks. - node_id_query: Workflow graph node ID. Required only when the session has multiple AML checks (one per node); omit it for single-AML sessions. + session_id: UUID of the verification session (the `session_id` returned by `POST /v3/session/`). Accepts both User Verification (KYC) and Business Verification (KYB) session IDs: the server resolves the ID across both session types. Must be a well-formed UUID; a malformed value does not match the route and returns an HTML `404`. + node_id: Workflow graph node ID of the AML check. Needed only when the session ran more than one AML step; without it the most recent AML check is used. An unknown node returns `404`. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "object", + "bytes", await self._client.request( - "patch", - "/v3/session/{sessionId}/bulk-update-aml-hit-status/", - cast_to=object, + "get", + "/v3/session/{sessionId}/aml-screening-history/export/", + cast_to=bytes, path_params={"sessionId": session_id}, auth="api_key", - query={"node_id": node_id_query}, - body={"hit_updates": hit_updates, "node_id": node_id}, + query={"node_id": node_id}, + accept="text/csv, application/json", extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -2538,88 +3902,91 @@ async def bulk_update_aml_hit_status( ), ) - async def update_poa_data( + async def add_images( self, session_id: str, *, - issuing_state: str | None | NotGiven = NOT_GIVEN, - document_type: Literal[ - "UTILITY_BILL", - "BANK_STATEMENT", - "GOVERNMENT_ISSUED_DOCUMENT", - "OTHER_POA_DOCUMENT", - "UNKNOWN", - ] - | str - | NotGiven = NOT_GIVEN, - document_language: str | None | NotGiven = NOT_GIVEN, - issuer: str | None | NotGiven = NOT_GIVEN, - issue_date: str | _dt.date | None | NotGiven = NOT_GIVEN, - poa_address: str | None | NotGiven = NOT_GIVEN, - name_on_document: str | None | NotGiven = NOT_GIVEN, - extra_fields: Mapping[str, object] | None | NotGiven = NOT_GIVEN, - poa_parsed_address: SessionsUpdatePoaDataParamsPoaParsedAddress - | None - | NotGiven = NOT_GIVEN, + file: FileTypes, + model_type: Literal["kyc", "poa"] | str, + file_password: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsAddImagesResponse: + """Add an image to a session. + + Attach one extra image or PDF to the ID verification (`model_type=kyc`) or Proof of Address (`model_type=poa`) result of a finished User Verification (KYC) session, for example evidence collected outside the flow. The session must be `Approved`, `Declined`, `In Review` or `Kyc Expired` and carry the result named by `model_type`; Business Verification (KYB) sessions are not supported. Each call appends one file (to the most recent result when the step ran more than once), keeps the existing files and records a `FILE_ADDED` entry in the session activity log; the upload sends no webhook and does not change the session status. The file is compressed on upload and is returned in `extra_files` of the session decision. This endpoint is limited to 10 requests per minute per API key. A scoped API key needs `sessions: write`, and without `media: read` it receives the `extra_files` links as `null`. + + ``PATCH /v3/session/{sessionId}/add-images/`` + + Args: + session_id: UUID of the User Verification (KYC) session to add the file to (the `session_id` returned by `POST /v3/session/`). A Business Verification (KYB) session ID returns `404`. + file: The image or PDF. Allowed extensions: `jpg`, `jpeg`, `png`, `webp`, `tiff`, `pdf`. At most 15 MB as uploaded; the file is then compressed and rejected with `400` if it is still larger than 2 MB (2,097,152 bytes). Empty files are rejected. + model_type: `kyc` adds the file to the ID verification result, `poa` to the Proof of Address result. + file_password: Password of an encrypted PDF. Without it, or with a wrong one, an encrypted PDF returns `400`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsAddImagesResponse", + await self._client.request( + "patch", + "/v3/session/{sessionId}/add-images/", + cast_to=SessionsAddImagesResponse, + path_params={"sessionId": session_id}, + auth="api_key", + body={"file": file, "model_type": model_type, "file_password": file_password}, + multipart=True, + file_fields=("file",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_aml_status( + self, + session_id: str, + *, + status: Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] + | str, node_id: str | NotGiven = NOT_GIVEN, + node_id_query: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsUpdatePoaDataResponse: - """Update Proof of Address data extracted from the POA document on a verification session. - - Correct the fields extracted from the user's Proof of Address document - utility bill, bank statement, or other address evidence - after the verification has finished: issuer, dates, the printed name, the address itself, and banking details. Every field in the body is optional: only the keys you send are written, everything else is left untouched (`PATCH` semantics). - - The session must belong to your application and be in `Approved`, `Declined`, `In Review`, or `Kyc Expired` status. Sessions in any other state (e.g. still `In Progress`) return `404` with `"No Session matches the given query."` - distinct from the `"Not found."` returned for an unknown or deleted `sessionId`. The session must also carry a POA record: sessions whose workflow never ran a Proof of Address step return `404` (`"The session does not have a POA."`). For graph workflows with multiple POA steps, target a specific step with `node_id` (query parameter or body field); when omitted, the most recent POA record is patched. - - `extra_fields` is merged key-by-key: nine known keys (`bank_iban`, `additional_names`, …) write through to dedicated POA columns, any other key is kept as a custom extra, and `null` / `""` deletes a key. `poa_parsed_address` accepts only the seven documented keys and recomputes `formatted_address` / `poa_formatted_address` after every edit. If the free-text `poa_address` changes, the server may also re-geocode it and refresh the structured address (skipped for sandbox sessions). + ) -> SessionsUpdateAmlStatusResponse: + """Update an AML check status. - Unlike the KYC variant of this endpoint, the `data.updated` [webhook](/integration/webhooks) (trigger `manual_review`) fires only when at least one stored value **actually changed** - re-sending identical values is a silent no-op. When values change, a `POA_DATA_UPDATED` activity entry recording the changed fields with previous and new values is appended to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and the console activity timeline. The session's status and decision are **not** re-evaluated - use [Update Status](#patch-/v3/session/-sessionId-/update-status/) to change the outcome. - - Accepts an API key or a console user token; either way the credential needs the `write:sessions` permission. Authentication and permission failures both return `403` - this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. This endpoint has a dedicated rate limit of 10 requests per minute per API key (invalid requests count too); the shared write budget of 300 requests per minute also applies. - - An empty JSON body `{}` is a no-op: it returns `200` echoing the current POA record without firing the webhook (`has_changes` stays false). + Set the status of a session's AML screening check as a whole (the `status` of an `aml_screenings[]` entry in the session decision), for example `Approved` after your team cleared every hit. Works for KYC and KYB session IDs; pass `node_id` (query parameter or body field) when the session ran more than one AML step, otherwise the most recent AML check is updated. When the status actually changes, an `AML_STATUS_UPDATED` activity entry is recorded and a `data.updated` [webhook](https://docs.didit.me/integration/webhooks) is sent; sending the current status returns `200` and changes nothing. The session status is not recomputed: use `PATCH /v3/session/{sessionId}/update-status/` for the session decision and `PATCH /v3/session/{sessionId}/update-aml-hit-status/` for single hits. A scoped API key needs `decisions: write`. - ``PATCH /v3/session/{sessionId}/update-poa-data/`` + ``PATCH /v3/session/{sessionId}/update-aml-status/`` Args: - session_id: UUID of the User Verification (KYC) session whose attached POA record you are correcting. - issuing_state: Country that issued the POA document, ISO 3166-1 alpha-3. Case-insensitive (normalized to uppercase); invalid codes return `400`. - document_type: POA document category. Values are uppercase - lowercase variants like `utility_bill` return `400`. - document_language: Language of the document, typically an ISO 639-1 code (e.g. `es`). Free text - not validated as a language code. - issuer: Organization that issued the document (utility company, bank, government agency). - issue_date: Issue date of the POA document in `YYYY-MM-DD`. Any other format returns `400`. - poa_address: Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it in the background of the same request and may overwrite `poa_parsed_address` / `poa_formatted_address` with the geocoder result. The re-geocode also requires `issuing_state` to be present on the POA record. - name_on_document: Recipient name as printed on the document. - extra_fields: Document-specific extras, merged key-by-key. Nine keys map to dedicated POA columns: `bank_account_number`, `bank_iban`, `bank_sort_code`, `bank_routing_number`, `bank_swift_bic`, `bank_branch_name`, `bank_branch_address`, `document_phone_number`, and `additional_names` (must be a list of strings or `null`, otherwise `400`). Any other key is stored as a custom extra field. Per-key semantics: a value sets the key; `null` or `""` deletes it. Must be a JSON object, otherwise `400`. - poa_parsed_address: Structured address. Only these nested keys are accepted: `address_type`, `city`, `region`, `street_1`, `street_2`, `postal_code`, `country` - any other key returns `400`. Per-key semantics: a value sets the key, `null` removes it. `formatted_address` (and the top-level `poa_formatted_address`) is recomputed server-side by joining `street_1`, `street_2`, `city`, `region`, `postal_code`, and `country` - you cannot set it directly. - node_id: Workflow graph node whose POA record to patch, for graph workflows with multiple Proof of Address steps (also accepted as a `node_id` body field). When omitted, the most recent POA record on the session is patched. If no POA exists for the given node, the call returns `404` (`"The session does not have a POA for node ''."`). + session_id: UUID of the verification session (the `session_id` returned by `POST /v3/session/`). Accepts both User Verification (KYC) and Business Verification (KYB) session IDs: the server resolves the ID across both session types. Must be a well-formed UUID; a malformed value does not match the route and returns an HTML `404`. + status: New status of the AML check. + node_id: Workflow graph node ID of the AML check, as an alternative to the query parameter. + node_id_query: Workflow graph node ID of the AML check to update. Needed only when the session ran more than one AML step. Takes precedence over `node_id` in the body. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsUpdatePoaDataResponse", + "SessionsUpdateAmlStatusResponse", await self._client.request( "patch", - "/v3/session/{sessionId}/update-poa-data/", - cast_to=SessionsUpdatePoaDataResponse, + "/v3/session/{sessionId}/update-aml-status/", + cast_to=SessionsUpdateAmlStatusResponse, path_params={"sessionId": session_id}, auth="api_key", - query={"node_id": node_id}, - body={ - "issuing_state": issuing_state, - "document_type": document_type, - "document_language": document_language, - "issuer": issuer, - "issue_date": issue_date, - "poa_address": poa_address, - "name_on_document": name_on_document, - "extra_fields": extra_fields, - "poa_parsed_address": poa_parsed_address, - }, + query={"node_id": node_id_query}, + body={"status": status, "node_id": node_id}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -2627,137 +3994,63 @@ async def update_poa_data( ), ) - async def update_kyb_company_data( + async def simulate( self, session_id: str, - company_uuid: str, *, - company_name: str | NotGiven = NOT_GIVEN, - registration_number: str | NotGiven = NOT_GIVEN, - country_code: str | NotGiven = NOT_GIVEN, - region: str | None | NotGiven = NOT_GIVEN, - incorporation_date: str | _dt.date | NotGiven = NOT_GIVEN, - registration_date: str | _dt.date | None | NotGiven = NOT_GIVEN, - dissolution_date: str | _dt.date | None | NotGiven = NOT_GIVEN, - tax_number: str | NotGiven = NOT_GIVEN, - legal_address: str | NotGiven = NOT_GIVEN, - company_type: str | NotGiven = NOT_GIVEN, - registry_status: Literal[ - "active", - "dissolved", - "deregistered", - "see full details", - "authorised", - "appointed representative", - "unauthorised", - "inactive", - "no longer authorised", - "closed", - "struck off", - ] - | str - | None - | NotGiven = NOT_GIVEN, - verification_status: Literal["verified", "failed", "unknown"] + new_status: Literal["Approved", "Declined", "In Review", "Expired", "Abandoned"] | str - | None | NotGiven = NOT_GIVEN, - alternative_names: str | NotGiven = NOT_GIVEN, - nature_of_business: str | NotGiven = NOT_GIVEN, - registered_capital: str | NotGiven = NOT_GIVEN, - registered_capital_amount: float | None | NotGiven = NOT_GIVEN, - registered_capital_currency: str | None | NotGiven = NOT_GIVEN, - website: str | NotGiven = NOT_GIVEN, - email: str | NotGiven = NOT_GIVEN, - phone: str | NotGiven = NOT_GIVEN, - legal_entity_identifier: str | NotGiven = NOT_GIVEN, - location_of_registration: str | NotGiven = NOT_GIVEN, - vat_number: str | NotGiven = NOT_GIVEN, - control_scheme: str | NotGiven = NOT_GIVEN, + comment: str | NotGiven = NOT_GIVEN, + scenario: str | NotGiven = NOT_GIVEN, + risk: str | NotGiven = NOT_GIVEN, + node_id: str | NotGiven = NOT_GIVEN, + send_webhooks: bool | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsUpdateKybCompanyDataResponse: - """Update registry-extracted company data on a Business Verification (KYB) session. - - Correct the registry-extracted company data on a Business Verification (KYB) session - company name, registration number, addresses, status, contact details - identified by `companyUuid`. Typical callers are a compliance analyst fixing a registry misread, or your back office syncing corrected company data. Every field in the body is optional (`PATCH` semantics); an empty JSON body (`{}`) is a no-op that simply echoes the current company record. + ) -> SandboxSimulateResponse: + """Simulate a sandbox session's status. - Edits are written in two places at once: fields with a canonical company column (name, registration number, country, incorporation date, tax number, type, statuses, capital fields (`registered_capital`, `registered_capital_amount`, `registered_capital_currency`), alternative names, nature of business, website, email, phone, LEI, location, and `legal_address` → `registered_address`) update that column, and **every** submitted key is also merged into the `registry_data` snapshot (dates as ISO strings) so the "extracted from registry" view reflects the correction. Fields without a column - `region`, `registration_date`, `dissolution_date`, `control_scheme` - land in `registry_data` only. `user_provided_data` (what the end user confirmed in the hosted flow) is never touched, and `last_console_edit_at` is stamped on every non-empty PATCH. + 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. - Unlike the KYC/POA update endpoints, there is **no session-status gate**: the company can be edited at any point in the business session's lifecycle. The company must belong to the session in the path (`404` with `"KYB company does not belong to the given session."` otherwise) and to your application. Every non-empty PATCH fires the `data.updated` [webhook](/integration/webhooks) on the business session (trigger `console_registry_edit`) - even when the submitted values equal the stored ones. The session's status and decision are **not** re-evaluated, and no review/audit entry is recorded. + Set `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. - Accepts an API key or a console user token; either way the credential needs the `write:businesses` permission. Authentication and permission failures both return `403` - this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. The shared write budget of 300 requests per minute per API key applies. + `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. - For User Verification sessions, the KYC counterpart is `PATCH /v3/session/{sessionId}/update-data/`; to change the session decision itself use `PATCH /v3/session/{sessionId}/update-status/`. + Requires 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. - ``PATCH /v3/session/{sessionId}/kyb/{companyUuid}/update-data/`` + ``POST /v3/session/{sessionId}/simulate/`` Args: - session_id: UUID of the parent Business Verification (KYB) session. - company_uuid: UUID of the company record to update (`registry_checks[].company.uuid` on the business session decision). Must belong to the session referenced by `sessionId`. - company_name: Legal name of the company. Written to the canonical column and the registry snapshot. - registration_number: Official registration number with the company registry. - country_code: Country of incorporation, ISO 3166-1 alpha-2. Case-insensitive (`us` is stored as `US`). Alpha-3 codes fail the 2-character limit with `400`. - region: Subdivision / region tag where the registry has one (e.g. US state). Stored in the registry snapshot only - it has no canonical column. - incorporation_date: Incorporation date in `YYYY-MM-DD`. Optional like every other field - the `PATCH` is partial. Any other date format returns `400`. - registration_date: Registration date where the registry distinguishes it from incorporation. Stored in the registry snapshot only. - dissolution_date: Dissolution date for companies that are no longer active. Stored in the registry snapshot only. - tax_number: Tax identification number. - legal_address: Legal / registered address. Aliased server-side to the canonical `registered_address` column - the response returns it under `registered_address`, while the registry snapshot keeps the `legal_address` key you sent. - company_type: Legal entity type (e.g. `Limited Liability Company`, `PLC`, `GmbH`). - registry_status: Registry-reported company status. Lowercase, some values contain spaces (e.g. `struck off`, `no longer authorised`). Any other value returns `400`. - verification_status: Outcome of the registry verification. Any other value returns `400`. - alternative_names: Trade names, former legal names, DBA names (free text). - nature_of_business: Short description of the company's activity. - registered_capital: Human-readable registered capital (e.g. `USD 50000`). Use `registered_capital_amount` + `registered_capital_currency` for structured values, or this field for free-form text. - registered_capital_amount: Numeric registered-capital amount (up to 20 digits, 2 decimal places). Sets the `registered_capital_amount` column and is mirrored into `registry_data` as a decimal string; the response echoes it as a decimal string (e.g. `"1500000.50"`). - registered_capital_currency: ISO 4217 currency code for `registered_capital_amount`. Case-insensitive (`usd` is stored as `USD`); values that are not exactly 3 letters return `400` with the ISO 4217 message. Sets the canonical `registered_capital_currency` column and is mirrored into `registry_data`. - website: Company website. Bare domains are accepted and normalized with an `https://` prefix (`docsprobe.example.com` is stored as `https://docsprobe.example.com`); values that still fail URL validation return `400`. - email: Contact email on file with the registry. - phone: Contact phone. Must match `+`, digits, spaces, dashes, dots, or parentheses, with 7-15 digits total - otherwise `400`. - legal_entity_identifier: LEI, where the company has one. - location_of_registration: City / subdivision of the registry of record. - vat_number: EU VAT number. Merged into the `registry_data` snapshot only - editing it here does not re-run VAT and tax ID validation, so `vat_validation_status` and the other `vat_*` result fields are unchanged. - control_scheme: Control / governance scheme when the registry exposes it. Stored in the registry snapshot only. + session_id: UUID of the sandbox verification session. Accepts both user (KYC) and business (KYB) session IDs. + new_status: The status to set. When you leave it out, it defaults to the default status of the risk case (`risk`, or the first risk the scenario's magic values carry), else to the scenario's default status. + comment: Note added to the activity entry the status change records. + scenario: A scenario slug from the catalog (`GET /v1/sandbox/scenarios/`). It supplies the default status and, without `risk`, the warning its magic values carry. An unknown slug returns `400`. + risk: A risk case from the catalog. Its warning is added to the session, with a log type that follows `new_status`. An unknown value returns `400`. + node_id: Workflow node the risk warning is attached to. Defaults to the session's current node. + send_webhooks: `true` sends exactly the webhooks a real status change sends to the sandbox application's destinations: `status.updated`, plus the `user.status.updated` / `user.data.updated` (or `business.status.updated` / `business.data.updated`) events the change causes, for destinations subscribed to them. `false` changes the status without logging or sending any webhook: the missed-webhook recovery never re-sends that decision, the console still updates live, and the next status change of the session sends webhooks as usual. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsUpdateKybCompanyDataResponse", + "SandboxSimulateResponse", await self._client.request( - "patch", - "/v3/session/{sessionId}/kyb/{companyUuid}/update-data/", - cast_to=SessionsUpdateKybCompanyDataResponse, - path_params={"sessionId": session_id, "companyUuid": company_uuid}, + "post", + "/v3/session/{sessionId}/simulate/", + cast_to=SandboxSimulateResponse, + path_params={"sessionId": session_id}, auth="api_key", body={ - "company_name": company_name, - "registration_number": registration_number, - "country_code": country_code, - "region": region, - "incorporation_date": incorporation_date, - "registration_date": registration_date, - "dissolution_date": dissolution_date, - "tax_number": tax_number, - "legal_address": legal_address, - "company_type": company_type, - "registry_status": registry_status, - "verification_status": verification_status, - "alternative_names": alternative_names, - "nature_of_business": nature_of_business, - "registered_capital": registered_capital, - "registered_capital_amount": registered_capital_amount, - "registered_capital_currency": registered_capital_currency, - "website": website, - "email": email, - "phone": phone, - "legal_entity_identifier": legal_entity_identifier, - "location_of_registration": location_of_registration, - "vat_number": vat_number, - "control_scheme": control_scheme, + "new_status": new_status, + "comment": comment, + "scenario": scenario, + "risk": risk, + "node_id": node_id, + "send_webhooks": send_webhooks, }, extra_headers=extra_headers, extra_query=extra_query, @@ -2766,49 +4059,40 @@ async def update_kyb_company_data( ), ) - async def update_kyb_party( + async def list_feature_overrides( self, session_id: str, - party_uuid: str, *, - is_skipped: bool, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsUpdateKybPartyResponse: - """Skip (or re-require) a Key People party on a Business Verification (KYB) session. - - Exempt a Key People party from verification, or put it back in scope. This is the operator escape hatch for a party that can never finish its own check - most often because the individual KYC session handed to a UBO or director was deleted, which freezes that child's status and leaves the KYB parked in `Awaiting User` with nothing left that could move it. + ) -> SessionsListFeatureOverridesResponse: + """List a session's check overrides. - Setting `is_skipped: true` drops the party out of the Key People aggregation and immediately recomputes the `KYBKeyPeopleCheck` and the parent session status, so the business session can settle and then be approved or declined. Sending `is_skipped: false` puts the party back in scope and the check goes back to waiting on it. - - Unlike the applicant-facing skip in the hosted Key People screen, this endpoint is **not** gated by the role's `allow_skip` setting - that flag limits what the applicant may decide, not what your analyst may override. `requires_verification` is deliberately left untouched, so an overridden party reads as `requires_verification: true` **and** `is_skipped: true` and the record still shows what the rules asked for. + Every override of an individual feature (step) status on a User Verification (KYC) or Business Verification (KYB) session, newest first and not paginated: applied ones, ones waiting for the person to finish the flow (`pending_until_flow_finishes`) and cancelled ones, each with who asked, their reason, and whether it applied while the person was still in the flow (`forced`). - The party must belong to the session in the path (`404` with `"Party does not belong to the given session."` otherwise) and to your application. A PATCH that actually changes the flag fires the `data.updated` [webhook](/integration/webhooks) on the business session with trigger `console_key_people_skip`; a PATCH that sets the flag to its current value is a no-op that echoes the party back. + `person_in_flow` tells you whether a new override with `defer_until_flow_finishes: true` would wait. `flow_finished_at` is when the person last finished the flow; it is `null` while they are still in it (a resubmission included) and when the session closed before they finished. - Accepts an API key or a console user token; either way the credential needs the `write:businesses` permission. + Requires the `read:sessions` privilege; a scoped API key needs read access to decisions. - ``PATCH /v3/session/{sessionId}/kyb/parties/{partyUuid}/`` + ``GET /v3/session/{sessionId}/features/overrides/`` Args: - session_id: UUID of the parent Business Verification (KYB) session. - party_uuid: UUID of the Key People party to update (`key_people_checks[].submitted.parties[].uuid` on the business session decision). Must belong to the session referenced by `sessionId`. - is_skipped: `true` exempts the party from verification so the Key People check can settle without it; `false` puts it back in scope. + session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsUpdateKybPartyResponse", + "SessionsListFeatureOverridesResponse", await self._client.request( - "patch", - "/v3/session/{sessionId}/kyb/parties/{partyUuid}/", - cast_to=SessionsUpdateKybPartyResponse, - path_params={"sessionId": session_id, "partyUuid": party_uuid}, + "get", + "/v3/session/{sessionId}/features/overrides/", + cast_to=SessionsListFeatureOverridesResponse, + path_params={"sessionId": session_id}, auth="api_key", - body={"is_skipped": is_skipped}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -2816,36 +4100,41 @@ async def update_kyb_party( ), ) - async def list_networks( + async def cancel_feature_override( self, session_id: str, + override_id: str, *, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsListNetworksResponse: - """List networks for a session. + ) -> FeatureStatusOverride: + """Cancel a check override that waits for the flow to finish. - List the networks the session's vendor user belongs to. Resolves through the session's vendor_data-linked user. + Cancel an override of a feature (step) status that is still `pending_until_flow_finishes`, so it never applies. The check keeps its status, the cancellation is recorded in the session's activity as `FEATURE_OVERRIDE_CANCELLED`, and no webhook is sent. The response is the override with `state: cancelled` and `cancel_reason: cancelled_by_reviewer`. - ``GET /v3/organization/{organization_id}/application/{application_id}/sessions/{session_id}/networks/`` + An override that already applied or was cancelled returns `409` with the code `feature_override_not_pending`. Find override IDs with `GET /v3/session/{sessionId}/features/overrides/` or in the `override` returned by the feature status update. + + Requires the `write:sessions` privilege; a scoped API key needs write access to decisions. + + ``POST /v3/session/{sessionId}/features/overrides/{overrideId}/cancel/`` Args: - session_id: Session UUID. + session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs. + override_id: The `id` of the override to cancel. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsListNetworksResponse", + "FeatureStatusOverride", await self._client.request( - "get", - "/v3/organization/{organization_id}/application/{application_id}/sessions/{session_id}/networks/", - cast_to=SessionsListNetworksResponse, - path_params={"session_id": session_id}, - scope="application", + "post", + "/v3/session/{sessionId}/features/overrides/{overrideId}/cancel/", + cast_to=FeatureStatusOverride, + path_params={"sessionId": session_id, "overrideId": override_id}, auth="api_key", extra_headers=extra_headers, extra_query=extra_query, @@ -2854,52 +4143,47 @@ async def list_networks( ), ) - async def preview_resubmission( + async def preview_decision_email( self, session_id: str, *, - email_address: str, - nodes_to_resubmit: Sequence[SessionsPreviewResubmissionParamsNodesToResubmit] - | NotGiven = NOT_GIVEN, + new_status: Literal["Approved", "Declined"] | str, email_language: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> SessionsPreviewResubmissionResponse: - """Preview a resubmission email. + ) -> SessionsPreviewDecisionEmailResponse: + """Preview a session's decision email. - Render the email that would accompany a KYC or KYB resubmission without changing the session, resetting checks, extending its expiry, running checks or sending mail. Requires write:sessions and an application-scoped session in Approved, Declined, In Review, Kyc Expired or Abandoned status. The server validates the selected workflow steps and renders its fixed translated template with the applicable verified white-label sender and branding. The request cannot set a subject, message body or action URL. + Render the email the person receives when you set a session to `Approved` or `Declined` with `send_email: true`, without changing the session or sending anything. The response shows the subject and HTML exactly as they are sent, the recipient, the sender, the language, and whether confirming the change sends the email at all (`will_be_sent`, `not_sent_reason`). - The response includes the recipient, sender, subject, HTML, resolved steps and a signed preview_token. requires_user_action describes whether the selected steps include an interactive check. Email delivery remains conditional: if automatic checks finalize the verification during the subsequent resubmission, no action-request email is sent. + The recipient is the person's own address: the one they verified in an email verification step of the flow, else the one given in `contact_details` when the session was created. The sender is your application's verified sender domain when white label applies (`sender_kind: customer`), else Didit's sender (`sender_kind: didit`). - This preview does not reserve session state or authorize delivery. After reviewing it, call Update Session Status with new_status=Resubmitted, the same selection, recipient and language, and email_preview_token set to the returned preview_token. The receipt expires after 15 minutes. When a receipt is supplied, changed inputs, message content, sender or session state return 409 before any mutation or send. Existing integrations may omit the receipt. Regenerate the preview after a conflict. Treat the response as sensitive verification content; responses use Cache-Control: no-store. Render HTML in a sandboxed frame, never directly in your application DOM. + To send exactly what you previewed, call [Update Session Status](/sessions-api/update-status) with the same `new_status`, `send_email: true`, `email_address` set to `recipient`, `email_language` set to `language` and `email_preview_token` set to `preview_token`. The receipt expires after 15 minutes. When the recipient, language, session status, sender or email changed since the preview, the update returns `409` before anything changes; preview again and confirm with the new receipt. - ``POST /v3/session/{sessionId}/resubmission-preview/`` + Works for User Verification (KYC) and Business Verification (KYB) sessions in `Approved`, `Declined`, `In Review`, `Kyc Expired`, `Abandoned` or `Resubmitted` status. The status check runs before body validation. Requires the `write:sessions` privilege; a scoped API key needs write access to decisions. Responses use `Cache-Control: no-store`: treat them as sensitive verification content. + + ``POST /v3/session/{sessionId}/decision-email-preview/`` Args: - session_id: UUID of the verification session to update. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. - email_address: Recipient shown in the preview. No email is sent. - nodes_to_resubmit: Steps to preview, using the same validation, selection and workflow ordering as a resubmission. Omit to auto-select recorded attempts needing resubmission. - email_language: Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`). + session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs. + new_status: The decision whose email you want to preview. + email_language: Language of the email, the same default as Update Session Status. A language that is not offered falls back to English; `language` in the response says which one is used. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "SessionsPreviewResubmissionResponse", + "SessionsPreviewDecisionEmailResponse", await self._client.request( "post", - "/v3/session/{sessionId}/resubmission-preview/", - cast_to=SessionsPreviewResubmissionResponse, + "/v3/session/{sessionId}/decision-email-preview/", + cast_to=SessionsPreviewDecisionEmailResponse, path_params={"sessionId": session_id}, auth="api_key", - body={ - "email_address": email_address, - "nodes_to_resubmit": nodes_to_resubmit, - "email_language": email_language, - }, + body={"new_status": new_status, "email_language": email_language}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -2932,6 +4216,21 @@ def __init__(self, resource: Sessions) -> None: self.update_kyb_party = to_raw_response_wrapper(resource.update_kyb_party) self.list_networks = to_raw_response_wrapper(resource.list_networks) self.preview_resubmission = to_raw_response_wrapper(resource.preview_resubmission) + self.update_document_ai_data = to_raw_response_wrapper(resource.update_document_ai_data) + self.regenerate_kyb_party = to_raw_response_wrapper(resource.regenerate_kyb_party) + self.list_events = to_raw_response_wrapper(resource.list_events) + self.list_aml_screening_history = to_raw_response_wrapper( + resource.list_aml_screening_history + ) + self.export_aml_screening_history = to_raw_response_wrapper( + resource.export_aml_screening_history + ) + self.add_images = to_raw_response_wrapper(resource.add_images) + self.update_aml_status = to_raw_response_wrapper(resource.update_aml_status) + self.simulate = to_raw_response_wrapper(resource.simulate) + self.list_feature_overrides = to_raw_response_wrapper(resource.list_feature_overrides) + self.cancel_feature_override = to_raw_response_wrapper(resource.cancel_feature_override) + self.preview_decision_email = to_raw_response_wrapper(resource.preview_decision_email) @cached_property def imports(self) -> SessionsImportsWithRawResponse: @@ -2945,6 +4244,18 @@ def reviews(self) -> SessionsReviewsWithRawResponse: return SessionsReviewsWithRawResponse(self._resource.reviews) + @cached_property + def signatures(self) -> SessionsSignaturesWithRawResponse: + from didit.resources.sessions.signatures import SessionsSignaturesWithRawResponse + + return SessionsSignaturesWithRawResponse(self._resource.signatures) + + @cached_property + def webhooks(self) -> SessionsWebhooksWithRawResponse: + from didit.resources.sessions.webhooks import SessionsWebhooksWithRawResponse + + return SessionsWebhooksWithRawResponse(self._resource.webhooks) + class AsyncSessionsWithRawResponse: def __init__(self, resource: AsyncSessions) -> None: @@ -2972,6 +4283,25 @@ def __init__(self, resource: AsyncSessions) -> None: self.update_kyb_party = async_to_raw_response_wrapper(resource.update_kyb_party) self.list_networks = async_to_raw_response_wrapper(resource.list_networks) self.preview_resubmission = async_to_raw_response_wrapper(resource.preview_resubmission) + self.update_document_ai_data = async_to_raw_response_wrapper( + resource.update_document_ai_data + ) + self.regenerate_kyb_party = async_to_raw_response_wrapper(resource.regenerate_kyb_party) + self.list_events = async_to_raw_response_wrapper(resource.list_events) + self.list_aml_screening_history = async_to_raw_response_wrapper( + resource.list_aml_screening_history + ) + self.export_aml_screening_history = async_to_raw_response_wrapper( + resource.export_aml_screening_history + ) + self.add_images = async_to_raw_response_wrapper(resource.add_images) + self.update_aml_status = async_to_raw_response_wrapper(resource.update_aml_status) + self.simulate = async_to_raw_response_wrapper(resource.simulate) + self.list_feature_overrides = async_to_raw_response_wrapper(resource.list_feature_overrides) + self.cancel_feature_override = async_to_raw_response_wrapper( + resource.cancel_feature_override + ) + self.preview_decision_email = async_to_raw_response_wrapper(resource.preview_decision_email) @cached_property def imports(self) -> AsyncSessionsImportsWithRawResponse: @@ -2985,6 +4315,18 @@ def reviews(self) -> AsyncSessionsReviewsWithRawResponse: return AsyncSessionsReviewsWithRawResponse(self._resource.reviews) + @cached_property + def signatures(self) -> AsyncSessionsSignaturesWithRawResponse: + from didit.resources.sessions.signatures import AsyncSessionsSignaturesWithRawResponse + + return AsyncSessionsSignaturesWithRawResponse(self._resource.signatures) + + @cached_property + def webhooks(self) -> AsyncSessionsWebhooksWithRawResponse: + from didit.resources.sessions.webhooks import AsyncSessionsWebhooksWithRawResponse + + return AsyncSessionsWebhooksWithRawResponse(self._resource.webhooks) + class SessionsWithStreamingResponse: def __init__(self, resource: Sessions) -> None: @@ -3012,6 +4354,25 @@ def __init__(self, resource: Sessions) -> None: self.update_kyb_party = to_streamed_response_wrapper(resource.update_kyb_party) self.list_networks = to_streamed_response_wrapper(resource.list_networks) self.preview_resubmission = to_streamed_response_wrapper(resource.preview_resubmission) + self.update_document_ai_data = to_streamed_response_wrapper( + resource.update_document_ai_data + ) + self.regenerate_kyb_party = to_streamed_response_wrapper(resource.regenerate_kyb_party) + self.list_events = to_streamed_response_wrapper(resource.list_events) + self.list_aml_screening_history = to_streamed_response_wrapper( + resource.list_aml_screening_history + ) + self.export_aml_screening_history = to_streamed_response_wrapper( + resource.export_aml_screening_history + ) + self.add_images = to_streamed_response_wrapper(resource.add_images) + self.update_aml_status = to_streamed_response_wrapper(resource.update_aml_status) + self.simulate = to_streamed_response_wrapper(resource.simulate) + self.list_feature_overrides = to_streamed_response_wrapper(resource.list_feature_overrides) + self.cancel_feature_override = to_streamed_response_wrapper( + resource.cancel_feature_override + ) + self.preview_decision_email = to_streamed_response_wrapper(resource.preview_decision_email) @cached_property def imports(self) -> SessionsImportsWithStreamingResponse: @@ -3025,6 +4386,18 @@ def reviews(self) -> SessionsReviewsWithStreamingResponse: return SessionsReviewsWithStreamingResponse(self._resource.reviews) + @cached_property + def signatures(self) -> SessionsSignaturesWithStreamingResponse: + from didit.resources.sessions.signatures import SessionsSignaturesWithStreamingResponse + + return SessionsSignaturesWithStreamingResponse(self._resource.signatures) + + @cached_property + def webhooks(self) -> SessionsWebhooksWithStreamingResponse: + from didit.resources.sessions.webhooks import SessionsWebhooksWithStreamingResponse + + return SessionsWebhooksWithStreamingResponse(self._resource.webhooks) + class AsyncSessionsWithStreamingResponse: def __init__(self, resource: AsyncSessions) -> None: @@ -3060,6 +4433,31 @@ def __init__(self, resource: AsyncSessions) -> None: self.preview_resubmission = async_to_streamed_response_wrapper( resource.preview_resubmission ) + self.update_document_ai_data = async_to_streamed_response_wrapper( + resource.update_document_ai_data + ) + self.regenerate_kyb_party = async_to_streamed_response_wrapper( + resource.regenerate_kyb_party + ) + self.list_events = async_to_streamed_response_wrapper(resource.list_events) + self.list_aml_screening_history = async_to_streamed_response_wrapper( + resource.list_aml_screening_history + ) + self.export_aml_screening_history = async_to_streamed_response_wrapper( + resource.export_aml_screening_history + ) + self.add_images = async_to_streamed_response_wrapper(resource.add_images) + self.update_aml_status = async_to_streamed_response_wrapper(resource.update_aml_status) + self.simulate = async_to_streamed_response_wrapper(resource.simulate) + self.list_feature_overrides = async_to_streamed_response_wrapper( + resource.list_feature_overrides + ) + self.cancel_feature_override = async_to_streamed_response_wrapper( + resource.cancel_feature_override + ) + self.preview_decision_email = async_to_streamed_response_wrapper( + resource.preview_decision_email + ) @cached_property def imports(self) -> AsyncSessionsImportsWithStreamingResponse: @@ -3072,3 +4470,15 @@ def reviews(self) -> AsyncSessionsReviewsWithStreamingResponse: from didit.resources.sessions.reviews import AsyncSessionsReviewsWithStreamingResponse return AsyncSessionsReviewsWithStreamingResponse(self._resource.reviews) + + @cached_property + def signatures(self) -> AsyncSessionsSignaturesWithStreamingResponse: + from didit.resources.sessions.signatures import AsyncSessionsSignaturesWithStreamingResponse + + return AsyncSessionsSignaturesWithStreamingResponse(self._resource.signatures) + + @cached_property + def webhooks(self) -> AsyncSessionsWebhooksWithStreamingResponse: + from didit.resources.sessions.webhooks import AsyncSessionsWebhooksWithStreamingResponse + + return AsyncSessionsWebhooksWithStreamingResponse(self._resource.webhooks) diff --git a/src/didit/resources/sessions/signatures.py b/src/didit/resources/sessions/signatures.py new file mode 100644 index 0000000..4865e3d --- /dev/null +++ b/src/didit/resources/sessions/signatures.py @@ -0,0 +1,450 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``sessions.signatures`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.sessions.signatures.validate_response import SessionsSignaturesValidateResponse + +__all__ = [ + "SessionsSignatures", + "AsyncSessionsSignatures", + "SessionsSignaturesWithRawResponse", + "AsyncSessionsSignaturesWithRawResponse", + "SessionsSignaturesWithStreamingResponse", + "AsyncSessionsSignaturesWithStreamingResponse", +] + + +class SessionsSignatures(SyncAPIResource): + """The ``sessions.signatures`` resource of the Didit API. + + Methods: ``download_document``, ``download_evidence``, ``download_field_image``, ``validate``. + """ + + @cached_property + def with_raw_response(self) -> SessionsSignaturesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return SessionsSignaturesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SessionsSignaturesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return SessionsSignaturesWithStreamingResponse(self) + + def download_document( + self, + session_id: str, + node_id: str, + document_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download a signed document. + + Download the signed PDF of one document of an Electronic Signature step. Use the `signed_file` path of `signatures[].documents[]` in the decision. The PDF holds the document the signer was shown with their field values stamped, the evidence page (unless `signature_evidence_page` is off) and the PAdES signature with its timestamp. Didit checks the file against `sha256_signed` before returning it. + + Works for User Verification (KYC) and Business Verification (KYB) sessions. Needs `read:sessions`; your API key works. See the [Electronic Signature report](/core-technology/electronic-signature/report-electronic-signature#download-the-signed-documents). + + ``GET /v3/session/{sessionId}/signatures/{nodeId}/documents/{documentId}/file/`` + + Args: + session_id: The User Verification (KYC) or Business Verification (KYB) session. + node_id: The Electronic Signature step: `node_id` of the item in `signatures[]`. + document_id: `id` of the document in `signatures[].documents[]`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + self._client.request( + "get", + "/v3/session/{sessionId}/signatures/{nodeId}/documents/{documentId}/file/", + cast_to=bytes, + path_params={"sessionId": session_id, "nodeId": node_id, "documentId": document_id}, + auth="api_key", + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def download_evidence( + self, + session_id: str, + node_id: str, + *, + signature_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download the signature evidence. + + Download a signer's evidence PDF: the level and format, the signer and their verified identity session, the code confirmation, the IP address, each document with its original SHA-256 and field values, the certificate, the timestamp and the SHA-256 of every signed file. Use the `evidence_file` path of the signer's item in `signatures[]`. + + Always pass `signature_id` when a Business Verification step has several signers: without it, the route picks one of the step's signers. Needs `read:sessions`; your API key works. + + ``GET /v3/session/{sessionId}/signatures/{nodeId}/evidence/`` + + Args: + session_id: The User Verification (KYC) or Business Verification (KYB) session. + node_id: The Electronic Signature step: `node_id` of the item in `signatures[]`. + signature_id: `id` of the signer's item in `signatures[]`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + self._client.request( + "get", + "/v3/session/{sessionId}/signatures/{nodeId}/evidence/", + cast_to=bytes, + path_params={"sessionId": session_id, "nodeId": node_id}, + auth="api_key", + query={"signature_id": signature_id}, + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def download_field_image( + self, + session_id: str, + node_id: str, + field_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download a signature image. + + Download the signature or initials a signer applied to a field, as PNG. Use the `image_url` of the field in `signatures[].documents[].fields[]`. Needs `read:sessions`; your API key works. + + ``GET /v3/session/{sessionId}/signatures/{nodeId}/fields/{fieldId}/image/`` + + Args: + session_id: The User Verification (KYC) or Business Verification (KYB) session. + node_id: The Electronic Signature step: `node_id` of the item in `signatures[]`. + field_id: The field on this signer's copy, as in the field's `image_url`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + self._client.request( + "get", + "/v3/session/{sessionId}/signatures/{nodeId}/fields/{fieldId}/image/", + cast_to=bytes, + path_params={"sessionId": session_id, "nodeId": node_id, "fieldId": field_id}, + auth="api_key", + accept="image/png, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def validate( + self, + session_id: str, + node_id: str, + *, + signature_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsSignaturesValidateResponse: + """Validate signatures again. + + Re-check the signed documents of an Electronic Signature step. Didit re-reads every signed PDF, checks its SHA-256 and validates the PAdES signature, the certificate, the revocation data and the timestamp, then saves the result in each signer's `validation` and returns the updated items. Nothing is signed again. Send `signature_id` to check one signer; without it, every signer of the step is checked, and the call fails if any of them hasn't signed. + + Needs `write:sessions`; your API key works. In the console it's the **Validate again** button. + + ``POST /v3/session/{sessionId}/signatures/{nodeId}/validate/`` + + Args: + session_id: The User Verification (KYC) or Business Verification (KYB) session. + node_id: The Electronic Signature step: `node_id` of the item in `signatures[]`. + signature_id: `id` of one signer's item in `signatures[]`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsSignaturesValidateResponse", + self._client.request( + "post", + "/v3/session/{sessionId}/signatures/{nodeId}/validate/", + cast_to=SessionsSignaturesValidateResponse, + path_params={"sessionId": session_id, "nodeId": node_id}, + auth="api_key", + body={"signature_id": signature_id}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncSessionsSignatures(AsyncAPIResource): + """The ``sessions.signatures`` resource of the Didit API. + + Methods: ``download_document``, ``download_evidence``, ``download_field_image``, ``validate``. + """ + + @cached_property + def with_raw_response(self) -> AsyncSessionsSignaturesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncSessionsSignaturesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSessionsSignaturesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncSessionsSignaturesWithStreamingResponse(self) + + async def download_document( + self, + session_id: str, + node_id: str, + document_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download a signed document. + + Download the signed PDF of one document of an Electronic Signature step. Use the `signed_file` path of `signatures[].documents[]` in the decision. The PDF holds the document the signer was shown with their field values stamped, the evidence page (unless `signature_evidence_page` is off) and the PAdES signature with its timestamp. Didit checks the file against `sha256_signed` before returning it. + + Works for User Verification (KYC) and Business Verification (KYB) sessions. Needs `read:sessions`; your API key works. See the [Electronic Signature report](/core-technology/electronic-signature/report-electronic-signature#download-the-signed-documents). + + ``GET /v3/session/{sessionId}/signatures/{nodeId}/documents/{documentId}/file/`` + + Args: + session_id: The User Verification (KYC) or Business Verification (KYB) session. + node_id: The Electronic Signature step: `node_id` of the item in `signatures[]`. + document_id: `id` of the document in `signatures[].documents[]`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + await self._client.request( + "get", + "/v3/session/{sessionId}/signatures/{nodeId}/documents/{documentId}/file/", + cast_to=bytes, + path_params={"sessionId": session_id, "nodeId": node_id, "documentId": document_id}, + auth="api_key", + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def download_evidence( + self, + session_id: str, + node_id: str, + *, + signature_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download the signature evidence. + + Download a signer's evidence PDF: the level and format, the signer and their verified identity session, the code confirmation, the IP address, each document with its original SHA-256 and field values, the certificate, the timestamp and the SHA-256 of every signed file. Use the `evidence_file` path of the signer's item in `signatures[]`. + + Always pass `signature_id` when a Business Verification step has several signers: without it, the route picks one of the step's signers. Needs `read:sessions`; your API key works. + + ``GET /v3/session/{sessionId}/signatures/{nodeId}/evidence/`` + + Args: + session_id: The User Verification (KYC) or Business Verification (KYB) session. + node_id: The Electronic Signature step: `node_id` of the item in `signatures[]`. + signature_id: `id` of the signer's item in `signatures[]`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + await self._client.request( + "get", + "/v3/session/{sessionId}/signatures/{nodeId}/evidence/", + cast_to=bytes, + path_params={"sessionId": session_id, "nodeId": node_id}, + auth="api_key", + query={"signature_id": signature_id}, + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def download_field_image( + self, + session_id: str, + node_id: str, + field_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download a signature image. + + Download the signature or initials a signer applied to a field, as PNG. Use the `image_url` of the field in `signatures[].documents[].fields[]`. Needs `read:sessions`; your API key works. + + ``GET /v3/session/{sessionId}/signatures/{nodeId}/fields/{fieldId}/image/`` + + Args: + session_id: The User Verification (KYC) or Business Verification (KYB) session. + node_id: The Electronic Signature step: `node_id` of the item in `signatures[]`. + field_id: The field on this signer's copy, as in the field's `image_url`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + await self._client.request( + "get", + "/v3/session/{sessionId}/signatures/{nodeId}/fields/{fieldId}/image/", + cast_to=bytes, + path_params={"sessionId": session_id, "nodeId": node_id, "fieldId": field_id}, + auth="api_key", + accept="image/png, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def validate( + self, + session_id: str, + node_id: str, + *, + signature_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsSignaturesValidateResponse: + """Validate signatures again. + + Re-check the signed documents of an Electronic Signature step. Didit re-reads every signed PDF, checks its SHA-256 and validates the PAdES signature, the certificate, the revocation data and the timestamp, then saves the result in each signer's `validation` and returns the updated items. Nothing is signed again. Send `signature_id` to check one signer; without it, every signer of the step is checked, and the call fails if any of them hasn't signed. + + Needs `write:sessions`; your API key works. In the console it's the **Validate again** button. + + ``POST /v3/session/{sessionId}/signatures/{nodeId}/validate/`` + + Args: + session_id: The User Verification (KYC) or Business Verification (KYB) session. + node_id: The Electronic Signature step: `node_id` of the item in `signatures[]`. + signature_id: `id` of one signer's item in `signatures[]`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsSignaturesValidateResponse", + await self._client.request( + "post", + "/v3/session/{sessionId}/signatures/{nodeId}/validate/", + cast_to=SessionsSignaturesValidateResponse, + path_params={"sessionId": session_id, "nodeId": node_id}, + auth="api_key", + body={"signature_id": signature_id}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class SessionsSignaturesWithRawResponse: + def __init__(self, resource: SessionsSignatures) -> None: + self._resource = resource + self.download_document = to_raw_response_wrapper(resource.download_document) + self.download_evidence = to_raw_response_wrapper(resource.download_evidence) + self.download_field_image = to_raw_response_wrapper(resource.download_field_image) + self.validate = to_raw_response_wrapper(resource.validate) + + +class AsyncSessionsSignaturesWithRawResponse: + def __init__(self, resource: AsyncSessionsSignatures) -> None: + self._resource = resource + self.download_document = async_to_raw_response_wrapper(resource.download_document) + self.download_evidence = async_to_raw_response_wrapper(resource.download_evidence) + self.download_field_image = async_to_raw_response_wrapper(resource.download_field_image) + self.validate = async_to_raw_response_wrapper(resource.validate) + + +class SessionsSignaturesWithStreamingResponse: + def __init__(self, resource: SessionsSignatures) -> None: + self._resource = resource + self.download_document = to_streamed_response_wrapper(resource.download_document) + self.download_evidence = to_streamed_response_wrapper(resource.download_evidence) + self.download_field_image = to_streamed_response_wrapper(resource.download_field_image) + self.validate = to_streamed_response_wrapper(resource.validate) + + +class AsyncSessionsSignaturesWithStreamingResponse: + def __init__(self, resource: AsyncSessionsSignatures) -> None: + self._resource = resource + self.download_document = async_to_streamed_response_wrapper(resource.download_document) + self.download_evidence = async_to_streamed_response_wrapper(resource.download_evidence) + self.download_field_image = async_to_streamed_response_wrapper( + resource.download_field_image + ) + self.validate = async_to_streamed_response_wrapper(resource.validate) diff --git a/src/didit/resources/sessions/webhooks.py b/src/didit/resources/sessions/webhooks.py new file mode 100644 index 0000000..974bc3f --- /dev/null +++ b/src/didit/resources/sessions/webhooks.py @@ -0,0 +1,325 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``sessions.webhooks`` resource.""" + +from __future__ import annotations + +import builtins +from functools import cached_property +from typing import cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.session_webhook_delivery import SessionWebhookDelivery + +__all__ = [ + "SessionsWebhooks", + "AsyncSessionsWebhooks", + "SessionsWebhooksWithRawResponse", + "AsyncSessionsWebhooksWithRawResponse", + "SessionsWebhooksWithStreamingResponse", + "AsyncSessionsWebhooksWithStreamingResponse", +] + + +class SessionsWebhooks(SyncAPIResource): + """The ``sessions.webhooks`` resource of the Didit API. + + Methods: ``list``, ``resend``, ``retrieve``. + """ + + @cached_property + def with_raw_response(self) -> SessionsWebhooksWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return SessionsWebhooksWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SessionsWebhooksWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return SessionsWebhooksWithStreamingResponse(self) + + def list( + self, + session_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.list[SessionWebhookDelivery]: + """List session webhook deliveries. + + List every webhook delivery Didit recorded for one verification session, user (KYC) or business (KYB), newest first, as a plain JSON array (not paginated). Each entry is one attempt to one destination: the first send, each automatic [retry](https://docs.didit.me/integration/webhooks#retry-policy) and each resend are separate entries with their own `uuid`, and retries and resends keep the payload's `event_id`. `request_header` and `request_body` are JSON text; parse them to read the signatures and the payload. Delivery records are available for about 30 days, so an empty array means no webhook has been sent for the session yet or every delivery is older than that. A scoped API key needs `sessions: read` and `decisions: read`. + + ``GET /v3/session/{sessionId}/webhooks/`` + + Args: + session_id: UUID of the verification session, user (KYC) or business (KYB): the `session_id` returned by `POST /v3/session/`. The server resolves it against both session types. **Must be a valid UUID.** The route only matches well-formed UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{"detail": ...}` envelope shown below. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.list[SessionWebhookDelivery]", + self._client.request( + "get", + "/v3/session/{sessionId}/webhooks/", + cast_to=builtins.list[SessionWebhookDelivery], + path_params={"sessionId": session_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + session_id: str, + webhook_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionWebhookDelivery: + """Get session webhook delivery. + + Retrieve one webhook delivery of a verification session: the URL it went to, the HTTP status your endpoint returned, and the exact headers and JSON body Didit sent. `request_header` and `request_body` are JSON text; to check your [signature verification](https://docs.didit.me/integration/webhooks#signature-verification), recompute `X-Signature-V2` from the parsed body with the secret of the destination it was sent to (or the application-level secret for a delivery to the legacy webhook URL). The path has no trailing slash: with one it returns `404`. A scoped API key needs `sessions: read` and `decisions: read`. + + ``GET /v3/session/{sessionId}/webhook/{webhook_uuid}`` + + Args: + session_id: UUID of the verification session, user (KYC) or business (KYB): the `session_id` returned by `POST /v3/session/`. The server resolves it against both session types. **Must be a valid UUID.** The route only matches well-formed UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{"detail": ...}` envelope shown below. + webhook_uuid: UUID of the delivery: the `uuid` of an entry returned by `GET /v3/session/{sessionId}/webhooks/`. It must belong to the session in the path. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionWebhookDelivery", + self._client.request( + "get", + "/v3/session/{sessionId}/webhook/{webhook_uuid}", + cast_to=SessionWebhookDelivery, + path_params={"sessionId": session_id, "webhook_uuid": webhook_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def resend( + self, + session_id: str, + webhook_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionWebhookDelivery: + """Resend session webhook delivery. + + Send a recorded session webhook again to the URL it first went to, with the same payload and `event_id` and a fresh `timestamp`, `X-Timestamp` header and signatures. Didit signs it with the current secret of the destination the delivery belongs to (or the application-level secret for a delivery to the legacy webhook URL), waits up to 5 seconds for your endpoint, records the attempt as a new delivery and returns it with `200` whatever your endpoint answered: read `response_status`. A resend is not retried automatically, and every call sends again, so make your endpoint idempotent on `event_id`. The path has no trailing slash and the request has no body. A scoped API key needs `webhooks: write` and `decisions: read`. + + ``POST /v3/session/{sessionId}/webhook/{webhook_uuid}/resend`` + + Args: + session_id: UUID of the verification session, user (KYC) or business (KYB): the `session_id` returned by `POST /v3/session/`. The server resolves it against both session types. **Must be a valid UUID.** The route only matches well-formed UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{"detail": ...}` envelope shown below. + webhook_uuid: UUID of the delivery to resend: the `uuid` of an entry returned by `GET /v3/session/{sessionId}/webhooks/`. It must belong to the session in the path. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionWebhookDelivery", + self._client.request( + "post", + "/v3/session/{sessionId}/webhook/{webhook_uuid}/resend", + cast_to=SessionWebhookDelivery, + path_params={"sessionId": session_id, "webhook_uuid": webhook_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncSessionsWebhooks(AsyncAPIResource): + """The ``sessions.webhooks`` resource of the Didit API. + + Methods: ``list``, ``resend``, ``retrieve``. + """ + + @cached_property + def with_raw_response(self) -> AsyncSessionsWebhooksWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncSessionsWebhooksWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSessionsWebhooksWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncSessionsWebhooksWithStreamingResponse(self) + + async def list( + self, + session_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.list[SessionWebhookDelivery]: + """List session webhook deliveries. + + List every webhook delivery Didit recorded for one verification session, user (KYC) or business (KYB), newest first, as a plain JSON array (not paginated). Each entry is one attempt to one destination: the first send, each automatic [retry](https://docs.didit.me/integration/webhooks#retry-policy) and each resend are separate entries with their own `uuid`, and retries and resends keep the payload's `event_id`. `request_header` and `request_body` are JSON text; parse them to read the signatures and the payload. Delivery records are available for about 30 days, so an empty array means no webhook has been sent for the session yet or every delivery is older than that. A scoped API key needs `sessions: read` and `decisions: read`. + + ``GET /v3/session/{sessionId}/webhooks/`` + + Args: + session_id: UUID of the verification session, user (KYC) or business (KYB): the `session_id` returned by `POST /v3/session/`. The server resolves it against both session types. **Must be a valid UUID.** The route only matches well-formed UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{"detail": ...}` envelope shown below. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.list[SessionWebhookDelivery]", + await self._client.request( + "get", + "/v3/session/{sessionId}/webhooks/", + cast_to=builtins.list[SessionWebhookDelivery], + path_params={"sessionId": session_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + session_id: str, + webhook_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionWebhookDelivery: + """Get session webhook delivery. + + Retrieve one webhook delivery of a verification session: the URL it went to, the HTTP status your endpoint returned, and the exact headers and JSON body Didit sent. `request_header` and `request_body` are JSON text; to check your [signature verification](https://docs.didit.me/integration/webhooks#signature-verification), recompute `X-Signature-V2` from the parsed body with the secret of the destination it was sent to (or the application-level secret for a delivery to the legacy webhook URL). The path has no trailing slash: with one it returns `404`. A scoped API key needs `sessions: read` and `decisions: read`. + + ``GET /v3/session/{sessionId}/webhook/{webhook_uuid}`` + + Args: + session_id: UUID of the verification session, user (KYC) or business (KYB): the `session_id` returned by `POST /v3/session/`. The server resolves it against both session types. **Must be a valid UUID.** The route only matches well-formed UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{"detail": ...}` envelope shown below. + webhook_uuid: UUID of the delivery: the `uuid` of an entry returned by `GET /v3/session/{sessionId}/webhooks/`. It must belong to the session in the path. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionWebhookDelivery", + await self._client.request( + "get", + "/v3/session/{sessionId}/webhook/{webhook_uuid}", + cast_to=SessionWebhookDelivery, + path_params={"sessionId": session_id, "webhook_uuid": webhook_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def resend( + self, + session_id: str, + webhook_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionWebhookDelivery: + """Resend session webhook delivery. + + Send a recorded session webhook again to the URL it first went to, with the same payload and `event_id` and a fresh `timestamp`, `X-Timestamp` header and signatures. Didit signs it with the current secret of the destination the delivery belongs to (or the application-level secret for a delivery to the legacy webhook URL), waits up to 5 seconds for your endpoint, records the attempt as a new delivery and returns it with `200` whatever your endpoint answered: read `response_status`. A resend is not retried automatically, and every call sends again, so make your endpoint idempotent on `event_id`. The path has no trailing slash and the request has no body. A scoped API key needs `webhooks: write` and `decisions: read`. + + ``POST /v3/session/{sessionId}/webhook/{webhook_uuid}/resend`` + + Args: + session_id: UUID of the verification session, user (KYC) or business (KYB): the `session_id` returned by `POST /v3/session/`. The server resolves it against both session types. **Must be a valid UUID.** The route only matches well-formed UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{"detail": ...}` envelope shown below. + webhook_uuid: UUID of the delivery to resend: the `uuid` of an entry returned by `GET /v3/session/{sessionId}/webhooks/`. It must belong to the session in the path. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionWebhookDelivery", + await self._client.request( + "post", + "/v3/session/{sessionId}/webhook/{webhook_uuid}/resend", + cast_to=SessionWebhookDelivery, + path_params={"sessionId": session_id, "webhook_uuid": webhook_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class SessionsWebhooksWithRawResponse: + def __init__(self, resource: SessionsWebhooks) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.resend = to_raw_response_wrapper(resource.resend) + + +class AsyncSessionsWebhooksWithRawResponse: + def __init__(self, resource: AsyncSessionsWebhooks) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.resend = async_to_raw_response_wrapper(resource.resend) + + +class SessionsWebhooksWithStreamingResponse: + def __init__(self, resource: SessionsWebhooks) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.resend = to_streamed_response_wrapper(resource.resend) + + +class AsyncSessionsWebhooksWithStreamingResponse: + def __init__(self, resource: AsyncSessionsWebhooks) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.resend = async_to_streamed_response_wrapper(resource.resend) diff --git a/src/didit/resources/transactions/__init__.py b/src/didit/resources/transactions/__init__.py index f5d5b42..62a6893 100644 --- a/src/didit/resources/transactions/__init__.py +++ b/src/didit/resources/transactions/__init__.py @@ -18,7 +18,9 @@ to_streamed_response_wrapper, ) from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.entity_network_membership_list import EntityNetworkMembershipList from didit.types.components.transaction_detail import TransactionDetail +from didit.types.components.transaction_summary import TransactionSummary from didit.types.transactions.create_params import ( TransactionsCreateParamsCounterparty, TransactionsCreateParamsNetworkSnapshot, @@ -27,16 +29,21 @@ TransactionsCreateParamsTravelRuleDetails, ) from didit.types.transactions.create_sdk_token_response import TransactionsCreateSdkTokenResponse -from didit.types.transactions.list_networks_response import TransactionsListNetworksResponse -from didit.types.transactions.list_party_networks_response import ( - TransactionsListPartyNetworksResponse, -) +from didit.types.transactions.list_related_response import TransactionsListRelatedResponse from didit.types.transactions.list_response import ( TransactionsListResponse, TransactionsListResponseResults, ) if TYPE_CHECKING: + from didit.resources.transactions.notes import ( + AsyncTransactionsNotes, + AsyncTransactionsNotesWithRawResponse, + AsyncTransactionsNotesWithStreamingResponse, + TransactionsNotes, + TransactionsNotesWithRawResponse, + TransactionsNotesWithStreamingResponse, + ) from didit.resources.transactions.rules import ( AsyncTransactionsRules, AsyncTransactionsRulesWithRawResponse, @@ -67,11 +74,17 @@ class Transactions(SyncAPIResource): """The ``transactions`` resource of the Didit API. - Methods: ``create``, ``create_sdk_token``, ``list``, ``list_networks``, ``list_party_networks``, ``retrieve``. + Methods: ``create``, ``create_sdk_token``, ``generate_pdf``, ``list``, ``list_networks``, ``list_party_networks``, ``list_related``, ``retrieve``. - Nested resources: ``rules``, ``travel_rule``. + Nested resources: ``notes``, ``rules``, ``travel_rule``. """ + @cached_property + def notes(self) -> TransactionsNotes: + from didit.resources.transactions.notes import TransactionsNotes + + return TransactionsNotes(self._client) + @cached_property def rules(self) -> TransactionsRules: from didit.resources.transactions.rules import TransactionsRules @@ -99,6 +112,7 @@ def list( *, limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, + travel_rule_status: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -113,6 +127,7 @@ def list( Args: limit: Page size. Defaults to 50 when omitted. There is no enforced maximum, but keep pages small for latency. offset: Zero-based offset of the first record to return. Prefer following the `next` URL from the previous page. + travel_rule_status: Return only transactions whose Travel Rule transfer is in this exact status (for example `ON_HOLD` or `AWAITING_COUNTERPARTY`). `count` follows this filter; `total_transactions` still counts every transaction in the application. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -126,7 +141,7 @@ def list( cast_to=TransactionsListResponse, item_type=TransactionsListResponseResults, auth="api_key", - query={"limit": limit, "offset": offset}, + query={"limit": limit, "offset": offset, "travel_rule_status": travel_rule_status}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -166,7 +181,7 @@ def create( ) -> TransactionDetail: """Create transaction. - Submit a transaction for synchronous AML/risk monitoring. The transaction is created `APPROVED`, then your rules (and crypto screening, when applicable) run inline and may flip the returned `status` to `IN_REVIEW` or `DECLINED`. **`transaction_id` is your idempotency key** (max 128 chars, unique per app); reusing it returns 400. A `transaction.created` webhook is dispatched on success (sandbox applications are not billed and do not emit webhooks). + Submit a transaction for synchronous AML/risk monitoring. The transaction is created `APPROVED`, then your rules (and crypto screening, when applicable) run inline and may flip the returned `status` to `IN_REVIEW` or `DECLINED`. **`transaction_id` is your idempotency key** (max 128 chars, unique per app); reusing it returns 400. A `transaction.created` webhook is dispatched on success for live applications. Sandbox applications are not billed and do not emit `transaction.created` or `transaction.status.updated` webhooks. Sandbox Travel Rule exchanges do emit `travel_rule.status.updated` webhooks with `environment: sandbox`. A Travel Rule transfer is stored and returned before any counterparty is contacted: `travel_rule.status` comes back `AWAITING_COUNTERPARTY` (or `NOT_APPLICABLE` below your threshold, `UNCONFIRMED_OWNERSHIP` for a declared self-hosted wallet, `COMPLETED` when the other wallet is your own verified wallet) and `travel_rule.protocol` is null. Didit then chooses the rail and sends the exchange, and every later status change of the transfer, including `COUNTERPARTY_VASP_NOT_FOUND` when no rail reaches the counterparty, arrives as a `travel_rule.status.updated` webhook. Your rules run again when the rail or the status changes. ``POST /v3/transactions/`` @@ -179,7 +194,7 @@ def create( time_zone: IANA time zone identifier, e.g. `Europe/Madrid`. counterparty: The counterparty of the transaction (optional). custom_properties: Arbitrary key-value pairs attached to the transaction. Each key can be referenced in rule conditions with the field path `custom_values.`. - travel_rule_details: Travel Rule compliance metadata. + travel_rule_details: Travel Rule data. When Travel Rule is enabled and transaction_category is travel_rule, Didit runs the exchange itself: it ignores status, protocol, required and obligations_count, and uses originator_data, beneficiary_data and metadata. Otherwise Didit stores these values as a passive Travel Rule record. While Travel Rule is on, an outbound transfer at or above your threshold must carry what its regime requires (EU TFR or FATF Recommendation 16), checked on the sides you send: a missing field answers 400 under `travel_rule_details.originator_data` or `travel_rule_details.beneficiary_data`, for example "EU TFR requires the originator name." network_snapshot: Pre-computed network graph snapshot. include_crypto_screening: Per-transaction override for crypto blockchain analytics screening. When `true`, crypto screening is performed regardless of the application default. When `false`, crypto screening is skipped even if enabled in the application settings. When omitted or `null`, the application-level default configured in the Console is used. Crypto screening requires `transaction_details.direction`, `currency_kind: "crypto"`, a chain/currency, and the wallet address selected by the direction: inbound pre-transfer screening uses `counterparty.payment_method.account_id`, inbound transaction-hash screening uses `subject.payment_method.account_id`, and outbound screening uses `counterparty.payment_method.account_id`. When `transaction_details.payment_reference_id` contains a blockchain transaction hash, Didit performs transaction screening and returns transaction summary enrichment when available. extra_headers: Headers to add to (or, with ``None``, remove from) this request. @@ -300,7 +315,7 @@ def list_networks( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TransactionsListNetworksResponse: + ) -> EntityNetworkMembershipList: """List networks for a transaction. List the networks the transaction's backing KYC/KYB subject belongs to (resolved through the transaction's session or business session). @@ -315,11 +330,11 @@ def list_networks( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "TransactionsListNetworksResponse", + "EntityNetworkMembershipList", self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/networks/", - cast_to=TransactionsListNetworksResponse, + cast_to=EntityNetworkMembershipList, path_params={"transaction_uuid": transaction_uuid}, scope="application", auth="api_key", @@ -339,7 +354,7 @@ def list_party_networks( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TransactionsListPartyNetworksResponse: + ) -> EntityNetworkMembershipList: """List networks for a transaction party. List the networks a transaction party (remitter, beneficiary, or counterparty) belongs to, resolved through the party itself rather than through the transaction's backing session. @@ -355,11 +370,11 @@ def list_party_networks( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "TransactionsListPartyNetworksResponse", + "EntityNetworkMembershipList", self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/parties/{transaction_party_uuid}/networks/", - cast_to=TransactionsListPartyNetworksResponse, + cast_to=EntityNetworkMembershipList, path_params={ "transaction_uuid": transaction_uuid, "transaction_party_uuid": transaction_party_uuid, @@ -373,15 +388,111 @@ def list_party_networks( ), ) + def list_related( + self, + transaction_id: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[TransactionSummary]: + """List related transactions. + + List the other transactions in your application that involve the same people or companies as this one. A transaction is related when any of its parties, in any role, has the same `vendor_data` as one of this transaction's parties. The transaction itself is never returned, and parties without `vendor_data` are ignored: when none of this transaction's parties has one, the result is always empty. + + Results use the summary shape of `GET /v3/transactions/`, newest first (`txn_date`, then `created_at`). Paginated with `limit` and `offset`; `count` is exact up to 100 and capped at 100 above that, so follow `next` until it is `null`. + + A scoped API key needs `transactions: read`; a key limited to some workflows or to approved sessions cannot call this endpoint. Send the same `vendor_data` for the same person or company on every [transaction](https://docs.didit.me/transaction-monitoring/transactions#subject-and-counterparty) to make this list complete. + + ``GET /v3/transactions/{transaction_id}/related/`` + + Args: + transaction_id: Didit transaction UUID: the `uuid` returned by `POST /v3/transactions/` and `GET /v3/transactions/`. Your own `txn_id` is not accepted. + limit: Number of transactions per page. Defaults to 50. A value that is not a positive integer falls back to 50; a value above 1000000 returns `400`. + offset: Zero-based position of the first transaction to return. Prefer following `next`. A value that is not a whole number falls back to 0; a value above 1000000 returns `400`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[TransactionSummary]", + self._client.request_page( + "get", + "/v3/transactions/{transaction_id}/related/", + cast_to=TransactionsListRelatedResponse, + item_type=TransactionSummary, + path_params={"transaction_id": transaction_id}, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def generate_pdf( + self, + transaction_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download a transaction PDF report. + + Render one monitored transaction as a PDF report: overview and verdict, parties and payment methods, custom properties, alerts and rule runs, screening and Travel Rule results, the network summary, the cost breakdown, up to 50 related transactions and the 20 most recent activity entries. The body is the PDF itself (`application/pdf`), not JSON. + + The report is rendered from the transaction's current data on every call, so request it again after a status change. Nothing is stored and nothing is billed. Rendering can take a few seconds, so use a generous client timeout. + + This endpoint lives on an application-scoped path: put your organization ID and the ID of the application your API key belongs to in the URL ([find your organization ID](https://docs.didit.me/getting-started/api-authentication#find-your-organization-id)). A PDF report counts as media: a scoped API key needs `media: read` and `transactions: read`, and a key limited to some workflows or to approved sessions cannot call it. + + ``GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/pdf/`` + + Args: + transaction_uuid: Didit transaction UUID: the `uuid` returned by `POST /v3/transactions/` and `GET /v3/transactions/`. Your own `txn_id` is not accepted. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/pdf/", + cast_to=bytes, + path_params={"transaction_uuid": transaction_uuid}, + scope="application", + auth="api_key", + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + class AsyncTransactions(AsyncAPIResource): """The ``transactions`` resource of the Didit API. - Methods: ``create``, ``create_sdk_token``, ``list``, ``list_networks``, ``list_party_networks``, ``retrieve``. + Methods: ``create``, ``create_sdk_token``, ``generate_pdf``, ``list``, ``list_networks``, ``list_party_networks``, ``list_related``, ``retrieve``. - Nested resources: ``rules``, ``travel_rule``. + Nested resources: ``notes``, ``rules``, ``travel_rule``. """ + @cached_property + def notes(self) -> AsyncTransactionsNotes: + from didit.resources.transactions.notes import AsyncTransactionsNotes + + return AsyncTransactionsNotes(self._client) + @cached_property def rules(self) -> AsyncTransactionsRules: from didit.resources.transactions.rules import AsyncTransactionsRules @@ -409,6 +520,7 @@ def list( *, limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, + travel_rule_status: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -423,6 +535,7 @@ def list( Args: limit: Page size. Defaults to 50 when omitted. There is no enforced maximum, but keep pages small for latency. offset: Zero-based offset of the first record to return. Prefer following the `next` URL from the previous page. + travel_rule_status: Return only transactions whose Travel Rule transfer is in this exact status (for example `ON_HOLD` or `AWAITING_COUNTERPARTY`). `count` follows this filter; `total_transactions` still counts every transaction in the application. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -436,7 +549,7 @@ def list( cast_to=TransactionsListResponse, item_type=TransactionsListResponseResults, auth="api_key", - query={"limit": limit, "offset": offset}, + query={"limit": limit, "offset": offset, "travel_rule_status": travel_rule_status}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -476,7 +589,7 @@ async def create( ) -> TransactionDetail: """Create transaction. - Submit a transaction for synchronous AML/risk monitoring. The transaction is created `APPROVED`, then your rules (and crypto screening, when applicable) run inline and may flip the returned `status` to `IN_REVIEW` or `DECLINED`. **`transaction_id` is your idempotency key** (max 128 chars, unique per app); reusing it returns 400. A `transaction.created` webhook is dispatched on success (sandbox applications are not billed and do not emit webhooks). + Submit a transaction for synchronous AML/risk monitoring. The transaction is created `APPROVED`, then your rules (and crypto screening, when applicable) run inline and may flip the returned `status` to `IN_REVIEW` or `DECLINED`. **`transaction_id` is your idempotency key** (max 128 chars, unique per app); reusing it returns 400. A `transaction.created` webhook is dispatched on success for live applications. Sandbox applications are not billed and do not emit `transaction.created` or `transaction.status.updated` webhooks. Sandbox Travel Rule exchanges do emit `travel_rule.status.updated` webhooks with `environment: sandbox`. A Travel Rule transfer is stored and returned before any counterparty is contacted: `travel_rule.status` comes back `AWAITING_COUNTERPARTY` (or `NOT_APPLICABLE` below your threshold, `UNCONFIRMED_OWNERSHIP` for a declared self-hosted wallet, `COMPLETED` when the other wallet is your own verified wallet) and `travel_rule.protocol` is null. Didit then chooses the rail and sends the exchange, and every later status change of the transfer, including `COUNTERPARTY_VASP_NOT_FOUND` when no rail reaches the counterparty, arrives as a `travel_rule.status.updated` webhook. Your rules run again when the rail or the status changes. ``POST /v3/transactions/`` @@ -489,7 +602,7 @@ async def create( time_zone: IANA time zone identifier, e.g. `Europe/Madrid`. counterparty: The counterparty of the transaction (optional). custom_properties: Arbitrary key-value pairs attached to the transaction. Each key can be referenced in rule conditions with the field path `custom_values.`. - travel_rule_details: Travel Rule compliance metadata. + travel_rule_details: Travel Rule data. When Travel Rule is enabled and transaction_category is travel_rule, Didit runs the exchange itself: it ignores status, protocol, required and obligations_count, and uses originator_data, beneficiary_data and metadata. Otherwise Didit stores these values as a passive Travel Rule record. While Travel Rule is on, an outbound transfer at or above your threshold must carry what its regime requires (EU TFR or FATF Recommendation 16), checked on the sides you send: a missing field answers 400 under `travel_rule_details.originator_data` or `travel_rule_details.beneficiary_data`, for example "EU TFR requires the originator name." network_snapshot: Pre-computed network graph snapshot. include_crypto_screening: Per-transaction override for crypto blockchain analytics screening. When `true`, crypto screening is performed regardless of the application default. When `false`, crypto screening is skipped even if enabled in the application settings. When omitted or `null`, the application-level default configured in the Console is used. Crypto screening requires `transaction_details.direction`, `currency_kind: "crypto"`, a chain/currency, and the wallet address selected by the direction: inbound pre-transfer screening uses `counterparty.payment_method.account_id`, inbound transaction-hash screening uses `subject.payment_method.account_id`, and outbound screening uses `counterparty.payment_method.account_id`. When `transaction_details.payment_reference_id` contains a blockchain transaction hash, Didit performs transaction screening and returns transaction summary enrichment when available. extra_headers: Headers to add to (or, with ``None``, remove from) this request. @@ -610,7 +723,7 @@ async def list_networks( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TransactionsListNetworksResponse: + ) -> EntityNetworkMembershipList: """List networks for a transaction. List the networks the transaction's backing KYC/KYB subject belongs to (resolved through the transaction's session or business session). @@ -625,11 +738,11 @@ async def list_networks( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "TransactionsListNetworksResponse", + "EntityNetworkMembershipList", await self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/networks/", - cast_to=TransactionsListNetworksResponse, + cast_to=EntityNetworkMembershipList, path_params={"transaction_uuid": transaction_uuid}, scope="application", auth="api_key", @@ -649,7 +762,7 @@ async def list_party_networks( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TransactionsListPartyNetworksResponse: + ) -> EntityNetworkMembershipList: """List networks for a transaction party. List the networks a transaction party (remitter, beneficiary, or counterparty) belongs to, resolved through the party itself rather than through the transaction's backing session. @@ -665,11 +778,11 @@ async def list_party_networks( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "TransactionsListPartyNetworksResponse", + "EntityNetworkMembershipList", await self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/parties/{transaction_party_uuid}/networks/", - cast_to=TransactionsListPartyNetworksResponse, + cast_to=EntityNetworkMembershipList, path_params={ "transaction_uuid": transaction_uuid, "transaction_party_uuid": transaction_party_uuid, @@ -683,6 +796,96 @@ async def list_party_networks( ), ) + def list_related( + self, + transaction_id: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[TransactionSummary]: + """List related transactions. + + List the other transactions in your application that involve the same people or companies as this one. A transaction is related when any of its parties, in any role, has the same `vendor_data` as one of this transaction's parties. The transaction itself is never returned, and parties without `vendor_data` are ignored: when none of this transaction's parties has one, the result is always empty. + + Results use the summary shape of `GET /v3/transactions/`, newest first (`txn_date`, then `created_at`). Paginated with `limit` and `offset`; `count` is exact up to 100 and capped at 100 above that, so follow `next` until it is `null`. + + A scoped API key needs `transactions: read`; a key limited to some workflows or to approved sessions cannot call this endpoint. Send the same `vendor_data` for the same person or company on every [transaction](https://docs.didit.me/transaction-monitoring/transactions#subject-and-counterparty) to make this list complete. + + ``GET /v3/transactions/{transaction_id}/related/`` + + Args: + transaction_id: Didit transaction UUID: the `uuid` returned by `POST /v3/transactions/` and `GET /v3/transactions/`. Your own `txn_id` is not accepted. + limit: Number of transactions per page. Defaults to 50. A value that is not a positive integer falls back to 50; a value above 1000000 returns `400`. + offset: Zero-based position of the first transaction to return. Prefer following `next`. A value that is not a whole number falls back to 0; a value above 1000000 returns `400`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[TransactionSummary]", + self._client.request_page( + "get", + "/v3/transactions/{transaction_id}/related/", + cast_to=TransactionsListRelatedResponse, + item_type=TransactionSummary, + path_params={"transaction_id": transaction_id}, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def generate_pdf( + self, + transaction_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download a transaction PDF report. + + Render one monitored transaction as a PDF report: overview and verdict, parties and payment methods, custom properties, alerts and rule runs, screening and Travel Rule results, the network summary, the cost breakdown, up to 50 related transactions and the 20 most recent activity entries. The body is the PDF itself (`application/pdf`), not JSON. + + The report is rendered from the transaction's current data on every call, so request it again after a status change. Nothing is stored and nothing is billed. Rendering can take a few seconds, so use a generous client timeout. + + This endpoint lives on an application-scoped path: put your organization ID and the ID of the application your API key belongs to in the URL ([find your organization ID](https://docs.didit.me/getting-started/api-authentication#find-your-organization-id)). A PDF report counts as media: a scoped API key needs `media: read` and `transactions: read`, and a key limited to some workflows or to approved sessions cannot call it. + + ``GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/pdf/`` + + Args: + transaction_uuid: Didit transaction UUID: the `uuid` returned by `POST /v3/transactions/` and `GET /v3/transactions/`. Your own `txn_id` is not accepted. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/pdf/", + cast_to=bytes, + path_params={"transaction_uuid": transaction_uuid}, + scope="application", + auth="api_key", + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + class TransactionsWithRawResponse: def __init__(self, resource: Transactions) -> None: @@ -693,6 +896,14 @@ def __init__(self, resource: Transactions) -> None: self.create_sdk_token = to_raw_response_wrapper(resource.create_sdk_token) self.list_networks = to_raw_response_wrapper(resource.list_networks) self.list_party_networks = to_raw_response_wrapper(resource.list_party_networks) + self.list_related = to_raw_response_wrapper(resource.list_related) + self.generate_pdf = to_raw_response_wrapper(resource.generate_pdf) + + @cached_property + def notes(self) -> TransactionsNotesWithRawResponse: + from didit.resources.transactions.notes import TransactionsNotesWithRawResponse + + return TransactionsNotesWithRawResponse(self._resource.notes) @cached_property def rules(self) -> TransactionsRulesWithRawResponse: @@ -716,6 +927,14 @@ def __init__(self, resource: AsyncTransactions) -> None: self.create_sdk_token = async_to_raw_response_wrapper(resource.create_sdk_token) self.list_networks = async_to_raw_response_wrapper(resource.list_networks) self.list_party_networks = async_to_raw_response_wrapper(resource.list_party_networks) + self.list_related = async_to_raw_response_wrapper(resource.list_related) + self.generate_pdf = async_to_raw_response_wrapper(resource.generate_pdf) + + @cached_property + def notes(self) -> AsyncTransactionsNotesWithRawResponse: + from didit.resources.transactions.notes import AsyncTransactionsNotesWithRawResponse + + return AsyncTransactionsNotesWithRawResponse(self._resource.notes) @cached_property def rules(self) -> AsyncTransactionsRulesWithRawResponse: @@ -741,6 +960,14 @@ def __init__(self, resource: Transactions) -> None: self.create_sdk_token = to_streamed_response_wrapper(resource.create_sdk_token) self.list_networks = to_streamed_response_wrapper(resource.list_networks) self.list_party_networks = to_streamed_response_wrapper(resource.list_party_networks) + self.list_related = to_streamed_response_wrapper(resource.list_related) + self.generate_pdf = to_streamed_response_wrapper(resource.generate_pdf) + + @cached_property + def notes(self) -> TransactionsNotesWithStreamingResponse: + from didit.resources.transactions.notes import TransactionsNotesWithStreamingResponse + + return TransactionsNotesWithStreamingResponse(self._resource.notes) @cached_property def rules(self) -> TransactionsRulesWithStreamingResponse: @@ -766,6 +993,14 @@ def __init__(self, resource: AsyncTransactions) -> None: self.create_sdk_token = async_to_streamed_response_wrapper(resource.create_sdk_token) self.list_networks = async_to_streamed_response_wrapper(resource.list_networks) self.list_party_networks = async_to_streamed_response_wrapper(resource.list_party_networks) + self.list_related = async_to_streamed_response_wrapper(resource.list_related) + self.generate_pdf = async_to_streamed_response_wrapper(resource.generate_pdf) + + @cached_property + def notes(self) -> AsyncTransactionsNotesWithStreamingResponse: + from didit.resources.transactions.notes import AsyncTransactionsNotesWithStreamingResponse + + return AsyncTransactionsNotesWithStreamingResponse(self._resource.notes) @cached_property def rules(self) -> AsyncTransactionsRulesWithStreamingResponse: diff --git a/src/didit/resources/transactions/notes.py b/src/didit/resources/transactions/notes.py new file mode 100644 index 0000000..645d223 --- /dev/null +++ b/src/didit/resources/transactions/notes.py @@ -0,0 +1,278 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``transactions.notes`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.transaction_note import TransactionNote +from didit.types.transactions.notes.list_response import TransactionsNotesListResponse + +__all__ = [ + "TransactionsNotes", + "AsyncTransactionsNotes", + "TransactionsNotesWithRawResponse", + "AsyncTransactionsNotesWithRawResponse", + "TransactionsNotesWithStreamingResponse", + "AsyncTransactionsNotesWithStreamingResponse", +] + + +class TransactionsNotes(SyncAPIResource): + """The ``transactions.notes`` resource of the Didit API. + + Methods: ``create``, ``list``. + """ + + @cached_property + def with_raw_response(self) -> TransactionsNotesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return TransactionsNotesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> TransactionsNotesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return TransactionsNotesWithStreamingResponse(self) + + def list( + self, + transaction_id: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[TransactionNote]: + """List transaction notes. + + List the notes on one monitored transaction, newest first (`created_at` descending): notes you add through the API, notes and status changes reviewers make in the Console, and notes Didit adds automatically (a rule's `add_note` action, or a problem creating the user-action verification session), which carry `actor_name` `Rule Engine`. `metadata` tells the note types apart. + + Paginated with `limit` and `offset`. `count` is exact up to 100 and capped at 100 above that, so follow `next` until it is `null` instead of computing pages from `count`. This is a read with no side effects. + + A scoped API key needs `transactions: read`; a key limited to some workflows or to approved sessions cannot call this endpoint. Reviewers see the same list under **Notes** on the [transaction detail](https://docs.didit.me/transaction-monitoring/console#transaction-detail). + + ``GET /v3/transactions/{transaction_id}/notes/`` + + Args: + transaction_id: Didit transaction UUID: the `uuid` returned by `POST /v3/transactions/` and `GET /v3/transactions/`. Your own `txn_id` is not accepted. + limit: Number of notes per page. Defaults to 50. A value that is not a positive integer falls back to 50; a value above 1000000 returns `400`. + offset: Zero-based position of the first note to return. Prefer following `next`. A value that is not a whole number falls back to 0; a value above 1000000 returns `400`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[TransactionNote]", + self._client.request_page( + "get", + "/v3/transactions/{transaction_id}/notes/", + cast_to=TransactionsNotesListResponse, + item_type=TransactionNote, + path_params={"transaction_id": transaction_id}, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + transaction_id: str, + *, + note: str, + mentioned_emails: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionNote: + """Create transaction note. + + Add a plain-text note to a monitored transaction's review timeline, where Console reviewers see it. A note is context, not a decision: the transaction's `status`, `score` and alerts do not change, no webhook is sent, and nothing is billed. The transaction's `activities` gain a `TRANSACTION_NOTE_ADDED` entry. + + Notes created with an API key have no author: `actor_email` and `actor_name` are `null`, so put the author in the text if you need it. Every address in `mentioned_emails` must belong to a member of your organization, otherwise the whole request fails with `400`; mentions are stored lower-cased and de-duplicated, and this endpoint does not notify the mentioned members. + + Each call creates a new note, so retrying a timed-out request can add it twice: list the notes before retrying. There is no API endpoint to edit or remove a note. A scoped API key needs `transactions: write`; a key limited to some workflows or to approved sessions cannot call this endpoint. + + ``POST /v3/transactions/{transaction_id}/notes/`` + + Args: + transaction_id: Didit transaction UUID: the `uuid` returned by `POST /v3/transactions/` and `GET /v3/transactions/`. Your own `txn_id` is not accepted. + note: Plain-text note. Leading and trailing whitespace is trimmed and a blank note is rejected. No length limit. + mentioned_emails: Emails of members of your organization to mention. Each must be a valid email of an organization member. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionNote", + self._client.request( + "post", + "/v3/transactions/{transaction_id}/notes/", + cast_to=TransactionNote, + path_params={"transaction_id": transaction_id}, + auth="api_key", + body={"note": note, "mentioned_emails": mentioned_emails}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncTransactionsNotes(AsyncAPIResource): + """The ``transactions.notes`` resource of the Didit API. + + Methods: ``create``, ``list``. + """ + + @cached_property + def with_raw_response(self) -> AsyncTransactionsNotesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncTransactionsNotesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncTransactionsNotesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncTransactionsNotesWithStreamingResponse(self) + + def list( + self, + transaction_id: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[TransactionNote]: + """List transaction notes. + + List the notes on one monitored transaction, newest first (`created_at` descending): notes you add through the API, notes and status changes reviewers make in the Console, and notes Didit adds automatically (a rule's `add_note` action, or a problem creating the user-action verification session), which carry `actor_name` `Rule Engine`. `metadata` tells the note types apart. + + Paginated with `limit` and `offset`. `count` is exact up to 100 and capped at 100 above that, so follow `next` until it is `null` instead of computing pages from `count`. This is a read with no side effects. + + A scoped API key needs `transactions: read`; a key limited to some workflows or to approved sessions cannot call this endpoint. Reviewers see the same list under **Notes** on the [transaction detail](https://docs.didit.me/transaction-monitoring/console#transaction-detail). + + ``GET /v3/transactions/{transaction_id}/notes/`` + + Args: + transaction_id: Didit transaction UUID: the `uuid` returned by `POST /v3/transactions/` and `GET /v3/transactions/`. Your own `txn_id` is not accepted. + limit: Number of notes per page. Defaults to 50. A value that is not a positive integer falls back to 50; a value above 1000000 returns `400`. + offset: Zero-based position of the first note to return. Prefer following `next`. A value that is not a whole number falls back to 0; a value above 1000000 returns `400`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[TransactionNote]", + self._client.request_page( + "get", + "/v3/transactions/{transaction_id}/notes/", + cast_to=TransactionsNotesListResponse, + item_type=TransactionNote, + path_params={"transaction_id": transaction_id}, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + transaction_id: str, + *, + note: str, + mentioned_emails: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionNote: + """Create transaction note. + + Add a plain-text note to a monitored transaction's review timeline, where Console reviewers see it. A note is context, not a decision: the transaction's `status`, `score` and alerts do not change, no webhook is sent, and nothing is billed. The transaction's `activities` gain a `TRANSACTION_NOTE_ADDED` entry. + + Notes created with an API key have no author: `actor_email` and `actor_name` are `null`, so put the author in the text if you need it. Every address in `mentioned_emails` must belong to a member of your organization, otherwise the whole request fails with `400`; mentions are stored lower-cased and de-duplicated, and this endpoint does not notify the mentioned members. + + Each call creates a new note, so retrying a timed-out request can add it twice: list the notes before retrying. There is no API endpoint to edit or remove a note. A scoped API key needs `transactions: write`; a key limited to some workflows or to approved sessions cannot call this endpoint. + + ``POST /v3/transactions/{transaction_id}/notes/`` + + Args: + transaction_id: Didit transaction UUID: the `uuid` returned by `POST /v3/transactions/` and `GET /v3/transactions/`. Your own `txn_id` is not accepted. + note: Plain-text note. Leading and trailing whitespace is trimmed and a blank note is rejected. No length limit. + mentioned_emails: Emails of members of your organization to mention. Each must be a valid email of an organization member. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionNote", + await self._client.request( + "post", + "/v3/transactions/{transaction_id}/notes/", + cast_to=TransactionNote, + path_params={"transaction_id": transaction_id}, + auth="api_key", + body={"note": note, "mentioned_emails": mentioned_emails}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class TransactionsNotesWithRawResponse: + def __init__(self, resource: TransactionsNotes) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + + +class AsyncTransactionsNotesWithRawResponse: + def __init__(self, resource: AsyncTransactionsNotes) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + + +class TransactionsNotesWithStreamingResponse: + def __init__(self, resource: TransactionsNotes) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + + +class AsyncTransactionsNotesWithStreamingResponse: + def __init__(self, resource: AsyncTransactionsNotes) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) diff --git a/src/didit/resources/transactions/rules.py b/src/didit/resources/transactions/rules.py index 867bca6..9e34c46 100644 --- a/src/didit/resources/transactions/rules.py +++ b/src/didit/resources/transactions/rules.py @@ -278,7 +278,7 @@ def update( ) -> TransactionRuleDetail: """Update rule. - Partially update a rule. Custom rules accept every configuration field. Library (PRESET) rules accept only `mode` - use it to activate, disable, or switch a preset to TEST; any other field returns 400. + Partially update a rule. Custom rules accept every configuration field. Library (PRESET) rules accept only `mode` - use it to activate, disable, or switch a preset to TEST; any other field returns 400. An installed copy of a withheld library rule is switched off: setting its `mode` to anything but `DISABLED` answers 400 ("This library rule cannot run yet: its conditions read data the rules engine cannot evaluate."). ``PATCH /v3/transactions/rules/{rule_uuid}/`` @@ -392,7 +392,7 @@ def list_library( ) -> SyncPage[TransactionRuleLibraryEntry]: """List rule library. - Browse the full preset rule library (150+ rules) with each entry's complete configuration and an `is_installed` flag for this application. Filter by bundle, category, or free-text search. + Browse the full preset rule library (150+ rules) with each entry's complete configuration and an `is_installed` flag for this application. Filter by bundle, category, or free-text search. A library rule whose conditions read data the rules engine cannot evaluate yet is withheld until it can run. Withheld rules are not listed. ``GET /v3/transactions/rules/library/`` @@ -441,7 +441,7 @@ def install_library( ) -> TransactionRuleInstallResult: """Install library rules. - Install one or more presets onto the application, by `library_keys` or a whole `bundle`. Installed presets evaluate immediately in their default mode; already-installed keys are skipped. + Install one or more presets onto the application, by `library_keys` or a whole `bundle`. An installed preset is active at once while transaction monitoring is on, and disabled otherwise; already-installed keys are skipped. A library rule whose conditions read data the rules engine cannot evaluate yet is withheld until it can run. A request that names one in `library_keys` answers 400 and installs nothing ("These library rules cannot run yet: their conditions read data the rules engine cannot evaluate." followed by the keys), and a `bundle` install skips it. A withheld rule comes back on its own once its conditions can be evaluated. A `library_keys` entry that is not in the library is skipped, not refused. ``POST /v3/transactions/rules/install/`` @@ -746,7 +746,7 @@ async def update( ) -> TransactionRuleDetail: """Update rule. - Partially update a rule. Custom rules accept every configuration field. Library (PRESET) rules accept only `mode` - use it to activate, disable, or switch a preset to TEST; any other field returns 400. + Partially update a rule. Custom rules accept every configuration field. Library (PRESET) rules accept only `mode` - use it to activate, disable, or switch a preset to TEST; any other field returns 400. An installed copy of a withheld library rule is switched off: setting its `mode` to anything but `DISABLED` answers 400 ("This library rule cannot run yet: its conditions read data the rules engine cannot evaluate."). ``PATCH /v3/transactions/rules/{rule_uuid}/`` @@ -860,7 +860,7 @@ def list_library( ) -> AsyncPaginator[TransactionRuleLibraryEntry]: """List rule library. - Browse the full preset rule library (150+ rules) with each entry's complete configuration and an `is_installed` flag for this application. Filter by bundle, category, or free-text search. + Browse the full preset rule library (150+ rules) with each entry's complete configuration and an `is_installed` flag for this application. Filter by bundle, category, or free-text search. A library rule whose conditions read data the rules engine cannot evaluate yet is withheld until it can run. Withheld rules are not listed. ``GET /v3/transactions/rules/library/`` @@ -909,7 +909,7 @@ async def install_library( ) -> TransactionRuleInstallResult: """Install library rules. - Install one or more presets onto the application, by `library_keys` or a whole `bundle`. Installed presets evaluate immediately in their default mode; already-installed keys are skipped. + Install one or more presets onto the application, by `library_keys` or a whole `bundle`. An installed preset is active at once while transaction monitoring is on, and disabled otherwise; already-installed keys are skipped. A library rule whose conditions read data the rules engine cannot evaluate yet is withheld until it can run. A request that names one in `library_keys` answers 400 and installs nothing ("These library rules cannot run yet: their conditions read data the rules engine cannot evaluate." followed by the keys), and a `bundle` install skips it. A withheld rule comes back on its own once its conditions can be evaluated. A `library_keys` entry that is not in the library is skipped, not refused. ``POST /v3/transactions/rules/install/`` diff --git a/src/didit/resources/transactions/travel_rule.py b/src/didit/resources/transactions/travel_rule.py index df15cba..3adf43e 100644 --- a/src/didit/resources/transactions/travel_rule.py +++ b/src/didit/resources/transactions/travel_rule.py @@ -3,8 +3,9 @@ from __future__ import annotations +import builtins from functools import cached_property -from typing import Literal, cast +from typing import Literal, Sequence, cast import httpx @@ -15,7 +16,11 @@ to_raw_response_wrapper, to_streamed_response_wrapper, ) -from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit._types import NOT_GIVEN, Body, FileTypes, Headers, NotGiven, Query +from didit.types.components.travel_rule_message import TravelRuleMessage +from didit.types.components.travel_rule_test_vasp_inbound_response import ( + TravelRuleTestVaspInboundResponse, +) from didit.types.components.travel_rule_transfer_detail import TravelRuleTransferDetail __all__ = [ @@ -31,7 +36,7 @@ class TransactionsTravelRule(SyncAPIResource): """The ``transactions.travel_rule`` resource of the Didit API. - Methods: ``confirm_ownership``, ``update_transfer``. + Methods: ``confirm_ownership``, ``create_message``, ``list_messages``, ``trigger_test_vasp_inbound``, ``update_transfer``. """ @cached_property @@ -49,22 +54,30 @@ def update_transfer( transaction_id: str, *, payment_txn_id: str | NotGiven = NOT_GIVEN, - action: Literal["cancel", "resend"] | str | NotGiven = NOT_GIVEN, + action: Literal["cancel", "resend", "update_compliance_email", "update_originator_wallet"] + | str + | NotGiven = NOT_GIVEN, + compliance_email: str | NotGiven = NOT_GIVEN, + wallet_address: str | NotGiven = NOT_GIVEN, + memo: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> TravelRuleTransferDetail: - """Finish, cancel, or resend a Travel Rule transfer. + """Finish, cancel, resend, or update the originator wallet of a Travel Rule transfer. - Provide `payment_txn_id` to report the on-chain hash and move an outbound COMPLETED transfer to FINISHED (its INTERNAL-rail counterpart also moves to FINISHED), `{"action": "cancel"}` to cancel a non-terminal transfer, or `{"action": "resend"}` to re-run routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA (improve the counterparty data first, e.g. add a travel_address to the destination wallet entry). + Provide `payment_txn_id` to report the on-chain hash and move an outbound COMPLETED transfer to FINISHED (its INTERNAL-rail counterpart also moves to FINISHED), `{"action": "cancel"}` to cancel a non-terminal transfer, or `{"action": "resend"}` to route an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA again (improve the counterparty data first, e.g. add a travel_address to the destination wallet entry). resend applies to outbound transfers only. `{"action": "update_compliance_email", "compliance_email": "desk@example.com"}` replaces the compliance email of an outbound transfer in one of those three statuses, the fix for `reason: email_undeliverable`: the status does not change, and the next `resend` sends the request to that address. A resend is queued, not run inside the request: the response shows the transfer back in AWAITING_COUNTERPARTY with a `resend queued` timeline entry and a queued `delivery`, and where it went arrives as a `travel_rule.status.updated` webhook. When the transfer went over GTR, VerifyVASP or Sygna Bridge, the network learns the on-chain hash or the cancellation after the response, with retries; `delivery` shows how that message stands. `{"action": "update_originator_wallet", "wallet_address": "..."}` records the wallet the funds actually left from on an outbound transfer, for example the address a UTXO withdrawal was broadcast from rather than the main wallet you declared. It changes the record only: nothing is sent to the counterparty again and the status stays. The originator becomes `chain_details.originator` with `source: updated`, the change is added to the `timeline` with the previous and the new wallet, and the INTERNAL-rail counterpart of the transfer is updated the same way. ``PATCH /v3/transactions/{transaction_id}/travel-rule/`` Args: transaction_id: Didit-stable transaction UUID that owns the transfer. payment_txn_id: The on-chain transaction hash. Requires the transfer to be an outbound transfer in COMPLETED status. - action: "cancel" cancels a non-terminal transfer. "resend" re-runs routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA. + action: "cancel" cancels a non-terminal transfer. "resend" re-runs routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA. "update_compliance_email" replaces the compliance email the request goes to, on an outbound transfer in one of those three statuses; send "resend" afterwards. "update_originator_wallet" records the wallet the funds actually left from (`wallet_address`, `memo`). + compliance_email: Required with `action: update_compliance_email`. Kept as this transfer's `beneficiary_data.counterparty_vasp_email`: it belongs to this transfer only and joins no directory entry and no contact list. + wallet_address: With `action: update_originator_wallet`: the wallet the funds left from. When the transfer's UTXO inputs are known it must be one of them. + memo: With `action: update_originator_wallet`: the wallet's memo or tag on a memo network. Optional. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -78,7 +91,13 @@ def update_transfer( cast_to=TravelRuleTransferDetail, path_params={"transaction_id": transaction_id}, auth="api_key", - body={"payment_txn_id": payment_txn_id, "action": action}, + body={ + "payment_txn_id": payment_txn_id, + "action": action, + "compliance_email": compliance_email, + "wallet_address": wallet_address, + "memo": memo, + }, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -91,6 +110,7 @@ def confirm_ownership( transaction_id: str, *, confirmed: bool, + wallet_address: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -98,13 +118,14 @@ def confirm_ownership( ) -> TravelRuleTransferDetail: """Confirm or deny wallet ownership. - Called by the beneficiary's application when its INTERNAL-rail transfer is UNCONFIRMED_OWNERSHIP. Confirming marks the matching wallet address book entry as ownership-verified and runs the beneficiary name match on both sides of the transfer; denying declines both sides. + Resolve a transfer that waits on wallet ownership: call it on your own side of the transfer when it is UNCONFIRMED_OWNERSHIP, or when it is an inbound transfer in ON_HOLD. Confirming marks the matching wallet address book entry as ownership-verified (for an inbound ON_HOLD transfer with no entry, Didit creates one from beneficiary_data.wallet_address) and runs the beneficiary name match on both sides of the transfer; denying declines both sides. An inbound transfer whose deposit already settled moves on to FINISHED when the match succeeds. When the counterparty wallet was declared self-hosted (counterparty.payment_method.method_type: unhosted_wallet), confirming records that the customer verified who controls that wallet and completes the transfer (COMPLETED) in both directions, or FINISHED for an inbound deposit whose on-chain hash is already known. The wallet verified is the counterparty's: the destination of an outbound withdrawal, or the source (originator's wallet) of an inbound deposit. If it is not in your wallet address book yet, Didit adds it as ownership-verified under the counterparty's name; if the book already holds it under a different holder name, the transfer moves to COUNTERPARTY_MISMATCHED_DATA. For an inbound TRP transfer in UNCONFIRMED_OWNERSHIP, ON_HOLD or COUNTERPARTY_MISMATCHED_DATA, Didit sends your decision to the originating VASP instead: approving returns the payment address (wallet_address, or the matched entry's address) and moves the transfer to COMPLETED, and denying declines it. The same call also decides an inbound transfer that a GTR or Sygna Bridge counterparty sent and that waits for your review (`UNCONFIRMED_OWNERSHIP`, `ON_HOLD` or `COUNTERPARTY_MISMATCHED_DATA`): `confirmed: true` approves it (pass `wallet_address` when the transfer names none) and `confirmed: false` declines it, and the network is answered after the response, with retries. VerifyVASP takes its answer when it asks, so a VerifyVASP transfer that would need your review is declined at once and cannot be decided later. ``POST /v3/transactions/{transaction_id}/travel-rule/ownership/`` Args: - transaction_id: Didit-stable transaction UUID that owns the transfer (the beneficiary side's transaction). - confirmed + transaction_id: Didit-stable UUID of the transaction your application holds for this transfer: the beneficiary side's transaction on an INTERNAL-rail transfer, or the transaction you created (outbound or inbound) when the counterparty wallet was declared self-hosted. + confirmed: true to confirm ownership, false to deny it. + wallet_address: Inbound TRP transfers: the payment address Didit returns to the originating VASP when you approve. Defaults to the matched address book entry's address, then beneficiary_data.wallet_address. On a declared self-hosted wallet transfer that names no counterparty wallet, the wallet whose ownership you verified; without it the request is rejected with 400. Ignored for other transfers. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -118,7 +139,152 @@ def confirm_ownership( cast_to=TravelRuleTransferDetail, path_params={"transaction_id": transaction_id}, auth="api_key", - body={"confirmed": confirmed}, + body={"confirmed": confirmed, "wallet_address": wallet_address}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_messages( + self, + transaction_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.list[TravelRuleMessage]: + """List Travel Rule messages. + + Messages and document requests exchanged with the counterparty VASP about this transfer. + + ``GET /v3/transactions/{transaction_id}/travel-rule/messages/`` + + Args: + transaction_id: Didit-stable transaction UUID that owns the transfer. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.list[TravelRuleMessage]", + self._client.request( + "get", + "/v3/transactions/{transaction_id}/travel-rule/messages/", + cast_to=builtins.list[TravelRuleMessage], + path_params={"transaction_id": transaction_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create_message( + self, + transaction_id: str, + *, + kind: Literal["MESSAGE", "DOCUMENT_REQUEST"] | str | NotGiven = NOT_GIVEN, + body: str | NotGiven = NOT_GIVEN, + requested_documents: Sequence[str] | NotGiven = NOT_GIVEN, + reply_to: str | None | NotGiven = NOT_GIVEN, + files: Sequence[FileTypes] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleMessage: + """Send a Travel Rule message or document request. + + Sends a message or a document request to the counterparty VASP. Works on INTERNAL transfers (the other Didit customer sees it on its side of the pair) and EMAIL transfers (the counterparty is emailed and answers in the pickup portal). A transfer over TRP, GTR, TRUST, VerifyVASP or Sygna carries no messages, because those protocols have none. Limits: 100 messages per transfer, 5000 characters per body, 10 requested documents, 5 files of 10 MB each. + + ``POST /v3/transactions/{transaction_id}/travel-rule/messages/`` + + Args: + transaction_id: Didit-stable transaction UUID that owns the transfer. + kind + body: Required for a MESSAGE unless a file is attached. + requested_documents: Required (at least one) for a DOCUMENT_REQUEST, and only allowed there. + reply_to: A message of the same transfer. A reply that carries a document fulfils the document request it answers. + files: Multipart only. Up to 5 files of 10 MB each. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleMessage", + self._client.request( + "post", + "/v3/transactions/{transaction_id}/travel-rule/messages/", + cast_to=TravelRuleMessage, + path_params={"transaction_id": transaction_id}, + auth="api_key", + body={ + "kind": kind, + "body": body, + "requested_documents": requested_documents, + "reply_to": reply_to, + "files": files, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def trigger_test_vasp_inbound( + self, + *, + beneficiary_wallet_address: str, + scenario: Literal["accept", "name_mismatch"] | str | NotGiven = NOT_GIVEN, + chain: str | NotGiven = NOT_GIVEN, + amount: str | NotGiven = NOT_GIVEN, + currency: str | NotGiven = NOT_GIVEN, + reference: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleTestVaspInboundResponse: + """Trigger an inbound transfer from the Didit Test VASP (sandbox). + + Sandbox applications only. The Didit Test VASP sends your application an inbound Travel Rule transfer, so you can exercise the receiving side of an exchange end to end before going live. The transfer is booked and decided by the same logic a real inbound transfer from a network runs against your wallet address book, so the inbox, the decision and the wallet ownership flow behave as they do live. Nothing leaves Didit. A live application receives 400. Send the same `reference` again to get the transfer the first call booked instead of a second one. + + ``POST /v3/travel-rule/test-vasp/inbound/`` + + Args: + beneficiary_wallet_address: The destination wallet address. The Didit Test VASP sends to it, matched against your wallet address book on the chain of the asset. A wallet with no matching entry is accepted and the inbound transaction is booked ON_HOLD. + scenario: What the Didit Test VASP declares the beneficiary to be. `accept` declares the holder's own name; `name_mismatch` declares a name no holder agrees with. The decision itself is always yours. + chain: Chain of the asset. + amount: Amount as a decimal string. + currency: Asset code. + reference: Optional idempotency key. A repeat with the same value returns the transfer the first call booked. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleTestVaspInboundResponse", + self._client.request( + "post", + "/v3/travel-rule/test-vasp/inbound/", + cast_to=TravelRuleTestVaspInboundResponse, + auth="api_key", + body={ + "beneficiary_wallet_address": beneficiary_wallet_address, + "scenario": scenario, + "chain": chain, + "amount": amount, + "currency": currency, + "reference": reference, + }, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -130,7 +296,7 @@ def confirm_ownership( class AsyncTransactionsTravelRule(AsyncAPIResource): """The ``transactions.travel_rule`` resource of the Didit API. - Methods: ``confirm_ownership``, ``update_transfer``. + Methods: ``confirm_ownership``, ``create_message``, ``list_messages``, ``trigger_test_vasp_inbound``, ``update_transfer``. """ @cached_property @@ -148,22 +314,30 @@ async def update_transfer( transaction_id: str, *, payment_txn_id: str | NotGiven = NOT_GIVEN, - action: Literal["cancel", "resend"] | str | NotGiven = NOT_GIVEN, + action: Literal["cancel", "resend", "update_compliance_email", "update_originator_wallet"] + | str + | NotGiven = NOT_GIVEN, + compliance_email: str | NotGiven = NOT_GIVEN, + wallet_address: str | NotGiven = NOT_GIVEN, + memo: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> TravelRuleTransferDetail: - """Finish, cancel, or resend a Travel Rule transfer. + """Finish, cancel, resend, or update the originator wallet of a Travel Rule transfer. - Provide `payment_txn_id` to report the on-chain hash and move an outbound COMPLETED transfer to FINISHED (its INTERNAL-rail counterpart also moves to FINISHED), `{"action": "cancel"}` to cancel a non-terminal transfer, or `{"action": "resend"}` to re-run routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA (improve the counterparty data first, e.g. add a travel_address to the destination wallet entry). + Provide `payment_txn_id` to report the on-chain hash and move an outbound COMPLETED transfer to FINISHED (its INTERNAL-rail counterpart also moves to FINISHED), `{"action": "cancel"}` to cancel a non-terminal transfer, or `{"action": "resend"}` to route an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA again (improve the counterparty data first, e.g. add a travel_address to the destination wallet entry). resend applies to outbound transfers only. `{"action": "update_compliance_email", "compliance_email": "desk@example.com"}` replaces the compliance email of an outbound transfer in one of those three statuses, the fix for `reason: email_undeliverable`: the status does not change, and the next `resend` sends the request to that address. A resend is queued, not run inside the request: the response shows the transfer back in AWAITING_COUNTERPARTY with a `resend queued` timeline entry and a queued `delivery`, and where it went arrives as a `travel_rule.status.updated` webhook. When the transfer went over GTR, VerifyVASP or Sygna Bridge, the network learns the on-chain hash or the cancellation after the response, with retries; `delivery` shows how that message stands. `{"action": "update_originator_wallet", "wallet_address": "..."}` records the wallet the funds actually left from on an outbound transfer, for example the address a UTXO withdrawal was broadcast from rather than the main wallet you declared. It changes the record only: nothing is sent to the counterparty again and the status stays. The originator becomes `chain_details.originator` with `source: updated`, the change is added to the `timeline` with the previous and the new wallet, and the INTERNAL-rail counterpart of the transfer is updated the same way. ``PATCH /v3/transactions/{transaction_id}/travel-rule/`` Args: transaction_id: Didit-stable transaction UUID that owns the transfer. payment_txn_id: The on-chain transaction hash. Requires the transfer to be an outbound transfer in COMPLETED status. - action: "cancel" cancels a non-terminal transfer. "resend" re-runs routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA. + action: "cancel" cancels a non-terminal transfer. "resend" re-runs routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA. "update_compliance_email" replaces the compliance email the request goes to, on an outbound transfer in one of those three statuses; send "resend" afterwards. "update_originator_wallet" records the wallet the funds actually left from (`wallet_address`, `memo`). + compliance_email: Required with `action: update_compliance_email`. Kept as this transfer's `beneficiary_data.counterparty_vasp_email`: it belongs to this transfer only and joins no directory entry and no contact list. + wallet_address: With `action: update_originator_wallet`: the wallet the funds left from. When the transfer's UTXO inputs are known it must be one of them. + memo: With `action: update_originator_wallet`: the wallet's memo or tag on a memo network. Optional. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -177,7 +351,13 @@ async def update_transfer( cast_to=TravelRuleTransferDetail, path_params={"transaction_id": transaction_id}, auth="api_key", - body={"payment_txn_id": payment_txn_id, "action": action}, + body={ + "payment_txn_id": payment_txn_id, + "action": action, + "compliance_email": compliance_email, + "wallet_address": wallet_address, + "memo": memo, + }, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -190,6 +370,7 @@ async def confirm_ownership( transaction_id: str, *, confirmed: bool, + wallet_address: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -197,13 +378,14 @@ async def confirm_ownership( ) -> TravelRuleTransferDetail: """Confirm or deny wallet ownership. - Called by the beneficiary's application when its INTERNAL-rail transfer is UNCONFIRMED_OWNERSHIP. Confirming marks the matching wallet address book entry as ownership-verified and runs the beneficiary name match on both sides of the transfer; denying declines both sides. + Resolve a transfer that waits on wallet ownership: call it on your own side of the transfer when it is UNCONFIRMED_OWNERSHIP, or when it is an inbound transfer in ON_HOLD. Confirming marks the matching wallet address book entry as ownership-verified (for an inbound ON_HOLD transfer with no entry, Didit creates one from beneficiary_data.wallet_address) and runs the beneficiary name match on both sides of the transfer; denying declines both sides. An inbound transfer whose deposit already settled moves on to FINISHED when the match succeeds. When the counterparty wallet was declared self-hosted (counterparty.payment_method.method_type: unhosted_wallet), confirming records that the customer verified who controls that wallet and completes the transfer (COMPLETED) in both directions, or FINISHED for an inbound deposit whose on-chain hash is already known. The wallet verified is the counterparty's: the destination of an outbound withdrawal, or the source (originator's wallet) of an inbound deposit. If it is not in your wallet address book yet, Didit adds it as ownership-verified under the counterparty's name; if the book already holds it under a different holder name, the transfer moves to COUNTERPARTY_MISMATCHED_DATA. For an inbound TRP transfer in UNCONFIRMED_OWNERSHIP, ON_HOLD or COUNTERPARTY_MISMATCHED_DATA, Didit sends your decision to the originating VASP instead: approving returns the payment address (wallet_address, or the matched entry's address) and moves the transfer to COMPLETED, and denying declines it. The same call also decides an inbound transfer that a GTR or Sygna Bridge counterparty sent and that waits for your review (`UNCONFIRMED_OWNERSHIP`, `ON_HOLD` or `COUNTERPARTY_MISMATCHED_DATA`): `confirmed: true` approves it (pass `wallet_address` when the transfer names none) and `confirmed: false` declines it, and the network is answered after the response, with retries. VerifyVASP takes its answer when it asks, so a VerifyVASP transfer that would need your review is declined at once and cannot be decided later. ``POST /v3/transactions/{transaction_id}/travel-rule/ownership/`` Args: - transaction_id: Didit-stable transaction UUID that owns the transfer (the beneficiary side's transaction). - confirmed + transaction_id: Didit-stable UUID of the transaction your application holds for this transfer: the beneficiary side's transaction on an INTERNAL-rail transfer, or the transaction you created (outbound or inbound) when the counterparty wallet was declared self-hosted. + confirmed: true to confirm ownership, false to deny it. + wallet_address: Inbound TRP transfers: the payment address Didit returns to the originating VASP when you approve. Defaults to the matched address book entry's address, then beneficiary_data.wallet_address. On a declared self-hosted wallet transfer that names no counterparty wallet, the wallet whose ownership you verified; without it the request is rejected with 400. Ignored for other transfers. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -217,7 +399,152 @@ async def confirm_ownership( cast_to=TravelRuleTransferDetail, path_params={"transaction_id": transaction_id}, auth="api_key", - body={"confirmed": confirmed}, + body={"confirmed": confirmed, "wallet_address": wallet_address}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_messages( + self, + transaction_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.list[TravelRuleMessage]: + """List Travel Rule messages. + + Messages and document requests exchanged with the counterparty VASP about this transfer. + + ``GET /v3/transactions/{transaction_id}/travel-rule/messages/`` + + Args: + transaction_id: Didit-stable transaction UUID that owns the transfer. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.list[TravelRuleMessage]", + await self._client.request( + "get", + "/v3/transactions/{transaction_id}/travel-rule/messages/", + cast_to=builtins.list[TravelRuleMessage], + path_params={"transaction_id": transaction_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create_message( + self, + transaction_id: str, + *, + kind: Literal["MESSAGE", "DOCUMENT_REQUEST"] | str | NotGiven = NOT_GIVEN, + body: str | NotGiven = NOT_GIVEN, + requested_documents: Sequence[str] | NotGiven = NOT_GIVEN, + reply_to: str | None | NotGiven = NOT_GIVEN, + files: Sequence[FileTypes] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleMessage: + """Send a Travel Rule message or document request. + + Sends a message or a document request to the counterparty VASP. Works on INTERNAL transfers (the other Didit customer sees it on its side of the pair) and EMAIL transfers (the counterparty is emailed and answers in the pickup portal). A transfer over TRP, GTR, TRUST, VerifyVASP or Sygna carries no messages, because those protocols have none. Limits: 100 messages per transfer, 5000 characters per body, 10 requested documents, 5 files of 10 MB each. + + ``POST /v3/transactions/{transaction_id}/travel-rule/messages/`` + + Args: + transaction_id: Didit-stable transaction UUID that owns the transfer. + kind + body: Required for a MESSAGE unless a file is attached. + requested_documents: Required (at least one) for a DOCUMENT_REQUEST, and only allowed there. + reply_to: A message of the same transfer. A reply that carries a document fulfils the document request it answers. + files: Multipart only. Up to 5 files of 10 MB each. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleMessage", + await self._client.request( + "post", + "/v3/transactions/{transaction_id}/travel-rule/messages/", + cast_to=TravelRuleMessage, + path_params={"transaction_id": transaction_id}, + auth="api_key", + body={ + "kind": kind, + "body": body, + "requested_documents": requested_documents, + "reply_to": reply_to, + "files": files, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def trigger_test_vasp_inbound( + self, + *, + beneficiary_wallet_address: str, + scenario: Literal["accept", "name_mismatch"] | str | NotGiven = NOT_GIVEN, + chain: str | NotGiven = NOT_GIVEN, + amount: str | NotGiven = NOT_GIVEN, + currency: str | NotGiven = NOT_GIVEN, + reference: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleTestVaspInboundResponse: + """Trigger an inbound transfer from the Didit Test VASP (sandbox). + + Sandbox applications only. The Didit Test VASP sends your application an inbound Travel Rule transfer, so you can exercise the receiving side of an exchange end to end before going live. The transfer is booked and decided by the same logic a real inbound transfer from a network runs against your wallet address book, so the inbox, the decision and the wallet ownership flow behave as they do live. Nothing leaves Didit. A live application receives 400. Send the same `reference` again to get the transfer the first call booked instead of a second one. + + ``POST /v3/travel-rule/test-vasp/inbound/`` + + Args: + beneficiary_wallet_address: The destination wallet address. The Didit Test VASP sends to it, matched against your wallet address book on the chain of the asset. A wallet with no matching entry is accepted and the inbound transaction is booked ON_HOLD. + scenario: What the Didit Test VASP declares the beneficiary to be. `accept` declares the holder's own name; `name_mismatch` declares a name no holder agrees with. The decision itself is always yours. + chain: Chain of the asset. + amount: Amount as a decimal string. + currency: Asset code. + reference: Optional idempotency key. A repeat with the same value returns the transfer the first call booked. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleTestVaspInboundResponse", + await self._client.request( + "post", + "/v3/travel-rule/test-vasp/inbound/", + cast_to=TravelRuleTestVaspInboundResponse, + auth="api_key", + body={ + "beneficiary_wallet_address": beneficiary_wallet_address, + "scenario": scenario, + "chain": chain, + "amount": amount, + "currency": currency, + "reference": reference, + }, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -231,6 +558,9 @@ def __init__(self, resource: TransactionsTravelRule) -> None: self._resource = resource self.update_transfer = to_raw_response_wrapper(resource.update_transfer) self.confirm_ownership = to_raw_response_wrapper(resource.confirm_ownership) + self.list_messages = to_raw_response_wrapper(resource.list_messages) + self.create_message = to_raw_response_wrapper(resource.create_message) + self.trigger_test_vasp_inbound = to_raw_response_wrapper(resource.trigger_test_vasp_inbound) class AsyncTransactionsTravelRuleWithRawResponse: @@ -238,6 +568,11 @@ def __init__(self, resource: AsyncTransactionsTravelRule) -> None: self._resource = resource self.update_transfer = async_to_raw_response_wrapper(resource.update_transfer) self.confirm_ownership = async_to_raw_response_wrapper(resource.confirm_ownership) + self.list_messages = async_to_raw_response_wrapper(resource.list_messages) + self.create_message = async_to_raw_response_wrapper(resource.create_message) + self.trigger_test_vasp_inbound = async_to_raw_response_wrapper( + resource.trigger_test_vasp_inbound + ) class TransactionsTravelRuleWithStreamingResponse: @@ -245,6 +580,11 @@ def __init__(self, resource: TransactionsTravelRule) -> None: self._resource = resource self.update_transfer = to_streamed_response_wrapper(resource.update_transfer) self.confirm_ownership = to_streamed_response_wrapper(resource.confirm_ownership) + self.list_messages = to_streamed_response_wrapper(resource.list_messages) + self.create_message = to_streamed_response_wrapper(resource.create_message) + self.trigger_test_vasp_inbound = to_streamed_response_wrapper( + resource.trigger_test_vasp_inbound + ) class AsyncTransactionsTravelRuleWithStreamingResponse: @@ -252,3 +592,8 @@ def __init__(self, resource: AsyncTransactionsTravelRule) -> None: self._resource = resource self.update_transfer = async_to_streamed_response_wrapper(resource.update_transfer) self.confirm_ownership = async_to_streamed_response_wrapper(resource.confirm_ownership) + self.list_messages = async_to_streamed_response_wrapper(resource.list_messages) + self.create_message = async_to_streamed_response_wrapper(resource.create_message) + self.trigger_test_vasp_inbound = async_to_streamed_response_wrapper( + resource.trigger_test_vasp_inbound + ) diff --git a/src/didit/resources/travel_rule/__init__.py b/src/didit/resources/travel_rule/__init__.py index 4d844e8..b7b41dc 100644 --- a/src/didit/resources/travel_rule/__init__.py +++ b/src/didit/resources/travel_rule/__init__.py @@ -17,8 +17,26 @@ to_streamed_response_wrapper, ) from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.travel_rule_counterparty_decision_response import ( + TravelRuleCounterpartyDecisionResponse, +) +from didit.types.components.travel_rule_counterparty_questionnaire import ( + TravelRuleCounterpartyQuestionnaire, +) +from didit.types.components.travel_rule_counterparty_questionnaire_list import ( + TravelRuleCounterpartyQuestionnaireList, +) +from didit.types.components.travel_rule_counterparty_screening_result import ( + TravelRuleCounterpartyScreeningResult, +) +from didit.types.components.travel_rule_counterparty_screening_status import ( + TravelRuleCounterpartyScreeningStatus, +) +from didit.types.components.travel_rule_data_sharing_fields import TravelRuleDataSharingFieldsParam from didit.types.components.travel_rule_inbound_response import TravelRuleInboundResponse +from didit.types.components.travel_rule_matching_rules import TravelRuleMatchingRulesParam from didit.types.components.travel_rule_settings_detail import TravelRuleSettingsDetail +from didit.types.components.travel_rule_utxo import TravelRuleUtxo from didit.types.components.travel_rule_widget_session_response import ( TravelRuleWidgetSessionResponse, ) @@ -27,8 +45,27 @@ from didit.types.travel_rule.register_inbound_params import ( TravelRuleRegisterInboundParamsOriginatingVasp, ) +from didit.types.travel_rule.send_compliance_email_confirmation_response import ( + TravelRuleSendComplianceEmailConfirmationResponse, +) if TYPE_CHECKING: + from didit.resources.travel_rule.counterparty_contacts import ( + AsyncTravelRuleCounterpartyContacts, + AsyncTravelRuleCounterpartyContactsWithRawResponse, + AsyncTravelRuleCounterpartyContactsWithStreamingResponse, + TravelRuleCounterpartyContacts, + TravelRuleCounterpartyContactsWithRawResponse, + TravelRuleCounterpartyContactsWithStreamingResponse, + ) + from didit.resources.travel_rule.transfers import ( + AsyncTravelRuleTransfers, + AsyncTravelRuleTransfersWithRawResponse, + AsyncTravelRuleTransfersWithStreamingResponse, + TravelRuleTransfers, + TravelRuleTransfersWithRawResponse, + TravelRuleTransfersWithStreamingResponse, + ) from didit.resources.travel_rule.wallet_addresses import ( AsyncTravelRuleWalletAddresses, AsyncTravelRuleWalletAddressesWithRawResponse, @@ -37,6 +74,14 @@ TravelRuleWalletAddressesWithRawResponse, TravelRuleWalletAddressesWithStreamingResponse, ) + from didit.resources.travel_rule.workspace import ( + AsyncTravelRuleWorkspace, + AsyncTravelRuleWorkspaceWithRawResponse, + AsyncTravelRuleWorkspaceWithStreamingResponse, + TravelRuleWorkspace, + TravelRuleWorkspaceWithRawResponse, + TravelRuleWorkspaceWithStreamingResponse, + ) __all__ = [ "TravelRule", @@ -51,17 +96,35 @@ class TravelRule(SyncAPIResource): """The ``travel_rule`` resource of the Didit API. - Methods: ``create_widget_session``, ``list_vasps``, ``register_inbound``, ``retrieve_settings``, ``update_settings``. + Methods: ``clear_vasp_decision``, ``create_widget_session``, ``list_vasp_questionnaires``, ``list_vasps``, ``register_inbound``, ``retrieve_settings``, ``retrieve_vasp``, ``retrieve_vasp_decision``, ``retrieve_vasp_screening``, ``screen_vasp``, ``send_compliance_email_confirmation``, ``send_vasp_questionnaire``, ``update_settings``, ``update_vasp_decision``. - Nested resources: ``wallet_addresses``. + Nested resources: ``counterparty_contacts``, ``transfers``, ``wallet_addresses``, ``workspace``. """ + @cached_property + def counterparty_contacts(self) -> TravelRuleCounterpartyContacts: + from didit.resources.travel_rule.counterparty_contacts import TravelRuleCounterpartyContacts + + return TravelRuleCounterpartyContacts(self._client) + + @cached_property + def transfers(self) -> TravelRuleTransfers: + from didit.resources.travel_rule.transfers import TravelRuleTransfers + + return TravelRuleTransfers(self._client) + @cached_property def wallet_addresses(self) -> TravelRuleWalletAddresses: from didit.resources.travel_rule.wallet_addresses import TravelRuleWalletAddresses return TravelRuleWalletAddresses(self._client) + @cached_property + def workspace(self) -> TravelRuleWorkspace: + from didit.resources.travel_rule.workspace import TravelRuleWorkspace + + return TravelRuleWorkspace(self._client) + @cached_property def with_raw_response(self) -> TravelRuleWithRawResponse: """Every method of this resource, returning the raw HTTP response.""" @@ -113,7 +176,7 @@ def update_settings( legal_name: str | NotGiven = NOT_GIVEN, lei: str | NotGiven = NOT_GIVEN, jurisdiction: str | NotGiven = NOT_GIVEN, - name_matching_strictness: Literal["NONE", "STRICT", "DEFAULT", "FUZZY"] + name_matching_strictness: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str | NotGiven = NOT_GIVEN, confirmation_timeout_hours: int | NotGiven = NOT_GIVEN, @@ -121,11 +184,22 @@ def update_settings( compliance_email: str | NotGiven = NOT_GIVEN, is_discoverable: bool | NotGiven = NOT_GIVEN, threshold_amount: str | NotGiven = NOT_GIVEN, + ownership_validation_mode: Literal["FULL", "SIMPLIFIED", "AUTOMATED"] + | str + | NotGiven = NOT_GIVEN, + simplified_validation_threshold: str | NotGiven = NOT_GIVEN, + data_sharing_fields: TravelRuleDataSharingFieldsParam | NotGiven = NOT_GIVEN, + matching_rules: TravelRuleMatchingRulesParam | NotGiven = NOT_GIVEN, inbound_auto_accept: bool | NotGiven = NOT_GIVEN, allow_self_declaration: bool | NotGiven = NOT_GIVEN, allow_screenshot_proof: bool | NotGiven = NOT_GIVEN, auto_wallet_verification: bool | NotGiven = NOT_GIVEN, vasp_attribution_enabled: bool | NotGiven = NOT_GIVEN, + counterparty_screening_enabled: bool | NotGiven = NOT_GIVEN, + sunrise_policy: Literal["", "HOLD", "REJECT", "PROCEED"] | str | NotGiven = NOT_GIVEN, + non_responsive_policy: Literal["", "HOLD", "REJECT", "PROCEED"] + | str + | NotGiven = NOT_GIVEN, disabled_networks: Sequence[Literal["GTR", "TRUST", "VERIFYVASP", "SYGNA"] | str] | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, @@ -144,17 +218,24 @@ def update_settings( legal_name lei jurisdiction - name_matching_strictness + name_matching_strictness: `CONTAINS` accepts one name whose words all appear in the other. confirmation_timeout_hours timeout_outcome - compliance_email - is_discoverable - threshold_amount: Decimal string. Minimum transfer amount that triggers a managed exchange. + compliance_email: Compliance contact email for your VASP profile. The Didit network lists your profile only once this address is confirmed through an emailed link (POST /v3/travel-rule/settings/compliance-email/confirmation/); changing it needs a new confirmation. + is_discoverable: Whether your VASP profile can be listed in the Didit network: found in GET /v3/travel-rule/vasps/ by other Didit customers and reached on the INTERNAL rail. Listing also needs a live application, Travel Rule on, a legal name and a confirmed compliance email; didit_network says which are missing. Default true. + threshold_amount: Decimal string, zero or greater, in your application's default currency. Minimum transfer amount that triggers a managed exchange. + ownership_validation_mode: See the response field of the same name. + simplified_validation_threshold: Decimal string, 0 or more. + data_sharing_fields: Merged per entity type: an entity type you leave out keeps its current choice. A choice that cannot meet the originator obligations of your `jurisdiction` regime (EU TFR or FATF Recommendation 16), an empty list included, is refused with `400`. + matching_rules: Merged per entity type, and per field inside it. A field that entity type does not compare (for example `date_of_birth` for `company`) is refused with `400`. inbound_auto_accept allow_self_declaration allow_screenshot_proof: When true, the wallet-ownership widget offers screenshot upload as an ownership evidence method. Default true. auto_wallet_verification vasp_attribution_enabled + counterparty_screening_enabled: Opt-in sanctions and adverse-media screening of counterparty VASPs, billed per screening at the Company AML Screening price. When on, Didit screens the counterparty an outbound exchange reaches (reusing a screening younger than 30 days) and you can screen one with POST /v3/travel-rule/vasps/{vasp_id}/screening/. Default false: nothing is screened or billed. + sunrise_policy: HOLD, REJECT, PROCEED, or empty to follow timeout_outcome. See the response field of the same name. + non_responsive_policy: HOLD, REJECT, PROCEED, or empty to follow timeout_outcome. See the response field of the same name. disabled_networks: Networks opted out of platform membership. Does not disable your connected own memberships or provision unavailable networks. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. @@ -179,11 +260,18 @@ def update_settings( "compliance_email": compliance_email, "is_discoverable": is_discoverable, "threshold_amount": threshold_amount, + "ownership_validation_mode": ownership_validation_mode, + "simplified_validation_threshold": simplified_validation_threshold, + "data_sharing_fields": data_sharing_fields, + "matching_rules": matching_rules, "inbound_auto_accept": inbound_auto_accept, "allow_self_declaration": allow_self_declaration, "allow_screenshot_proof": allow_screenshot_proof, "auto_wallet_verification": auto_wallet_verification, "vasp_attribution_enabled": vasp_attribution_enabled, + "counterparty_screening_enabled": counterparty_screening_enabled, + "sunrise_policy": sunrise_policy, + "non_responsive_policy": non_responsive_policy, "disabled_networks": disabled_networks, }, extra_headers=extra_headers, @@ -193,10 +281,54 @@ def update_settings( ), ) + def send_compliance_email_confirmation( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleSendComplianceEmailConfirmationResponse: + """Send the compliance email confirmation. + + Emails a single-use confirmation link to your VASP profile's compliance_email. Opening the link and pressing its confirm button confirms the address, which the Didit network needs before it lists your profile. A new request replaces the previous link; the link stops working when the compliance email changes. One email per minute per address. + + ``POST /v3/travel-rule/settings/compliance-email/confirmation/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleSendComplianceEmailConfirmationResponse", + self._client.request( + "post", + "/v3/travel-rule/settings/compliance-email/confirmation/", + cast_to=TravelRuleSendComplianceEmailConfirmationResponse, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + def list_vasps( self, *, search: str | NotGiven = NOT_GIVEN, + network: Literal["GTR", "TRUST", "VERIFYVASP", "SYGNA", "TRISA"] + | str + | NotGiven = NOT_GIVEN, + jurisdiction: str | NotGiven = NOT_GIVEN, + reachable: bool | NotGiven = NOT_GIVEN, + kind: Literal["directory", "didit"] | str | NotGiven = NOT_GIVEN, + risk: Literal["LOW", "MEDIUM", "HIGH"] | str | NotGiven = NOT_GIVEN, + responsiveness: Literal["RESPONSIVE", "UNRESPONSIVE", "UNKNOWN"] + | str + | NotGiven = NOT_GIVEN, limit: int | NotGiven = NOT_GIVEN, offset: int | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, @@ -206,12 +338,18 @@ def list_vasps( ) -> SyncPage[VaspDirectoryEntry]: """Search the VASP directory. - Paginated search across discoverable Didit customers with Travel Rule enabled, plus VASPs catalogued in the internal counterparty registry. Excludes your own application's profile. + Paginated, name-sorted search of the VASP directory: the shared directory entries, which Didit builds from the ESMA interim MiCA register, the public TRISA directory, the global LEI register and its own curation, plus the members of the Didit network. Each row carries the routes your application has to it today. Your own application's profile is never listed. Rows also carry the shared due-diligence assessment and your application's own decision, entity screening and questionnaires. ``GET /v3/travel-rule/vasps/`` Args: - search: Case-insensitive substring match on VASP name. + search: A name or alias fragment (case-insensitive), an exact LEI, or a domain (example.com or a URL on it). Didit network members match on their legal name, LEI or the domain of their compliance email. + network: Only directory entries a network's member directory currently lists. + jurisdiction: ISO 3166 alpha-2 or alpha-3 country code. 400 for anything else. + reachable: true: only VASPs your application has an available route to (Didit network members included). false: only directory entries it cannot reach today. + kind: Only directory entries, or only Didit network members. + risk: Only directory entries whose risk_level is this band. Didit network members, which are not scored, never match. + responsiveness: Only directory entries whose responsiveness.status is this value. Didit network members never match. limit offset extra_headers: Headers to add to (or, with ``None``, remove from) this request. @@ -227,7 +365,17 @@ def list_vasps( cast_to=TravelRuleListVaspsResponse, item_type=VaspDirectoryEntry, auth="api_key", - query={"search": search, "limit": limit, "offset": offset}, + query={ + "search": search, + "network": network, + "jurisdiction": jurisdiction, + "reachable": reachable, + "kind": kind, + "risk": risk, + "responsiveness": responsiveness, + "limit": limit, + "offset": offset, + }, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -235,59 +383,36 @@ def list_vasps( ), ) - def register_inbound( + def retrieve_vasp( self, + vasp_id: str, *, - chain: str, - txid: str, - wallet_address: str, - amount: str, - currency: str, - originator_data: Mapping[str, object] | NotGiven = NOT_GIVEN, - beneficiary_data: Mapping[str, object] | NotGiven = NOT_GIVEN, - originating_vasp: TravelRuleRegisterInboundParamsOriginatingVasp | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TravelRuleInboundResponse: - """Register an after-deposit (sunrise) inbound transfer. + ) -> VaspDirectoryEntry: + """Get a VASP directory entry. - Register Travel Rule data for a crypto deposit that already settled on-chain before any exchange took place. Creates an inbound travelRule transaction (or reuses an unclaimed deposit) and resolves the transfer against your wallet address book. A transaction.created webhook fires for newly-created transactions. + One directory entry or Didit network member by the id the search returned, with the same fields and the routes your application has to it. - ``POST /v3/travel-rule/inbound/`` + ``GET /v3/travel-rule/vasps/{vasp_id}/`` Args: - chain: Chain the deposit settled on, e.g. "ethereum". - txid: On-chain transaction hash. Deduped together with wallet_address. - wallet_address: The destination wallet you received the deposit on. - amount: Deposit amount as a decimal string. - currency: Deposit asset, e.g. "USDC". - originator_data: IVMS-101 originator payload from the sending VASP, if known. - beneficiary_data: IVMS-101 beneficiary payload (your customer). - originating_vasp: Optional description of the sending VASP. + vasp_id: The id of a row of GET /v3/travel-rule/vasps/. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "TravelRuleInboundResponse", + "VaspDirectoryEntry", self._client.request( - "post", - "/v3/travel-rule/inbound/", - cast_to=TravelRuleInboundResponse, + "get", + "/v3/travel-rule/vasps/{vasp_id}/", + cast_to=VaspDirectoryEntry, + path_params={"vasp_id": vasp_id}, auth="api_key", - body={ - "chain": chain, - "txid": txid, - "wallet_address": wallet_address, - "amount": amount, - "currency": currency, - "originator_data": originator_data, - "beneficiary_data": beneficiary_data, - "originating_vasp": originating_vasp, - }, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -295,59 +420,36 @@ def register_inbound( ), ) - def create_widget_session( + def retrieve_vasp_decision( self, + vasp_id: str, *, - wallet_address: str, - chain: str, - holder_name: str | NotGiven = NOT_GIVEN, - vendor_data: str | NotGiven = NOT_GIVEN, - transaction_id: str | NotGiven = NOT_GIVEN, - satoshi_deposit_address: str | NotGiven = NOT_GIVEN, - callback_url: str | NotGiven = NOT_GIVEN, - expires_in_minutes: int | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TravelRuleWidgetSessionResponse: - """Mint a wallet-ownership widget session. + ) -> TravelRuleCounterpartyDecisionResponse: + """Get your decision on a VASP. - Mint a hosted wallet-ownership widget URL/token. Send the customer to the returned url to prove control of a wallet (message signing, Satoshi test, screenshot, or self-declaration). The widget is web-only in this release - open the url in the system browser or a custom tab, never inside a mobile SDK webview. + Your application's decision on a counterparty (a directory entry or a Didit network member): approved, restricted or blocked. An entity of a brand with no decision of its own follows the brand's decision, named in inherited_from. Private to your application. - ``POST /v3/travel-rule/widget-session/`` + ``GET /v3/travel-rule/vasps/{vasp_id}/decision/`` Args: - wallet_address: The wallet address to verify. - chain: Chain identifier for the address, e.g. "ethereum", "bitcoin", "solana". - holder_name: Name of the wallet holder, used for the beneficiary name match. - vendor_data: Your internal identifier for the holder, carried onto the created address-book entry. - transaction_id: Optional. Didit transaction UUID of a Travel Rule transfer to advance when the proof verifies. - satoshi_deposit_address: Optional. A deposit address you control; supplying it enables the SATOSHI_TEST proof method. - callback_url: Optional. Where the widget returns the customer after completion. - expires_in_minutes: Optional. Lifetime of the minted link, in minutes. Automatically extended to cover the Satoshi test window when SATOSHI_TEST is offered. + vasp_id: The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "TravelRuleWidgetSessionResponse", + "TravelRuleCounterpartyDecisionResponse", self._client.request( - "post", - "/v3/travel-rule/widget-session/", - cast_to=TravelRuleWidgetSessionResponse, + "get", + "/v3/travel-rule/vasps/{vasp_id}/decision/", + cast_to=TravelRuleCounterpartyDecisionResponse, + path_params={"vasp_id": vasp_id}, auth="api_key", - body={ - "wallet_address": wallet_address, - "chain": chain, - "holder_name": holder_name, - "vendor_data": vendor_data, - "transaction_id": transaction_id, - "satoshi_deposit_address": satoshi_deposit_address, - "callback_url": callback_url, - "expires_in_minutes": expires_in_minutes, - }, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -355,58 +457,41 @@ def create_widget_session( ), ) - -class AsyncTravelRule(AsyncAPIResource): - """The ``travel_rule`` resource of the Didit API. - - Methods: ``create_widget_session``, ``list_vasps``, ``register_inbound``, ``retrieve_settings``, ``update_settings``. - - Nested resources: ``wallet_addresses``. - """ - - @cached_property - def wallet_addresses(self) -> AsyncTravelRuleWalletAddresses: - from didit.resources.travel_rule.wallet_addresses import AsyncTravelRuleWalletAddresses - - return AsyncTravelRuleWalletAddresses(self._client) - - @cached_property - def with_raw_response(self) -> AsyncTravelRuleWithRawResponse: - """Every method of this resource, returning the raw HTTP response.""" - return AsyncTravelRuleWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncTravelRuleWithStreamingResponse: - """Every method of this resource, returning the HTTP response before reading its body.""" - return AsyncTravelRuleWithStreamingResponse(self) - - async def retrieve_settings( + def update_vasp_decision( self, + vasp_id: str, *, + decision: Literal["APPROVED", "RESTRICTED", "BLOCKED"] | str, + reason: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TravelRuleSettingsDetail: - """Get Travel Rule settings. + ) -> TravelRuleCounterpartyDecisionResponse: + """Set your decision on a VASP. - Read the application's Travel Rule settings and VASP profile. Auto-creates a default (disabled) settings row and an empty profile on first read. + Make or change your application's decision on a counterparty: APPROVED, RESTRICTED or BLOCKED, with an optional reason. It never changes the shared directory entry and acts only through your transaction rules (counterparty_dd_status). A decision on a brand also covers each of its entities that has no decision of its own. - ``GET /v3/travel-rule/settings/`` + ``PUT /v3/travel-rule/vasps/{vasp_id}/decision/`` Args: + vasp_id: The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member. + decision + reason: Optional. Replaces the previous reason; omit it to clear it. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "TravelRuleSettingsDetail", - await self._client.request( - "get", - "/v3/travel-rule/settings/", - cast_to=TravelRuleSettingsDetail, + "TravelRuleCounterpartyDecisionResponse", + self._client.request( + "put", + "/v3/travel-rule/vasps/{vasp_id}/decision/", + cast_to=TravelRuleCounterpartyDecisionResponse, + path_params={"vasp_id": vasp_id}, auth="api_key", + body={"decision": decision, "reason": reason}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -414,86 +499,36 @@ async def retrieve_settings( ), ) - async def update_settings( + def clear_vasp_decision( self, + vasp_id: str, *, - is_enabled: bool | NotGiven = NOT_GIVEN, - legal_name: str | NotGiven = NOT_GIVEN, - lei: str | NotGiven = NOT_GIVEN, - jurisdiction: str | NotGiven = NOT_GIVEN, - name_matching_strictness: Literal["NONE", "STRICT", "DEFAULT", "FUZZY"] - | str - | NotGiven = NOT_GIVEN, - confirmation_timeout_hours: int | NotGiven = NOT_GIVEN, - timeout_outcome: Literal["HOLD", "REJECT", "PROCEED"] | str | NotGiven = NOT_GIVEN, - compliance_email: str | NotGiven = NOT_GIVEN, - is_discoverable: bool | NotGiven = NOT_GIVEN, - threshold_amount: str | NotGiven = NOT_GIVEN, - inbound_auto_accept: bool | NotGiven = NOT_GIVEN, - allow_self_declaration: bool | NotGiven = NOT_GIVEN, - allow_screenshot_proof: bool | NotGiven = NOT_GIVEN, - auto_wallet_verification: bool | NotGiven = NOT_GIVEN, - vasp_attribution_enabled: bool | NotGiven = NOT_GIVEN, - disabled_networks: Sequence[Literal["GTR", "TRUST", "VERIFYVASP", "SYGNA"] | str] - | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TravelRuleSettingsDetail: - """Update Travel Rule settings. + ) -> TravelRuleCounterpartyDecisionResponse: + """Clear your decision on a VASP. - Upsert the application's Travel Rule settings and VASP profile. Partial update - only send the fields you want to change. Setting `is_enabled: true` requires a non-blank `legal_name` (already stored or included in the same request), otherwise returns 400. + Clear your application's own decision on a counterparty. The response shows the decision that applies afterwards: null, or for an entity of a brand the brand's decision, which the entity follows again. - ``PUT /v3/travel-rule/settings/`` + ``DELETE /v3/travel-rule/vasps/{vasp_id}/decision/`` Args: - is_enabled - legal_name - lei - jurisdiction - name_matching_strictness - confirmation_timeout_hours - timeout_outcome - compliance_email - is_discoverable - threshold_amount: Decimal string. Minimum transfer amount that triggers a managed exchange. - inbound_auto_accept - allow_self_declaration - allow_screenshot_proof: When true, the wallet-ownership widget offers screenshot upload as an ownership evidence method. Default true. - auto_wallet_verification - vasp_attribution_enabled - disabled_networks: Networks opted out of platform membership. Does not disable your connected own memberships or provision unavailable networks. + vasp_id: The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "TravelRuleSettingsDetail", - await self._client.request( - "put", - "/v3/travel-rule/settings/", - cast_to=TravelRuleSettingsDetail, + "TravelRuleCounterpartyDecisionResponse", + self._client.request( + "delete", + "/v3/travel-rule/vasps/{vasp_id}/decision/", + cast_to=TravelRuleCounterpartyDecisionResponse, + path_params={"vasp_id": vasp_id}, auth="api_key", - body={ - "is_enabled": is_enabled, - "legal_name": legal_name, - "lei": lei, - "jurisdiction": jurisdiction, - "name_matching_strictness": name_matching_strictness, - "confirmation_timeout_hours": confirmation_timeout_hours, - "timeout_outcome": timeout_outcome, - "compliance_email": compliance_email, - "is_discoverable": is_discoverable, - "threshold_amount": threshold_amount, - "inbound_auto_accept": inbound_auto_accept, - "allow_self_declaration": allow_self_declaration, - "allow_screenshot_proof": allow_screenshot_proof, - "auto_wallet_verification": auto_wallet_verification, - "vasp_attribution_enabled": vasp_attribution_enabled, - "disabled_networks": disabled_networks, - }, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -501,41 +536,36 @@ async def update_settings( ), ) - def list_vasps( + def retrieve_vasp_screening( self, + vasp_id: str, *, - search: str | NotGiven = NOT_GIVEN, - limit: int | NotGiven = NOT_GIVEN, - offset: int | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> AsyncPaginator[VaspDirectoryEntry]: - """Search the VASP directory. + ) -> TravelRuleCounterpartyScreeningStatus: + """Get a VASP's entity screening. - Paginated search across discoverable Didit customers with Travel Rule enabled, plus VASPs catalogued in the internal counterparty registry. Excludes your own application's profile. + Whether counterparty screening is on for your application, what one screening costs your organization, how long a result stays fresh, and your latest sanctions and adverse-media screening of the counterparty. Reading it is free. - ``GET /v3/travel-rule/vasps/`` + ``GET /v3/travel-rule/vasps/{vasp_id}/screening/`` Args: - search: Case-insensitive substring match on VASP name. - limit - offset + vasp_id: The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "AsyncPaginator[VaspDirectoryEntry]", - self._client.request_page( + "TravelRuleCounterpartyScreeningStatus", + self._client.request( "get", - "/v3/travel-rule/vasps/", - cast_to=TravelRuleListVaspsResponse, - item_type=VaspDirectoryEntry, + "/v3/travel-rule/vasps/{vasp_id}/screening/", + cast_to=TravelRuleCounterpartyScreeningStatus, + path_params={"vasp_id": vasp_id}, auth="api_key", - query={"search": search, "limit": limit, "offset": offset}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -543,59 +573,39 @@ def list_vasps( ), ) - async def register_inbound( + def screen_vasp( self, + vasp_id: str, *, - chain: str, - txid: str, - wallet_address: str, - amount: str, - currency: str, - originator_data: Mapping[str, object] | NotGiven = NOT_GIVEN, - beneficiary_data: Mapping[str, object] | NotGiven = NOT_GIVEN, - originating_vasp: TravelRuleRegisterInboundParamsOriginatingVasp | NotGiven = NOT_GIVEN, + refresh: bool | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> TravelRuleInboundResponse: - """Register an after-deposit (sunrise) inbound transfer. + ) -> TravelRuleCounterpartyScreeningResult: + """Screen a VASP. - Register Travel Rule data for a crypto deposit that already settled on-chain before any exchange took place. Creates an inbound travelRule transaction (or reuses an unclaimed deposit) and resolves the transfer against your wallet address book. A transaction.created webhook fires for newly-created transactions. + Screen the counterparty entity against sanctions and adverse media for your application. Opt-in: counterparty_screening_enabled must be on in your Travel Rule settings. Each new screening is billed at your organization's Company AML Screening price; a screening younger than 30 days is returned instead (200, not billed) unless you send refresh: true. One screening of a counterparty runs at a time per application. The result is private to your application. Sandbox applications are never screened. - ``POST /v3/travel-rule/inbound/`` + ``POST /v3/travel-rule/vasps/{vasp_id}/screening/`` Args: - chain: Chain the deposit settled on, e.g. "ethereum". - txid: On-chain transaction hash. Deduped together with wallet_address. - wallet_address: The destination wallet you received the deposit on. - amount: Deposit amount as a decimal string. - currency: Deposit asset, e.g. "USDC". - originator_data: IVMS-101 originator payload from the sending VASP, if known. - beneficiary_data: IVMS-101 beneficiary payload (your customer). - originating_vasp: Optional description of the sending VASP. + vasp_id: The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member. + refresh: true screens again (and bills again) even when a fresh screening exists. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "TravelRuleInboundResponse", - await self._client.request( + "TravelRuleCounterpartyScreeningResult", + self._client.request( "post", - "/v3/travel-rule/inbound/", - cast_to=TravelRuleInboundResponse, + "/v3/travel-rule/vasps/{vasp_id}/screening/", + cast_to=TravelRuleCounterpartyScreeningResult, + path_params={"vasp_id": vasp_id}, auth="api_key", - body={ - "chain": chain, - "txid": txid, - "wallet_address": wallet_address, - "amount": amount, - "currency": currency, - "originator_data": originator_data, - "beneficiary_data": beneficiary_data, - "originating_vasp": originating_vasp, - }, + body={"refresh": refresh}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -603,14 +613,883 @@ async def register_inbound( ), ) - async def create_widget_session( + def list_vasp_questionnaires( + self, + vasp_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleCounterpartyQuestionnaireList: + """List due-diligence questionnaires with a VASP. + + Every due-diligence questionnaire your application sent the counterparty, newest first, with its status and, once answered, the answers. Private to your application. + + ``GET /v3/travel-rule/vasps/{vasp_id}/questionnaires/`` + + Args: + vasp_id: The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleCounterpartyQuestionnaireList", + self._client.request( + "get", + "/v3/travel-rule/vasps/{vasp_id}/questionnaires/", + cast_to=TravelRuleCounterpartyQuestionnaireList, + path_params={"vasp_id": vasp_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def send_vasp_questionnaire( + self, + vasp_id: str, + *, + sent_to: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleCounterpartyQuestionnaire: + """Send a due-diligence questionnaire to a VASP. + + Email Didit's standard due-diligence questionnaire to a compliance address on record for the counterparty, with a single-use answer link. The counterparty has 30 days to answer every question; the answers stay current for 365 days. A new send replaces your earlier unanswered questionnaire to the same counterparty, whose link stops working. One email per minute per address. + + ``POST /v3/travel-rule/vasps/{vasp_id}/questionnaires/`` + + Args: + vasp_id: The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member. + sent_to: Optional. A compliance address on record for the counterparty: for a directory entry its compliance address or a contact your application sees, for a Didit network member its confirmed compliance email. Default: the address a Travel Rule email to the counterparty goes to (your own contact first, then a platform contact, then the entry's compliance address). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleCounterpartyQuestionnaire", + self._client.request( + "post", + "/v3/travel-rule/vasps/{vasp_id}/questionnaires/", + cast_to=TravelRuleCounterpartyQuestionnaire, + path_params={"vasp_id": vasp_id}, + auth="api_key", + body={"sent_to": sent_to}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def register_inbound( + self, + *, + chain: str, + txid: str, + wallet_address: str, + amount: str, + currency: str, + originator_data: Mapping[str, object] | NotGiven = NOT_GIVEN, + beneficiary_data: Mapping[str, object] | NotGiven = NOT_GIVEN, + originating_vasp: TravelRuleRegisterInboundParamsOriginatingVasp | NotGiven = NOT_GIVEN, + memo: str | NotGiven = NOT_GIVEN, + utxo: TravelRuleUtxo | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleInboundResponse: + """Register an after-deposit (sunrise) inbound transfer. + + Register Travel Rule data for a crypto deposit that already settled on-chain before any exchange took place. Creates an inbound travelRule transaction (or reuses an unclaimed deposit) and resolves the transfer against your wallet address book. For a newly created transaction, Didit sends transaction.created first and the travel_rule.status.updated webhooks after it. + + ``POST /v3/travel-rule/inbound/`` + + Args: + chain: Chain the deposit settled on, for example `ETH`. Stored lowercase and compared case-insensitively with your wallet address book entries, so use the same chain token there. + txid: On-chain transaction hash. Deduped together with wallet_address. + wallet_address: The destination wallet you received the deposit on. An XRP X-address is split into its classic address and tag. + amount: Deposit amount as a decimal string, zero or greater, with up to 18 decimal places. + currency: Deposit asset, e.g. "USDC". + originator_data: Originator details from the sending VASP, if known, as a flat object such as `{"name": "..."}`. Didit reads name (or full_name), first_name, last_name and vendor_data from it. + beneficiary_data: Beneficiary details (your customer), as a flat object such as `{"name": "..."}`. Didit matches name against the holder_name of the matched address book entry, and sets wallet_address and chain from the request. + originating_vasp: Optional description of the sending VASP. It stays on this transfer only (`metadata.originating_vasp`, and the transfer's originator VASP in GET /v3/travel-rule/transfers/); it never creates, edits or links a shared VASP directory entry. + memo: Optional. The deposit's destination tag or memo on a memo network: on a shared deposit address it is what tells your customers apart, and the address-book entry for that memo is matched. XRP: a destination tag, a whole number from 0 to 4294967295 (a JSON number is accepted). XLM: a text memo of up to 28 bytes, a 64-bit id or a 32-byte hash in hex. EOS: up to 256 bytes. ATOM: up to 256 characters. HBAR: up to 100 bytes. Bitcoin, EVM, Solana, Tron, Litecoin, Bitcoin Cash, Dogecoin and Lightning wallets take no memo: one there is a `400`. Alias: `destinationTag` or `destination_tag`. + utxo: Optional, UTXO chains only. The deposit transaction's inputs and outputs: the sender's wallet is the declared `originator_data.wallet_address`, else the largest input. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleInboundResponse", + self._client.request( + "post", + "/v3/travel-rule/inbound/", + cast_to=TravelRuleInboundResponse, + auth="api_key", + body={ + "chain": chain, + "txid": txid, + "wallet_address": wallet_address, + "amount": amount, + "currency": currency, + "originator_data": originator_data, + "beneficiary_data": beneficiary_data, + "originating_vasp": originating_vasp, + "memo": memo, + "utxo": utxo, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create_widget_session( + self, + *, + wallet_address: str | NotGiven = NOT_GIVEN, + chain: str | NotGiven = NOT_GIVEN, + holder_name: str | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + transaction_id: str | None | NotGiven = NOT_GIVEN, + satoshi_deposit_address: str | NotGiven = NOT_GIVEN, + callback_url: str | NotGiven = NOT_GIVEN, + expires_in_minutes: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleWidgetSessionResponse: + """Mint a wallet-ownership widget session. + + Mint a hosted wallet-ownership widget URL/token. Send the customer to the returned url to prove control of a wallet (message signing, Satoshi test, screenshot, or self-declaration). The widget is web-only in this release - open the url in the system browser or a custom tab, never inside a mobile SDK webview. + + ``POST /v3/travel-rule/widget-session/`` + + Args: + wallet_address: The wallet address to verify. Optional; when empty, the address must be supplied when a proof is started. + chain: Chain token for the address, for example `ETH`, `BTC` or `SOL`. Use the same token as in your wallet address book. Message signing and the Satoshi test are offered only for a supported chain. + holder_name: Name of the wallet holder, used for the beneficiary name match. + vendor_data: Your internal identifier for the holder, carried onto the created address-book entry. + transaction_id: Optional. Didit transaction UUID of a Travel Rule transfer to advance when the proof verifies. + satoshi_deposit_address: Optional. A deposit address you control; supplying it enables the SATOSHI_TEST proof method. + callback_url: Optional. Absolute http(s) URL where the widget returns the customer after completion. + expires_in_minutes: Optional. Lifetime of the minted link, in minutes. Automatically extended to cover the Satoshi test window when SATOSHI_TEST is offered. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleWidgetSessionResponse", + self._client.request( + "post", + "/v3/travel-rule/widget-session/", + cast_to=TravelRuleWidgetSessionResponse, + auth="api_key", + body={ + "wallet_address": wallet_address, + "chain": chain, + "holder_name": holder_name, + "vendor_data": vendor_data, + "transaction_id": transaction_id, + "satoshi_deposit_address": satoshi_deposit_address, + "callback_url": callback_url, + "expires_in_minutes": expires_in_minutes, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncTravelRule(AsyncAPIResource): + """The ``travel_rule`` resource of the Didit API. + + Methods: ``clear_vasp_decision``, ``create_widget_session``, ``list_vasp_questionnaires``, ``list_vasps``, ``register_inbound``, ``retrieve_settings``, ``retrieve_vasp``, ``retrieve_vasp_decision``, ``retrieve_vasp_screening``, ``screen_vasp``, ``send_compliance_email_confirmation``, ``send_vasp_questionnaire``, ``update_settings``, ``update_vasp_decision``. + + Nested resources: ``counterparty_contacts``, ``transfers``, ``wallet_addresses``, ``workspace``. + """ + + @cached_property + def counterparty_contacts(self) -> AsyncTravelRuleCounterpartyContacts: + from didit.resources.travel_rule.counterparty_contacts import ( + AsyncTravelRuleCounterpartyContacts, + ) + + return AsyncTravelRuleCounterpartyContacts(self._client) + + @cached_property + def transfers(self) -> AsyncTravelRuleTransfers: + from didit.resources.travel_rule.transfers import AsyncTravelRuleTransfers + + return AsyncTravelRuleTransfers(self._client) + + @cached_property + def wallet_addresses(self) -> AsyncTravelRuleWalletAddresses: + from didit.resources.travel_rule.wallet_addresses import AsyncTravelRuleWalletAddresses + + return AsyncTravelRuleWalletAddresses(self._client) + + @cached_property + def workspace(self) -> AsyncTravelRuleWorkspace: + from didit.resources.travel_rule.workspace import AsyncTravelRuleWorkspace + + return AsyncTravelRuleWorkspace(self._client) + + @cached_property + def with_raw_response(self) -> AsyncTravelRuleWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncTravelRuleWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncTravelRuleWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncTravelRuleWithStreamingResponse(self) + + async def retrieve_settings( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleSettingsDetail: + """Get Travel Rule settings. + + Read the application's Travel Rule settings and VASP profile. Auto-creates a default (disabled) settings row and an empty profile on first read. + + ``GET /v3/travel-rule/settings/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleSettingsDetail", + await self._client.request( + "get", + "/v3/travel-rule/settings/", + cast_to=TravelRuleSettingsDetail, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_settings( + self, + *, + is_enabled: bool | NotGiven = NOT_GIVEN, + legal_name: str | NotGiven = NOT_GIVEN, + lei: str | NotGiven = NOT_GIVEN, + jurisdiction: str | NotGiven = NOT_GIVEN, + name_matching_strictness: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] + | str + | NotGiven = NOT_GIVEN, + confirmation_timeout_hours: int | NotGiven = NOT_GIVEN, + timeout_outcome: Literal["HOLD", "REJECT", "PROCEED"] | str | NotGiven = NOT_GIVEN, + compliance_email: str | NotGiven = NOT_GIVEN, + is_discoverable: bool | NotGiven = NOT_GIVEN, + threshold_amount: str | NotGiven = NOT_GIVEN, + ownership_validation_mode: Literal["FULL", "SIMPLIFIED", "AUTOMATED"] + | str + | NotGiven = NOT_GIVEN, + simplified_validation_threshold: str | NotGiven = NOT_GIVEN, + data_sharing_fields: TravelRuleDataSharingFieldsParam | NotGiven = NOT_GIVEN, + matching_rules: TravelRuleMatchingRulesParam | NotGiven = NOT_GIVEN, + inbound_auto_accept: bool | NotGiven = NOT_GIVEN, + allow_self_declaration: bool | NotGiven = NOT_GIVEN, + allow_screenshot_proof: bool | NotGiven = NOT_GIVEN, + auto_wallet_verification: bool | NotGiven = NOT_GIVEN, + vasp_attribution_enabled: bool | NotGiven = NOT_GIVEN, + counterparty_screening_enabled: bool | NotGiven = NOT_GIVEN, + sunrise_policy: Literal["", "HOLD", "REJECT", "PROCEED"] | str | NotGiven = NOT_GIVEN, + non_responsive_policy: Literal["", "HOLD", "REJECT", "PROCEED"] + | str + | NotGiven = NOT_GIVEN, + disabled_networks: Sequence[Literal["GTR", "TRUST", "VERIFYVASP", "SYGNA"] | str] + | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleSettingsDetail: + """Update Travel Rule settings. + + Upsert the application's Travel Rule settings and VASP profile. Partial update - only send the fields you want to change. Setting `is_enabled: true` requires a non-blank `legal_name` (already stored or included in the same request), otherwise returns 400. + + ``PUT /v3/travel-rule/settings/`` + + Args: + is_enabled + legal_name + lei + jurisdiction + name_matching_strictness: `CONTAINS` accepts one name whose words all appear in the other. + confirmation_timeout_hours + timeout_outcome + compliance_email: Compliance contact email for your VASP profile. The Didit network lists your profile only once this address is confirmed through an emailed link (POST /v3/travel-rule/settings/compliance-email/confirmation/); changing it needs a new confirmation. + is_discoverable: Whether your VASP profile can be listed in the Didit network: found in GET /v3/travel-rule/vasps/ by other Didit customers and reached on the INTERNAL rail. Listing also needs a live application, Travel Rule on, a legal name and a confirmed compliance email; didit_network says which are missing. Default true. + threshold_amount: Decimal string, zero or greater, in your application's default currency. Minimum transfer amount that triggers a managed exchange. + ownership_validation_mode: See the response field of the same name. + simplified_validation_threshold: Decimal string, 0 or more. + data_sharing_fields: Merged per entity type: an entity type you leave out keeps its current choice. A choice that cannot meet the originator obligations of your `jurisdiction` regime (EU TFR or FATF Recommendation 16), an empty list included, is refused with `400`. + matching_rules: Merged per entity type, and per field inside it. A field that entity type does not compare (for example `date_of_birth` for `company`) is refused with `400`. + inbound_auto_accept + allow_self_declaration + allow_screenshot_proof: When true, the wallet-ownership widget offers screenshot upload as an ownership evidence method. Default true. + auto_wallet_verification + vasp_attribution_enabled + counterparty_screening_enabled: Opt-in sanctions and adverse-media screening of counterparty VASPs, billed per screening at the Company AML Screening price. When on, Didit screens the counterparty an outbound exchange reaches (reusing a screening younger than 30 days) and you can screen one with POST /v3/travel-rule/vasps/{vasp_id}/screening/. Default false: nothing is screened or billed. + sunrise_policy: HOLD, REJECT, PROCEED, or empty to follow timeout_outcome. See the response field of the same name. + non_responsive_policy: HOLD, REJECT, PROCEED, or empty to follow timeout_outcome. See the response field of the same name. + disabled_networks: Networks opted out of platform membership. Does not disable your connected own memberships or provision unavailable networks. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleSettingsDetail", + await self._client.request( + "put", + "/v3/travel-rule/settings/", + cast_to=TravelRuleSettingsDetail, + auth="api_key", + body={ + "is_enabled": is_enabled, + "legal_name": legal_name, + "lei": lei, + "jurisdiction": jurisdiction, + "name_matching_strictness": name_matching_strictness, + "confirmation_timeout_hours": confirmation_timeout_hours, + "timeout_outcome": timeout_outcome, + "compliance_email": compliance_email, + "is_discoverable": is_discoverable, + "threshold_amount": threshold_amount, + "ownership_validation_mode": ownership_validation_mode, + "simplified_validation_threshold": simplified_validation_threshold, + "data_sharing_fields": data_sharing_fields, + "matching_rules": matching_rules, + "inbound_auto_accept": inbound_auto_accept, + "allow_self_declaration": allow_self_declaration, + "allow_screenshot_proof": allow_screenshot_proof, + "auto_wallet_verification": auto_wallet_verification, + "vasp_attribution_enabled": vasp_attribution_enabled, + "counterparty_screening_enabled": counterparty_screening_enabled, + "sunrise_policy": sunrise_policy, + "non_responsive_policy": non_responsive_policy, + "disabled_networks": disabled_networks, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def send_compliance_email_confirmation( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleSendComplianceEmailConfirmationResponse: + """Send the compliance email confirmation. + + Emails a single-use confirmation link to your VASP profile's compliance_email. Opening the link and pressing its confirm button confirms the address, which the Didit network needs before it lists your profile. A new request replaces the previous link; the link stops working when the compliance email changes. One email per minute per address. + + ``POST /v3/travel-rule/settings/compliance-email/confirmation/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleSendComplianceEmailConfirmationResponse", + await self._client.request( + "post", + "/v3/travel-rule/settings/compliance-email/confirmation/", + cast_to=TravelRuleSendComplianceEmailConfirmationResponse, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_vasps( + self, + *, + search: str | NotGiven = NOT_GIVEN, + network: Literal["GTR", "TRUST", "VERIFYVASP", "SYGNA", "TRISA"] + | str + | NotGiven = NOT_GIVEN, + jurisdiction: str | NotGiven = NOT_GIVEN, + reachable: bool | NotGiven = NOT_GIVEN, + kind: Literal["directory", "didit"] | str | NotGiven = NOT_GIVEN, + risk: Literal["LOW", "MEDIUM", "HIGH"] | str | NotGiven = NOT_GIVEN, + responsiveness: Literal["RESPONSIVE", "UNRESPONSIVE", "UNKNOWN"] + | str + | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[VaspDirectoryEntry]: + """Search the VASP directory. + + Paginated, name-sorted search of the VASP directory: the shared directory entries, which Didit builds from the ESMA interim MiCA register, the public TRISA directory, the global LEI register and its own curation, plus the members of the Didit network. Each row carries the routes your application has to it today. Your own application's profile is never listed. Rows also carry the shared due-diligence assessment and your application's own decision, entity screening and questionnaires. + + ``GET /v3/travel-rule/vasps/`` + + Args: + search: A name or alias fragment (case-insensitive), an exact LEI, or a domain (example.com or a URL on it). Didit network members match on their legal name, LEI or the domain of their compliance email. + network: Only directory entries a network's member directory currently lists. + jurisdiction: ISO 3166 alpha-2 or alpha-3 country code. 400 for anything else. + reachable: true: only VASPs your application has an available route to (Didit network members included). false: only directory entries it cannot reach today. + kind: Only directory entries, or only Didit network members. + risk: Only directory entries whose risk_level is this band. Didit network members, which are not scored, never match. + responsiveness: Only directory entries whose responsiveness.status is this value. Didit network members never match. + limit + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[VaspDirectoryEntry]", + self._client.request_page( + "get", + "/v3/travel-rule/vasps/", + cast_to=TravelRuleListVaspsResponse, + item_type=VaspDirectoryEntry, + auth="api_key", + query={ + "search": search, + "network": network, + "jurisdiction": jurisdiction, + "reachable": reachable, + "kind": kind, + "risk": risk, + "responsiveness": responsiveness, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_vasp( + self, + vasp_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> VaspDirectoryEntry: + """Get a VASP directory entry. + + One directory entry or Didit network member by the id the search returned, with the same fields and the routes your application has to it. + + ``GET /v3/travel-rule/vasps/{vasp_id}/`` + + Args: + vasp_id: The id of a row of GET /v3/travel-rule/vasps/. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "VaspDirectoryEntry", + await self._client.request( + "get", + "/v3/travel-rule/vasps/{vasp_id}/", + cast_to=VaspDirectoryEntry, + path_params={"vasp_id": vasp_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_vasp_decision( + self, + vasp_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleCounterpartyDecisionResponse: + """Get your decision on a VASP. + + Your application's decision on a counterparty (a directory entry or a Didit network member): approved, restricted or blocked. An entity of a brand with no decision of its own follows the brand's decision, named in inherited_from. Private to your application. + + ``GET /v3/travel-rule/vasps/{vasp_id}/decision/`` + + Args: + vasp_id: The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleCounterpartyDecisionResponse", + await self._client.request( + "get", + "/v3/travel-rule/vasps/{vasp_id}/decision/", + cast_to=TravelRuleCounterpartyDecisionResponse, + path_params={"vasp_id": vasp_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_vasp_decision( + self, + vasp_id: str, + *, + decision: Literal["APPROVED", "RESTRICTED", "BLOCKED"] | str, + reason: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleCounterpartyDecisionResponse: + """Set your decision on a VASP. + + Make or change your application's decision on a counterparty: APPROVED, RESTRICTED or BLOCKED, with an optional reason. It never changes the shared directory entry and acts only through your transaction rules (counterparty_dd_status). A decision on a brand also covers each of its entities that has no decision of its own. + + ``PUT /v3/travel-rule/vasps/{vasp_id}/decision/`` + + Args: + vasp_id: The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member. + decision + reason: Optional. Replaces the previous reason; omit it to clear it. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleCounterpartyDecisionResponse", + await self._client.request( + "put", + "/v3/travel-rule/vasps/{vasp_id}/decision/", + cast_to=TravelRuleCounterpartyDecisionResponse, + path_params={"vasp_id": vasp_id}, + auth="api_key", + body={"decision": decision, "reason": reason}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def clear_vasp_decision( + self, + vasp_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleCounterpartyDecisionResponse: + """Clear your decision on a VASP. + + Clear your application's own decision on a counterparty. The response shows the decision that applies afterwards: null, or for an entity of a brand the brand's decision, which the entity follows again. + + ``DELETE /v3/travel-rule/vasps/{vasp_id}/decision/`` + + Args: + vasp_id: The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleCounterpartyDecisionResponse", + await self._client.request( + "delete", + "/v3/travel-rule/vasps/{vasp_id}/decision/", + cast_to=TravelRuleCounterpartyDecisionResponse, + path_params={"vasp_id": vasp_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_vasp_screening( + self, + vasp_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleCounterpartyScreeningStatus: + """Get a VASP's entity screening. + + Whether counterparty screening is on for your application, what one screening costs your organization, how long a result stays fresh, and your latest sanctions and adverse-media screening of the counterparty. Reading it is free. + + ``GET /v3/travel-rule/vasps/{vasp_id}/screening/`` + + Args: + vasp_id: The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleCounterpartyScreeningStatus", + await self._client.request( + "get", + "/v3/travel-rule/vasps/{vasp_id}/screening/", + cast_to=TravelRuleCounterpartyScreeningStatus, + path_params={"vasp_id": vasp_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def screen_vasp( + self, + vasp_id: str, + *, + refresh: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleCounterpartyScreeningResult: + """Screen a VASP. + + Screen the counterparty entity against sanctions and adverse media for your application. Opt-in: counterparty_screening_enabled must be on in your Travel Rule settings. Each new screening is billed at your organization's Company AML Screening price; a screening younger than 30 days is returned instead (200, not billed) unless you send refresh: true. One screening of a counterparty runs at a time per application. The result is private to your application. Sandbox applications are never screened. + + ``POST /v3/travel-rule/vasps/{vasp_id}/screening/`` + + Args: + vasp_id: The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member. + refresh: true screens again (and bills again) even when a fresh screening exists. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleCounterpartyScreeningResult", + await self._client.request( + "post", + "/v3/travel-rule/vasps/{vasp_id}/screening/", + cast_to=TravelRuleCounterpartyScreeningResult, + path_params={"vasp_id": vasp_id}, + auth="api_key", + body={"refresh": refresh}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_vasp_questionnaires( + self, + vasp_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleCounterpartyQuestionnaireList: + """List due-diligence questionnaires with a VASP. + + Every due-diligence questionnaire your application sent the counterparty, newest first, with its status and, once answered, the answers. Private to your application. + + ``GET /v3/travel-rule/vasps/{vasp_id}/questionnaires/`` + + Args: + vasp_id: The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleCounterpartyQuestionnaireList", + await self._client.request( + "get", + "/v3/travel-rule/vasps/{vasp_id}/questionnaires/", + cast_to=TravelRuleCounterpartyQuestionnaireList, + path_params={"vasp_id": vasp_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def send_vasp_questionnaire( + self, + vasp_id: str, + *, + sent_to: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleCounterpartyQuestionnaire: + """Send a due-diligence questionnaire to a VASP. + + Email Didit's standard due-diligence questionnaire to a compliance address on record for the counterparty, with a single-use answer link. The counterparty has 30 days to answer every question; the answers stay current for 365 days. A new send replaces your earlier unanswered questionnaire to the same counterparty, whose link stops working. One email per minute per address. + + ``POST /v3/travel-rule/vasps/{vasp_id}/questionnaires/`` + + Args: + vasp_id: The id of a row of GET /v3/travel-rule/vasps/: a directory entry or a Didit network member. + sent_to: Optional. A compliance address on record for the counterparty: for a directory entry its compliance address or a contact your application sees, for a Didit network member its confirmed compliance email. Default: the address a Travel Rule email to the counterparty goes to (your own contact first, then a platform contact, then the entry's compliance address). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleCounterpartyQuestionnaire", + await self._client.request( + "post", + "/v3/travel-rule/vasps/{vasp_id}/questionnaires/", + cast_to=TravelRuleCounterpartyQuestionnaire, + path_params={"vasp_id": vasp_id}, + auth="api_key", + body={"sent_to": sent_to}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def register_inbound( self, *, - wallet_address: str, chain: str, + txid: str, + wallet_address: str, + amount: str, + currency: str, + originator_data: Mapping[str, object] | NotGiven = NOT_GIVEN, + beneficiary_data: Mapping[str, object] | NotGiven = NOT_GIVEN, + originating_vasp: TravelRuleRegisterInboundParamsOriginatingVasp | NotGiven = NOT_GIVEN, + memo: str | NotGiven = NOT_GIVEN, + utxo: TravelRuleUtxo | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleInboundResponse: + """Register an after-deposit (sunrise) inbound transfer. + + Register Travel Rule data for a crypto deposit that already settled on-chain before any exchange took place. Creates an inbound travelRule transaction (or reuses an unclaimed deposit) and resolves the transfer against your wallet address book. For a newly created transaction, Didit sends transaction.created first and the travel_rule.status.updated webhooks after it. + + ``POST /v3/travel-rule/inbound/`` + + Args: + chain: Chain the deposit settled on, for example `ETH`. Stored lowercase and compared case-insensitively with your wallet address book entries, so use the same chain token there. + txid: On-chain transaction hash. Deduped together with wallet_address. + wallet_address: The destination wallet you received the deposit on. An XRP X-address is split into its classic address and tag. + amount: Deposit amount as a decimal string, zero or greater, with up to 18 decimal places. + currency: Deposit asset, e.g. "USDC". + originator_data: Originator details from the sending VASP, if known, as a flat object such as `{"name": "..."}`. Didit reads name (or full_name), first_name, last_name and vendor_data from it. + beneficiary_data: Beneficiary details (your customer), as a flat object such as `{"name": "..."}`. Didit matches name against the holder_name of the matched address book entry, and sets wallet_address and chain from the request. + originating_vasp: Optional description of the sending VASP. It stays on this transfer only (`metadata.originating_vasp`, and the transfer's originator VASP in GET /v3/travel-rule/transfers/); it never creates, edits or links a shared VASP directory entry. + memo: Optional. The deposit's destination tag or memo on a memo network: on a shared deposit address it is what tells your customers apart, and the address-book entry for that memo is matched. XRP: a destination tag, a whole number from 0 to 4294967295 (a JSON number is accepted). XLM: a text memo of up to 28 bytes, a 64-bit id or a 32-byte hash in hex. EOS: up to 256 bytes. ATOM: up to 256 characters. HBAR: up to 100 bytes. Bitcoin, EVM, Solana, Tron, Litecoin, Bitcoin Cash, Dogecoin and Lightning wallets take no memo: one there is a `400`. Alias: `destinationTag` or `destination_tag`. + utxo: Optional, UTXO chains only. The deposit transaction's inputs and outputs: the sender's wallet is the declared `originator_data.wallet_address`, else the largest input. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleInboundResponse", + await self._client.request( + "post", + "/v3/travel-rule/inbound/", + cast_to=TravelRuleInboundResponse, + auth="api_key", + body={ + "chain": chain, + "txid": txid, + "wallet_address": wallet_address, + "amount": amount, + "currency": currency, + "originator_data": originator_data, + "beneficiary_data": beneficiary_data, + "originating_vasp": originating_vasp, + "memo": memo, + "utxo": utxo, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create_widget_session( + self, + *, + wallet_address: str | NotGiven = NOT_GIVEN, + chain: str | NotGiven = NOT_GIVEN, holder_name: str | NotGiven = NOT_GIVEN, vendor_data: str | NotGiven = NOT_GIVEN, - transaction_id: str | NotGiven = NOT_GIVEN, + transaction_id: str | None | NotGiven = NOT_GIVEN, satoshi_deposit_address: str | NotGiven = NOT_GIVEN, callback_url: str | NotGiven = NOT_GIVEN, expires_in_minutes: int | NotGiven = NOT_GIVEN, @@ -626,13 +1505,13 @@ async def create_widget_session( ``POST /v3/travel-rule/widget-session/`` Args: - wallet_address: The wallet address to verify. - chain: Chain identifier for the address, e.g. "ethereum", "bitcoin", "solana". + wallet_address: The wallet address to verify. Optional; when empty, the address must be supplied when a proof is started. + chain: Chain token for the address, for example `ETH`, `BTC` or `SOL`. Use the same token as in your wallet address book. Message signing and the Satoshi test are offered only for a supported chain. holder_name: Name of the wallet holder, used for the beneficiary name match. vendor_data: Your internal identifier for the holder, carried onto the created address-book entry. transaction_id: Optional. Didit transaction UUID of a Travel Rule transfer to advance when the proof verifies. satoshi_deposit_address: Optional. A deposit address you control; supplying it enables the SATOSHI_TEST proof method. - callback_url: Optional. Where the widget returns the customer after completion. + callback_url: Optional. Absolute http(s) URL where the widget returns the customer after completion. expires_in_minutes: Optional. Lifetime of the minted link, in minutes. Automatically extended to cover the Satoshi test window when SATOSHI_TEST is offered. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. @@ -669,10 +1548,35 @@ def __init__(self, resource: TravelRule) -> None: self._resource = resource self.retrieve_settings = to_raw_response_wrapper(resource.retrieve_settings) self.update_settings = to_raw_response_wrapper(resource.update_settings) + self.send_compliance_email_confirmation = to_raw_response_wrapper( + resource.send_compliance_email_confirmation + ) self.list_vasps = to_raw_response_wrapper(resource.list_vasps) + self.retrieve_vasp = to_raw_response_wrapper(resource.retrieve_vasp) + self.retrieve_vasp_decision = to_raw_response_wrapper(resource.retrieve_vasp_decision) + self.update_vasp_decision = to_raw_response_wrapper(resource.update_vasp_decision) + self.clear_vasp_decision = to_raw_response_wrapper(resource.clear_vasp_decision) + self.retrieve_vasp_screening = to_raw_response_wrapper(resource.retrieve_vasp_screening) + self.screen_vasp = to_raw_response_wrapper(resource.screen_vasp) + self.list_vasp_questionnaires = to_raw_response_wrapper(resource.list_vasp_questionnaires) + self.send_vasp_questionnaire = to_raw_response_wrapper(resource.send_vasp_questionnaire) self.register_inbound = to_raw_response_wrapper(resource.register_inbound) self.create_widget_session = to_raw_response_wrapper(resource.create_widget_session) + @cached_property + def counterparty_contacts(self) -> TravelRuleCounterpartyContactsWithRawResponse: + from didit.resources.travel_rule.counterparty_contacts import ( + TravelRuleCounterpartyContactsWithRawResponse, + ) + + return TravelRuleCounterpartyContactsWithRawResponse(self._resource.counterparty_contacts) + + @cached_property + def transfers(self) -> TravelRuleTransfersWithRawResponse: + from didit.resources.travel_rule.transfers import TravelRuleTransfersWithRawResponse + + return TravelRuleTransfersWithRawResponse(self._resource.transfers) + @cached_property def wallet_addresses(self) -> TravelRuleWalletAddressesWithRawResponse: from didit.resources.travel_rule.wallet_addresses import ( @@ -681,16 +1585,55 @@ def wallet_addresses(self) -> TravelRuleWalletAddressesWithRawResponse: return TravelRuleWalletAddressesWithRawResponse(self._resource.wallet_addresses) + @cached_property + def workspace(self) -> TravelRuleWorkspaceWithRawResponse: + from didit.resources.travel_rule.workspace import TravelRuleWorkspaceWithRawResponse + + return TravelRuleWorkspaceWithRawResponse(self._resource.workspace) + class AsyncTravelRuleWithRawResponse: def __init__(self, resource: AsyncTravelRule) -> None: self._resource = resource self.retrieve_settings = async_to_raw_response_wrapper(resource.retrieve_settings) self.update_settings = async_to_raw_response_wrapper(resource.update_settings) + self.send_compliance_email_confirmation = async_to_raw_response_wrapper( + resource.send_compliance_email_confirmation + ) self.list_vasps = async_to_raw_response_wrapper(resource.list_vasps) + self.retrieve_vasp = async_to_raw_response_wrapper(resource.retrieve_vasp) + self.retrieve_vasp_decision = async_to_raw_response_wrapper(resource.retrieve_vasp_decision) + self.update_vasp_decision = async_to_raw_response_wrapper(resource.update_vasp_decision) + self.clear_vasp_decision = async_to_raw_response_wrapper(resource.clear_vasp_decision) + self.retrieve_vasp_screening = async_to_raw_response_wrapper( + resource.retrieve_vasp_screening + ) + self.screen_vasp = async_to_raw_response_wrapper(resource.screen_vasp) + self.list_vasp_questionnaires = async_to_raw_response_wrapper( + resource.list_vasp_questionnaires + ) + self.send_vasp_questionnaire = async_to_raw_response_wrapper( + resource.send_vasp_questionnaire + ) self.register_inbound = async_to_raw_response_wrapper(resource.register_inbound) self.create_widget_session = async_to_raw_response_wrapper(resource.create_widget_session) + @cached_property + def counterparty_contacts(self) -> AsyncTravelRuleCounterpartyContactsWithRawResponse: + from didit.resources.travel_rule.counterparty_contacts import ( + AsyncTravelRuleCounterpartyContactsWithRawResponse, + ) + + return AsyncTravelRuleCounterpartyContactsWithRawResponse( + self._resource.counterparty_contacts + ) + + @cached_property + def transfers(self) -> AsyncTravelRuleTransfersWithRawResponse: + from didit.resources.travel_rule.transfers import AsyncTravelRuleTransfersWithRawResponse + + return AsyncTravelRuleTransfersWithRawResponse(self._resource.transfers) + @cached_property def wallet_addresses(self) -> AsyncTravelRuleWalletAddressesWithRawResponse: from didit.resources.travel_rule.wallet_addresses import ( @@ -699,16 +1642,55 @@ def wallet_addresses(self) -> AsyncTravelRuleWalletAddressesWithRawResponse: return AsyncTravelRuleWalletAddressesWithRawResponse(self._resource.wallet_addresses) + @cached_property + def workspace(self) -> AsyncTravelRuleWorkspaceWithRawResponse: + from didit.resources.travel_rule.workspace import AsyncTravelRuleWorkspaceWithRawResponse + + return AsyncTravelRuleWorkspaceWithRawResponse(self._resource.workspace) + class TravelRuleWithStreamingResponse: def __init__(self, resource: TravelRule) -> None: self._resource = resource self.retrieve_settings = to_streamed_response_wrapper(resource.retrieve_settings) self.update_settings = to_streamed_response_wrapper(resource.update_settings) + self.send_compliance_email_confirmation = to_streamed_response_wrapper( + resource.send_compliance_email_confirmation + ) self.list_vasps = to_streamed_response_wrapper(resource.list_vasps) + self.retrieve_vasp = to_streamed_response_wrapper(resource.retrieve_vasp) + self.retrieve_vasp_decision = to_streamed_response_wrapper(resource.retrieve_vasp_decision) + self.update_vasp_decision = to_streamed_response_wrapper(resource.update_vasp_decision) + self.clear_vasp_decision = to_streamed_response_wrapper(resource.clear_vasp_decision) + self.retrieve_vasp_screening = to_streamed_response_wrapper( + resource.retrieve_vasp_screening + ) + self.screen_vasp = to_streamed_response_wrapper(resource.screen_vasp) + self.list_vasp_questionnaires = to_streamed_response_wrapper( + resource.list_vasp_questionnaires + ) + self.send_vasp_questionnaire = to_streamed_response_wrapper( + resource.send_vasp_questionnaire + ) self.register_inbound = to_streamed_response_wrapper(resource.register_inbound) self.create_widget_session = to_streamed_response_wrapper(resource.create_widget_session) + @cached_property + def counterparty_contacts(self) -> TravelRuleCounterpartyContactsWithStreamingResponse: + from didit.resources.travel_rule.counterparty_contacts import ( + TravelRuleCounterpartyContactsWithStreamingResponse, + ) + + return TravelRuleCounterpartyContactsWithStreamingResponse( + self._resource.counterparty_contacts + ) + + @cached_property + def transfers(self) -> TravelRuleTransfersWithStreamingResponse: + from didit.resources.travel_rule.transfers import TravelRuleTransfersWithStreamingResponse + + return TravelRuleTransfersWithStreamingResponse(self._resource.transfers) + @cached_property def wallet_addresses(self) -> TravelRuleWalletAddressesWithStreamingResponse: from didit.resources.travel_rule.wallet_addresses import ( @@ -717,18 +1699,63 @@ def wallet_addresses(self) -> TravelRuleWalletAddressesWithStreamingResponse: return TravelRuleWalletAddressesWithStreamingResponse(self._resource.wallet_addresses) + @cached_property + def workspace(self) -> TravelRuleWorkspaceWithStreamingResponse: + from didit.resources.travel_rule.workspace import TravelRuleWorkspaceWithStreamingResponse + + return TravelRuleWorkspaceWithStreamingResponse(self._resource.workspace) + class AsyncTravelRuleWithStreamingResponse: def __init__(self, resource: AsyncTravelRule) -> None: self._resource = resource self.retrieve_settings = async_to_streamed_response_wrapper(resource.retrieve_settings) self.update_settings = async_to_streamed_response_wrapper(resource.update_settings) + self.send_compliance_email_confirmation = async_to_streamed_response_wrapper( + resource.send_compliance_email_confirmation + ) self.list_vasps = async_to_streamed_response_wrapper(resource.list_vasps) + self.retrieve_vasp = async_to_streamed_response_wrapper(resource.retrieve_vasp) + self.retrieve_vasp_decision = async_to_streamed_response_wrapper( + resource.retrieve_vasp_decision + ) + self.update_vasp_decision = async_to_streamed_response_wrapper( + resource.update_vasp_decision + ) + self.clear_vasp_decision = async_to_streamed_response_wrapper(resource.clear_vasp_decision) + self.retrieve_vasp_screening = async_to_streamed_response_wrapper( + resource.retrieve_vasp_screening + ) + self.screen_vasp = async_to_streamed_response_wrapper(resource.screen_vasp) + self.list_vasp_questionnaires = async_to_streamed_response_wrapper( + resource.list_vasp_questionnaires + ) + self.send_vasp_questionnaire = async_to_streamed_response_wrapper( + resource.send_vasp_questionnaire + ) self.register_inbound = async_to_streamed_response_wrapper(resource.register_inbound) self.create_widget_session = async_to_streamed_response_wrapper( resource.create_widget_session ) + @cached_property + def counterparty_contacts(self) -> AsyncTravelRuleCounterpartyContactsWithStreamingResponse: + from didit.resources.travel_rule.counterparty_contacts import ( + AsyncTravelRuleCounterpartyContactsWithStreamingResponse, + ) + + return AsyncTravelRuleCounterpartyContactsWithStreamingResponse( + self._resource.counterparty_contacts + ) + + @cached_property + def transfers(self) -> AsyncTravelRuleTransfersWithStreamingResponse: + from didit.resources.travel_rule.transfers import ( + AsyncTravelRuleTransfersWithStreamingResponse, + ) + + return AsyncTravelRuleTransfersWithStreamingResponse(self._resource.transfers) + @cached_property def wallet_addresses(self) -> AsyncTravelRuleWalletAddressesWithStreamingResponse: from didit.resources.travel_rule.wallet_addresses import ( @@ -736,3 +1763,11 @@ def wallet_addresses(self) -> AsyncTravelRuleWalletAddressesWithStreamingRespons ) return AsyncTravelRuleWalletAddressesWithStreamingResponse(self._resource.wallet_addresses) + + @cached_property + def workspace(self) -> AsyncTravelRuleWorkspaceWithStreamingResponse: + from didit.resources.travel_rule.workspace import ( + AsyncTravelRuleWorkspaceWithStreamingResponse, + ) + + return AsyncTravelRuleWorkspaceWithStreamingResponse(self._resource.workspace) diff --git a/src/didit/resources/travel_rule/counterparty_contacts.py b/src/didit/resources/travel_rule/counterparty_contacts.py new file mode 100644 index 0000000..1f73476 --- /dev/null +++ b/src/didit/resources/travel_rule/counterparty_contacts.py @@ -0,0 +1,338 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``travel_rule.counterparty_contacts`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.travel_rule_counterparty_contact import TravelRuleCounterpartyContact +from didit.types.travel_rule.counterparty_contacts.list_response import ( + TravelRuleCounterpartyContactsListResponse, +) + +__all__ = [ + "TravelRuleCounterpartyContacts", + "AsyncTravelRuleCounterpartyContacts", + "TravelRuleCounterpartyContactsWithRawResponse", + "AsyncTravelRuleCounterpartyContactsWithRawResponse", + "TravelRuleCounterpartyContactsWithStreamingResponse", + "AsyncTravelRuleCounterpartyContactsWithStreamingResponse", +] + + +class TravelRuleCounterpartyContacts(SyncAPIResource): + """The ``travel_rule.counterparty_contacts`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``. + """ + + @cached_property + def with_raw_response(self) -> TravelRuleCounterpartyContactsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return TravelRuleCounterpartyContactsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> TravelRuleCounterpartyContactsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return TravelRuleCounterpartyContactsWithStreamingResponse(self) + + def list( + self, + *, + counterparty: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[TravelRuleCounterpartyContact]: + """List counterparty contacts. + + The compliance addresses the EMAIL rail may write to for your application: Didit's platform contacts plus your application's own private ones, bounced ones included, newest first. Paginated with `limit` (default 50) and `offset`; `count` stops at 100, so 100 means 100 or more. + + ``GET /v3/travel-rule/counterparty-contacts/`` + + Args: + counterparty: Only the contacts of this directory entry. Must be a UUID; an empty value means no filter. + limit: Page size, default 50. + offset: Rows to skip. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[TravelRuleCounterpartyContact]", + self._client.request_page( + "get", + "/v3/travel-rule/counterparty-contacts/", + cast_to=TravelRuleCounterpartyContactsListResponse, + item_type=TravelRuleCounterpartyContact, + auth="api_key", + query={"counterparty": counterparty, "limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + counterparty: str, + email: str, + name: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleCounterpartyContact: + """Add a counterparty contact. + + Adds a private compliance address for one directory entry. Only your application sees and uses it, and the EMAIL rail writes to it before any platform contact. Adding an address the entry already has among your own contacts returns that contact. + + ``POST /v3/travel-rule/counterparty-contacts/`` + + Args: + counterparty: The directory entry's `id`. + email + name: Optional label. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleCounterpartyContact", + self._client.request( + "post", + "/v3/travel-rule/counterparty-contacts/", + cast_to=TravelRuleCounterpartyContact, + auth="api_key", + body={"counterparty": counterparty, "email": email, "name": name}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + contact_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete a counterparty contact. + + Deletes one of your application's own contacts. The EMAIL rail stops writing to it at once. Platform contacts cannot be deleted. + + ``DELETE /v3/travel-rule/counterparty-contacts/{contact_uuid}/`` + + Args: + contact_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/travel-rule/counterparty-contacts/{contact_uuid}/", + cast_to=None, + path_params={"contact_uuid": contact_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncTravelRuleCounterpartyContacts(AsyncAPIResource): + """The ``travel_rule.counterparty_contacts`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``. + """ + + @cached_property + def with_raw_response(self) -> AsyncTravelRuleCounterpartyContactsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncTravelRuleCounterpartyContactsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncTravelRuleCounterpartyContactsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncTravelRuleCounterpartyContactsWithStreamingResponse(self) + + def list( + self, + *, + counterparty: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[TravelRuleCounterpartyContact]: + """List counterparty contacts. + + The compliance addresses the EMAIL rail may write to for your application: Didit's platform contacts plus your application's own private ones, bounced ones included, newest first. Paginated with `limit` (default 50) and `offset`; `count` stops at 100, so 100 means 100 or more. + + ``GET /v3/travel-rule/counterparty-contacts/`` + + Args: + counterparty: Only the contacts of this directory entry. Must be a UUID; an empty value means no filter. + limit: Page size, default 50. + offset: Rows to skip. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[TravelRuleCounterpartyContact]", + self._client.request_page( + "get", + "/v3/travel-rule/counterparty-contacts/", + cast_to=TravelRuleCounterpartyContactsListResponse, + item_type=TravelRuleCounterpartyContact, + auth="api_key", + query={"counterparty": counterparty, "limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + counterparty: str, + email: str, + name: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleCounterpartyContact: + """Add a counterparty contact. + + Adds a private compliance address for one directory entry. Only your application sees and uses it, and the EMAIL rail writes to it before any platform contact. Adding an address the entry already has among your own contacts returns that contact. + + ``POST /v3/travel-rule/counterparty-contacts/`` + + Args: + counterparty: The directory entry's `id`. + email + name: Optional label. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleCounterpartyContact", + await self._client.request( + "post", + "/v3/travel-rule/counterparty-contacts/", + cast_to=TravelRuleCounterpartyContact, + auth="api_key", + body={"counterparty": counterparty, "email": email, "name": name}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + contact_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete a counterparty contact. + + Deletes one of your application's own contacts. The EMAIL rail stops writing to it at once. Platform contacts cannot be deleted. + + ``DELETE /v3/travel-rule/counterparty-contacts/{contact_uuid}/`` + + Args: + contact_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/travel-rule/counterparty-contacts/{contact_uuid}/", + cast_to=None, + path_params={"contact_uuid": contact_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class TravelRuleCounterpartyContactsWithRawResponse: + def __init__(self, resource: TravelRuleCounterpartyContacts) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.delete = to_raw_response_wrapper(resource.delete) + + +class AsyncTravelRuleCounterpartyContactsWithRawResponse: + def __init__(self, resource: AsyncTravelRuleCounterpartyContacts) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.delete = async_to_raw_response_wrapper(resource.delete) + + +class TravelRuleCounterpartyContactsWithStreamingResponse: + def __init__(self, resource: TravelRuleCounterpartyContacts) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.delete = to_streamed_response_wrapper(resource.delete) + + +class AsyncTravelRuleCounterpartyContactsWithStreamingResponse: + def __init__(self, resource: AsyncTravelRuleCounterpartyContacts) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.delete = async_to_streamed_response_wrapper(resource.delete) diff --git a/src/didit/resources/travel_rule/transfers.py b/src/didit/resources/travel_rule/transfers.py new file mode 100644 index 0000000..b58c83e --- /dev/null +++ b/src/didit/resources/travel_rule/transfers.py @@ -0,0 +1,223 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``travel_rule.transfers`` resource.""" + +from __future__ import annotations + +import datetime as _dt +from functools import cached_property +from typing import cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.travel_rule_transfer_row import TravelRuleTransferRow +from didit.types.travel_rule.transfers.list_response import TravelRuleTransfersListResponse + +__all__ = [ + "TravelRuleTransfers", + "AsyncTravelRuleTransfers", + "TravelRuleTransfersWithRawResponse", + "AsyncTravelRuleTransfersWithRawResponse", + "TravelRuleTransfersWithStreamingResponse", + "AsyncTravelRuleTransfersWithStreamingResponse", +] + + +class TravelRuleTransfers(SyncAPIResource): + """The ``travel_rule.transfers`` resource of the Didit API. + + Methods: ``list``. + """ + + @cached_property + def with_raw_response(self) -> TravelRuleTransfersWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return TravelRuleTransfersWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> TravelRuleTransfersWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return TravelRuleTransfersWithStreamingResponse(self) + + def list( + self, + *, + direction: str | NotGiven = NOT_GIVEN, + status: str | NotGiven = NOT_GIVEN, + rail: str | NotGiven = NOT_GIVEN, + kind: str | NotGiven = NOT_GIVEN, + currency: str | NotGiven = NOT_GIVEN, + start_date: str | _dt.date | NotGiven = NOT_GIVEN, + end_date: str | _dt.date | NotGiven = NOT_GIVEN, + counterparty: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[TravelRuleTransferRow]: + """List Travel Rule transfers. + + Your application's Travel Rule transfers, newest first, paginated with `limit` (default 50) and `offset`. Rows carry VASP names, wallets and memos only, never person data. Comma-separated values select several. An unknown value answers 400 naming the parameter, for example `{"direction": ["Unknown value(s): inbound."]}`. + + ``GET /v3/travel-rule/transfers/`` + + Args: + direction: INBOUND, OUTBOUND, or both comma-separated. Case-sensitive. + status: One or more transfer statuses, comma-separated. + rail: One or more of INTERNAL, TRP, GTR, TRUST, VERIFYVASP, SYGNA, EMAIL, TEST_VASP, comma-separated. + kind: withdrawal, deposit or message, comma-separated. + currency: The transaction's currency, case-insensitive. + start_date: Transfers created on or after this day (UTC), YYYY-MM-DD. + end_date: Transfers created on or before this day (UTC), YYYY-MM-DD. + counterparty: The `id` of a directory entry or Didit network member from GET /v3/travel-rule/vasps/: your transfers with that counterparty. A value that is not a valid id answers 400. + limit: Page size, default 50. + offset: Rows to skip. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[TravelRuleTransferRow]", + self._client.request_page( + "get", + "/v3/travel-rule/transfers/", + cast_to=TravelRuleTransfersListResponse, + item_type=TravelRuleTransferRow, + auth="api_key", + query={ + "direction": direction, + "status": status, + "rail": rail, + "kind": kind, + "currency": currency, + "start_date": start_date, + "end_date": end_date, + "counterparty": counterparty, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncTravelRuleTransfers(AsyncAPIResource): + """The ``travel_rule.transfers`` resource of the Didit API. + + Methods: ``list``. + """ + + @cached_property + def with_raw_response(self) -> AsyncTravelRuleTransfersWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncTravelRuleTransfersWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncTravelRuleTransfersWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncTravelRuleTransfersWithStreamingResponse(self) + + def list( + self, + *, + direction: str | NotGiven = NOT_GIVEN, + status: str | NotGiven = NOT_GIVEN, + rail: str | NotGiven = NOT_GIVEN, + kind: str | NotGiven = NOT_GIVEN, + currency: str | NotGiven = NOT_GIVEN, + start_date: str | _dt.date | NotGiven = NOT_GIVEN, + end_date: str | _dt.date | NotGiven = NOT_GIVEN, + counterparty: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[TravelRuleTransferRow]: + """List Travel Rule transfers. + + Your application's Travel Rule transfers, newest first, paginated with `limit` (default 50) and `offset`. Rows carry VASP names, wallets and memos only, never person data. Comma-separated values select several. An unknown value answers 400 naming the parameter, for example `{"direction": ["Unknown value(s): inbound."]}`. + + ``GET /v3/travel-rule/transfers/`` + + Args: + direction: INBOUND, OUTBOUND, or both comma-separated. Case-sensitive. + status: One or more transfer statuses, comma-separated. + rail: One or more of INTERNAL, TRP, GTR, TRUST, VERIFYVASP, SYGNA, EMAIL, TEST_VASP, comma-separated. + kind: withdrawal, deposit or message, comma-separated. + currency: The transaction's currency, case-insensitive. + start_date: Transfers created on or after this day (UTC), YYYY-MM-DD. + end_date: Transfers created on or before this day (UTC), YYYY-MM-DD. + counterparty: The `id` of a directory entry or Didit network member from GET /v3/travel-rule/vasps/: your transfers with that counterparty. A value that is not a valid id answers 400. + limit: Page size, default 50. + offset: Rows to skip. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[TravelRuleTransferRow]", + self._client.request_page( + "get", + "/v3/travel-rule/transfers/", + cast_to=TravelRuleTransfersListResponse, + item_type=TravelRuleTransferRow, + auth="api_key", + query={ + "direction": direction, + "status": status, + "rail": rail, + "kind": kind, + "currency": currency, + "start_date": start_date, + "end_date": end_date, + "counterparty": counterparty, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class TravelRuleTransfersWithRawResponse: + def __init__(self, resource: TravelRuleTransfers) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + + +class AsyncTravelRuleTransfersWithRawResponse: + def __init__(self, resource: AsyncTravelRuleTransfers) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + + +class TravelRuleTransfersWithStreamingResponse: + def __init__(self, resource: TravelRuleTransfers) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + + +class AsyncTravelRuleTransfersWithStreamingResponse: + def __init__(self, resource: AsyncTravelRuleTransfers) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) diff --git a/src/didit/resources/travel_rule/wallet_addresses.py b/src/didit/resources/travel_rule/wallet_addresses.py index d988e6c..e6687fa 100644 --- a/src/didit/resources/travel_rule/wallet_addresses.py +++ b/src/didit/resources/travel_rule/wallet_addresses.py @@ -4,7 +4,7 @@ from __future__ import annotations from functools import cached_property -from typing import cast +from typing import Sequence, cast import httpx @@ -19,6 +19,11 @@ from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query from didit.types.components.wallet_address_entry import WalletAddressEntry from didit.types.components.wallet_address_entry_update import WalletAddressEntryUpdate +from didit.types.components.wallet_address_import_report import WalletAddressImportReport +from didit.types.components.wallet_address_import_row import WalletAddressImportRow +from didit.types.travel_rule.wallet_addresses.bulk_import_params import ( + TravelRuleWalletAddressesBulkImportParamsVariant1, +) from didit.types.travel_rule.wallet_addresses.list_response import ( TravelRuleWalletAddressesListResponse, ) @@ -36,7 +41,7 @@ class TravelRuleWalletAddresses(SyncAPIResource): """The ``travel_rule.wallet_addresses`` resource of the Didit API. - Methods: ``create``, ``delete``, ``list``, ``update``. + Methods: ``bulk_import``, ``create``, ``delete``, ``list``, ``update``. """ @cached_property @@ -99,6 +104,7 @@ def create( entity_type: str | NotGiven = NOT_GIVEN, self_declared: bool | NotGiven = NOT_GIVEN, travel_address: str | NotGiven = NOT_GIVEN, + memo: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -111,13 +117,14 @@ def create( ``POST /v3/travel-rule/wallet-addresses/`` Args: - address - chain + address: The wallet address. An XRP X-address is stored as its classic address and its tag, so it matches transfers that name either form. + chain: Chain token for the address, for example `ETH` or `BTC`. When Didit routes or registers a transfer, it matches address book entries on the wallet address and, when the transfer carries a chain, on this value too, ignoring case. A transfer's chain is `beneficiary_data.chain`, filled from the transaction's crypto chain or currency symbol (for example `ETH`) when you do not send it, or the `chain` of an after-deposit registration, so use the same token everywhere. An entry with an empty chain matches any chain. holder_vendor_data holder_name entity_type - self_declared: Write-only. When true, the entry is created already ownership-verified via a SELF_DECLARATION proof. - travel_address: Optional. The counterparty VASP's travel address for this wallet, enabling TRP routing. + self_declared: Write-only. When true, the entry is created already ownership-verified via a SELF_DECLARATION proof. Returns 400 when `allow_self_declaration` is off in your Travel Rule settings. + travel_address: Optional. The counterparty VASP's travel address for this wallet, enabling TRP routing for the assets TRP carries (BTC, ETH, and USDC on a named network). An invalid travel address returns 400. + memo: Optional. The destination tag or memo that is part of the wallet. XRP: a destination tag, a whole number from 0 to 4294967295 (a JSON number is accepted). XLM: a text memo of up to 28 bytes, a 64-bit id or a 32-byte hash in hex. EOS: up to 256 bytes. ATOM: up to 256 characters. HBAR: up to 100 bytes. Bitcoin, EVM, Solana, Tron, Litecoin, Bitcoin Cash, Dogecoin and Lightning wallets take no memo: one there is a `400`. Alias: `destinationTag` or `destination_tag`. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -138,6 +145,7 @@ def create( "entity_type": entity_type, "self_declared": self_declared, "travel_address": travel_address, + "memo": memo, }, extra_headers=extra_headers, extra_query=extra_query, @@ -157,7 +165,7 @@ def delete( ) -> None: """Delete a wallet address. - Deletes the entry. It stops matching new transfers immediately and the address is freed up for re-registration. + Deletes the entry together with its ownership proofs and the screenshots they stored. It stops matching new transfers immediately and the address is freed up for re-registration. Transfers that already used the wallet keep their own record of it and read the same. Deletion is irreversible. ``DELETE /v3/travel-rule/wallet-addresses/{entry_uuid}/`` @@ -234,11 +242,50 @@ def update( ), ) + def bulk_import( + self, + *, + body: TravelRuleWalletAddressesBulkImportParamsVariant1 | Sequence[WalletAddressImportRow], + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WalletAddressImportReport: + """Import wallet addresses in bulk. + + Add up to 10,000 wallets to your Travel Rule address book in one call, as JSON or as a CSV file. A wallet can be sent by its address, by the SHA-256 of its lowercased address (`address_hash`) to keep the address private, or both. The import is idempotent: a wallet already in the book is reported as `existing` and left untouched, so the same file can be sent again. Each row is validated on its own, so one bad row never fails the others. A wallet imported by its hash only matches transfers to that address and can be proven with the wallet-ownership widget like any other entry. + + ``POST /v3/travel-rule/wallet-addresses/import/`` + + Args: + body + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WalletAddressImportReport", + self._client.request( + "post", + "/v3/travel-rule/wallet-addresses/import/", + cast_to=WalletAddressImportReport, + auth="api_key", + body=body, + multipart="auto", + file_fields=("file",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + class AsyncTravelRuleWalletAddresses(AsyncAPIResource): """The ``travel_rule.wallet_addresses`` resource of the Didit API. - Methods: ``create``, ``delete``, ``list``, ``update``. + Methods: ``bulk_import``, ``create``, ``delete``, ``list``, ``update``. """ @cached_property @@ -301,6 +348,7 @@ async def create( entity_type: str | NotGiven = NOT_GIVEN, self_declared: bool | NotGiven = NOT_GIVEN, travel_address: str | NotGiven = NOT_GIVEN, + memo: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -313,13 +361,14 @@ async def create( ``POST /v3/travel-rule/wallet-addresses/`` Args: - address - chain + address: The wallet address. An XRP X-address is stored as its classic address and its tag, so it matches transfers that name either form. + chain: Chain token for the address, for example `ETH` or `BTC`. When Didit routes or registers a transfer, it matches address book entries on the wallet address and, when the transfer carries a chain, on this value too, ignoring case. A transfer's chain is `beneficiary_data.chain`, filled from the transaction's crypto chain or currency symbol (for example `ETH`) when you do not send it, or the `chain` of an after-deposit registration, so use the same token everywhere. An entry with an empty chain matches any chain. holder_vendor_data holder_name entity_type - self_declared: Write-only. When true, the entry is created already ownership-verified via a SELF_DECLARATION proof. - travel_address: Optional. The counterparty VASP's travel address for this wallet, enabling TRP routing. + self_declared: Write-only. When true, the entry is created already ownership-verified via a SELF_DECLARATION proof. Returns 400 when `allow_self_declaration` is off in your Travel Rule settings. + travel_address: Optional. The counterparty VASP's travel address for this wallet, enabling TRP routing for the assets TRP carries (BTC, ETH, and USDC on a named network). An invalid travel address returns 400. + memo: Optional. The destination tag or memo that is part of the wallet. XRP: a destination tag, a whole number from 0 to 4294967295 (a JSON number is accepted). XLM: a text memo of up to 28 bytes, a 64-bit id or a 32-byte hash in hex. EOS: up to 256 bytes. ATOM: up to 256 characters. HBAR: up to 100 bytes. Bitcoin, EVM, Solana, Tron, Litecoin, Bitcoin Cash, Dogecoin and Lightning wallets take no memo: one there is a `400`. Alias: `destinationTag` or `destination_tag`. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -340,6 +389,7 @@ async def create( "entity_type": entity_type, "self_declared": self_declared, "travel_address": travel_address, + "memo": memo, }, extra_headers=extra_headers, extra_query=extra_query, @@ -359,7 +409,7 @@ async def delete( ) -> None: """Delete a wallet address. - Deletes the entry. It stops matching new transfers immediately and the address is freed up for re-registration. + Deletes the entry together with its ownership proofs and the screenshots they stored. It stops matching new transfers immediately and the address is freed up for re-registration. Transfers that already used the wallet keep their own record of it and read the same. Deletion is irreversible. ``DELETE /v3/travel-rule/wallet-addresses/{entry_uuid}/`` @@ -436,6 +486,45 @@ async def update( ), ) + async def bulk_import( + self, + *, + body: TravelRuleWalletAddressesBulkImportParamsVariant1 | Sequence[WalletAddressImportRow], + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WalletAddressImportReport: + """Import wallet addresses in bulk. + + Add up to 10,000 wallets to your Travel Rule address book in one call, as JSON or as a CSV file. A wallet can be sent by its address, by the SHA-256 of its lowercased address (`address_hash`) to keep the address private, or both. The import is idempotent: a wallet already in the book is reported as `existing` and left untouched, so the same file can be sent again. Each row is validated on its own, so one bad row never fails the others. A wallet imported by its hash only matches transfers to that address and can be proven with the wallet-ownership widget like any other entry. + + ``POST /v3/travel-rule/wallet-addresses/import/`` + + Args: + body + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WalletAddressImportReport", + await self._client.request( + "post", + "/v3/travel-rule/wallet-addresses/import/", + cast_to=WalletAddressImportReport, + auth="api_key", + body=body, + multipart="auto", + file_fields=("file",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + class TravelRuleWalletAddressesWithRawResponse: def __init__(self, resource: TravelRuleWalletAddresses) -> None: @@ -444,6 +533,7 @@ def __init__(self, resource: TravelRuleWalletAddresses) -> None: self.create = to_raw_response_wrapper(resource.create) self.delete = to_raw_response_wrapper(resource.delete) self.update = to_raw_response_wrapper(resource.update) + self.bulk_import = to_raw_response_wrapper(resource.bulk_import) class AsyncTravelRuleWalletAddressesWithRawResponse: @@ -453,6 +543,7 @@ def __init__(self, resource: AsyncTravelRuleWalletAddresses) -> None: self.create = async_to_raw_response_wrapper(resource.create) self.delete = async_to_raw_response_wrapper(resource.delete) self.update = async_to_raw_response_wrapper(resource.update) + self.bulk_import = async_to_raw_response_wrapper(resource.bulk_import) class TravelRuleWalletAddressesWithStreamingResponse: @@ -462,6 +553,7 @@ def __init__(self, resource: TravelRuleWalletAddresses) -> None: self.create = to_streamed_response_wrapper(resource.create) self.delete = to_streamed_response_wrapper(resource.delete) self.update = to_streamed_response_wrapper(resource.update) + self.bulk_import = to_streamed_response_wrapper(resource.bulk_import) class AsyncTravelRuleWalletAddressesWithStreamingResponse: @@ -471,3 +563,4 @@ def __init__(self, resource: AsyncTravelRuleWalletAddresses) -> None: self.create = async_to_streamed_response_wrapper(resource.create) self.delete = async_to_streamed_response_wrapper(resource.delete) self.update = async_to_streamed_response_wrapper(resource.update) + self.bulk_import = async_to_streamed_response_wrapper(resource.bulk_import) diff --git a/src/didit/resources/travel_rule/workspace.py b/src/didit/resources/travel_rule/workspace.py new file mode 100644 index 0000000..a3907b5 --- /dev/null +++ b/src/didit/resources/travel_rule/workspace.py @@ -0,0 +1,535 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``travel_rule.workspace`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.travel_rule_workspace import TravelRuleWorkspace +from didit.types.components.travel_rule_workspace_domain_code_response import ( + TravelRuleWorkspaceDomainCodeResponse, +) + +__all__ = [ + "TravelRuleWorkspace", + "AsyncTravelRuleWorkspace", + "TravelRuleWorkspaceWithRawResponse", + "AsyncTravelRuleWorkspaceWithRawResponse", + "TravelRuleWorkspaceWithStreamingResponse", + "AsyncTravelRuleWorkspaceWithStreamingResponse", +] + + +class TravelRuleWorkspace(SyncAPIResource): + """The ``travel_rule.workspace`` resource of the Didit API. + + Methods: ``claim``, ``confirm_domain``, ``enable_billing``, ``get``, ``send_domain_code``, ``verify_domain_dns``. + """ + + @cached_property + def with_raw_response(self) -> TravelRuleWorkspaceWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return TravelRuleWorkspaceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> TravelRuleWorkspaceWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return TravelRuleWorkspaceWithStreamingResponse(self) + + def get( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleWorkspace: + """Get the free Travel Rule workspace. + + Reads the free workspace of an application that joined free: directory claim, domain proof, billing and Didit network listing. + + ``GET /v3/travel-rule/workspace/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleWorkspace", + self._client.request( + "get", + "/v3/travel-rule/workspace/", + cast_to=TravelRuleWorkspace, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def claim( + self, + *, + vasp_id: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleWorkspace: + """Claim a directory entry. + + Claims the directory entry this workspace is. When the work email is at a domain the entry lists, the claim is proved and applies at once: the VASP profile and Travel Rule settings are filled from the entry and Travel Rule is turned on, so the next transfer to it routes over the Didit network. Otherwise the claim waits for review (`pending_review`). + + ``POST /v3/travel-rule/workspace/claim/`` + + Args: + vasp_id: The directory entry this workspace is. Read ids from `GET /v3/travel-rule/vasps/`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleWorkspace", + self._client.request( + "post", + "/v3/travel-rule/workspace/claim/", + cast_to=TravelRuleWorkspace, + auth="api_key", + body={"vasp_id": vasp_id}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def send_domain_code( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleWorkspaceDomainCodeResponse: + """Email a domain verification code. + + Emails a single-use code to the workspace's work address to prove it controls the email domain. Throttled, and a limited number of wrong attempts is accepted per code. Also returns the TXT record for the DNS alternative. + + ``POST /v3/travel-rule/workspace/domain/code/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleWorkspaceDomainCodeResponse", + self._client.request( + "post", + "/v3/travel-rule/workspace/domain/code/", + cast_to=TravelRuleWorkspaceDomainCodeResponse, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def confirm_domain( + self, + *, + code: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleWorkspace: + """Confirm the domain verification code. + + Spends the emailed code and marks the email domain verified. + + ``POST /v3/travel-rule/workspace/domain/confirm/`` + + Args: + code + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleWorkspace", + self._client.request( + "post", + "/v3/travel-rule/workspace/domain/confirm/", + cast_to=TravelRuleWorkspace, + auth="api_key", + body={"code": code}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def verify_domain_dns( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleWorkspace: + """Verify the domain from its TXT record. + + Checks for the `didit-tr-verify.` TXT record returned by the code endpoint and marks the domain verified when it matches. One of the two proofs, the emailed code or the TXT record, is enough. + + ``POST /v3/travel-rule/workspace/domain/dns/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleWorkspace", + self._client.request( + "post", + "/v3/travel-rule/workspace/domain/dns/", + cast_to=TravelRuleWorkspace, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def enable_billing( + self, + *, + enabled: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleWorkspace: + """Enable outbound billing. + + Turns outbound Travel Rule billing on for the workspace. Receiving stays free. This cannot be turned back off through the API: contact Didit support. + + ``POST /v3/travel-rule/workspace/billing/`` + + Args: + enabled: Only `true` is accepted: outbound billing cannot be turned back off through the API. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleWorkspace", + self._client.request( + "post", + "/v3/travel-rule/workspace/billing/", + cast_to=TravelRuleWorkspace, + auth="api_key", + body={"enabled": enabled}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncTravelRuleWorkspace(AsyncAPIResource): + """The ``travel_rule.workspace`` resource of the Didit API. + + Methods: ``claim``, ``confirm_domain``, ``enable_billing``, ``get``, ``send_domain_code``, ``verify_domain_dns``. + """ + + @cached_property + def with_raw_response(self) -> AsyncTravelRuleWorkspaceWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncTravelRuleWorkspaceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncTravelRuleWorkspaceWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncTravelRuleWorkspaceWithStreamingResponse(self) + + async def get( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleWorkspace: + """Get the free Travel Rule workspace. + + Reads the free workspace of an application that joined free: directory claim, domain proof, billing and Didit network listing. + + ``GET /v3/travel-rule/workspace/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleWorkspace", + await self._client.request( + "get", + "/v3/travel-rule/workspace/", + cast_to=TravelRuleWorkspace, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def claim( + self, + *, + vasp_id: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleWorkspace: + """Claim a directory entry. + + Claims the directory entry this workspace is. When the work email is at a domain the entry lists, the claim is proved and applies at once: the VASP profile and Travel Rule settings are filled from the entry and Travel Rule is turned on, so the next transfer to it routes over the Didit network. Otherwise the claim waits for review (`pending_review`). + + ``POST /v3/travel-rule/workspace/claim/`` + + Args: + vasp_id: The directory entry this workspace is. Read ids from `GET /v3/travel-rule/vasps/`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleWorkspace", + await self._client.request( + "post", + "/v3/travel-rule/workspace/claim/", + cast_to=TravelRuleWorkspace, + auth="api_key", + body={"vasp_id": vasp_id}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def send_domain_code( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleWorkspaceDomainCodeResponse: + """Email a domain verification code. + + Emails a single-use code to the workspace's work address to prove it controls the email domain. Throttled, and a limited number of wrong attempts is accepted per code. Also returns the TXT record for the DNS alternative. + + ``POST /v3/travel-rule/workspace/domain/code/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleWorkspaceDomainCodeResponse", + await self._client.request( + "post", + "/v3/travel-rule/workspace/domain/code/", + cast_to=TravelRuleWorkspaceDomainCodeResponse, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def confirm_domain( + self, + *, + code: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleWorkspace: + """Confirm the domain verification code. + + Spends the emailed code and marks the email domain verified. + + ``POST /v3/travel-rule/workspace/domain/confirm/`` + + Args: + code + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleWorkspace", + await self._client.request( + "post", + "/v3/travel-rule/workspace/domain/confirm/", + cast_to=TravelRuleWorkspace, + auth="api_key", + body={"code": code}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def verify_domain_dns( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleWorkspace: + """Verify the domain from its TXT record. + + Checks for the `didit-tr-verify.` TXT record returned by the code endpoint and marks the domain verified when it matches. One of the two proofs, the emailed code or the TXT record, is enough. + + ``POST /v3/travel-rule/workspace/domain/dns/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleWorkspace", + await self._client.request( + "post", + "/v3/travel-rule/workspace/domain/dns/", + cast_to=TravelRuleWorkspace, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def enable_billing( + self, + *, + enabled: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleWorkspace: + """Enable outbound billing. + + Turns outbound Travel Rule billing on for the workspace. Receiving stays free. This cannot be turned back off through the API: contact Didit support. + + ``POST /v3/travel-rule/workspace/billing/`` + + Args: + enabled: Only `true` is accepted: outbound billing cannot be turned back off through the API. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleWorkspace", + await self._client.request( + "post", + "/v3/travel-rule/workspace/billing/", + cast_to=TravelRuleWorkspace, + auth="api_key", + body={"enabled": enabled}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class TravelRuleWorkspaceWithRawResponse: + def __init__(self, resource: TravelRuleWorkspace) -> None: + self._resource = resource + self.get = to_raw_response_wrapper(resource.get) + self.claim = to_raw_response_wrapper(resource.claim) + self.send_domain_code = to_raw_response_wrapper(resource.send_domain_code) + self.confirm_domain = to_raw_response_wrapper(resource.confirm_domain) + self.verify_domain_dns = to_raw_response_wrapper(resource.verify_domain_dns) + self.enable_billing = to_raw_response_wrapper(resource.enable_billing) + + +class AsyncTravelRuleWorkspaceWithRawResponse: + def __init__(self, resource: AsyncTravelRuleWorkspace) -> None: + self._resource = resource + self.get = async_to_raw_response_wrapper(resource.get) + self.claim = async_to_raw_response_wrapper(resource.claim) + self.send_domain_code = async_to_raw_response_wrapper(resource.send_domain_code) + self.confirm_domain = async_to_raw_response_wrapper(resource.confirm_domain) + self.verify_domain_dns = async_to_raw_response_wrapper(resource.verify_domain_dns) + self.enable_billing = async_to_raw_response_wrapper(resource.enable_billing) + + +class TravelRuleWorkspaceWithStreamingResponse: + def __init__(self, resource: TravelRuleWorkspace) -> None: + self._resource = resource + self.get = to_streamed_response_wrapper(resource.get) + self.claim = to_streamed_response_wrapper(resource.claim) + self.send_domain_code = to_streamed_response_wrapper(resource.send_domain_code) + self.confirm_domain = to_streamed_response_wrapper(resource.confirm_domain) + self.verify_domain_dns = to_streamed_response_wrapper(resource.verify_domain_dns) + self.enable_billing = to_streamed_response_wrapper(resource.enable_billing) + + +class AsyncTravelRuleWorkspaceWithStreamingResponse: + def __init__(self, resource: AsyncTravelRuleWorkspace) -> None: + self._resource = resource + self.get = async_to_streamed_response_wrapper(resource.get) + self.claim = async_to_streamed_response_wrapper(resource.claim) + self.send_domain_code = async_to_streamed_response_wrapper(resource.send_domain_code) + self.confirm_domain = async_to_streamed_response_wrapper(resource.confirm_domain) + self.verify_domain_dns = async_to_streamed_response_wrapper(resource.verify_domain_dns) + self.enable_billing = async_to_streamed_response_wrapper(resource.enable_billing) diff --git a/src/didit/resources/users.py b/src/didit/resources/users.py index b10d881..193f8be 100644 --- a/src/didit/resources/users.py +++ b/src/didit/resources/users.py @@ -18,24 +18,16 @@ to_streamed_response_wrapper, ) from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.aml_monitoring_job import AMLMonitoringJob +from didit.types.components.aml_monitoring_toggle_result import AMLMonitoringToggleResult +from didit.types.components.entity_network_membership_list import EntityNetworkMembershipList +from didit.types.components.ongoing_monitoring_job import OngoingMonitoringJob +from didit.types.components.ongoing_monitoring_toggle_result import OngoingMonitoringToggleResult from didit.types.components.user_detail_item import UserDetailItem from didit.types.components.user_list_item import UserListItem from didit.types.users.batch_delete_response import UsersBatchDeleteResponse -from didit.types.users.list_networks_response import UsersListNetworksResponse +from didit.types.users.create_response import UsersCreateResponse from didit.types.users.list_response import UsersListResponse -from didit.types.users.retrieve_ongoing_monitoring_job_response import ( - UsersRetrieveOngoingMonitoringJobResponse, -) -from didit.types.users.set_aml_monitoring_response import ( - UsersSetAmlMonitoringResponse202, - UsersSetAmlMonitoringResponseVariant1, - UsersSetAmlMonitoringResponseVariant2, -) -from didit.types.users.set_ongoing_monitoring_response import ( - UsersSetOngoingMonitoringResponse202, - UsersSetOngoingMonitoringResponseVariant1, - UsersSetOngoingMonitoringResponseVariant2, -) from didit.types.users.upload_face_response import UsersUploadFaceResponse __all__ = [ @@ -153,6 +145,7 @@ def update( approved_emails: Sequence[str] | NotGiven = NOT_GIVEN, approved_phones: Sequence[str] | NotGiven = NOT_GIVEN, issuing_states: Sequence[str] | NotGiven = NOT_GIVEN, + reason: str | None | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -160,7 +153,7 @@ def update( ) -> UserDetailItem: """Update user. - Partial update of a user: `display_name`, `full_name`, `date_of_birth`, `status`, `metadata`, `approved_emails`/`approved_phones`/`issuing_states`. `metadata` fully replaces. **Note:** unlike [Update User Status](#patch-/v3/users/-vendor_data-/update-status/), changing `status` here does NOT record a status-change activity and does NOT sync the system blocklist - use the dedicated update-status endpoint for `BLOCKED`/unblock flows. + Partial update of a user: `display_name`, `full_name`, `date_of_birth`, `status`, `metadata`, `approved_emails`/`approved_phones`/`issuing_states`. `metadata` fully replaces. Changing `status` here works like [Update User Status](#patch-/v3/users/-vendor_data-/update-status/): it records a `STATUS_CHANGED` activity with the optional `reason`, and `BLOCKED` adds the `vendor_data` to the system blocklist (moving away from `BLOCKED` removes that entry). Changes to `display_name`, `full_name`, `date_of_birth` or `metadata` are recorded as a `PROFILE_UPDATED` activity. ``PATCH /v3/users/{vendor_data}/`` @@ -169,11 +162,12 @@ def update( full_name: Full name of the user display_name: Custom display name for this user date_of_birth: Date of birth in YYYY-MM-DD format - status: User lifecycle status. Changing it via this generic PATCH does NOT sync the system blocklist or record a status-change activity; prefer the update-status endpoint. + status: User lifecycle status. A change is recorded as a `STATUS_CHANGED` activity, and `BLOCKED` adds the `vendor_data` to the system blocklist (moving away from `BLOCKED` removes that entry). metadata: Arbitrary JSON object you attach to the user. **Fully replaces** the existing metadata on update - merge client-side if you only want to add keys. approved_emails: Verified email addresses for this user, e.g. `["john@example.com"]`. Fully replaces the stored list. approved_phones: Verified phone numbers for this user. Fully replaces the stored list. issuing_states: Issuing countries (ISO 3166-1 alpha-3), e.g. `["USA"]`. Fully replaces the stored list. + reason: Why the status changed. Recorded on the status-change entry of the user's activity (`comments[].comment`, and **Activity** in the Business Console), only when the status actually changes. Webhooks do not carry it. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -196,6 +190,7 @@ def update( "approved_emails": approved_emails, "approved_phones": approved_phones, "issuing_states": issuing_states, + "reason": reason, }, extra_headers=extra_headers, extra_query=extra_query, @@ -271,7 +266,7 @@ def batch_delete( ) -> UsersBatchDeleteResponse: """Batch delete users. - **Permanently delete** users - the user records are removed permanently. For everyday blocking prefer [Update User Status](#patch-/v3/users/-vendor_data-/update-status/) with `BLOCKED`. Select users with `vendor_data_list`, with `didit_internal_id_list`, or with both at once; or pass `delete_all: true`. The Console's `filter_*` batch params are rejected with a `400` rather than ignored, so a narrowed intent can never turn into a full wipe. + **Permanently delete** users: the user records are removed permanently. For everyday blocking prefer [Update User Status](#patch-/v3/users/-vendor_data-/update-status/) with `BLOCKED`. Select users with `vendor_data_list`, with `didit_internal_id_list`, or with both at once; or pass `delete_all: true`. This endpoint never narrows `delete_all` with filters. The Console's People filters are refused with a `400` (see the `400` response), so a narrowed intent does not turn into a full wipe. Two exceptions: `filter_has_approved_sessions` and `filter_has_declined_sessions` are not refused and not applied, so with `delete_all: true` they still delete every user; do not send them. **Biometric templates:** every image-free biometric template retained for these users after session deletion (see `/v3/biometric-templates/`) is purged before the user is deleted, so the person stops matching in duplicate detection and Face Search. If a template cannot be purged because a biometric store is unavailable, the call returns `503`, no user in the request is deleted, and repeating the request finishes the job. @@ -316,11 +311,7 @@ def set_aml_monitoring( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ( - UsersSetAmlMonitoringResponseVariant1 - | UsersSetAmlMonitoringResponseVariant2 - | UsersSetAmlMonitoringResponse202 - ): + ) -> AMLMonitoringToggleResult | AMLMonitoringJob: """Enable or disable AML ongoing monitoring for users. Turns AML ongoing monitoring on or off for the selected users, with the same semantics as the Business Console. Exactly one monitored AML screening is kept per user, so a profile that verified several times is monitored and billed once; disabling turns monitoring off across all of the profile's screenings. Users without any AML screening are screened first (billed as a regular AML screening) when they have the full name, date of birth and country needed; otherwise they are reported as `skipped_no_identity_data`. @@ -340,15 +331,11 @@ def set_aml_monitoring( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "UsersSetAmlMonitoringResponseVariant1 | UsersSetAmlMonitoringResponseVariant2 | UsersSetAmlMonitoringResponse202", + "AMLMonitoringToggleResult | AMLMonitoringJob", self._client.request( "post", "/v3/users/aml-monitoring/", - cast_to={ - 200: UsersSetAmlMonitoringResponseVariant1 - | UsersSetAmlMonitoringResponseVariant2, - 202: UsersSetAmlMonitoringResponse202, - }, + cast_to={200: AMLMonitoringToggleResult, 202: AMLMonitoringJob}, auth="api_key", body={ "is_enabled": is_enabled, @@ -368,6 +355,7 @@ def update_status( vendor_data: str, *, status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str, + reason: str | None | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -375,13 +363,14 @@ def update_status( ) -> UserDetailItem: """Update user status. - Flip only the lifecycle `status` of a user - `ACTIVE`/`FLAGGED`/`BLOCKED` (NOT session statuses). `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. Each change is recorded as a `STATUS_CHANGED` activity on the user. + Flip only the lifecycle `status` of a user: `ACTIVE`, `FLAGGED` or `BLOCKED` (not session statuses). `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. Each change is recorded as a `STATUS_CHANGED` activity on the user, with the optional `reason` as its `comment`. Sending the current status changes nothing and records no `reason`. The `user.status.updated` webhook does not carry `reason`. ``PATCH /v3/users/{vendor_data}/update-status/`` Args: vendor_data: Your unique identifier for the user. status: New lifecycle status. `BLOCKED` also adds the `vendor_data` to the system blocklist. + reason: Why the status changed. Recorded on the status-change entry of the user's activity (`comments[].comment`, and **Activity** in the Business Console), only when the status actually changes. Webhooks do not carry it. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -395,7 +384,7 @@ def update_status( cast_to=UserDetailItem, path_params={"vendor_data": vendor_data}, auth="api_key", - body={"status": status}, + body={"status": status, "reason": reason}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -415,19 +404,33 @@ def create( approved_emails: Sequence[str] | NotGiven = NOT_GIVEN, approved_phones: Sequence[str] | NotGiven = NOT_GIVEN, issuing_states: Sequence[str] | NotGiven = NOT_GIVEN, + first_name: str | NotGiven = NOT_GIVEN, + last_name: str | NotGiven = NOT_GIVEN, + email: str | NotGiven = NOT_GIVEN, + phone_number: str | NotGiven = NOT_GIVEN, + country: str | NotGiven = NOT_GIVEN, + send_verification_link: bool | NotGiven = NOT_GIVEN, + workflow_id: str | NotGiven = NOT_GIVEN, + language: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> UserDetailItem: + ) -> UsersCreateResponse: """Create user. - Pre-create a [user](/entities/users) by `vendor_data` *without* a verification session. `vendor_data` must be unique among non-deleted users for the application (exact match; conflicts return 400). Not idempotent. + Create a [user](/entities/users) by `vendor_data`, before or without a verification session. `vendor_data` must be unique among the application's users (a duplicate returns `400`). Not idempotent. + + **Contact details:** `email` and `phone_number` are stored in `metadata.contact`, and `country` in `metadata.address`. When you send `first_name` or `last_name` without `full_name`, Didit joins them into `full_name`. + + **Verification link:** with `send_verification_link: true`, Didit also creates a verification session for the person on `workflow_id` and emails them the link at `email`. The session runs the same checks as [Create Session](/sessions-api/create-session), such as the blocklist and your credit balance. The person and the session are created together or not at all: if any check fails, nothing is written and no email is sent. The response's `verification_link` describes the session, and is `null` when you don't ask for a link. + + **API key access:** a scoped API key needs `users` write. With `send_verification_link: true` it also needs `sessions` write, or the call answers `403` before anything is written. Full-access keys are unaffected. See [API keys](/console/api-keys). ``POST /v3/users/create/`` Args: - vendor_data: Your unique identifier for this user (free-form string, NOT a UUID). Must be unique among non-deleted users for the application; matched exactly as sent. + vendor_data: Your unique identifier for this user (free-form string, NOT a UUID). Must be unique among the application's users; matched exactly as sent. full_name: Full legal name of the user. display_name: Friendly display name shown in the console (takes precedence over `full_name` for UI display). date_of_birth: Date of birth in `YYYY-MM-DD` format. @@ -436,17 +439,25 @@ def create( approved_emails: Pre-trusted email addresses for this user, e.g. `["john@example.com"]`. approved_phones: Pre-trusted phone numbers for this user, e.g. `["+14155551234"]`. issuing_states: Pre-recorded issuing countries (ISO 3166-1 alpha-3), e.g. `["USA"]`. + first_name: Given name. When `full_name` is not sent, Didit joins `first_name` and `last_name` with a space into `full_name`. Not returned as a separate field. + last_name: Family name. Joined with `first_name` into `full_name` when `full_name` is not sent. Not returned as a separate field. + email: Contact email, stored in `metadata.contact.email` (it replaces an `email` you send inside `metadata.contact`). Required when `send_verification_link` is `true`: the link is sent to this address. If `metadata.contact` is not a JSON object, the call answers `400` on this field. + phone_number: Contact phone number, stored in `metadata.contact.phone_number`. If `metadata.contact` is not a JSON object, the call answers `400` on this field. + country: Country as an ISO 3166 code, alpha-2 or alpha-3 (`ES` or `ESP`). Stored uppercased in `metadata.address.country`. If `metadata.address` is not a JSON object, the call answers `400` on this field. + send_verification_link: When `true`, Didit creates a verification session for the person on `workflow_id` and emails them the link at `email`. Requires `email` and `workflow_id`. A scoped API key also needs `sessions` write. + workflow_id: The workflow of the verification session. Required when `send_verification_link` is `true`. It must verify a person: a Business Verification (KYB) workflow is refused. Ignored otherwise. + language: Language of the verification email, as a [supported language](/integration/supported-languages) code. English when omitted. Ignored when `send_verification_link` is not `true`. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "UserDetailItem", + "UsersCreateResponse", self._client.request( "post", "/v3/users/create/", - cast_to=UserDetailItem, + cast_to=UsersCreateResponse, auth="api_key", body={ "vendor_data": vendor_data, @@ -458,6 +469,14 @@ def create( "approved_emails": approved_emails, "approved_phones": approved_phones, "issuing_states": issuing_states, + "first_name": first_name, + "last_name": last_name, + "email": email, + "phone_number": phone_number, + "country": country, + "send_verification_link": send_verification_link, + "workflow_id": workflow_id, + "language": language, }, extra_headers=extra_headers, extra_query=extra_query, @@ -525,11 +544,7 @@ def set_ongoing_monitoring( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ( - UsersSetOngoingMonitoringResponseVariant1 - | UsersSetOngoingMonitoringResponseVariant2 - | UsersSetOngoingMonitoringResponse202 - ): + ) -> OngoingMonitoringToggleResult | OngoingMonitoringJob: """Toggle AML ongoing monitoring for users. Enables or disables AML ongoing monitoring at the user level. Exactly one monitored AML screening is kept per user; users without any AML screening (for example imported users) get one created and screened first, billed as a regular AML screening. Set estimate_only to preview counts and costs without writing. Batches of 20 or fewer resolve inline (200, COMPLETED); larger batches return 202 and run asynchronously. @@ -552,15 +567,11 @@ def set_ongoing_monitoring( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "UsersSetOngoingMonitoringResponseVariant1 | UsersSetOngoingMonitoringResponseVariant2 | UsersSetOngoingMonitoringResponse202", + "OngoingMonitoringToggleResult | OngoingMonitoringJob", self._client.request( "patch", "/v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/", - cast_to={ - 200: UsersSetOngoingMonitoringResponseVariant1 - | UsersSetOngoingMonitoringResponseVariant2, - 202: UsersSetOngoingMonitoringResponse202, - }, + cast_to={200: OngoingMonitoringToggleResult, 202: OngoingMonitoringJob}, scope="application", auth="api_key", body={ @@ -589,7 +600,7 @@ def retrieve_ongoing_monitoring_job( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> UsersRetrieveOngoingMonitoringJobResponse: + ) -> OngoingMonitoringJob: """Get AML ongoing monitoring job. Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING. @@ -604,11 +615,11 @@ def retrieve_ongoing_monitoring_job( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "UsersRetrieveOngoingMonitoringJobResponse", + "OngoingMonitoringJob", self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/{job_id}/", - cast_to=UsersRetrieveOngoingMonitoringJobResponse, + cast_to=OngoingMonitoringJob, path_params={"job_id": job_id}, scope="application", auth="api_key", @@ -627,7 +638,7 @@ def list_networks( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> UsersListNetworksResponse: + ) -> EntityNetworkMembershipList: """List networks for a user. List the networks a vendor user belongs to. @@ -642,11 +653,11 @@ def list_networks( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "UsersListNetworksResponse", + "EntityNetworkMembershipList", self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/users/{vendor_user_uuid}/networks/", - cast_to=UsersListNetworksResponse, + cast_to=EntityNetworkMembershipList, path_params={"vendor_user_uuid": vendor_user_uuid}, scope="application", auth="api_key", @@ -763,6 +774,7 @@ async def update( approved_emails: Sequence[str] | NotGiven = NOT_GIVEN, approved_phones: Sequence[str] | NotGiven = NOT_GIVEN, issuing_states: Sequence[str] | NotGiven = NOT_GIVEN, + reason: str | None | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -770,7 +782,7 @@ async def update( ) -> UserDetailItem: """Update user. - Partial update of a user: `display_name`, `full_name`, `date_of_birth`, `status`, `metadata`, `approved_emails`/`approved_phones`/`issuing_states`. `metadata` fully replaces. **Note:** unlike [Update User Status](#patch-/v3/users/-vendor_data-/update-status/), changing `status` here does NOT record a status-change activity and does NOT sync the system blocklist - use the dedicated update-status endpoint for `BLOCKED`/unblock flows. + Partial update of a user: `display_name`, `full_name`, `date_of_birth`, `status`, `metadata`, `approved_emails`/`approved_phones`/`issuing_states`. `metadata` fully replaces. Changing `status` here works like [Update User Status](#patch-/v3/users/-vendor_data-/update-status/): it records a `STATUS_CHANGED` activity with the optional `reason`, and `BLOCKED` adds the `vendor_data` to the system blocklist (moving away from `BLOCKED` removes that entry). Changes to `display_name`, `full_name`, `date_of_birth` or `metadata` are recorded as a `PROFILE_UPDATED` activity. ``PATCH /v3/users/{vendor_data}/`` @@ -779,11 +791,12 @@ async def update( full_name: Full name of the user display_name: Custom display name for this user date_of_birth: Date of birth in YYYY-MM-DD format - status: User lifecycle status. Changing it via this generic PATCH does NOT sync the system blocklist or record a status-change activity; prefer the update-status endpoint. + status: User lifecycle status. A change is recorded as a `STATUS_CHANGED` activity, and `BLOCKED` adds the `vendor_data` to the system blocklist (moving away from `BLOCKED` removes that entry). metadata: Arbitrary JSON object you attach to the user. **Fully replaces** the existing metadata on update - merge client-side if you only want to add keys. approved_emails: Verified email addresses for this user, e.g. `["john@example.com"]`. Fully replaces the stored list. approved_phones: Verified phone numbers for this user. Fully replaces the stored list. issuing_states: Issuing countries (ISO 3166-1 alpha-3), e.g. `["USA"]`. Fully replaces the stored list. + reason: Why the status changed. Recorded on the status-change entry of the user's activity (`comments[].comment`, and **Activity** in the Business Console), only when the status actually changes. Webhooks do not carry it. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -806,6 +819,7 @@ async def update( "approved_emails": approved_emails, "approved_phones": approved_phones, "issuing_states": issuing_states, + "reason": reason, }, extra_headers=extra_headers, extra_query=extra_query, @@ -881,7 +895,7 @@ async def batch_delete( ) -> UsersBatchDeleteResponse: """Batch delete users. - **Permanently delete** users - the user records are removed permanently. For everyday blocking prefer [Update User Status](#patch-/v3/users/-vendor_data-/update-status/) with `BLOCKED`. Select users with `vendor_data_list`, with `didit_internal_id_list`, or with both at once; or pass `delete_all: true`. The Console's `filter_*` batch params are rejected with a `400` rather than ignored, so a narrowed intent can never turn into a full wipe. + **Permanently delete** users: the user records are removed permanently. For everyday blocking prefer [Update User Status](#patch-/v3/users/-vendor_data-/update-status/) with `BLOCKED`. Select users with `vendor_data_list`, with `didit_internal_id_list`, or with both at once; or pass `delete_all: true`. This endpoint never narrows `delete_all` with filters. The Console's People filters are refused with a `400` (see the `400` response), so a narrowed intent does not turn into a full wipe. Two exceptions: `filter_has_approved_sessions` and `filter_has_declined_sessions` are not refused and not applied, so with `delete_all: true` they still delete every user; do not send them. **Biometric templates:** every image-free biometric template retained for these users after session deletion (see `/v3/biometric-templates/`) is purged before the user is deleted, so the person stops matching in duplicate detection and Face Search. If a template cannot be purged because a biometric store is unavailable, the call returns `503`, no user in the request is deleted, and repeating the request finishes the job. @@ -926,11 +940,7 @@ async def set_aml_monitoring( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ( - UsersSetAmlMonitoringResponseVariant1 - | UsersSetAmlMonitoringResponseVariant2 - | UsersSetAmlMonitoringResponse202 - ): + ) -> AMLMonitoringToggleResult | AMLMonitoringJob: """Enable or disable AML ongoing monitoring for users. Turns AML ongoing monitoring on or off for the selected users, with the same semantics as the Business Console. Exactly one monitored AML screening is kept per user, so a profile that verified several times is monitored and billed once; disabling turns monitoring off across all of the profile's screenings. Users without any AML screening are screened first (billed as a regular AML screening) when they have the full name, date of birth and country needed; otherwise they are reported as `skipped_no_identity_data`. @@ -950,15 +960,11 @@ async def set_aml_monitoring( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "UsersSetAmlMonitoringResponseVariant1 | UsersSetAmlMonitoringResponseVariant2 | UsersSetAmlMonitoringResponse202", + "AMLMonitoringToggleResult | AMLMonitoringJob", await self._client.request( "post", "/v3/users/aml-monitoring/", - cast_to={ - 200: UsersSetAmlMonitoringResponseVariant1 - | UsersSetAmlMonitoringResponseVariant2, - 202: UsersSetAmlMonitoringResponse202, - }, + cast_to={200: AMLMonitoringToggleResult, 202: AMLMonitoringJob}, auth="api_key", body={ "is_enabled": is_enabled, @@ -978,6 +984,7 @@ async def update_status( vendor_data: str, *, status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str, + reason: str | None | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, @@ -985,13 +992,14 @@ async def update_status( ) -> UserDetailItem: """Update user status. - Flip only the lifecycle `status` of a user - `ACTIVE`/`FLAGGED`/`BLOCKED` (NOT session statuses). `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. Each change is recorded as a `STATUS_CHANGED` activity on the user. + Flip only the lifecycle `status` of a user: `ACTIVE`, `FLAGGED` or `BLOCKED` (not session statuses). `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. Each change is recorded as a `STATUS_CHANGED` activity on the user, with the optional `reason` as its `comment`. Sending the current status changes nothing and records no `reason`. The `user.status.updated` webhook does not carry `reason`. ``PATCH /v3/users/{vendor_data}/update-status/`` Args: vendor_data: Your unique identifier for the user. status: New lifecycle status. `BLOCKED` also adds the `vendor_data` to the system blocklist. + reason: Why the status changed. Recorded on the status-change entry of the user's activity (`comments[].comment`, and **Activity** in the Business Console), only when the status actually changes. Webhooks do not carry it. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. @@ -1005,7 +1013,7 @@ async def update_status( cast_to=UserDetailItem, path_params={"vendor_data": vendor_data}, auth="api_key", - body={"status": status}, + body={"status": status, "reason": reason}, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, @@ -1025,19 +1033,33 @@ async def create( approved_emails: Sequence[str] | NotGiven = NOT_GIVEN, approved_phones: Sequence[str] | NotGiven = NOT_GIVEN, issuing_states: Sequence[str] | NotGiven = NOT_GIVEN, + first_name: str | NotGiven = NOT_GIVEN, + last_name: str | NotGiven = NOT_GIVEN, + email: str | NotGiven = NOT_GIVEN, + phone_number: str | NotGiven = NOT_GIVEN, + country: str | NotGiven = NOT_GIVEN, + send_verification_link: bool | NotGiven = NOT_GIVEN, + workflow_id: str | NotGiven = NOT_GIVEN, + language: str | NotGiven = NOT_GIVEN, extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> UserDetailItem: + ) -> UsersCreateResponse: """Create user. - Pre-create a [user](/entities/users) by `vendor_data` *without* a verification session. `vendor_data` must be unique among non-deleted users for the application (exact match; conflicts return 400). Not idempotent. + Create a [user](/entities/users) by `vendor_data`, before or without a verification session. `vendor_data` must be unique among the application's users (a duplicate returns `400`). Not idempotent. + + **Contact details:** `email` and `phone_number` are stored in `metadata.contact`, and `country` in `metadata.address`. When you send `first_name` or `last_name` without `full_name`, Didit joins them into `full_name`. + + **Verification link:** with `send_verification_link: true`, Didit also creates a verification session for the person on `workflow_id` and emails them the link at `email`. The session runs the same checks as [Create Session](/sessions-api/create-session), such as the blocklist and your credit balance. The person and the session are created together or not at all: if any check fails, nothing is written and no email is sent. The response's `verification_link` describes the session, and is `null` when you don't ask for a link. + + **API key access:** a scoped API key needs `users` write. With `send_verification_link: true` it also needs `sessions` write, or the call answers `403` before anything is written. Full-access keys are unaffected. See [API keys](/console/api-keys). ``POST /v3/users/create/`` Args: - vendor_data: Your unique identifier for this user (free-form string, NOT a UUID). Must be unique among non-deleted users for the application; matched exactly as sent. + vendor_data: Your unique identifier for this user (free-form string, NOT a UUID). Must be unique among the application's users; matched exactly as sent. full_name: Full legal name of the user. display_name: Friendly display name shown in the console (takes precedence over `full_name` for UI display). date_of_birth: Date of birth in `YYYY-MM-DD` format. @@ -1046,17 +1068,25 @@ async def create( approved_emails: Pre-trusted email addresses for this user, e.g. `["john@example.com"]`. approved_phones: Pre-trusted phone numbers for this user, e.g. `["+14155551234"]`. issuing_states: Pre-recorded issuing countries (ISO 3166-1 alpha-3), e.g. `["USA"]`. + first_name: Given name. When `full_name` is not sent, Didit joins `first_name` and `last_name` with a space into `full_name`. Not returned as a separate field. + last_name: Family name. Joined with `first_name` into `full_name` when `full_name` is not sent. Not returned as a separate field. + email: Contact email, stored in `metadata.contact.email` (it replaces an `email` you send inside `metadata.contact`). Required when `send_verification_link` is `true`: the link is sent to this address. If `metadata.contact` is not a JSON object, the call answers `400` on this field. + phone_number: Contact phone number, stored in `metadata.contact.phone_number`. If `metadata.contact` is not a JSON object, the call answers `400` on this field. + country: Country as an ISO 3166 code, alpha-2 or alpha-3 (`ES` or `ESP`). Stored uppercased in `metadata.address.country`. If `metadata.address` is not a JSON object, the call answers `400` on this field. + send_verification_link: When `true`, Didit creates a verification session for the person on `workflow_id` and emails them the link at `email`. Requires `email` and `workflow_id`. A scoped API key also needs `sessions` write. + workflow_id: The workflow of the verification session. Required when `send_verification_link` is `true`. It must verify a person: a Business Verification (KYB) workflow is refused. Ignored otherwise. + language: Language of the verification email, as a [supported language](/integration/supported-languages) code. English when omitted. Ignored when `send_verification_link` is not `true`. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. extra_body: Top-level JSON fields to add to this request's body. timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "UserDetailItem", + "UsersCreateResponse", await self._client.request( "post", "/v3/users/create/", - cast_to=UserDetailItem, + cast_to=UsersCreateResponse, auth="api_key", body={ "vendor_data": vendor_data, @@ -1068,6 +1098,14 @@ async def create( "approved_emails": approved_emails, "approved_phones": approved_phones, "issuing_states": issuing_states, + "first_name": first_name, + "last_name": last_name, + "email": email, + "phone_number": phone_number, + "country": country, + "send_verification_link": send_verification_link, + "workflow_id": workflow_id, + "language": language, }, extra_headers=extra_headers, extra_query=extra_query, @@ -1135,11 +1173,7 @@ async def set_ongoing_monitoring( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> ( - UsersSetOngoingMonitoringResponseVariant1 - | UsersSetOngoingMonitoringResponseVariant2 - | UsersSetOngoingMonitoringResponse202 - ): + ) -> OngoingMonitoringToggleResult | OngoingMonitoringJob: """Toggle AML ongoing monitoring for users. Enables or disables AML ongoing monitoring at the user level. Exactly one monitored AML screening is kept per user; users without any AML screening (for example imported users) get one created and screened first, billed as a regular AML screening. Set estimate_only to preview counts and costs without writing. Batches of 20 or fewer resolve inline (200, COMPLETED); larger batches return 202 and run asynchronously. @@ -1162,15 +1196,11 @@ async def set_ongoing_monitoring( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "UsersSetOngoingMonitoringResponseVariant1 | UsersSetOngoingMonitoringResponseVariant2 | UsersSetOngoingMonitoringResponse202", + "OngoingMonitoringToggleResult | OngoingMonitoringJob", await self._client.request( "patch", "/v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/", - cast_to={ - 200: UsersSetOngoingMonitoringResponseVariant1 - | UsersSetOngoingMonitoringResponseVariant2, - 202: UsersSetOngoingMonitoringResponse202, - }, + cast_to={200: OngoingMonitoringToggleResult, 202: OngoingMonitoringJob}, scope="application", auth="api_key", body={ @@ -1199,7 +1229,7 @@ async def retrieve_ongoing_monitoring_job( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> UsersRetrieveOngoingMonitoringJobResponse: + ) -> OngoingMonitoringJob: """Get AML ongoing monitoring job. Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING. @@ -1214,11 +1244,11 @@ async def retrieve_ongoing_monitoring_job( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "UsersRetrieveOngoingMonitoringJobResponse", + "OngoingMonitoringJob", await self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/{job_id}/", - cast_to=UsersRetrieveOngoingMonitoringJobResponse, + cast_to=OngoingMonitoringJob, path_params={"job_id": job_id}, scope="application", auth="api_key", @@ -1237,7 +1267,7 @@ async def list_networks( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> UsersListNetworksResponse: + ) -> EntityNetworkMembershipList: """List networks for a user. List the networks a vendor user belongs to. @@ -1252,11 +1282,11 @@ async def list_networks( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "UsersListNetworksResponse", + "EntityNetworkMembershipList", await self._client.request( "get", "/v3/organization/{organization_id}/application/{application_id}/users/{vendor_user_uuid}/networks/", - cast_to=UsersListNetworksResponse, + cast_to=EntityNetworkMembershipList, path_params={"vendor_user_uuid": vendor_user_uuid}, scope="application", auth="api_key", diff --git a/src/didit/resources/wallet_screening.py b/src/didit/resources/wallet_screening.py index 78c66a3..8be9ccb 100644 --- a/src/didit/resources/wallet_screening.py +++ b/src/didit/resources/wallet_screening.py @@ -71,6 +71,9 @@ def screen( "BASE", "OP", "ARB", + "CELO", + "MODE", + "PLUME", ] | str, direction: Literal["inbound", "outbound", "deposit", "withdrawal"] @@ -89,7 +92,7 @@ def screen( Args: wallet_address: The crypto address or network identifier to screen. Must match the selected network (e.g. a `0x`-prefixed 40-hex-char address for EVM chains). Alias: `address`. - blockchain: Network identifier. Managed screening supports BTC, ETH, LTC, XRP, BCH, DOGE, TRX, SOL, MATIC, BNB, BASE (Base), OP (Optimism), and ARB (Arbitrum One). USDT and USDC are legacy Ethereum aliases, not multi-network searches. Connected-provider coverage varies. LIGHTNING requires a supported connected provider. Celo, Mode and Plume are not currently supported. Alias: `currency`. + blockchain: Network identifier. Managed screening supports BTC, ETH, LTC, XRP, BCH, DOGE, TRX, SOL, MATIC, BNB, BASE (Base), OP (Optimism), and ARB (Arbitrum One). USDT and USDC are legacy Ethereum aliases, not multi-network searches. Connected-provider coverage varies. LIGHTNING requires a supported connected provider. CELO (Celo), MODE (Mode) and PLUME (Plume) are screened through a connected TRM Labs account; managed screening answers 400 for them. Alias: `currency`. direction: Optional. Whether the address is being screened as an inbound (deposit) or outbound (withdrawal) counterparty. Accepts `inbound`, `outbound`, `deposit`, or `withdrawal`. Defaults to a neutral pre-transfer screen when omitted. This is passed to the provider as context only; it does not change the returned `risk_score`, `severity`, or the `source_of_funds`/`destination_of_funds` breakdowns, which are always both returned regardless of `direction`. Changing or omitting `direction` therefore does not require re-screening an address. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. @@ -274,6 +277,9 @@ async def screen( "BASE", "OP", "ARB", + "CELO", + "MODE", + "PLUME", ] | str, direction: Literal["inbound", "outbound", "deposit", "withdrawal"] @@ -292,7 +298,7 @@ async def screen( Args: wallet_address: The crypto address or network identifier to screen. Must match the selected network (e.g. a `0x`-prefixed 40-hex-char address for EVM chains). Alias: `address`. - blockchain: Network identifier. Managed screening supports BTC, ETH, LTC, XRP, BCH, DOGE, TRX, SOL, MATIC, BNB, BASE (Base), OP (Optimism), and ARB (Arbitrum One). USDT and USDC are legacy Ethereum aliases, not multi-network searches. Connected-provider coverage varies. LIGHTNING requires a supported connected provider. Celo, Mode and Plume are not currently supported. Alias: `currency`. + blockchain: Network identifier. Managed screening supports BTC, ETH, LTC, XRP, BCH, DOGE, TRX, SOL, MATIC, BNB, BASE (Base), OP (Optimism), and ARB (Arbitrum One). USDT and USDC are legacy Ethereum aliases, not multi-network searches. Connected-provider coverage varies. LIGHTNING requires a supported connected provider. CELO (Celo), MODE (Mode) and PLUME (Plume) are screened through a connected TRM Labs account; managed screening answers 400 for them. Alias: `currency`. direction: Optional. Whether the address is being screened as an inbound (deposit) or outbound (withdrawal) counterparty. Accepts `inbound`, `outbound`, `deposit`, or `withdrawal`. Defaults to a neutral pre-transfer screen when omitted. This is passed to the provider as context only; it does not change the returned `risk_score`, `severity`, or the `source_of_funds`/`destination_of_funds` breakdowns, which are always both returned regardless of `direction`. Changing or omitting `direction` therefore does not require re-screening an address. extra_headers: Headers to add to (or, with ``None``, remove from) this request. extra_query: Query parameters to add to this request. diff --git a/src/didit/resources/webhooks/__init__.py b/src/didit/resources/webhooks/__init__.py index cb7e1d4..5edcbda 100644 --- a/src/didit/resources/webhooks/__init__.py +++ b/src/didit/resources/webhooks/__init__.py @@ -4,9 +4,19 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal, Sequence, cast + +import httpx from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.webhook_config import WebhookConfig from didit.webhooks._mixin import WebhooksMixin if TYPE_CHECKING: @@ -32,6 +42,8 @@ class Webhooks(WebhooksMixin, SyncAPIResource): """The ``webhooks`` resource of the Didit API. + Methods: ``retrieve_config``, ``update_config``. + Nested resources: ``destinations``. """ @@ -51,10 +63,108 @@ def with_streaming_response(self) -> WebhooksWithStreamingResponse: """Every method of this resource, returning the HTTP response before reading its body.""" return WebhooksWithStreamingResponse(self) + def retrieve_config( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WebhookConfig: + """Get webhook configuration. + + Return the application's webhook settings in one call: every [webhook destination](https://docs.didit.me/management-api/webhook/list-destinations) with its signing secret, the event types a destination can subscribe to, the application-level (legacy) webhook URL, version and secret, the data and biometric-template retention policy, and the Digital ID wallet return hosts. Didit delivers webhooks to destinations, each signed with its own `secret_shared_key`; the top-level `webhook_url`, `webhook_version` and `secret_shared_key` are used only while no destination is enabled, and then only for `status.updated` and `data.updated`. The response contains signing secrets, so keep it out of logs and client-side code. A scoped API key needs `webhooks: read`. + + ``GET /v3/webhook/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WebhookConfig", + self._client.request( + "get", + "/v3/webhook/", + cast_to=WebhookConfig, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update_config( + self, + *, + webhook_url: str | None | NotGiven = NOT_GIVEN, + webhook_version: Literal["v1", "v2", "v3"] | str | NotGiven = NOT_GIVEN, + rotate_secret_key: bool | NotGiven = NOT_GIVEN, + capture_method: Literal["MOBILE", "MOBILE_AND_DESKTOP"] | str | NotGiven = NOT_GIVEN, + data_retention_months: int | None | NotGiven = NOT_GIVEN, + face_retention_policy: Literal["delete_with_session", "retain_until_user_deleted"] + | str + | NotGiven = NOT_GIVEN, + face_retention_days: int | None | NotGiven = NOT_GIVEN, + allowed_return_hosts: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WebhookConfig: + """Update webhook configuration. + + Partially update application-level settings: the legacy webhook URL, version and secret, the data and biometric-template retention policy, and the Digital ID wallet return hosts. Send only the fields to change; the response is the full configuration, the same shape as `GET /v3/webhook/`. Destinations are not managed here: create, update and delete them with the `/v3/webhook/destinations/` endpoints, and do not send an `endpoints` field to this endpoint. A `data_retention_months` change also applies to existing sessions, and sessions it deletes cannot be restored; read [Data retention](https://docs.didit.me/console/data-retention) first. A scoped API key needs `webhooks: write`. + + ``PATCH /v3/webhook/`` + + Args: + webhook_url: Legacy application-level webhook URL. Didit uses it only while the application has no enabled destination, and then only for `status.updated` and `data.updated`. It does not create a destination, and the next create, update or delete of a destination replaces it. To receive webhooks, create a destination with `POST /v3/webhook/destinations/` instead. + webhook_version: Payload version of the legacy `webhook_url`. + rotate_secret_key: `true` replaces the application-level `secret_shared_key` with a new random value at once; the new value is in the response. Destination secrets are not changed. Every call with `true` generates a new secret, so it is not idempotent. + capture_method: Legacy application setting. The verification flow does not read it. + data_retention_months: Automatic deletion window in months (a month counts as 30 days), or `null` for no automatic deletion. A change applies to existing sessions too: every user verification session older than the new window is deleted automatically, and that deletion is irreversible. + face_retention_policy: What happens to a session's face embedding when the session is deleted. `retain_until_user_deleted` needs `face_retention_days` in the same request or already set. A change applies to session deletions from then on; it creates or purges no existing biometric template. + face_retention_days: Retention period, in days, of biometric templates under `retain_until_user_deleted`. + allowed_return_hosts: Replaces the whole list of Digital ID wallet return hosts. Bare hostnames only, such as `shop.example.com`: no scheme, port, path or wildcard. Values are lowercased and duplicates are dropped. Send `[]` to clear the list. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WebhookConfig", + self._client.request( + "patch", + "/v3/webhook/", + cast_to=WebhookConfig, + auth="api_key", + body={ + "webhook_url": webhook_url, + "webhook_version": webhook_version, + "rotate_secret_key": rotate_secret_key, + "capture_method": capture_method, + "data_retention_months": data_retention_months, + "face_retention_policy": face_retention_policy, + "face_retention_days": face_retention_days, + "allowed_return_hosts": allowed_return_hosts, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + class AsyncWebhooks(WebhooksMixin, AsyncAPIResource): """The ``webhooks`` resource of the Didit API. + Methods: ``retrieve_config``, ``update_config``. + Nested resources: ``destinations``. """ @@ -74,10 +184,108 @@ def with_streaming_response(self) -> AsyncWebhooksWithStreamingResponse: """Every method of this resource, returning the HTTP response before reading its body.""" return AsyncWebhooksWithStreamingResponse(self) + async def retrieve_config( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WebhookConfig: + """Get webhook configuration. + + Return the application's webhook settings in one call: every [webhook destination](https://docs.didit.me/management-api/webhook/list-destinations) with its signing secret, the event types a destination can subscribe to, the application-level (legacy) webhook URL, version and secret, the data and biometric-template retention policy, and the Digital ID wallet return hosts. Didit delivers webhooks to destinations, each signed with its own `secret_shared_key`; the top-level `webhook_url`, `webhook_version` and `secret_shared_key` are used only while no destination is enabled, and then only for `status.updated` and `data.updated`. The response contains signing secrets, so keep it out of logs and client-side code. A scoped API key needs `webhooks: read`. + + ``GET /v3/webhook/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WebhookConfig", + await self._client.request( + "get", + "/v3/webhook/", + cast_to=WebhookConfig, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_config( + self, + *, + webhook_url: str | None | NotGiven = NOT_GIVEN, + webhook_version: Literal["v1", "v2", "v3"] | str | NotGiven = NOT_GIVEN, + rotate_secret_key: bool | NotGiven = NOT_GIVEN, + capture_method: Literal["MOBILE", "MOBILE_AND_DESKTOP"] | str | NotGiven = NOT_GIVEN, + data_retention_months: int | None | NotGiven = NOT_GIVEN, + face_retention_policy: Literal["delete_with_session", "retain_until_user_deleted"] + | str + | NotGiven = NOT_GIVEN, + face_retention_days: int | None | NotGiven = NOT_GIVEN, + allowed_return_hosts: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WebhookConfig: + """Update webhook configuration. + + Partially update application-level settings: the legacy webhook URL, version and secret, the data and biometric-template retention policy, and the Digital ID wallet return hosts. Send only the fields to change; the response is the full configuration, the same shape as `GET /v3/webhook/`. Destinations are not managed here: create, update and delete them with the `/v3/webhook/destinations/` endpoints, and do not send an `endpoints` field to this endpoint. A `data_retention_months` change also applies to existing sessions, and sessions it deletes cannot be restored; read [Data retention](https://docs.didit.me/console/data-retention) first. A scoped API key needs `webhooks: write`. + + ``PATCH /v3/webhook/`` + + Args: + webhook_url: Legacy application-level webhook URL. Didit uses it only while the application has no enabled destination, and then only for `status.updated` and `data.updated`. It does not create a destination, and the next create, update or delete of a destination replaces it. To receive webhooks, create a destination with `POST /v3/webhook/destinations/` instead. + webhook_version: Payload version of the legacy `webhook_url`. + rotate_secret_key: `true` replaces the application-level `secret_shared_key` with a new random value at once; the new value is in the response. Destination secrets are not changed. Every call with `true` generates a new secret, so it is not idempotent. + capture_method: Legacy application setting. The verification flow does not read it. + data_retention_months: Automatic deletion window in months (a month counts as 30 days), or `null` for no automatic deletion. A change applies to existing sessions too: every user verification session older than the new window is deleted automatically, and that deletion is irreversible. + face_retention_policy: What happens to a session's face embedding when the session is deleted. `retain_until_user_deleted` needs `face_retention_days` in the same request or already set. A change applies to session deletions from then on; it creates or purges no existing biometric template. + face_retention_days: Retention period, in days, of biometric templates under `retain_until_user_deleted`. + allowed_return_hosts: Replaces the whole list of Digital ID wallet return hosts. Bare hostnames only, such as `shop.example.com`: no scheme, port, path or wildcard. Values are lowercased and duplicates are dropped. Send `[]` to clear the list. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WebhookConfig", + await self._client.request( + "patch", + "/v3/webhook/", + cast_to=WebhookConfig, + auth="api_key", + body={ + "webhook_url": webhook_url, + "webhook_version": webhook_version, + "rotate_secret_key": rotate_secret_key, + "capture_method": capture_method, + "data_retention_months": data_retention_months, + "face_retention_policy": face_retention_policy, + "face_retention_days": face_retention_days, + "allowed_return_hosts": allowed_return_hosts, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + class WebhooksWithRawResponse: def __init__(self, resource: Webhooks) -> None: self._resource = resource + self.retrieve_config = to_raw_response_wrapper(resource.retrieve_config) + self.update_config = to_raw_response_wrapper(resource.update_config) @cached_property def destinations(self) -> WebhooksDestinationsWithRawResponse: @@ -89,6 +297,8 @@ def destinations(self) -> WebhooksDestinationsWithRawResponse: class AsyncWebhooksWithRawResponse: def __init__(self, resource: AsyncWebhooks) -> None: self._resource = resource + self.retrieve_config = async_to_raw_response_wrapper(resource.retrieve_config) + self.update_config = async_to_raw_response_wrapper(resource.update_config) @cached_property def destinations(self) -> AsyncWebhooksDestinationsWithRawResponse: @@ -100,6 +310,8 @@ def destinations(self) -> AsyncWebhooksDestinationsWithRawResponse: class WebhooksWithStreamingResponse: def __init__(self, resource: Webhooks) -> None: self._resource = resource + self.retrieve_config = to_streamed_response_wrapper(resource.retrieve_config) + self.update_config = to_streamed_response_wrapper(resource.update_config) @cached_property def destinations(self) -> WebhooksDestinationsWithStreamingResponse: @@ -111,6 +323,8 @@ def destinations(self) -> WebhooksDestinationsWithStreamingResponse: class AsyncWebhooksWithStreamingResponse: def __init__(self, resource: AsyncWebhooks) -> None: self._resource = resource + self.retrieve_config = async_to_streamed_response_wrapper(resource.retrieve_config) + self.update_config = async_to_streamed_response_wrapper(resource.update_config) @cached_property def destinations(self) -> AsyncWebhooksDestinationsWithStreamingResponse: diff --git a/src/didit/resources/webhooks/destinations.py b/src/didit/resources/webhooks/destinations.py index ff9f549..309c19b 100644 --- a/src/didit/resources/webhooks/destinations.py +++ b/src/didit/resources/webhooks/destinations.py @@ -4,11 +4,13 @@ from __future__ import annotations import builtins +import datetime as _dt from functools import cached_property from typing import Literal, cast import httpx +from didit._pagination import AsyncPaginator, SyncPage from didit._resource import AsyncAPIResource, SyncAPIResource from didit._response import ( async_to_raw_response_wrapper, @@ -17,9 +19,19 @@ to_streamed_response_wrapper, ) from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.webhook_destination_delivery import WebhookDestinationDelivery +from didit.types.components.webhook_destination_delivery_detail import ( + WebhookDestinationDeliveryDetail, +) from didit.types.components.webhook_subscribed_events import WebhookSubscribedEventsParam from didit.types.webhooks.destinations.create_response import WebhooksDestinationsCreateResponse +from didit.types.webhooks.destinations.list_deliveries_response import ( + WebhooksDestinationsListDeliveriesResponse, +) from didit.types.webhooks.destinations.list_response import WebhooksDestinationsListResponseItem +from didit.types.webhooks.destinations.retrieve_analytics_response import ( + WebhooksDestinationsRetrieveAnalyticsResponse, +) from didit.types.webhooks.destinations.retrieve_response import WebhooksDestinationsRetrieveResponse from didit.types.webhooks.destinations.update_response import WebhooksDestinationsUpdateResponse @@ -36,7 +48,7 @@ class WebhooksDestinations(SyncAPIResource): """The ``webhooks.destinations`` resource of the Didit API. - Methods: ``create``, ``delete``, ``list``, ``retrieve``, ``update``. + Methods: ``create``, ``delete``, ``list``, ``list_deliveries``, ``resend_delivery``, ``retrieve``, ``retrieve_analytics``, ``retrieve_delivery``, ``update``. """ @cached_property @@ -59,7 +71,7 @@ def list( ) -> builtins.list[WebhooksDestinationsListResponseItem]: """List webhook destinations. - List all webhook destinations (unpaginated array, oldest first). Each row includes URL, subscribed events, and delivery health metrics. The signing secret is NOT included in the list - fetch a single destination (`GET /v3/webhook/destinations/{destination_uuid}/`) to read `secret_shared_key`. + List all webhook destinations (unpaginated array, oldest first). Each item carries the destination's configuration, its delivery metrics for the last 30 days (`metrics_window_days`) with one `daily_points` entry per day, and its health (`health_status`, `failing_streak`, `failing_since`, `last_success_at`). The list has no `summary` object and never includes the signing secret: each item shows only `secret_last4`. Fetch a single destination (`GET /v3/webhook/destinations/{destination_uuid}/`) to read `secret_shared_key`. ``GET /v3/webhook/destinations/`` @@ -98,13 +110,13 @@ def create( ) -> WebhooksDestinationsCreateResponse: """Create webhook destination. - Create a webhook destination. The response includes the auto-generated `secret_shared_key` HMAC signing secret (also readable later via `GET /v3/webhook/destinations/{destination_uuid}/`). Always send `subscribed_events` with at least one event (no wildcards): an explicit empty list is rejected with 400, and omitting the field creates a destination subscribed to nothing, which never receives a webhook. The `(application, url)` pair must be unique among non-deleted destinations. + Create a webhook destination. The response includes the auto-generated `secret_shared_key` HMAC signing secret (also readable later via `GET /v3/webhook/destinations/{destination_uuid}/`). Always send `subscribed_events` with at least one event (no wildcards): an explicit empty list is rejected with 400, and omitting the field creates a destination subscribed to nothing, which never receives a webhook. The `(application, url)` pair must be unique among the application's destinations. ``POST /v3/webhook/destinations/`` Args: label: Human-readable name displayed in Console → Webhooks. Helps distinguish destinations when you have several (e.g. "Prod", "Staging mirror"). - url: HTTPS URL that will receive `POST` requests. Must be unique per application among non-deleted destinations. Use a stable, hardened endpoint - Didit retries failed deliveries on a backoff schedule. + url: HTTPS URL that will receive `POST` requests. Must be unique among the application's destinations. Use a stable, hardened endpoint: Didit retries failed deliveries on a backoff schedule. enabled: When `false`, the destination is saved but Didit will not push any webhooks to it. Useful for staging configs you want to keep parked. webhook_version: Payload schema version Didit will send. `v3` is the current/recommended format; older versions are kept for backward compatibility with legacy integrations. subscribed_events: Events to deliver. Effectively required: an explicit `[]` is rejected with 400, and omitting the field creates a destination with no subscriptions that receives nothing. @@ -145,7 +157,7 @@ def retrieve( ) -> WebhooksDestinationsRetrieveResponse: """Get webhook destination. - Retrieve one webhook destination with its `secret_shared_key` signing secret and delivery health summary. API-key callers always receive the secret in full; Console users without `read:webhooks` see an empty string. + Retrieve one webhook destination with its `secret_shared_key` signing secret, its health (`health_status`, `failing_streak`, `failing_since`, `last_success_at`) and a delivery `summary` for the last 30 days. API-key callers always receive the secret in full; Console users without `read:webhooks` see an empty string. ``GET /v3/webhook/destinations/{destination_uuid}/`` @@ -182,7 +194,7 @@ def delete( ) -> None: """Delete webhook destination. - Delete a webhook destination. Future events stop being pushed to its URL. Past delivery records are not affected and stay queryable from the Business Console, so deleting a destination is not a way to remove your delivery history. Calling again returns 404. + Permanently delete a webhook destination, its signing secret and its entire delivery log. This cannot be undone. No new webhook is sent to the destination's URL; other destinations subscribed to the same events keep receiving them. Afterwards the destination no longer appears in `GET /v3/webhook/destinations/`, and every route under `/v3/webhook/destinations/{destination_uuid}/`, including its deliveries, a delivery's detail and its resend, answers 404. The application's legacy `webhook_url` is set to the URL of the oldest remaining enabled destination, or cleared when no enabled destination is left. Calling DELETE again returns 404. ``DELETE /v3/webhook/destinations/{destination_uuid}/`` @@ -224,7 +236,7 @@ def update( ) -> WebhooksDestinationsUpdateResponse: """Update webhook destination. - Update a webhook destination. **`subscribed_events` is REPLACED wholesale, not merged** - fetch first, append, resend. Secret is not rotated. + Update a webhook destination. **`subscribed_events` is REPLACED wholesale, not merged**: fetch first, append, resend. Secret is not rotated. A `uuid` in the body is ignored: the path names the destination. ``PATCH /v3/webhook/destinations/{destination_uuid}/`` @@ -262,11 +274,183 @@ def update( ), ) + def retrieve_analytics( + self, + destination_uuid: str, + *, + date_from: str | _dt.date | NotGiven = NOT_GIVEN, + date_to: str | _dt.date | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WebhooksDestinationsRetrieveAnalyticsResponse: + """Get webhook destination analytics. + + Delivery totals, failure rate and response times for one webhook destination, over the whole range (`summary`) and per UTC day (`points`). Every attempt counts as a delivery: the first send of an event, each automatic [retry](https://docs.didit.me/integration/webhooks#retry-policy) and each resend. A delivery is failed when its recorded HTTP status is 400 or above, so timeouts (recorded as `504`) and connection failures (`503`) count as failed; a delivery still queued counts in `total_deliveries` only. Delivery records are available for about 30 days. A scoped API key needs `webhooks: read`. + + ``GET /v3/webhook/destinations/{destination_uuid}/analytics/`` + + Args: + destination_uuid: UUID of the destination (the `uuid` returned by `GET /v3/webhook/destinations/`). + date_from: First UTC day to include, as `YYYY-MM-DD` (inclusive). Omit to start from the oldest delivery available. `dateFrom` is accepted as an alias. + date_to: Last UTC day to include, as `YYYY-MM-DD` (inclusive). Must not be earlier than `date_from`. `dateTo` is accepted as an alias. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WebhooksDestinationsRetrieveAnalyticsResponse", + self._client.request( + "get", + "/v3/webhook/destinations/{destination_uuid}/analytics/", + cast_to=WebhooksDestinationsRetrieveAnalyticsResponse, + path_params={"destination_uuid": destination_uuid}, + auth="api_key", + query={"date_from": date_from, "date_to": date_to}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_deliveries( + self, + destination_uuid: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + date_from: str | _dt.date | NotGiven = NOT_GIVEN, + date_to: str | _dt.date | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[WebhookDestinationDelivery]: + """List webhook destination deliveries. + + Page through every delivery attempt to one webhook destination, newest first. The first send of each event, each automatic [retry](https://docs.didit.me/integration/webhooks#retry-policy) and each resend are separate entries. Entries carry the HTTP status and response time but not the payload: fetch one delivery to read its headers and body. `count` is exact up to 100 and reads 100 above that, so follow `next` until it is `null`. Delivery records are available for about 30 days. A scoped API key needs `webhooks: read` and `decisions: read`; a key limited to some workflows or to approved sessions cannot use this endpoint. + + ``GET /v3/webhook/destinations/{destination_uuid}/deliveries/`` + + Args: + destination_uuid: UUID of the destination (the `uuid` returned by `GET /v3/webhook/destinations/`). + limit: Maximum number of deliveries per page. Defaults to 50. A value that is not a positive integer falls back to 50; a value above 1000000 returns `400`. + offset: Number of deliveries to skip. Defaults to 0. Prefer following the `next` and `previous` URLs from the response. + date_from: First UTC day to include, as `YYYY-MM-DD` (inclusive). Omit to start from the oldest delivery available. `dateFrom` is accepted as an alias. + date_to: Last UTC day to include, as `YYYY-MM-DD` (inclusive). Must not be earlier than `date_from`. `dateTo` is accepted as an alias. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[WebhookDestinationDelivery]", + self._client.request_page( + "get", + "/v3/webhook/destinations/{destination_uuid}/deliveries/", + cast_to=WebhooksDestinationsListDeliveriesResponse, + item_type=WebhookDestinationDelivery, + path_params={"destination_uuid": destination_uuid}, + auth="api_key", + query={ + "limit": limit, + "offset": offset, + "date_from": date_from, + "date_to": date_to, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_delivery( + self, + destination_uuid: str, + delivery_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WebhookDestinationDeliveryDetail: + """Get webhook destination delivery. + + Retrieve one delivery attempt to a webhook destination with the exact request headers and JSON body Didit sent, the HTTP status your endpoint returned and the response time. `request_header` and `request_body` are JSON text: parse them to read the signature headers and the payload, for example to check your [signature verification](https://docs.didit.me/integration/webhooks#signature-verification) against the destination's `secret_shared_key`. A delivery never contains the signing secret, and Didit does not record your endpoint's response body. A scoped API key needs `webhooks: read` and `decisions: read`; a key limited to some workflows or to approved sessions cannot use this endpoint. + + ``GET /v3/webhook/destinations/{destination_uuid}/deliveries/{delivery_uuid}/`` + + Args: + destination_uuid: UUID of the destination the delivery was sent to. + delivery_uuid: UUID of the delivery: the `uuid` of an entry returned by `GET /v3/webhook/destinations/{destination_uuid}/deliveries/`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WebhookDestinationDeliveryDetail", + self._client.request( + "get", + "/v3/webhook/destinations/{destination_uuid}/deliveries/{delivery_uuid}/", + cast_to=WebhookDestinationDeliveryDetail, + path_params={"destination_uuid": destination_uuid, "delivery_uuid": delivery_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def resend_delivery( + self, + destination_uuid: str, + delivery_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WebhookDestinationDeliveryDetail: + """Resend webhook destination delivery. + + Send a past delivery again, for example after your endpoint was down. Didit takes the stored payload, sets a fresh `timestamp`, signs it again with the destination's current `secret_shared_key` (`X-Signature`, `X-Signature-V2`, `X-Signature-Simple` and `X-Timestamp`) and posts it to the delivery's `request_url`, even if the destination's URL has changed since or the destination is disabled. `event_id` and the rest of the payload are unchanged, so deduplicate on `event_id`. The call waits up to 5 seconds for your endpoint and returns the new delivery with `200` whatever your endpoint answered: read `response_status`. A resend is not retried automatically, every call sends again and creates a new delivery, and the request has no body. A scoped API key needs `webhooks: write` and `decisions: read`; a key limited to some workflows or to approved sessions cannot use this endpoint. + + ``POST /v3/webhook/destinations/{destination_uuid}/deliveries/{delivery_uuid}/resend/`` + + Args: + destination_uuid: UUID of the destination the delivery was sent to. + delivery_uuid: UUID of the delivery to resend: the `uuid` of an entry returned by `GET /v3/webhook/destinations/{destination_uuid}/deliveries/`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WebhookDestinationDeliveryDetail", + self._client.request( + "post", + "/v3/webhook/destinations/{destination_uuid}/deliveries/{delivery_uuid}/resend/", + cast_to=WebhookDestinationDeliveryDetail, + path_params={"destination_uuid": destination_uuid, "delivery_uuid": delivery_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + class AsyncWebhooksDestinations(AsyncAPIResource): """The ``webhooks.destinations`` resource of the Didit API. - Methods: ``create``, ``delete``, ``list``, ``retrieve``, ``update``. + Methods: ``create``, ``delete``, ``list``, ``list_deliveries``, ``resend_delivery``, ``retrieve``, ``retrieve_analytics``, ``retrieve_delivery``, ``update``. """ @cached_property @@ -289,7 +473,7 @@ async def list( ) -> builtins.list[WebhooksDestinationsListResponseItem]: """List webhook destinations. - List all webhook destinations (unpaginated array, oldest first). Each row includes URL, subscribed events, and delivery health metrics. The signing secret is NOT included in the list - fetch a single destination (`GET /v3/webhook/destinations/{destination_uuid}/`) to read `secret_shared_key`. + List all webhook destinations (unpaginated array, oldest first). Each item carries the destination's configuration, its delivery metrics for the last 30 days (`metrics_window_days`) with one `daily_points` entry per day, and its health (`health_status`, `failing_streak`, `failing_since`, `last_success_at`). The list has no `summary` object and never includes the signing secret: each item shows only `secret_last4`. Fetch a single destination (`GET /v3/webhook/destinations/{destination_uuid}/`) to read `secret_shared_key`. ``GET /v3/webhook/destinations/`` @@ -328,13 +512,13 @@ async def create( ) -> WebhooksDestinationsCreateResponse: """Create webhook destination. - Create a webhook destination. The response includes the auto-generated `secret_shared_key` HMAC signing secret (also readable later via `GET /v3/webhook/destinations/{destination_uuid}/`). Always send `subscribed_events` with at least one event (no wildcards): an explicit empty list is rejected with 400, and omitting the field creates a destination subscribed to nothing, which never receives a webhook. The `(application, url)` pair must be unique among non-deleted destinations. + Create a webhook destination. The response includes the auto-generated `secret_shared_key` HMAC signing secret (also readable later via `GET /v3/webhook/destinations/{destination_uuid}/`). Always send `subscribed_events` with at least one event (no wildcards): an explicit empty list is rejected with 400, and omitting the field creates a destination subscribed to nothing, which never receives a webhook. The `(application, url)` pair must be unique among the application's destinations. ``POST /v3/webhook/destinations/`` Args: label: Human-readable name displayed in Console → Webhooks. Helps distinguish destinations when you have several (e.g. "Prod", "Staging mirror"). - url: HTTPS URL that will receive `POST` requests. Must be unique per application among non-deleted destinations. Use a stable, hardened endpoint - Didit retries failed deliveries on a backoff schedule. + url: HTTPS URL that will receive `POST` requests. Must be unique among the application's destinations. Use a stable, hardened endpoint: Didit retries failed deliveries on a backoff schedule. enabled: When `false`, the destination is saved but Didit will not push any webhooks to it. Useful for staging configs you want to keep parked. webhook_version: Payload schema version Didit will send. `v3` is the current/recommended format; older versions are kept for backward compatibility with legacy integrations. subscribed_events: Events to deliver. Effectively required: an explicit `[]` is rejected with 400, and omitting the field creates a destination with no subscriptions that receives nothing. @@ -375,7 +559,7 @@ async def retrieve( ) -> WebhooksDestinationsRetrieveResponse: """Get webhook destination. - Retrieve one webhook destination with its `secret_shared_key` signing secret and delivery health summary. API-key callers always receive the secret in full; Console users without `read:webhooks` see an empty string. + Retrieve one webhook destination with its `secret_shared_key` signing secret, its health (`health_status`, `failing_streak`, `failing_since`, `last_success_at`) and a delivery `summary` for the last 30 days. API-key callers always receive the secret in full; Console users without `read:webhooks` see an empty string. ``GET /v3/webhook/destinations/{destination_uuid}/`` @@ -412,7 +596,7 @@ async def delete( ) -> None: """Delete webhook destination. - Delete a webhook destination. Future events stop being pushed to its URL. Past delivery records are not affected and stay queryable from the Business Console, so deleting a destination is not a way to remove your delivery history. Calling again returns 404. + Permanently delete a webhook destination, its signing secret and its entire delivery log. This cannot be undone. No new webhook is sent to the destination's URL; other destinations subscribed to the same events keep receiving them. Afterwards the destination no longer appears in `GET /v3/webhook/destinations/`, and every route under `/v3/webhook/destinations/{destination_uuid}/`, including its deliveries, a delivery's detail and its resend, answers 404. The application's legacy `webhook_url` is set to the URL of the oldest remaining enabled destination, or cleared when no enabled destination is left. Calling DELETE again returns 404. ``DELETE /v3/webhook/destinations/{destination_uuid}/`` @@ -454,7 +638,7 @@ async def update( ) -> WebhooksDestinationsUpdateResponse: """Update webhook destination. - Update a webhook destination. **`subscribed_events` is REPLACED wholesale, not merged** - fetch first, append, resend. Secret is not rotated. + Update a webhook destination. **`subscribed_events` is REPLACED wholesale, not merged**: fetch first, append, resend. Secret is not rotated. A `uuid` in the body is ignored: the path names the destination. ``PATCH /v3/webhook/destinations/{destination_uuid}/`` @@ -492,6 +676,178 @@ async def update( ), ) + async def retrieve_analytics( + self, + destination_uuid: str, + *, + date_from: str | _dt.date | NotGiven = NOT_GIVEN, + date_to: str | _dt.date | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WebhooksDestinationsRetrieveAnalyticsResponse: + """Get webhook destination analytics. + + Delivery totals, failure rate and response times for one webhook destination, over the whole range (`summary`) and per UTC day (`points`). Every attempt counts as a delivery: the first send of an event, each automatic [retry](https://docs.didit.me/integration/webhooks#retry-policy) and each resend. A delivery is failed when its recorded HTTP status is 400 or above, so timeouts (recorded as `504`) and connection failures (`503`) count as failed; a delivery still queued counts in `total_deliveries` only. Delivery records are available for about 30 days. A scoped API key needs `webhooks: read`. + + ``GET /v3/webhook/destinations/{destination_uuid}/analytics/`` + + Args: + destination_uuid: UUID of the destination (the `uuid` returned by `GET /v3/webhook/destinations/`). + date_from: First UTC day to include, as `YYYY-MM-DD` (inclusive). Omit to start from the oldest delivery available. `dateFrom` is accepted as an alias. + date_to: Last UTC day to include, as `YYYY-MM-DD` (inclusive). Must not be earlier than `date_from`. `dateTo` is accepted as an alias. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WebhooksDestinationsRetrieveAnalyticsResponse", + await self._client.request( + "get", + "/v3/webhook/destinations/{destination_uuid}/analytics/", + cast_to=WebhooksDestinationsRetrieveAnalyticsResponse, + path_params={"destination_uuid": destination_uuid}, + auth="api_key", + query={"date_from": date_from, "date_to": date_to}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_deliveries( + self, + destination_uuid: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + date_from: str | _dt.date | NotGiven = NOT_GIVEN, + date_to: str | _dt.date | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[WebhookDestinationDelivery]: + """List webhook destination deliveries. + + Page through every delivery attempt to one webhook destination, newest first. The first send of each event, each automatic [retry](https://docs.didit.me/integration/webhooks#retry-policy) and each resend are separate entries. Entries carry the HTTP status and response time but not the payload: fetch one delivery to read its headers and body. `count` is exact up to 100 and reads 100 above that, so follow `next` until it is `null`. Delivery records are available for about 30 days. A scoped API key needs `webhooks: read` and `decisions: read`; a key limited to some workflows or to approved sessions cannot use this endpoint. + + ``GET /v3/webhook/destinations/{destination_uuid}/deliveries/`` + + Args: + destination_uuid: UUID of the destination (the `uuid` returned by `GET /v3/webhook/destinations/`). + limit: Maximum number of deliveries per page. Defaults to 50. A value that is not a positive integer falls back to 50; a value above 1000000 returns `400`. + offset: Number of deliveries to skip. Defaults to 0. Prefer following the `next` and `previous` URLs from the response. + date_from: First UTC day to include, as `YYYY-MM-DD` (inclusive). Omit to start from the oldest delivery available. `dateFrom` is accepted as an alias. + date_to: Last UTC day to include, as `YYYY-MM-DD` (inclusive). Must not be earlier than `date_from`. `dateTo` is accepted as an alias. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[WebhookDestinationDelivery]", + self._client.request_page( + "get", + "/v3/webhook/destinations/{destination_uuid}/deliveries/", + cast_to=WebhooksDestinationsListDeliveriesResponse, + item_type=WebhookDestinationDelivery, + path_params={"destination_uuid": destination_uuid}, + auth="api_key", + query={ + "limit": limit, + "offset": offset, + "date_from": date_from, + "date_to": date_to, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_delivery( + self, + destination_uuid: str, + delivery_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WebhookDestinationDeliveryDetail: + """Get webhook destination delivery. + + Retrieve one delivery attempt to a webhook destination with the exact request headers and JSON body Didit sent, the HTTP status your endpoint returned and the response time. `request_header` and `request_body` are JSON text: parse them to read the signature headers and the payload, for example to check your [signature verification](https://docs.didit.me/integration/webhooks#signature-verification) against the destination's `secret_shared_key`. A delivery never contains the signing secret, and Didit does not record your endpoint's response body. A scoped API key needs `webhooks: read` and `decisions: read`; a key limited to some workflows or to approved sessions cannot use this endpoint. + + ``GET /v3/webhook/destinations/{destination_uuid}/deliveries/{delivery_uuid}/`` + + Args: + destination_uuid: UUID of the destination the delivery was sent to. + delivery_uuid: UUID of the delivery: the `uuid` of an entry returned by `GET /v3/webhook/destinations/{destination_uuid}/deliveries/`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WebhookDestinationDeliveryDetail", + await self._client.request( + "get", + "/v3/webhook/destinations/{destination_uuid}/deliveries/{delivery_uuid}/", + cast_to=WebhookDestinationDeliveryDetail, + path_params={"destination_uuid": destination_uuid, "delivery_uuid": delivery_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def resend_delivery( + self, + destination_uuid: str, + delivery_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WebhookDestinationDeliveryDetail: + """Resend webhook destination delivery. + + Send a past delivery again, for example after your endpoint was down. Didit takes the stored payload, sets a fresh `timestamp`, signs it again with the destination's current `secret_shared_key` (`X-Signature`, `X-Signature-V2`, `X-Signature-Simple` and `X-Timestamp`) and posts it to the delivery's `request_url`, even if the destination's URL has changed since or the destination is disabled. `event_id` and the rest of the payload are unchanged, so deduplicate on `event_id`. The call waits up to 5 seconds for your endpoint and returns the new delivery with `200` whatever your endpoint answered: read `response_status`. A resend is not retried automatically, every call sends again and creates a new delivery, and the request has no body. A scoped API key needs `webhooks: write` and `decisions: read`; a key limited to some workflows or to approved sessions cannot use this endpoint. + + ``POST /v3/webhook/destinations/{destination_uuid}/deliveries/{delivery_uuid}/resend/`` + + Args: + destination_uuid: UUID of the destination the delivery was sent to. + delivery_uuid: UUID of the delivery to resend: the `uuid` of an entry returned by `GET /v3/webhook/destinations/{destination_uuid}/deliveries/`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WebhookDestinationDeliveryDetail", + await self._client.request( + "post", + "/v3/webhook/destinations/{destination_uuid}/deliveries/{delivery_uuid}/resend/", + cast_to=WebhookDestinationDeliveryDetail, + path_params={"destination_uuid": destination_uuid, "delivery_uuid": delivery_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + class WebhooksDestinationsWithRawResponse: def __init__(self, resource: WebhooksDestinations) -> None: @@ -501,6 +857,10 @@ def __init__(self, resource: WebhooksDestinations) -> None: self.retrieve = to_raw_response_wrapper(resource.retrieve) self.delete = to_raw_response_wrapper(resource.delete) self.update = to_raw_response_wrapper(resource.update) + self.retrieve_analytics = to_raw_response_wrapper(resource.retrieve_analytics) + self.list_deliveries = to_raw_response_wrapper(resource.list_deliveries) + self.retrieve_delivery = to_raw_response_wrapper(resource.retrieve_delivery) + self.resend_delivery = to_raw_response_wrapper(resource.resend_delivery) class AsyncWebhooksDestinationsWithRawResponse: @@ -511,6 +871,10 @@ def __init__(self, resource: AsyncWebhooksDestinations) -> None: self.retrieve = async_to_raw_response_wrapper(resource.retrieve) self.delete = async_to_raw_response_wrapper(resource.delete) self.update = async_to_raw_response_wrapper(resource.update) + self.retrieve_analytics = async_to_raw_response_wrapper(resource.retrieve_analytics) + self.list_deliveries = async_to_raw_response_wrapper(resource.list_deliveries) + self.retrieve_delivery = async_to_raw_response_wrapper(resource.retrieve_delivery) + self.resend_delivery = async_to_raw_response_wrapper(resource.resend_delivery) class WebhooksDestinationsWithStreamingResponse: @@ -521,6 +885,10 @@ def __init__(self, resource: WebhooksDestinations) -> None: self.retrieve = to_streamed_response_wrapper(resource.retrieve) self.delete = to_streamed_response_wrapper(resource.delete) self.update = to_streamed_response_wrapper(resource.update) + self.retrieve_analytics = to_streamed_response_wrapper(resource.retrieve_analytics) + self.list_deliveries = to_streamed_response_wrapper(resource.list_deliveries) + self.retrieve_delivery = to_streamed_response_wrapper(resource.retrieve_delivery) + self.resend_delivery = to_streamed_response_wrapper(resource.resend_delivery) class AsyncWebhooksDestinationsWithStreamingResponse: @@ -531,3 +899,7 @@ def __init__(self, resource: AsyncWebhooksDestinations) -> None: self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) self.delete = async_to_streamed_response_wrapper(resource.delete) self.update = async_to_streamed_response_wrapper(resource.update) + self.retrieve_analytics = async_to_streamed_response_wrapper(resource.retrieve_analytics) + self.list_deliveries = async_to_streamed_response_wrapper(resource.list_deliveries) + self.retrieve_delivery = async_to_streamed_response_wrapper(resource.retrieve_delivery) + self.resend_delivery = async_to_streamed_response_wrapper(resource.resend_delivery) diff --git a/src/didit/resources/workflows.py b/src/didit/resources/workflows.py index c31d2f8..be18643 100644 --- a/src/didit/resources/workflows.py +++ b/src/didit/resources/workflows.py @@ -3,7 +3,6 @@ from __future__ import annotations -import builtins from functools import cached_property from typing import Literal, Sequence, cast @@ -19,6 +18,7 @@ ) from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query from didit.types.components.simple_workflow_feature import SimpleWorkflowFeature +from didit.types.components.workflow_detail import WorkflowDetail from didit.types.components.workflow_list_item import WorkflowListItem from didit.types.workflows.list_response import WorkflowsListResponse @@ -106,7 +106,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> builtins.dict[str, object]: + ) -> WorkflowDetail: """Create workflow. Create a simple workflow. Send `features` in execution order (ordering rules apply); defaults to `published` v1. The body accepts ONLY the fields listed in `SimpleWorkflowRequest` - any other key (including `workflow_type`) is rejected with a 400. Not idempotent - each call creates a new workflow. Maximum 50 workflows per application. @@ -131,11 +131,11 @@ def create( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "builtins.dict[str, object]", + "WorkflowDetail", self._client.request( "post", "/v3/workflows/", - cast_to=builtins.dict[str, object], + cast_to=WorkflowDetail, auth="api_key", body={ "features": features, @@ -165,7 +165,7 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> object: + ) -> WorkflowDetail: """Get workflow. Fetch full configuration for one workflow version by `settings_uuid`: feature toggles, thresholds, allowed documents, graph, retry policy, callbacks, and pricing. @@ -180,11 +180,11 @@ def retrieve( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "object", + "WorkflowDetail", self._client.request( "get", "/v3/workflows/{settings_uuid}/", - cast_to=object, + cast_to=WorkflowDetail, path_params={"settings_uuid": settings_uuid}, auth="api_key", extra_headers=extra_headers, @@ -250,7 +250,7 @@ def update( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> builtins.dict[str, object]: + ) -> WorkflowDetail: """Update workflow. Update a workflow. Versioning depends on the `status` field you send and the current state of the version: (1) no `status` on a **published** version → the published version is updated in place (same `uuid`, same `version`); (2) no `status` on a **draft** → the draft is updated and immediately published; (3) `status: "published"` on a **published** version → a NEW published version is created under the same `workflow_id` (new `uuid`, `version` + 1) and returned, and any existing draft in the group is deleted; (4) `status: "draft"` keeps a draft as a draft (autosave) but is rejected with 400 on a published version (`"Cannot revert a published version to draft."`). The body accepts the same strict field whitelist as create - unknown keys are rejected with 400. @@ -276,11 +276,11 @@ def update( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "builtins.dict[str, object]", + "WorkflowDetail", self._client.request( "patch", "/v3/workflows/{settings_uuid}/", - cast_to=builtins.dict[str, object], + cast_to=WorkflowDetail, path_params={"settings_uuid": settings_uuid}, auth="api_key", body={ @@ -378,7 +378,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> builtins.dict[str, object]: + ) -> WorkflowDetail: """Create workflow. Create a simple workflow. Send `features` in execution order (ordering rules apply); defaults to `published` v1. The body accepts ONLY the fields listed in `SimpleWorkflowRequest` - any other key (including `workflow_type`) is rejected with a 400. Not idempotent - each call creates a new workflow. Maximum 50 workflows per application. @@ -403,11 +403,11 @@ async def create( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "builtins.dict[str, object]", + "WorkflowDetail", await self._client.request( "post", "/v3/workflows/", - cast_to=builtins.dict[str, object], + cast_to=WorkflowDetail, auth="api_key", body={ "features": features, @@ -437,7 +437,7 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> object: + ) -> WorkflowDetail: """Get workflow. Fetch full configuration for one workflow version by `settings_uuid`: feature toggles, thresholds, allowed documents, graph, retry policy, callbacks, and pricing. @@ -452,11 +452,11 @@ async def retrieve( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "object", + "WorkflowDetail", await self._client.request( "get", "/v3/workflows/{settings_uuid}/", - cast_to=object, + cast_to=WorkflowDetail, path_params={"settings_uuid": settings_uuid}, auth="api_key", extra_headers=extra_headers, @@ -522,7 +522,7 @@ async def update( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> builtins.dict[str, object]: + ) -> WorkflowDetail: """Update workflow. Update a workflow. Versioning depends on the `status` field you send and the current state of the version: (1) no `status` on a **published** version → the published version is updated in place (same `uuid`, same `version`); (2) no `status` on a **draft** → the draft is updated and immediately published; (3) `status: "published"` on a **published** version → a NEW published version is created under the same `workflow_id` (new `uuid`, `version` + 1) and returned, and any existing draft in the group is deleted; (4) `status: "draft"` keeps a draft as a draft (autosave) but is rejected with 400 on a published version (`"Cannot revert a published version to draft."`). The body accepts the same strict field whitelist as create - unknown keys are rejected with 400. @@ -548,11 +548,11 @@ async def update( timeout: Overrides the client's timeout for this request, in seconds. """ return cast( - "builtins.dict[str, object]", + "WorkflowDetail", await self._client.request( "patch", "/v3/workflows/{settings_uuid}/", - cast_to=builtins.dict[str, object], + cast_to=WorkflowDetail, path_params={"settings_uuid": settings_uuid}, auth="api_key", body={ diff --git a/src/didit/types/__init__.py b/src/didit/types/__init__.py index 49c3297..8711041 100644 --- a/src/didit/types/__init__.py +++ b/src/didit/types/__init__.py @@ -22,6 +22,9 @@ from didit.types.account.resend_otp_params import ( AccountResendOtpParams as AccountResendOtpParams, ) + from didit.types.account.resend_otp_response import ( + AccountResendOtpResponse as AccountResendOtpResponse, + ) from didit.types.account.verify_email_params import ( AccountVerifyEmailParams as AccountVerifyEmailParams, ) @@ -43,6 +46,9 @@ from didit.types.age_estimation.estimate_response import ( AgeEstimationEstimateResponseAgeEstimationWarnings as AgeEstimationEstimateResponseAgeEstimationWarnings, ) + from didit.types.aml.retrieve_dataset_freshness_response import ( + AmlRetrieveDatasetFreshnessResponse as AmlRetrieveDatasetFreshnessResponse, + ) from didit.types.aml.screen_params import AmlScreenParams as AmlScreenParams from didit.types.aml.screen_response import AmlScreenResponse as AmlScreenResponse from didit.types.aml.screen_response import AmlScreenResponseAml as AmlScreenResponseAml @@ -103,8 +109,12 @@ from didit.types.biometric_templates.retrieve_response import ( BiometricTemplatesRetrieveResponse as BiometricTemplatesRetrieveResponse, ) - from didit.types.business_sessions.list_networks_response import ( - BusinessSessionsListNetworksResponse as BusinessSessionsListNetworksResponse, + from didit.types.blocklist.add_params import BlocklistAddParams as BlocklistAddParams + from didit.types.blocklist.add_response import BlocklistAddResponse as BlocklistAddResponse + from didit.types.blocklist.list_response import BlocklistListResponse as BlocklistListResponse + from didit.types.blocklist.remove_params import BlocklistRemoveParams as BlocklistRemoveParams + from didit.types.blocklist.remove_response import ( + BlocklistRemoveResponse as BlocklistRemoveResponse, ) from didit.types.businesses.batch_delete_params import ( BusinessesBatchDeleteParams as BusinessesBatchDeleteParams, @@ -115,108 +125,18 @@ from didit.types.businesses.create_params import ( BusinessesCreateParams as BusinessesCreateParams, ) - from didit.types.businesses.list_networks_response import ( - BusinessesListNetworksResponse as BusinessesListNetworksResponse, - ) from didit.types.businesses.list_response import ( BusinessesListResponse as BusinessesListResponse, ) - from didit.types.businesses.retrieve_ongoing_monitoring_job_response import ( - BusinessesRetrieveOngoingMonitoringJobResponse as BusinessesRetrieveOngoingMonitoringJobResponse, - ) - from didit.types.businesses.retrieve_ongoing_monitoring_job_response import ( - BusinessesRetrieveOngoingMonitoringJobResponseEstimate as BusinessesRetrieveOngoingMonitoringJobResponseEstimate, - ) - from didit.types.businesses.retrieve_ongoing_monitoring_job_response import ( - BusinessesRetrieveOngoingMonitoringJobResponseOutcomes as BusinessesRetrieveOngoingMonitoringJobResponseOutcomes, - ) - from didit.types.businesses.retrieve_session_ongoing_monitoring_job_response import ( - BusinessesRetrieveSessionOngoingMonitoringJobResponse as BusinessesRetrieveSessionOngoingMonitoringJobResponse, - ) - from didit.types.businesses.retrieve_session_ongoing_monitoring_job_response import ( - BusinessesRetrieveSessionOngoingMonitoringJobResponseEstimate as BusinessesRetrieveSessionOngoingMonitoringJobResponseEstimate, - ) - from didit.types.businesses.retrieve_session_ongoing_monitoring_job_response import ( - BusinessesRetrieveSessionOngoingMonitoringJobResponseOutcomes as BusinessesRetrieveSessionOngoingMonitoringJobResponseOutcomes, - ) from didit.types.businesses.set_aml_monitoring_params import ( BusinessesSetAmlMonitoringParams as BusinessesSetAmlMonitoringParams, ) - from didit.types.businesses.set_aml_monitoring_response import ( - BusinessesSetAmlMonitoringResponse202 as BusinessesSetAmlMonitoringResponse202, - ) - from didit.types.businesses.set_aml_monitoring_response import ( - BusinessesSetAmlMonitoringResponse202Estimate as BusinessesSetAmlMonitoringResponse202Estimate, - ) - from didit.types.businesses.set_aml_monitoring_response import ( - BusinessesSetAmlMonitoringResponse202Outcomes as BusinessesSetAmlMonitoringResponse202Outcomes, - ) - from didit.types.businesses.set_aml_monitoring_response import ( - BusinessesSetAmlMonitoringResponse202Results as BusinessesSetAmlMonitoringResponse202Results, - ) - from didit.types.businesses.set_aml_monitoring_response import ( - BusinessesSetAmlMonitoringResponseVariant1 as BusinessesSetAmlMonitoringResponseVariant1, - ) - from didit.types.businesses.set_aml_monitoring_response import ( - BusinessesSetAmlMonitoringResponseVariant2 as BusinessesSetAmlMonitoringResponseVariant2, - ) - from didit.types.businesses.set_aml_monitoring_response import ( - BusinessesSetAmlMonitoringResponseVariant2Estimate as BusinessesSetAmlMonitoringResponseVariant2Estimate, - ) - from didit.types.businesses.set_aml_monitoring_response import ( - BusinessesSetAmlMonitoringResponseVariant2Outcomes as BusinessesSetAmlMonitoringResponseVariant2Outcomes, - ) - from didit.types.businesses.set_aml_monitoring_response import ( - BusinessesSetAmlMonitoringResponseVariant2Results as BusinessesSetAmlMonitoringResponseVariant2Results, - ) from didit.types.businesses.set_ongoing_monitoring_params import ( BusinessesSetOngoingMonitoringParams as BusinessesSetOngoingMonitoringParams, ) - from didit.types.businesses.set_ongoing_monitoring_response import ( - BusinessesSetOngoingMonitoringResponse202 as BusinessesSetOngoingMonitoringResponse202, - ) - from didit.types.businesses.set_ongoing_monitoring_response import ( - BusinessesSetOngoingMonitoringResponse202Estimate as BusinessesSetOngoingMonitoringResponse202Estimate, - ) - from didit.types.businesses.set_ongoing_monitoring_response import ( - BusinessesSetOngoingMonitoringResponse202Outcomes as BusinessesSetOngoingMonitoringResponse202Outcomes, - ) - from didit.types.businesses.set_ongoing_monitoring_response import ( - BusinessesSetOngoingMonitoringResponseVariant1 as BusinessesSetOngoingMonitoringResponseVariant1, - ) - from didit.types.businesses.set_ongoing_monitoring_response import ( - BusinessesSetOngoingMonitoringResponseVariant2 as BusinessesSetOngoingMonitoringResponseVariant2, - ) - from didit.types.businesses.set_ongoing_monitoring_response import ( - BusinessesSetOngoingMonitoringResponseVariant2Estimate as BusinessesSetOngoingMonitoringResponseVariant2Estimate, - ) - from didit.types.businesses.set_ongoing_monitoring_response import ( - BusinessesSetOngoingMonitoringResponseVariant2Outcomes as BusinessesSetOngoingMonitoringResponseVariant2Outcomes, - ) from didit.types.businesses.set_session_ongoing_monitoring_params import ( BusinessesSetSessionOngoingMonitoringParams as BusinessesSetSessionOngoingMonitoringParams, ) - from didit.types.businesses.set_session_ongoing_monitoring_response import ( - BusinessesSetSessionOngoingMonitoringResponse202 as BusinessesSetSessionOngoingMonitoringResponse202, - ) - from didit.types.businesses.set_session_ongoing_monitoring_response import ( - BusinessesSetSessionOngoingMonitoringResponse202Estimate as BusinessesSetSessionOngoingMonitoringResponse202Estimate, - ) - from didit.types.businesses.set_session_ongoing_monitoring_response import ( - BusinessesSetSessionOngoingMonitoringResponse202Outcomes as BusinessesSetSessionOngoingMonitoringResponse202Outcomes, - ) - from didit.types.businesses.set_session_ongoing_monitoring_response import ( - BusinessesSetSessionOngoingMonitoringResponseVariant1 as BusinessesSetSessionOngoingMonitoringResponseVariant1, - ) - from didit.types.businesses.set_session_ongoing_monitoring_response import ( - BusinessesSetSessionOngoingMonitoringResponseVariant2 as BusinessesSetSessionOngoingMonitoringResponseVariant2, - ) - from didit.types.businesses.set_session_ongoing_monitoring_response import ( - BusinessesSetSessionOngoingMonitoringResponseVariant2Estimate as BusinessesSetSessionOngoingMonitoringResponseVariant2Estimate, - ) - from didit.types.businesses.set_session_ongoing_monitoring_response import ( - BusinessesSetSessionOngoingMonitoringResponseVariant2Outcomes as BusinessesSetSessionOngoingMonitoringResponseVariant2Outcomes, - ) from didit.types.businesses.update_params import ( BusinessesUpdateParams as BusinessesUpdateParams, ) @@ -236,9 +156,6 @@ from didit.types.cases.blueprints.install_presets_params import ( CasesBlueprintsInstallPresetsParams as CasesBlueprintsInstallPresetsParams, ) - from didit.types.cases.blueprints.install_presets_response import ( - CasesBlueprintsInstallPresetsResponse as CasesBlueprintsInstallPresetsResponse, - ) from didit.types.cases.blueprints.list_presets_response import ( CasesBlueprintsListPresetsResponse as CasesBlueprintsListPresetsResponse, ) @@ -258,21 +175,9 @@ CasesBlueprintsUpdateParams as CasesBlueprintsUpdateParams, ) from didit.types.cases.bulk_assign_params import CasesBulkAssignParams as CasesBulkAssignParams - from didit.types.cases.bulk_assign_response import ( - CasesBulkAssignResponse as CasesBulkAssignResponse, - ) - from didit.types.cases.bulk_assign_response import ( - CasesBulkAssignResponseFailures as CasesBulkAssignResponseFailures, - ) from didit.types.cases.bulk_update_status_params import ( CasesBulkUpdateStatusParams as CasesBulkUpdateStatusParams, ) - from didit.types.cases.bulk_update_status_response import ( - CasesBulkUpdateStatusResponse as CasesBulkUpdateStatusResponse, - ) - from didit.types.cases.bulk_update_status_response import ( - CasesBulkUpdateStatusResponseFailures as CasesBulkUpdateStatusResponseFailures, - ) from didit.types.cases.checklist.toggle_item_params import ( CasesChecklistToggleItemParams as CasesChecklistToggleItemParams, ) @@ -316,9 +221,6 @@ from didit.types.cases.regulatory_reports.create_params import ( CasesRegulatoryReportsCreateParamsNarrative as CasesRegulatoryReportsCreateParamsNarrative, ) - from didit.types.cases.regulatory_reports.list_response import ( - CasesRegulatoryReportsListResponse as CasesRegulatoryReportsListResponse, - ) from didit.types.cases.reject_resolution_params import ( CasesRejectResolutionParams as CasesRejectResolutionParams, ) @@ -328,6 +230,9 @@ from didit.types.cases.requests.preview_params import ( CasesRequestsPreviewParams as CasesRequestsPreviewParams, ) + from didit.types.cases.requests.remind_response import ( + CasesRequestsRemindResponse as CasesRequestsRemindResponse, + ) from didit.types.cases.resolve_params import CasesResolveParams as CasesResolveParams from didit.types.cases.retrieve_statistics_response import ( CasesRetrieveStatisticsResponse as CasesRetrieveStatisticsResponse, @@ -388,10 +293,200 @@ ) from didit.types.cases.transfer_params import CasesTransferParams as CasesTransferParams from didit.types.cases.update_params import CasesUpdateParams as CasesUpdateParams + from didit.types.components.aml_dataset_freshness import ( + AMLDatasetFreshness as AMLDatasetFreshness, + ) + from didit.types.components.aml_dataset_freshness import ( + AMLDatasetFreshnessSources as AMLDatasetFreshnessSources, + ) + from didit.types.components.aml_dataset_source_freshness import ( + AMLDatasetSourceFreshness as AMLDatasetSourceFreshness, + ) + from didit.types.components.aml_monitoring_job import AMLMonitoringJob as AMLMonitoringJob + from didit.types.components.aml_monitoring_job import ( + AMLMonitoringJobEstimate as AMLMonitoringJobEstimate, + ) + from didit.types.components.aml_monitoring_job import ( + AMLMonitoringJobOutcomes as AMLMonitoringJobOutcomes, + ) + from didit.types.components.aml_monitoring_job import ( + AMLMonitoringJobResults as AMLMonitoringJobResults, + ) + from didit.types.components.aml_monitoring_toggle_result import ( + AMLMonitoringToggleResult as AMLMonitoringToggleResult, + ) + from didit.types.components.aml_monitoring_toggle_result import ( + AMLMonitoringToggleResultVariant1 as AMLMonitoringToggleResultVariant1, + ) + from didit.types.components.aml_monitoring_toggle_result import ( + AMLMonitoringToggleResultVariant2 as AMLMonitoringToggleResultVariant2, + ) + from didit.types.components.aml_monitoring_toggle_result import ( + AMLMonitoringToggleResultVariant2Estimate as AMLMonitoringToggleResultVariant2Estimate, + ) + from didit.types.components.aml_monitoring_toggle_result import ( + AMLMonitoringToggleResultVariant2Outcomes as AMLMonitoringToggleResultVariant2Outcomes, + ) + from didit.types.components.aml_monitoring_toggle_result import ( + AMLMonitoringToggleResultVariant2Results as AMLMonitoringToggleResultVariant2Results, + ) + from didit.types.components.aml_screening import AMLScreening as AMLScreening + from didit.types.components.aml_screening import AMLScreeningHits as AMLScreeningHits + from didit.types.components.aml_screening import ( + AMLScreeningHitsAdverseMediaDetails as AMLScreeningHitsAdverseMediaDetails, + ) + from didit.types.components.aml_screening import ( + AMLScreeningHitsAdverseMediaMatches as AMLScreeningHitsAdverseMediaMatches, + ) + from didit.types.components.aml_screening import ( + AMLScreeningHitsLinkedEntities as AMLScreeningHitsLinkedEntities, + ) + from didit.types.components.aml_screening import ( + AMLScreeningHitsPepMatches as AMLScreeningHitsPepMatches, + ) + from didit.types.components.aml_screening import ( + AMLScreeningHitsProperties as AMLScreeningHitsProperties, + ) + from didit.types.components.aml_screening import ( + AMLScreeningHitsRiskView as AMLScreeningHitsRiskView, + ) + from didit.types.components.aml_screening import ( + AMLScreeningHitsRiskViewCategories as AMLScreeningHitsRiskViewCategories, + ) + from didit.types.components.aml_screening import ( + AMLScreeningHitsRiskViewCountries as AMLScreeningHitsRiskViewCountries, + ) + from didit.types.components.aml_screening import ( + AMLScreeningHitsRiskViewCrimes as AMLScreeningHitsRiskViewCrimes, + ) + from didit.types.components.aml_screening import ( + AMLScreeningHitsSanctionMatches as AMLScreeningHitsSanctionMatches, + ) + from didit.types.components.aml_screening import ( + AMLScreeningHitsScoreBreakdown as AMLScreeningHitsScoreBreakdown, + ) + from didit.types.components.aml_screening import ( + AMLScreeningHitsWarningMatches as AMLScreeningHitsWarningMatches, + ) + from didit.types.components.aml_screening import ( + AMLScreeningHitsWarningMatchesAdditionalData as AMLScreeningHitsWarningMatchesAdditionalData, + ) + from didit.types.components.aml_screening import ( + AMLScreeningHitsWarningMatchesAdditionalDataData as AMLScreeningHitsWarningMatchesAdditionalDataData, + ) + from didit.types.components.aml_screening import ( + AMLScreeningHitsWarningMatchesAdditionalDataDataCaseDetails as AMLScreeningHitsWarningMatchesAdditionalDataDataCaseDetails, + ) + from didit.types.components.aml_screening import ( + AMLScreeningScreenedData as AMLScreeningScreenedData, + ) + from didit.types.components.aml_screening import AMLScreeningWarnings as AMLScreeningWarnings + from didit.types.components.aml_screening_run import AMLScreeningRun as AMLScreeningRun from didit.types.components.application import Application as Application + from didit.types.components.application_analytics_country_count import ( + ApplicationAnalyticsCountryCount as ApplicationAnalyticsCountryCount, + ) + from didit.types.components.application_analytics_data_point import ( + ApplicationAnalyticsDataPoint as ApplicationAnalyticsDataPoint, + ) + from didit.types.components.application_analytics_demographic_point import ( + ApplicationAnalyticsDemographicPoint as ApplicationAnalyticsDemographicPoint, + ) + from didit.types.components.application_analytics_demographic_stats import ( + ApplicationAnalyticsDemographicStats as ApplicationAnalyticsDemographicStats, + ) + from didit.types.components.application_analytics_demographic_stats import ( + ApplicationAnalyticsDemographicStatsAgeDistribution as ApplicationAnalyticsDemographicStatsAgeDistribution, + ) + from didit.types.components.application_analytics_device_details import ( + ApplicationAnalyticsDeviceDetails as ApplicationAnalyticsDeviceDetails, + ) + from didit.types.components.application_analytics_device_details import ( + ApplicationAnalyticsDeviceDetailsBrowsers as ApplicationAnalyticsDeviceDetailsBrowsers, + ) + from didit.types.components.application_analytics_device_details import ( + ApplicationAnalyticsDeviceDetailsDeviceModels as ApplicationAnalyticsDeviceDetailsDeviceModels, + ) + from didit.types.components.application_analytics_device_details import ( + ApplicationAnalyticsDeviceDetailsOperatingSystems as ApplicationAnalyticsDeviceDetailsOperatingSystems, + ) + from didit.types.components.application_analytics_device_point import ( + ApplicationAnalyticsDevicePoint as ApplicationAnalyticsDevicePoint, + ) + from didit.types.components.application_analytics_location_stats import ( + ApplicationAnalyticsLocationStats as ApplicationAnalyticsLocationStats, + ) + from didit.types.components.application_analytics_location_stats import ( + ApplicationAnalyticsLocationStatsCities as ApplicationAnalyticsLocationStatsCities, + ) + from didit.types.components.application_analytics_location_stats import ( + ApplicationAnalyticsLocationStatsConversionByCountry as ApplicationAnalyticsLocationStatsConversionByCountry, + ) + from didit.types.components.application_analytics_original_status_point import ( + ApplicationAnalyticsOriginalStatusPoint as ApplicationAnalyticsOriginalStatusPoint, + ) + from didit.types.components.application_analytics_original_status_stats import ( + ApplicationAnalyticsOriginalStatusStats as ApplicationAnalyticsOriginalStatusStats, + ) + from didit.types.components.application_analytics_request_breakdown import ( + ApplicationAnalyticsRequestBreakdown as ApplicationAnalyticsRequestBreakdown, + ) + from didit.types.components.application_analytics_resubmission_point import ( + ApplicationAnalyticsResubmissionPoint as ApplicationAnalyticsResubmissionPoint, + ) + from didit.types.components.application_analytics_resubmission_stats import ( + ApplicationAnalyticsResubmissionStats as ApplicationAnalyticsResubmissionStats, + ) + from didit.types.components.application_analytics_summary import ( + ApplicationAnalyticsSummary as ApplicationAnalyticsSummary, + ) + from didit.types.components.application_analytics_verification_time_point import ( + ApplicationAnalyticsVerificationTimePoint as ApplicationAnalyticsVerificationTimePoint, + ) + from didit.types.components.application_analytics_verification_time_stats import ( + ApplicationAnalyticsVerificationTimeStats as ApplicationAnalyticsVerificationTimeStats, + ) + from didit.types.components.application_analytics_verification_time_stats import ( + ApplicationAnalyticsVerificationTimeStatsPerFeatureValue as ApplicationAnalyticsVerificationTimeStatsPerFeatureValue, + ) + from didit.types.components.application_analytics_verification_time_stats import ( + ApplicationAnalyticsVerificationTimeStatsPerSubStepValueValue as ApplicationAnalyticsVerificationTimeStatsPerSubStepValueValue, + ) + from didit.types.components.application_analytics_warning_stats import ( + ApplicationAnalyticsWarningStats as ApplicationAnalyticsWarningStats, + ) + from didit.types.components.application_analytics_warning_stats import ( + ApplicationAnalyticsWarningStatsValue as ApplicationAnalyticsWarningStatsValue, + ) + from didit.types.components.application_analytics_warning_stats import ( + ApplicationAnalyticsWarningStatsValueRisksValue as ApplicationAnalyticsWarningStatsValueRisksValue, + ) + from didit.types.components.application_analytics_widget import ( + ApplicationAnalyticsWidget as ApplicationAnalyticsWidget, + ) + from didit.types.components.application_analytics_widget import ( + ApplicationAnalyticsWidgetSearch as ApplicationAnalyticsWidgetSearch, + ) + from didit.types.components.application_analytics_widget import ( + ApplicationAnalyticsWidgetThresholds as ApplicationAnalyticsWidgetThresholds, + ) + from didit.types.components.application_analytics_widget_response import ( + ApplicationAnalyticsWidgetResponse as ApplicationAnalyticsWidgetResponse, + ) + from didit.types.components.application_analytics_workflow_funnel import ( + ApplicationAnalyticsWorkflowFunnel as ApplicationAnalyticsWorkflowFunnel, + ) + from didit.types.components.application_analytics_workflow_funnel import ( + ApplicationAnalyticsWorkflowFunnelValue as ApplicationAnalyticsWorkflowFunnelValue, + ) + from didit.types.components.application_analytics_workflow_funnel import ( + ApplicationAnalyticsWorkflowFunnelValueStepsValue as ApplicationAnalyticsWorkflowFunnelValueStepsValue, + ) from didit.types.components.application_credentials_summary import ( ApplicationCredentialsSummary as ApplicationCredentialsSummary, ) + from didit.types.components.application_product import ApplicationProduct as ApplicationProduct + from didit.types.components.blocklisted_item import BlocklistedItem as BlocklistedItem from didit.types.components.branding_customization import ( BrandingCustomization as BrandingCustomization, ) @@ -403,6 +498,9 @@ from didit.types.components.business_list_item import ( BusinessListItemTags as BusinessListItemTags, ) + from didit.types.components.business_session_list_item import ( + BusinessSessionListItem as BusinessSessionListItem, + ) from didit.types.components.case_action_action_enum import ( CaseActionActionEnum as CaseActionActionEnum, ) @@ -458,9 +556,19 @@ from didit.types.components.case_blueprint_list_item import ( CaseBlueprintListItem as CaseBlueprintListItem, ) + from didit.types.components.case_blueprint_preset_install_result import ( + CaseBlueprintPresetInstallResult as CaseBlueprintPresetInstallResult, + ) from didit.types.components.case_blueprint_summary import ( CaseBlueprintSummary as CaseBlueprintSummary, ) + from didit.types.components.case_bulk_update_result import ( + CaseBulkUpdateResult as CaseBulkUpdateResult, + ) + from didit.types.components.case_bulk_update_result import ( + CaseBulkUpdateResultFailures as CaseBulkUpdateResultFailures, + ) + from didit.types.components.case_checker import CaseChecker as CaseChecker from didit.types.components.case_checklist_item import CaseChecklistItem as CaseChecklistItem from didit.types.components.case_collaboration_case_note import ( CaseCollaborationCaseNote as CaseCollaborationCaseNote, @@ -551,6 +659,9 @@ from didit.types.components.case_event import CaseEvent as CaseEvent from didit.types.components.case_link import CaseLink as CaseLink from didit.types.components.case_list_item import CaseListItem as CaseListItem + from didit.types.components.case_list_item import ( + CaseListItemPendingResolution as CaseListItemPendingResolution, + ) from didit.types.components.case_note import CaseNote as CaseNote from didit.types.components.case_note import CaseNoteAttachments as CaseNoteAttachments from didit.types.components.case_publication_assignment_method_enum import ( @@ -596,6 +707,14 @@ CaseRoutingRuleCreate as CaseRoutingRuleCreate, ) from didit.types.components.case_subject import CaseSubject as CaseSubject + from didit.types.components.case_team_summary import CaseTeamSummary as CaseTeamSummary + from didit.types.components.compliance_list import ComplianceList as ComplianceList + from didit.types.components.compliance_list_entry import ( + ComplianceListEntry as ComplianceListEntry, + ) + from didit.types.components.compliance_list_entry import ( + ComplianceListEntryMetadata as ComplianceListEntryMetadata, + ) from didit.types.components.customer_messaging_case_request import ( CustomerMessagingCaseRequest as CustomerMessagingCaseRequest, ) @@ -755,6 +874,15 @@ from didit.types.components.entity_network_membership import ( EntityNetworkMembership as EntityNetworkMembership, ) + from didit.types.components.entity_network_membership_list import ( + EntityNetworkMembershipList as EntityNetworkMembershipList, + ) + from didit.types.components.feature_status_override import ( + FeatureStatusOverride as FeatureStatusOverride, + ) + from didit.types.components.feature_status_override import ( + FeatureStatusOverrideRequestedBy as FeatureStatusOverrideRequestedBy, + ) from didit.types.components.fraud_availability_enum import ( FraudAvailabilityEnum as FraudAvailabilityEnum, ) @@ -801,8 +929,43 @@ from didit.types.components.fraud_result_enum import FraudResultEnum as FraudResultEnum from didit.types.components.fraud_status_enum import FraudStatusEnum as FraudStatusEnum from didit.types.components.fraud_type_enum import FraudTypeEnum as FraudTypeEnum + from didit.types.components.investigator_batch_accept_response import ( + InvestigatorBatchAcceptResponse as InvestigatorBatchAcceptResponse, + ) + from didit.types.components.investigator_batch_accept_response import ( + InvestigatorBatchAcceptResponseResults as InvestigatorBatchAcceptResponseResults, + ) + from didit.types.components.investigator_block import InvestigatorBlock as InvestigatorBlock + from didit.types.components.investigator_decision import ( + InvestigatorDecision as InvestigatorDecision, + ) + from didit.types.components.investigator_decision import ( + InvestigatorDecisionOriginalOutcome as InvestigatorDecisionOriginalOutcome, + ) + from didit.types.components.investigator_decision import ( + InvestigatorDecisionOverrides as InvestigatorDecisionOverrides, + ) + from didit.types.components.investigator_queue_row import ( + InvestigatorQueueRow as InvestigatorQueueRow, + ) + from didit.types.components.investigator_run_detail import ( + InvestigatorRunDetail as InvestigatorRunDetail, + ) + from didit.types.components.investigator_settings import ( + InvestigatorSettings as InvestigatorSettings, + ) + from didit.types.components.investigator_settings import ( + InvestigatorSettingsSpendLimit as InvestigatorSettingsSpendLimit, + ) from didit.types.components.ip_risk_response import IPRiskResponse as IPRiskResponse from didit.types.components.ip_risk_response import IPRiskResponseIp as IPRiskResponseIp + from didit.types.components.kyb_key_people_party import KYBKeyPeopleParty as KYBKeyPeopleParty + from didit.types.components.kyb_key_people_party import ( + KYBKeyPeoplePartyRoles as KYBKeyPeoplePartyRoles, + ) + from didit.types.components.kyb_key_people_party import ( + KYBKeyPeoplePartyVerificationWorkflowFeatures as KYBKeyPeoplePartyVerificationWorkflowFeatures, + ) from didit.types.components.kyb_registry_address import KYBRegistryAddress as KYBRegistryAddress from didit.types.components.kyb_registry_industry import ( KYBRegistryIndustry as KYBRegistryIndustry, @@ -813,15 +976,101 @@ from didit.types.components.kyb_registry_ownership import ( KYBRegistryOwnershipUnresolved as KYBRegistryOwnershipUnresolved, ) + from didit.types.components.kyb_registry_search_results import ( + KYBRegistrySearchResults as KYBRegistrySearchResults, + ) + from didit.types.components.kyb_registry_search_results import ( + KYBRegistrySearchResultsCompanies as KYBRegistrySearchResultsCompanies, + ) + from didit.types.components.kyb_registry_search_results import ( + KYBRegistrySearchResultsPagination as KYBRegistrySearchResultsPagination, + ) from didit.types.components.kyb_registry_shareholding import ( KYBRegistryShareholding as KYBRegistryShareholding, ) from didit.types.components.login_response import LoginResponse as LoginResponse + from didit.types.components.monitoring_event import MonitoringEvent as MonitoringEvent + from didit.types.components.monitoring_event_create_request import ( + MonitoringEventCreateRequest as MonitoringEventCreateRequest, + ) + from didit.types.components.monitoring_event_create_request import ( + MonitoringEventCreateRequestBusiness as MonitoringEventCreateRequestBusiness, + ) + from didit.types.components.monitoring_event_create_request import ( + MonitoringEventCreateRequestDeviceContext as MonitoringEventCreateRequestDeviceContext, + ) + from didit.types.components.monitoring_event_create_request import ( + MonitoringEventCreateRequestDeviceContextNetworkContext as MonitoringEventCreateRequestDeviceContextNetworkContext, + ) + from didit.types.components.monitoring_event_create_request import ( + MonitoringEventCreateRequestSession as MonitoringEventCreateRequestSession, + ) + from didit.types.components.monitoring_event_create_request import ( + MonitoringEventCreateRequestSubject as MonitoringEventCreateRequestSubject, + ) + from didit.types.components.monitoring_event_create_request import ( + MonitoringEventCreateRequestTransactionContext as MonitoringEventCreateRequestTransactionContext, + ) + from didit.types.components.monitoring_event_detail import ( + MonitoringEventDetail as MonitoringEventDetail, + ) + from didit.types.components.monitoring_event_detail import ( + MonitoringEventDetailIpEnrichment as MonitoringEventDetailIpEnrichment, + ) + from didit.types.components.monitoring_event_detail import ( + MonitoringEventDetailSubjectProfile as MonitoringEventDetailSubjectProfile, + ) + from didit.types.components.monitoring_event_ingest_result import ( + MonitoringEventIngestResult as MonitoringEventIngestResult, + ) + from didit.types.components.monitoring_event_ingest_result import ( + MonitoringEventIngestResultRuleMatches as MonitoringEventIngestResultRuleMatches, + ) + from didit.types.components.monitoring_event_page import ( + MonitoringEventPage as MonitoringEventPage, + ) + from didit.types.components.monitoring_event_rule_run import ( + MonitoringEventRuleRun as MonitoringEventRuleRun, + ) from didit.types.components.network_detail import NetworkDetail as NetworkDetail + from didit.types.components.network_detail_response import ( + NetworkDetailResponse as NetworkDetailResponse, + ) from didit.types.components.network_graph import NetworkGraph as NetworkGraph from didit.types.components.network_graph import NetworkGraphMetrics as NetworkGraphMetrics from didit.types.components.network_graph_edge import NetworkGraphEdge as NetworkGraphEdge from didit.types.components.network_graph_node import NetworkGraphNode as NetworkGraphNode + from didit.types.components.network_list_counts import NetworkListCounts as NetworkListCounts + from didit.types.components.network_list_counts import ( + NetworkListCountsFacets as NetworkListCountsFacets, + ) + from didit.types.components.network_list_counts import ( + NetworkListCountsFacetsAssignee as NetworkListCountsFacetsAssignee, + ) + from didit.types.components.network_list_counts import ( + NetworkListCountsFacetsAssigneeUsers as NetworkListCountsFacetsAssigneeUsers, + ) + from didit.types.components.network_list_counts import ( + NetworkListCountsFacetsLastActivityDays as NetworkListCountsFacetsLastActivityDays, + ) + from didit.types.components.network_list_counts import ( + NetworkListCountsFacetsMinSize as NetworkListCountsFacetsMinSize, + ) + from didit.types.components.network_list_counts import ( + NetworkListCountsFacetsPriority as NetworkListCountsFacetsPriority, + ) + from didit.types.components.network_list_counts import ( + NetworkListCountsFacetsRiskMin as NetworkListCountsFacetsRiskMin, + ) + from didit.types.components.network_list_counts import ( + NetworkListCountsFacetsSignalTypes as NetworkListCountsFacetsSignalTypes, + ) + from didit.types.components.network_list_counts import ( + NetworkListCountsScopes as NetworkListCountsScopes, + ) + from didit.types.components.network_list_counts import ( + NetworkListCountsViews as NetworkListCountsViews, + ) from didit.types.components.network_list_item import NetworkListItem as NetworkListItem from didit.types.components.network_map import NetworkMap as NetworkMap from didit.types.components.network_map import NetworkMapPoints as NetworkMapPoints @@ -831,15 +1080,85 @@ ) from didit.types.components.network_node_tag import NetworkNodeTag as NetworkNodeTag from didit.types.components.network_note import NetworkNote as NetworkNote + from didit.types.components.network_outcomes import NetworkOutcomes as NetworkOutcomes + from didit.types.components.network_pattern_score import ( + NetworkPatternScore as NetworkPatternScore, + ) from didit.types.components.network_risk_display import NetworkRiskDisplay as NetworkRiskDisplay from didit.types.components.network_signal import NetworkSignal as NetworkSignal + from didit.types.components.network_tag import NetworkTag as NetworkTag + from didit.types.components.network_timeline import NetworkTimeline as NetworkTimeline + from didit.types.components.network_timeline_account import ( + NetworkTimelineAccount as NetworkTimelineAccount, + ) + from didit.types.components.network_timeline_amount import ( + NetworkTimelineAmount as NetworkTimelineAmount, + ) + from didit.types.components.network_timeline_attempt import ( + NetworkTimelineAttempt as NetworkTimelineAttempt, + ) + from didit.types.components.network_timeline_case_linked import ( + NetworkTimelineCaseLinked as NetworkTimelineCaseLinked, + ) + from didit.types.components.network_timeline_decline_reason import ( + NetworkTimelineDeclineReason as NetworkTimelineDeclineReason, + ) + from didit.types.components.network_timeline_device import ( + NetworkTimelineDevice as NetworkTimelineDevice, + ) from didit.types.components.network_timeline_event import ( NetworkTimelineEvent as NetworkTimelineEvent, ) + from didit.types.components.network_timeline_ip import NetworkTimelineIP as NetworkTimelineIP + from didit.types.components.network_timeline_login import ( + NetworkTimelineLogin as NetworkTimelineLogin, + ) + from didit.types.components.network_timeline_note_added import ( + NetworkTimelineNoteAdded as NetworkTimelineNoteAdded, + ) + from didit.types.components.network_timeline_page import ( + NetworkTimelinePage as NetworkTimelinePage, + ) + from didit.types.components.network_timeline_route import ( + NetworkTimelineRoute as NetworkTimelineRoute, + ) + from didit.types.components.network_timeline_rule_hit import ( + NetworkTimelineRuleHit as NetworkTimelineRuleHit, + ) + from didit.types.components.network_timeline_source_ref import ( + NetworkTimelineSourceRef as NetworkTimelineSourceRef, + ) + from didit.types.components.network_timeline_transaction import ( + NetworkTimelineTransaction as NetworkTimelineTransaction, + ) + from didit.types.components.network_typed_timeline_event import ( + NetworkTypedTimelineEvent as NetworkTypedTimelineEvent, + ) + from didit.types.components.ongoing_monitoring_job import ( + OngoingMonitoringJob as OngoingMonitoringJob, + ) + from didit.types.components.ongoing_monitoring_job import ( + OngoingMonitoringJobEstimate as OngoingMonitoringJobEstimate, + ) + from didit.types.components.ongoing_monitoring_job import ( + OngoingMonitoringJobOutcomes as OngoingMonitoringJobOutcomes, + ) + from didit.types.components.ongoing_monitoring_toggle_result import ( + OngoingMonitoringToggleResult as OngoingMonitoringToggleResult, + ) + from didit.types.components.ongoing_monitoring_toggle_result import ( + OngoingMonitoringToggleResultVariant1 as OngoingMonitoringToggleResultVariant1, + ) from didit.types.components.organization import Organization as Organization from didit.types.components.organization_summary import ( OrganizationSummary as OrganizationSummary, ) + from didit.types.components.paginated_application_list import ( + PaginatedApplicationList as PaginatedApplicationList, + ) + from didit.types.components.paginated_organization_list import ( + PaginatedOrganizationList as PaginatedOrganizationList, + ) from didit.types.components.phone_intelligence import PhoneIntelligence as PhoneIntelligence from didit.types.components.phone_match_item import PhoneMatchItem as PhoneMatchItem from didit.types.components.phone_risk_response import PhoneRiskResponse as PhoneRiskResponse @@ -889,6 +1208,9 @@ from didit.types.components.regulatory_report_detail import ( RegulatoryReportDetailValidationErrors as RegulatoryReportDetailValidationErrors, ) + from didit.types.components.regulatory_report_list import ( + RegulatoryReportList as RegulatoryReportList, + ) from didit.types.components.report_template_detail import ( ReportTemplateDetail as ReportTemplateDetail, ) @@ -920,156 +1242,678 @@ from didit.types.components.routing_publication_routing_condition import ( RoutingPublicationRoutingCondition as RoutingPublicationRoutingCondition, ) - from didit.types.components.session_import_error import SessionImportError as SessionImportError - from didit.types.components.session_import_job import SessionImportJob as SessionImportJob - from didit.types.components.simple_questionnaire_element import ( - SimpleQuestionnaireElement as SimpleQuestionnaireElement, + from didit.types.components.sandbox_simulate_response import ( + SandboxSimulateResponse as SandboxSimulateResponse, ) - from didit.types.components.simple_questionnaire_element import ( - SimpleQuestionnaireElementOptions as SimpleQuestionnaireElementOptions, + from didit.types.components.session_decision import SessionDecision as SessionDecision + from didit.types.components.session_decision import ( + SessionDecisionContactDetails as SessionDecisionContactDetails, ) - from didit.types.components.simple_questionnaire_element import ( - SimpleQuestionnaireElementValidation as SimpleQuestionnaireElementValidation, + from didit.types.components.session_decision import ( + SessionDecisionDatabaseValidations as SessionDecisionDatabaseValidations, ) - from didit.types.components.simple_workflow_feature import ( - SimpleWorkflowFeature as SimpleWorkflowFeature, + from didit.types.components.session_decision import ( + SessionDecisionDatabaseValidationsErrors as SessionDecisionDatabaseValidationsErrors, ) - from didit.types.components.social_footprint_category_summary import ( - SocialFootprintCategorySummary as SocialFootprintCategorySummary, + from didit.types.components.session_decision import ( + SessionDecisionDatabaseValidationsScreenedData as SessionDecisionDatabaseValidationsScreenedData, ) - from didit.types.components.social_footprint_platform import ( - SocialFootprintPlatform as SocialFootprintPlatform, + from didit.types.components.session_decision import ( + SessionDecisionDatabaseValidationsValidations as SessionDecisionDatabaseValidationsValidations, ) - from didit.types.components.social_footprint_profile import ( - SocialFootprintProfile as SocialFootprintProfile, + from didit.types.components.session_decision import ( + SessionDecisionDatabaseValidationsValidationsValidation as SessionDecisionDatabaseValidationsValidationsValidation, ) - from didit.types.components.transaction_detail import TransactionDetail as TransactionDetail - from didit.types.components.transaction_detail import ( - TransactionDetailActionRequiredVerificationSession as TransactionDetailActionRequiredVerificationSession, + from didit.types.components.session_decision import ( + SessionDecisionDatabaseValidationsWarnings as SessionDecisionDatabaseValidationsWarnings, ) - from didit.types.components.transaction_detail import ( - TransactionDetailActionRequiredWalletOwnership as TransactionDetailActionRequiredWalletOwnership, + from didit.types.components.session_decision import ( + SessionDecisionDocumentAiDocuments as SessionDecisionDocumentAiDocuments, ) - from didit.types.components.transaction_detail import ( - TransactionDetailActivities as TransactionDetailActivities, + from didit.types.components.session_decision import ( + SessionDecisionDocumentAiDocumentsItems as SessionDecisionDocumentAiDocumentsItems, ) - from didit.types.components.transaction_detail import ( - TransactionDetailAlerts as TransactionDetailAlerts, + from didit.types.components.session_decision import ( + SessionDecisionDocumentAiDocumentsItemsFields as SessionDecisionDocumentAiDocumentsItemsFields, ) - from didit.types.components.transaction_detail import ( - TransactionDetailDocuments as TransactionDetailDocuments, + from didit.types.components.session_decision import ( + SessionDecisionDocumentVerifications as SessionDecisionDocumentVerifications, ) - from didit.types.components.transaction_detail import ( - TransactionDetailDocumentsFiles as TransactionDetailDocumentsFiles, + from didit.types.components.session_decision import ( + SessionDecisionDocumentVerificationsGroupsValue as SessionDecisionDocumentVerificationsGroupsValue, ) - from didit.types.components.transaction_detail import ( - TransactionDetailNetworkSnapshot as TransactionDetailNetworkSnapshot, + from didit.types.components.session_decision import ( + SessionDecisionDocumentVerificationsItems as SessionDecisionDocumentVerificationsItems, ) - from didit.types.components.transaction_detail import ( - TransactionDetailParties as TransactionDetailParties, + from didit.types.components.session_decision import ( + SessionDecisionDocumentVerificationsItemsDocumentMetadata as SessionDecisionDocumentVerificationsItemsDocumentMetadata, ) - from didit.types.components.transaction_detail import ( - TransactionDetailPartiesDevice as TransactionDetailPartiesDevice, + from didit.types.components.session_decision import ( + SessionDecisionDocumentVerificationsItemsDocumentMetadataOverlayManipulation as SessionDecisionDocumentVerificationsItemsDocumentMetadataOverlayManipulation, ) - from didit.types.components.transaction_detail import ( - TransactionDetailPartiesDeviceNetworkContext as TransactionDetailPartiesDeviceNetworkContext, + from didit.types.components.session_decision import ( + SessionDecisionDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions as SessionDecisionDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions, ) - from didit.types.components.transaction_detail import ( - TransactionDetailPaymentMethods as TransactionDetailPaymentMethods, + from didit.types.components.session_decision import ( + SessionDecisionEmailVerifications as SessionDecisionEmailVerifications, ) - from didit.types.components.transaction_detail import ( - TransactionDetailProviderResults as TransactionDetailProviderResults, + from didit.types.components.session_decision import ( + SessionDecisionEmailVerificationsBreaches as SessionDecisionEmailVerificationsBreaches, ) - from didit.types.components.transaction_detail import ( - TransactionDetailRemediation as TransactionDetailRemediation, + from didit.types.components.session_decision import ( + SessionDecisionEmailVerificationsLifecycle as SessionDecisionEmailVerificationsLifecycle, ) - from didit.types.components.transaction_detail import ( - TransactionDetailRuleRuns as TransactionDetailRuleRuns, + from didit.types.components.session_decision import ( + SessionDecisionEmailVerificationsLifecycleDetails as SessionDecisionEmailVerificationsLifecycleDetails, ) - from didit.types.components.transaction_detail import ( - TransactionDetailTags as TransactionDetailTags, + from didit.types.components.session_decision import ( + SessionDecisionEmailVerificationsMatches as SessionDecisionEmailVerificationsMatches, ) - from didit.types.components.transaction_detail import ( - TransactionDetailTravelRule as TransactionDetailTravelRule, + from didit.types.components.session_decision import ( + SessionDecisionEmailVerificationsWarnings as SessionDecisionEmailVerificationsWarnings, ) - from didit.types.components.transaction_rule_action import ( - TransactionRuleAction as TransactionRuleAction, + from didit.types.components.session_decision import ( + SessionDecisionExpectedDetails as SessionDecisionExpectedDetails, ) - from didit.types.components.transaction_rule_action import ( - TransactionRuleActionParam as TransactionRuleActionParam, + from didit.types.components.session_decision import ( + SessionDecisionFaceMatches as SessionDecisionFaceMatches, ) - from didit.types.components.transaction_rule_action import ( - TransactionRuleActionVariant1 as TransactionRuleActionVariant1, + from didit.types.components.session_decision import ( + SessionDecisionFaceMatchesWarnings as SessionDecisionFaceMatchesWarnings, ) - from didit.types.components.transaction_rule_action import ( - TransactionRuleActionVariant1Param as TransactionRuleActionVariant1Param, + from didit.types.components.session_decision import ( + SessionDecisionIdVerifications as SessionDecisionIdVerifications, ) - from didit.types.components.transaction_rule_action import ( - TransactionRuleActionVariant2 as TransactionRuleActionVariant2, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsBackImageQualityScore as SessionDecisionIdVerificationsBackImageQualityScore, ) - from didit.types.components.transaction_rule_action import ( - TransactionRuleActionVariant2Param as TransactionRuleActionVariant2Param, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsDocumentLiveness as SessionDecisionIdVerificationsDocumentLiveness, ) - from didit.types.components.transaction_rule_action import ( - TransactionRuleActionVariant3 as TransactionRuleActionVariant3, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsDocumentLivenessPortraitReplace as SessionDecisionIdVerificationsDocumentLivenessPortraitReplace, ) - from didit.types.components.transaction_rule_action import ( - TransactionRuleActionVariant3Param as TransactionRuleActionVariant3Param, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsDocumentLivenessPrintedCopy as SessionDecisionIdVerificationsDocumentLivenessPrintedCopy, ) - from didit.types.components.transaction_rule_action import ( - TransactionRuleActionVariant4 as TransactionRuleActionVariant4, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsDocumentLivenessScreenReplay as SessionDecisionIdVerificationsDocumentLivenessScreenReplay, ) - from didit.types.components.transaction_rule_action import ( - TransactionRuleActionVariant4Param as TransactionRuleActionVariant4Param, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsDocumentSelfieFaceMatches as SessionDecisionIdVerificationsDocumentSelfieFaceMatches, ) - from didit.types.components.transaction_rule_action import ( - TransactionRuleActionVariant5 as TransactionRuleActionVariant5, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsDocumentSelfieFaceMatchesUserDetails as SessionDecisionIdVerificationsDocumentSelfieFaceMatchesUserDetails, ) - from didit.types.components.transaction_rule_action import ( - TransactionRuleActionVariant5Param as TransactionRuleActionVariant5Param, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsExtraFields as SessionDecisionIdVerificationsExtraFields, ) - from didit.types.components.transaction_rule_action import ( - TransactionRuleActionVariant6 as TransactionRuleActionVariant6, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsFallbackFrom as SessionDecisionIdVerificationsFallbackFrom, ) - from didit.types.components.transaction_rule_action import ( - TransactionRuleActionVariant6Param as TransactionRuleActionVariant6Param, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsFrontImageQualityScore as SessionDecisionIdVerificationsFrontImageQualityScore, ) - from didit.types.components.transaction_rule_aggregation import ( - TransactionRuleAggregation as TransactionRuleAggregation, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsIdLookup as SessionDecisionIdVerificationsIdLookup, ) - from didit.types.components.transaction_rule_aggregation import ( - TransactionRuleAggregationParam as TransactionRuleAggregationParam, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsIdLookupComparison as SessionDecisionIdVerificationsIdLookupComparison, ) - from didit.types.components.transaction_rule_backtest_result import ( - TransactionRuleBacktestResult as TransactionRuleBacktestResult, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsMatches as SessionDecisionIdVerificationsMatches, ) - from didit.types.components.transaction_rule_condition import ( - TransactionRuleCondition as TransactionRuleCondition, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsMatchesUserDetails as SessionDecisionIdVerificationsMatchesUserDetails, ) - from didit.types.components.transaction_rule_condition import ( - TransactionRuleConditionParam as TransactionRuleConditionParam, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsMrz as SessionDecisionIdVerificationsMrz, ) - from didit.types.components.transaction_rule_detail import ( - TransactionRuleDetail as TransactionRuleDetail, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsParsedAddress as SessionDecisionIdVerificationsParsedAddress, ) - from didit.types.components.transaction_rule_install_result import ( - TransactionRuleInstallResult as TransactionRuleInstallResult, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsParsedAddressDocumentLocation as SessionDecisionIdVerificationsParsedAddressDocumentLocation, ) - from didit.types.components.transaction_rule_library_entry import ( - TransactionRuleLibraryEntry as TransactionRuleLibraryEntry, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsParsedAddressRawResults as SessionDecisionIdVerificationsParsedAddressRawResults, ) - from didit.types.components.transaction_rule_scope import ( - TransactionRuleScope as TransactionRuleScope, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsParsedAddressRawResultsAddressComponents as SessionDecisionIdVerificationsParsedAddressRawResultsAddressComponents, ) - from didit.types.components.transaction_rule_scope import ( - TransactionRuleScopeParam as TransactionRuleScopeParam, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsParsedAddressRawResultsGeometry as SessionDecisionIdVerificationsParsedAddressRawResultsGeometry, ) - from didit.types.components.travel_rule_inbound_response import ( - TravelRuleInboundResponse as TravelRuleInboundResponse, + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsParsedAddressRawResultsGeometryLocation as SessionDecisionIdVerificationsParsedAddressRawResultsGeometryLocation, + ) + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewport as SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewport, + ) + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewportNortheast as SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewportNortheast, + ) + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest as SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest, + ) + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsParsedAddressRawResultsNavigationPoints as SessionDecisionIdVerificationsParsedAddressRawResultsNavigationPoints, + ) + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsParsedAddressRawResultsNavigationPointsLocation as SessionDecisionIdVerificationsParsedAddressRawResultsNavigationPointsLocation, + ) + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsWalletVerification as SessionDecisionIdVerificationsWalletVerification, + ) + from didit.types.components.session_decision import ( + SessionDecisionIdVerificationsWarnings as SessionDecisionIdVerificationsWarnings, + ) + from didit.types.components.session_decision import ( + SessionDecisionIpAnalyses as SessionDecisionIpAnalyses, + ) + from didit.types.components.session_decision import ( + SessionDecisionIpAnalysesIdDocument as SessionDecisionIpAnalysesIdDocument, + ) + from didit.types.components.session_decision import ( + SessionDecisionIpAnalysesIdDocumentLocation as SessionDecisionIpAnalysesIdDocumentLocation, + ) + from didit.types.components.session_decision import ( + SessionDecisionIpAnalysesIp as SessionDecisionIpAnalysesIp, + ) + from didit.types.components.session_decision import ( + SessionDecisionIpAnalysesIpLocation as SessionDecisionIpAnalysesIpLocation, + ) + from didit.types.components.session_decision import ( + SessionDecisionIpAnalysesMatches as SessionDecisionIpAnalysesMatches, + ) + from didit.types.components.session_decision import ( + SessionDecisionIpAnalysesMatchesDeviceInfo as SessionDecisionIpAnalysesMatchesDeviceInfo, + ) + from didit.types.components.session_decision import ( + SessionDecisionIpAnalysesMatchesLocationInfo as SessionDecisionIpAnalysesMatchesLocationInfo, + ) + from didit.types.components.session_decision import ( + SessionDecisionIpAnalysesPoaDocument as SessionDecisionIpAnalysesPoaDocument, + ) + from didit.types.components.session_decision import ( + SessionDecisionIpAnalysesPoaDocumentLocation as SessionDecisionIpAnalysesPoaDocumentLocation, + ) + from didit.types.components.session_decision import ( + SessionDecisionIpAnalysesWarnings as SessionDecisionIpAnalysesWarnings, + ) + from didit.types.components.session_decision import ( + SessionDecisionKeyPeopleChecks as SessionDecisionKeyPeopleChecks, + ) + from didit.types.components.session_decision import ( + SessionDecisionKeyPeopleChecksRegistry as SessionDecisionKeyPeopleChecksRegistry, + ) + from didit.types.components.session_decision import ( + SessionDecisionKeyPeopleChecksRegistryBeneficialOwners as SessionDecisionKeyPeopleChecksRegistryBeneficialOwners, + ) + from didit.types.components.session_decision import ( + SessionDecisionKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories as SessionDecisionKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories, + ) + from didit.types.components.session_decision import ( + SessionDecisionKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures as SessionDecisionKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures, + ) + from didit.types.components.session_decision import ( + SessionDecisionKeyPeopleChecksRegistryOfficers as SessionDecisionKeyPeopleChecksRegistryOfficers, + ) + from didit.types.components.session_decision import ( + SessionDecisionKeyPeopleChecksRegistryOfficersKycAmlCategories as SessionDecisionKeyPeopleChecksRegistryOfficersKycAmlCategories, + ) + from didit.types.components.session_decision import ( + SessionDecisionKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures as SessionDecisionKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures, + ) + from didit.types.components.session_decision import ( + SessionDecisionKeyPeopleChecksSubmitted as SessionDecisionKeyPeopleChecksSubmitted, + ) + from didit.types.components.session_decision import ( + SessionDecisionKeyPeopleChecksSubmittedParties as SessionDecisionKeyPeopleChecksSubmittedParties, + ) + from didit.types.components.session_decision import ( + SessionDecisionKeyPeopleChecksSubmittedPartiesRoles as SessionDecisionKeyPeopleChecksSubmittedPartiesRoles, + ) + from didit.types.components.session_decision import ( + SessionDecisionKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures as SessionDecisionKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures, + ) + from didit.types.components.session_decision import ( + SessionDecisionKeyPeopleChecksUboKycSummary as SessionDecisionKeyPeopleChecksUboKycSummary, + ) + from didit.types.components.session_decision import ( + SessionDecisionLivenessChecks as SessionDecisionLivenessChecks, + ) + from didit.types.components.session_decision import ( + SessionDecisionLivenessChecksMatches as SessionDecisionLivenessChecksMatches, + ) + from didit.types.components.session_decision import ( + SessionDecisionLivenessChecksMatchesUserDetails as SessionDecisionLivenessChecksMatchesUserDetails, + ) + from didit.types.components.session_decision import ( + SessionDecisionLivenessChecksWarnings as SessionDecisionLivenessChecksWarnings, + ) + from didit.types.components.session_decision import ( + SessionDecisionNfcVerifications as SessionDecisionNfcVerifications, + ) + from didit.types.components.session_decision import ( + SessionDecisionNfcVerificationsAuthenticity as SessionDecisionNfcVerificationsAuthenticity, + ) + from didit.types.components.session_decision import ( + SessionDecisionNfcVerificationsCertificateSummary as SessionDecisionNfcVerificationsCertificateSummary, + ) + from didit.types.components.session_decision import ( + SessionDecisionNfcVerificationsChipData as SessionDecisionNfcVerificationsChipData, + ) + from didit.types.components.session_decision import ( + SessionDecisionNfcVerificationsJpki as SessionDecisionNfcVerificationsJpki, + ) + from didit.types.components.session_decision import ( + SessionDecisionNfcVerificationsJpkiAttributes as SessionDecisionNfcVerificationsJpkiAttributes, + ) + from didit.types.components.session_decision import ( + SessionDecisionNfcVerificationsJpkiCertificates as SessionDecisionNfcVerificationsJpkiCertificates, + ) + from didit.types.components.session_decision import ( + SessionDecisionNfcVerificationsWarnings as SessionDecisionNfcVerificationsWarnings, + ) + from didit.types.components.session_decision import ( + SessionDecisionPhoneVerifications as SessionDecisionPhoneVerifications, + ) + from didit.types.components.session_decision import ( + SessionDecisionPhoneVerificationsCarrier as SessionDecisionPhoneVerificationsCarrier, + ) + from didit.types.components.session_decision import ( + SessionDecisionPhoneVerificationsLifecycle as SessionDecisionPhoneVerificationsLifecycle, + ) + from didit.types.components.session_decision import ( + SessionDecisionPhoneVerificationsLifecycleDetails as SessionDecisionPhoneVerificationsLifecycleDetails, + ) + from didit.types.components.session_decision import ( + SessionDecisionPhoneVerificationsMatches as SessionDecisionPhoneVerificationsMatches, + ) + from didit.types.components.session_decision import ( + SessionDecisionPhoneVerificationsWarnings as SessionDecisionPhoneVerificationsWarnings, + ) + from didit.types.components.session_decision import ( + SessionDecisionPoaVerifications as SessionDecisionPoaVerifications, + ) + from didit.types.components.session_decision import ( + SessionDecisionPoaVerificationsDocumentMetadata as SessionDecisionPoaVerificationsDocumentMetadata, + ) + from didit.types.components.session_decision import ( + SessionDecisionPoaVerificationsDocumentMetadataOverlayManipulation as SessionDecisionPoaVerificationsDocumentMetadataOverlayManipulation, + ) + from didit.types.components.session_decision import ( + SessionDecisionPoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions as SessionDecisionPoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions, + ) + from didit.types.components.session_decision import ( + SessionDecisionPoaVerificationsExtraFields as SessionDecisionPoaVerificationsExtraFields, + ) + from didit.types.components.session_decision import ( + SessionDecisionPoaVerificationsPoaParsedAddress as SessionDecisionPoaVerificationsPoaParsedAddress, + ) + from didit.types.components.session_decision import ( + SessionDecisionPoaVerificationsPoaParsedAddressDocumentLocation as SessionDecisionPoaVerificationsPoaParsedAddressDocumentLocation, + ) + from didit.types.components.session_decision import ( + SessionDecisionPoaVerificationsWarnings as SessionDecisionPoaVerificationsWarnings, + ) + from didit.types.components.session_decision import ( + SessionDecisionQuestionnaireResponses as SessionDecisionQuestionnaireResponses, + ) + from didit.types.components.session_decision import ( + SessionDecisionQuestionnaireResponsesSections as SessionDecisionQuestionnaireResponsesSections, + ) + from didit.types.components.session_decision import ( + SessionDecisionQuestionnaireResponsesSectionsItems as SessionDecisionQuestionnaireResponsesSectionsItems, + ) + from didit.types.components.session_decision import ( + SessionDecisionQuestionnaireResponsesSectionsItemsAnswer as SessionDecisionQuestionnaireResponsesSectionsItemsAnswer, + ) + from didit.types.components.session_decision import ( + SessionDecisionQuestionnaireResponsesSectionsItemsChoices as SessionDecisionQuestionnaireResponsesSectionsItemsChoices, + ) + from didit.types.components.session_decision import ( + SessionDecisionRegistryChecks as SessionDecisionRegistryChecks, + ) + from didit.types.components.session_decision import ( + SessionDecisionRegistryChecksCompany as SessionDecisionRegistryChecksCompany, + ) + from didit.types.components.session_decision import ( + SessionDecisionRegistryChecksCompanyBeneficialOwners as SessionDecisionRegistryChecksCompanyBeneficialOwners, + ) + from didit.types.components.session_decision import ( + SessionDecisionRegistryChecksCompanyOfficers as SessionDecisionRegistryChecksCompanyOfficers, + ) + from didit.types.components.session_decision import ( + SessionDecisionResubmissions as SessionDecisionResubmissions, + ) + from didit.types.components.session_decision import ( + SessionDecisionResubmissionsNodes as SessionDecisionResubmissionsNodes, + ) + from didit.types.components.session_decision import ( + SessionDecisionResubmissionsRequestedBy as SessionDecisionResubmissionsRequestedBy, + ) + from didit.types.components.session_decision import ( + SessionDecisionReviews as SessionDecisionReviews, + ) + from didit.types.components.session_deletion_job import SessionDeletionJob as SessionDeletionJob + from didit.types.components.session_deletion_job import ( + SessionDeletionJobOutcomes as SessionDeletionJobOutcomes, + ) + from didit.types.components.session_event import SessionEvent as SessionEvent + from didit.types.components.session_import_error import SessionImportError as SessionImportError + from didit.types.components.session_import_job import SessionImportJob as SessionImportJob + from didit.types.components.session_sign_document import ( + SessionSignDocument as SessionSignDocument, + ) + from didit.types.components.session_webhook_delivery import ( + SessionWebhookDelivery as SessionWebhookDelivery, + ) + from didit.types.components.signature_field import SignatureField as SignatureField + from didit.types.components.signature_field_anchor import ( + SignatureFieldAnchor as SignatureFieldAnchor, + ) + from didit.types.components.signature_field_result import ( + SignatureFieldResult as SignatureFieldResult, + ) + from didit.types.components.signature_field_validation import ( + SignatureFieldValidation as SignatureFieldValidation, + ) + from didit.types.components.signature_result import SignatureResult as SignatureResult + from didit.types.components.signature_result import ( + SignatureResultAdoptedSignature as SignatureResultAdoptedSignature, + ) + from didit.types.components.signature_result import ( + SignatureResultCertificate as SignatureResultCertificate, + ) + from didit.types.components.signature_result import ( + SignatureResultConfirmation as SignatureResultConfirmation, + ) + from didit.types.components.signature_result import ( + SignatureResultDocuments as SignatureResultDocuments, + ) + from didit.types.components.signature_result import ( + SignatureResultDocumentsConversion as SignatureResultDocumentsConversion, + ) + from didit.types.components.signature_result import ( + SignatureResultIdentityUsed as SignatureResultIdentityUsed, + ) + from didit.types.components.signature_result import ( + SignatureResultSigner as SignatureResultSigner, + ) + from didit.types.components.signature_result import ( + SignatureResultTimestamp as SignatureResultTimestamp, + ) + from didit.types.components.signature_result import ( + SignatureResultValidation as SignatureResultValidation, + ) + from didit.types.components.signature_result import ( + SignatureResultValidationDocuments as SignatureResultValidationDocuments, + ) + from didit.types.components.signature_result import ( + SignatureResultValidationDocumentsCertificate as SignatureResultValidationDocumentsCertificate, + ) + from didit.types.components.signature_result import ( + SignatureResultValidationDocumentsTimestamp as SignatureResultValidationDocumentsTimestamp, + ) + from didit.types.components.signature_result import ( + SignatureResultValidationDocumentsTrustedList as SignatureResultValidationDocumentsTrustedList, + ) + from didit.types.components.signature_result import ( + SignatureResultWarnings as SignatureResultWarnings, + ) + from didit.types.components.simple_questionnaire_element import ( + SimpleQuestionnaireElement as SimpleQuestionnaireElement, + ) + from didit.types.components.simple_questionnaire_element import ( + SimpleQuestionnaireElementOptions as SimpleQuestionnaireElementOptions, + ) + from didit.types.components.simple_questionnaire_element import ( + SimpleQuestionnaireElementValidation as SimpleQuestionnaireElementValidation, + ) + from didit.types.components.simple_workflow_feature import ( + SimpleWorkflowFeature as SimpleWorkflowFeature, + ) + from didit.types.components.social_footprint_category_summary import ( + SocialFootprintCategorySummary as SocialFootprintCategorySummary, + ) + from didit.types.components.social_footprint_platform import ( + SocialFootprintPlatform as SocialFootprintPlatform, + ) + from didit.types.components.social_footprint_profile import ( + SocialFootprintProfile as SocialFootprintProfile, + ) + from didit.types.components.transaction_detail import TransactionDetail as TransactionDetail + from didit.types.components.transaction_detail import ( + TransactionDetailActivities as TransactionDetailActivities, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailAlerts as TransactionDetailAlerts, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailDocuments as TransactionDetailDocuments, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailDocumentsFiles as TransactionDetailDocumentsFiles, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailNetworkSnapshot as TransactionDetailNetworkSnapshot, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailParties as TransactionDetailParties, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailPartiesDevice as TransactionDetailPartiesDevice, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailPartiesDeviceNetworkContext as TransactionDetailPartiesDeviceNetworkContext, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailPaymentMethods as TransactionDetailPaymentMethods, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailProviderResults as TransactionDetailProviderResults, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailRemediation as TransactionDetailRemediation, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailRuleRuns as TransactionDetailRuleRuns, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailTags as TransactionDetailTags, + ) + from didit.types.components.transaction_note import TransactionNote as TransactionNote + from didit.types.components.transaction_note import ( + TransactionNoteMetadata as TransactionNoteMetadata, + ) + from didit.types.components.transaction_rule_action import ( + TransactionRuleAction as TransactionRuleAction, + ) + from didit.types.components.transaction_rule_action import ( + TransactionRuleActionParam as TransactionRuleActionParam, + ) + from didit.types.components.transaction_rule_action_add_note import ( + TransactionRuleActionAddNote as TransactionRuleActionAddNote, + ) + from didit.types.components.transaction_rule_action_add_note import ( + TransactionRuleActionAddNoteParam as TransactionRuleActionAddNoteParam, + ) + from didit.types.components.transaction_rule_action_add_score import ( + TransactionRuleActionAddScore as TransactionRuleActionAddScore, + ) + from didit.types.components.transaction_rule_action_add_score import ( + TransactionRuleActionAddScoreParam as TransactionRuleActionAddScoreParam, + ) + from didit.types.components.transaction_rule_action_add_tags import ( + TransactionRuleActionAddTags as TransactionRuleActionAddTags, + ) + from didit.types.components.transaction_rule_action_add_tags import ( + TransactionRuleActionAddTagsParam as TransactionRuleActionAddTagsParam, + ) + from didit.types.components.transaction_rule_action_add_to_list import ( + TransactionRuleActionAddToList as TransactionRuleActionAddToList, + ) + from didit.types.components.transaction_rule_action_add_to_list import ( + TransactionRuleActionAddToListParam as TransactionRuleActionAddToListParam, + ) + from didit.types.components.transaction_rule_action_change_status import ( + TransactionRuleActionChangeStatus as TransactionRuleActionChangeStatus, + ) + from didit.types.components.transaction_rule_action_change_status import ( + TransactionRuleActionChangeStatusParam as TransactionRuleActionChangeStatusParam, + ) + from didit.types.components.transaction_rule_action_open_case import ( + TransactionRuleActionOpenCase as TransactionRuleActionOpenCase, + ) + from didit.types.components.transaction_rule_action_open_case import ( + TransactionRuleActionOpenCaseParam as TransactionRuleActionOpenCaseParam, + ) + from didit.types.components.transaction_rule_aggregation import ( + TransactionRuleAggregation as TransactionRuleAggregation, + ) + from didit.types.components.transaction_rule_aggregation import ( + TransactionRuleAggregationParam as TransactionRuleAggregationParam, + ) + from didit.types.components.transaction_rule_backtest_result import ( + TransactionRuleBacktestResult as TransactionRuleBacktestResult, + ) + from didit.types.components.transaction_rule_condition import ( + TransactionRuleCondition as TransactionRuleCondition, + ) + from didit.types.components.transaction_rule_condition import ( + TransactionRuleConditionParam as TransactionRuleConditionParam, + ) + from didit.types.components.transaction_rule_detail import ( + TransactionRuleDetail as TransactionRuleDetail, + ) + from didit.types.components.transaction_rule_install_result import ( + TransactionRuleInstallResult as TransactionRuleInstallResult, + ) + from didit.types.components.transaction_rule_library_entry import ( + TransactionRuleLibraryEntry as TransactionRuleLibraryEntry, + ) + from didit.types.components.transaction_rule_scope import ( + TransactionRuleScope as TransactionRuleScope, + ) + from didit.types.components.transaction_rule_scope import ( + TransactionRuleScopeParam as TransactionRuleScopeParam, + ) + from didit.types.components.transaction_summary import TransactionSummary as TransactionSummary + from didit.types.components.transaction_summary import ( + TransactionSummaryTags as TransactionSummaryTags, + ) + from didit.types.components.transaction_travel_rule import ( + TransactionTravelRule as TransactionTravelRule, + ) + from didit.types.components.transaction_travel_rule import ( + TransactionTravelRuleCounterpartyVasp as TransactionTravelRuleCounterpartyVasp, + ) + from didit.types.components.transaction_travel_rule import ( + TransactionTravelRuleCounterpartyVaspDdScoreBreakdownValue as TransactionTravelRuleCounterpartyVaspDdScoreBreakdownValue, + ) + from didit.types.components.transaction_verification_session_action import ( + TransactionVerificationSessionAction as TransactionVerificationSessionAction, + ) + from didit.types.components.transaction_wallet_ownership_action import ( + TransactionWalletOwnershipAction as TransactionWalletOwnershipAction, + ) + from didit.types.components.travel_rule_chain_details import ( + TravelRuleChainDetails as TravelRuleChainDetails, + ) + from didit.types.components.travel_rule_chain_details import ( + TravelRuleChainDetailsUtxo as TravelRuleChainDetailsUtxo, + ) + from didit.types.components.travel_rule_chain_side import ( + TravelRuleChainSide as TravelRuleChainSide, + ) + from didit.types.components.travel_rule_counterparty_contact import ( + TravelRuleCounterpartyContact as TravelRuleCounterpartyContact, + ) + from didit.types.components.travel_rule_counterparty_decision import ( + TravelRuleCounterpartyDecision as TravelRuleCounterpartyDecision, + ) + from didit.types.components.travel_rule_counterparty_decision import ( + TravelRuleCounterpartyDecisionInheritedFrom as TravelRuleCounterpartyDecisionInheritedFrom, + ) + from didit.types.components.travel_rule_counterparty_decision_response import ( + TravelRuleCounterpartyDecisionResponse as TravelRuleCounterpartyDecisionResponse, + ) + from didit.types.components.travel_rule_counterparty_questionnaire import ( + TravelRuleCounterpartyQuestionnaire as TravelRuleCounterpartyQuestionnaire, + ) + from didit.types.components.travel_rule_counterparty_questionnaire_list import ( + TravelRuleCounterpartyQuestionnaireList as TravelRuleCounterpartyQuestionnaireList, + ) + from didit.types.components.travel_rule_counterparty_screening import ( + TravelRuleCounterpartyScreening as TravelRuleCounterpartyScreening, + ) + from didit.types.components.travel_rule_counterparty_screening_result import ( + TravelRuleCounterpartyScreeningResult as TravelRuleCounterpartyScreeningResult, + ) + from didit.types.components.travel_rule_counterparty_screening_status import ( + TravelRuleCounterpartyScreeningStatus as TravelRuleCounterpartyScreeningStatus, + ) + from didit.types.components.travel_rule_data_match import ( + TravelRuleDataMatch as TravelRuleDataMatch, + ) + from didit.types.components.travel_rule_data_match import ( + TravelRuleDataMatchFieldsValue as TravelRuleDataMatchFieldsValue, + ) + from didit.types.components.travel_rule_data_sharing_fields import ( + TravelRuleDataSharingFields as TravelRuleDataSharingFields, + ) + from didit.types.components.travel_rule_data_sharing_fields import ( + TravelRuleDataSharingFieldsParam as TravelRuleDataSharingFieldsParam, + ) + from didit.types.components.travel_rule_data_unmasked import ( + TravelRuleDataUnmasked as TravelRuleDataUnmasked, + ) + from didit.types.components.travel_rule_delivery import TravelRuleDelivery as TravelRuleDelivery + from didit.types.components.travel_rule_discovery_step import ( + TravelRuleDiscoveryStep as TravelRuleDiscoveryStep, + ) + from didit.types.components.travel_rule_inbound_response import ( + TravelRuleInboundResponse as TravelRuleInboundResponse, + ) + from didit.types.components.travel_rule_matching_rules import ( + TravelRuleMatchingRules as TravelRuleMatchingRules, + ) + from didit.types.components.travel_rule_matching_rules import ( + TravelRuleMatchingRulesCompany as TravelRuleMatchingRulesCompany, + ) + from didit.types.components.travel_rule_matching_rules import ( + TravelRuleMatchingRulesCompanyParam as TravelRuleMatchingRulesCompanyParam, + ) + from didit.types.components.travel_rule_matching_rules import ( + TravelRuleMatchingRulesIndividual as TravelRuleMatchingRulesIndividual, + ) + from didit.types.components.travel_rule_matching_rules import ( + TravelRuleMatchingRulesIndividualParam as TravelRuleMatchingRulesIndividualParam, + ) + from didit.types.components.travel_rule_matching_rules import ( + TravelRuleMatchingRulesParam as TravelRuleMatchingRulesParam, + ) + from didit.types.components.travel_rule_message import TravelRuleMessage as TravelRuleMessage + from didit.types.components.travel_rule_message_attachment import ( + TravelRuleMessageAttachment as TravelRuleMessageAttachment, + ) + from didit.types.components.travel_rule_message_attachment import ( + TravelRuleMessageAttachmentFiles as TravelRuleMessageAttachmentFiles, + ) + from didit.types.components.travel_rule_name_match import ( + TravelRuleNameMatch as TravelRuleNameMatch, + ) + from didit.types.components.travel_rule_next_action import ( + TravelRuleNextAction as TravelRuleNextAction, ) from didit.types.components.travel_rule_settings_detail import ( TravelRuleSettingsDetail as TravelRuleSettingsDetail, ) + from didit.types.components.travel_rule_settings_detail import ( + TravelRuleSettingsDetailDiditNetwork as TravelRuleSettingsDetailDiditNetwork, + ) from didit.types.components.travel_rule_settings_detail import ( TravelRuleSettingsDetailNetworks as TravelRuleSettingsDetailNetworks, ) @@ -1085,28 +1929,128 @@ from didit.types.components.travel_rule_settings_detail import ( TravelRuleSettingsDetailNetworksVerifyvasp as TravelRuleSettingsDetailNetworksVerifyvasp, ) + from didit.types.components.travel_rule_test_vasp_inbound_response import ( + TravelRuleTestVaspInboundResponse as TravelRuleTestVaspInboundResponse, + ) from didit.types.components.travel_rule_transfer_detail import ( TravelRuleTransferDetail as TravelRuleTransferDetail, ) from didit.types.components.travel_rule_transfer_detail import ( TravelRuleTransferDetailCounterpartyVasp as TravelRuleTransferDetailCounterpartyVasp, ) + from didit.types.components.travel_rule_transfer_detail import ( + TravelRuleTransferDetailCounterpartyVaspDdScoreBreakdownValue as TravelRuleTransferDetailCounterpartyVaspDdScoreBreakdownValue, + ) from didit.types.components.travel_rule_transfer_detail import ( TravelRuleTransferDetailTimeline as TravelRuleTransferDetailTimeline, ) + from didit.types.components.travel_rule_transfer_row import ( + TravelRuleTransferRow as TravelRuleTransferRow, + ) + from didit.types.components.travel_rule_utxo import TravelRuleUtxo as TravelRuleUtxo + from didit.types.components.travel_rule_utxo_entry import ( + TravelRuleUtxoEntry as TravelRuleUtxoEntry, + ) + from didit.types.components.travel_rule_utxo_entry import ( + TravelRuleUtxoEntryParam as TravelRuleUtxoEntryParam, + ) from didit.types.components.travel_rule_widget_session_response import ( TravelRuleWidgetSessionResponse as TravelRuleWidgetSessionResponse, ) - from didit.types.components.user_detail_item import UserDetailItem as UserDetailItem - from didit.types.components.user_detail_item import ( - UserDetailItemComments as UserDetailItemComments, + from didit.types.components.travel_rule_workspace import ( + TravelRuleWorkspace as TravelRuleWorkspace, + ) + from didit.types.components.travel_rule_workspace import ( + TravelRuleWorkspaceClaim as TravelRuleWorkspaceClaim, + ) + from didit.types.components.travel_rule_workspace import ( + TravelRuleWorkspaceDomain as TravelRuleWorkspaceDomain, + ) + from didit.types.components.travel_rule_workspace_domain_code_response import ( + TravelRuleWorkspaceDomainCodeResponse as TravelRuleWorkspaceDomainCodeResponse, + ) + from didit.types.components.travel_rule_workspace_txt_record import ( + TravelRuleWorkspaceTxtRecord as TravelRuleWorkspaceTxtRecord, + ) + from didit.types.components.user_detail_item import UserDetailItem as UserDetailItem + from didit.types.components.user_detail_item import ( + UserDetailItemComments as UserDetailItemComments, + ) + from didit.types.components.user_list_item import UserListItem as UserListItem + from didit.types.components.user_list_item import ( + UserListItemFeaturesList as UserListItemFeaturesList, + ) + from didit.types.components.user_list_item import UserListItemTags as UserListItemTags + from didit.types.components.user_session_list_item import ( + UserSessionListItem as UserSessionListItem, + ) + from didit.types.components.user_session_list_item import ( + UserSessionListItemAdditionalDetails as UserSessionListItemAdditionalDetails, + ) + from didit.types.components.user_session_list_item import ( + UserSessionListItemAdditionalDetailsAml as UserSessionListItemAdditionalDetailsAml, + ) + from didit.types.components.user_session_list_item import ( + UserSessionListItemAdditionalDetailsAmlScreenedData as UserSessionListItemAdditionalDetailsAmlScreenedData, + ) + from didit.types.components.user_session_list_item import ( + UserSessionListItemAdditionalDetailsDatabaseValidation as UserSessionListItemAdditionalDetailsDatabaseValidation, + ) + from didit.types.components.user_session_list_item import ( + UserSessionListItemAdditionalDetailsEmailVerification as UserSessionListItemAdditionalDetailsEmailVerification, + ) + from didit.types.components.user_session_list_item import ( + UserSessionListItemAdditionalDetailsFaceMatch as UserSessionListItemAdditionalDetailsFaceMatch, + ) + from didit.types.components.user_session_list_item import ( + UserSessionListItemAdditionalDetailsLiveness as UserSessionListItemAdditionalDetailsLiveness, + ) + from didit.types.components.user_session_list_item import ( + UserSessionListItemAdditionalDetailsPhoneVerification as UserSessionListItemAdditionalDetailsPhoneVerification, + ) + from didit.types.components.user_session_list_item import ( + UserSessionListItemAdditionalDetailsPoa as UserSessionListItemAdditionalDetailsPoa, + ) + from didit.types.components.user_session_list_item import ( + UserSessionListItemEmailAddress as UserSessionListItemEmailAddress, + ) + from didit.types.components.user_session_list_item import ( + UserSessionListItemFeatures as UserSessionListItemFeatures, + ) + from didit.types.components.user_session_list_item import ( + UserSessionListItemPhoneNumber as UserSessionListItemPhoneNumber, + ) + from didit.types.components.vasp_directory_entry import VaspDirectoryEntry as VaspDirectoryEntry + from didit.types.components.vasp_directory_entry import ( + VaspDirectoryEntryAliases as VaspDirectoryEntryAliases, + ) + from didit.types.components.vasp_directory_entry import ( + VaspDirectoryEntryContacts as VaspDirectoryEntryContacts, + ) + from didit.types.components.vasp_directory_entry import ( + VaspDirectoryEntryDdScoreBreakdownValue as VaspDirectoryEntryDdScoreBreakdownValue, + ) + from didit.types.components.vasp_directory_entry import ( + VaspDirectoryEntryEntities as VaspDirectoryEntryEntities, + ) + from didit.types.components.vasp_directory_entry import ( + VaspDirectoryEntryEssentialChecks as VaspDirectoryEntryEssentialChecks, + ) + from didit.types.components.vasp_directory_entry import ( + VaspDirectoryEntryEssentialChecksChecks as VaspDirectoryEntryEssentialChecksChecks, + ) + from didit.types.components.vasp_directory_entry import ( + VaspDirectoryEntryNetworks as VaspDirectoryEntryNetworks, + ) + from didit.types.components.vasp_directory_entry import ( + VaspDirectoryEntryResponsiveness as VaspDirectoryEntryResponsiveness, ) - from didit.types.components.user_list_item import UserListItem as UserListItem - from didit.types.components.user_list_item import ( - UserListItemFeaturesList as UserListItemFeaturesList, + from didit.types.components.vasp_directory_entry import ( + VaspDirectoryEntryRoutes as VaspDirectoryEntryRoutes, + ) + from didit.types.components.vasp_directory_entry import ( + VaspDirectoryEntryUmbrella as VaspDirectoryEntryUmbrella, ) - from didit.types.components.user_list_item import UserListItemTags as UserListItemTags - from didit.types.components.vasp_directory_entry import VaspDirectoryEntry as VaspDirectoryEntry from didit.types.components.verify_email_response import ( VerifyEmailResponse as VerifyEmailResponse, ) @@ -1114,6 +2058,15 @@ from didit.types.components.wallet_address_entry_update import ( WalletAddressEntryUpdate as WalletAddressEntryUpdate, ) + from didit.types.components.wallet_address_import_report import ( + WalletAddressImportReport as WalletAddressImportReport, + ) + from didit.types.components.wallet_address_import_report import ( + WalletAddressImportReportRows as WalletAddressImportReportRows, + ) + from didit.types.components.wallet_address_import_row import ( + WalletAddressImportRow as WalletAddressImportRow, + ) from didit.types.components.wallet_screening_result import ( WalletScreeningResult as WalletScreeningResult, ) @@ -1129,6 +2082,19 @@ from didit.types.components.wallet_screening_result import ( WalletScreeningResultSourceOfFunds as WalletScreeningResultSourceOfFunds, ) + from didit.types.components.webhook_config import WebhookConfig as WebhookConfig + from didit.types.components.webhook_config import ( + WebhookConfigAvailableEvents as WebhookConfigAvailableEvents, + ) + from didit.types.components.webhook_config_destination import ( + WebhookConfigDestination as WebhookConfigDestination, + ) + from didit.types.components.webhook_destination_delivery import ( + WebhookDestinationDelivery as WebhookDestinationDelivery, + ) + from didit.types.components.webhook_destination_delivery_detail import ( + WebhookDestinationDeliveryDetail as WebhookDestinationDeliveryDetail, + ) from didit.types.components.webhook_event import WebhookEvent as WebhookEvent from didit.types.components.webhook_subscribed_events import ( WebhookSubscribedEvents as WebhookSubscribedEvents, @@ -1136,6 +2102,7 @@ from didit.types.components.webhook_subscribed_events import ( WebhookSubscribedEventsParam as WebhookSubscribedEventsParam, ) + from didit.types.components.workflow_detail import WorkflowDetail as WorkflowDetail from didit.types.components.workflow_list_item import WorkflowListItem as WorkflowListItem from didit.types.components.workflow_list_item import ( WorkflowListItemCreatedBy as WorkflowListItemCreatedBy, @@ -1345,6 +2312,42 @@ from didit.types.id_wallets.start_vipps_params import ( IdWalletsStartVippsParams as IdWalletsStartVippsParams, ) + from didit.types.investigator.accept_run_params import ( + InvestigatorAcceptRunParams as InvestigatorAcceptRunParams, + ) + from didit.types.investigator.batch_accept_runs_params import ( + InvestigatorBatchAcceptRunsParams as InvestigatorBatchAcceptRunsParams, + ) + from didit.types.investigator.list_activity_response import ( + InvestigatorListActivityResponse as InvestigatorListActivityResponse, + ) + from didit.types.investigator.list_guardrails_response import ( + InvestigatorListGuardrailsResponse as InvestigatorListGuardrailsResponse, + ) + from didit.types.investigator.list_guardrails_response import ( + InvestigatorListGuardrailsResponseResults as InvestigatorListGuardrailsResponseResults, + ) + from didit.types.investigator.list_queue_response import ( + InvestigatorListQueueResponse as InvestigatorListQueueResponse, + ) + from didit.types.investigator.override_run_params import ( + InvestigatorOverrideRunParams as InvestigatorOverrideRunParams, + ) + from didit.types.investigator.retrieve_dashboard_response import ( + InvestigatorRetrieveDashboardResponse as InvestigatorRetrieveDashboardResponse, + ) + from didit.types.investigator.retrieve_performance_response import ( + InvestigatorRetrievePerformanceResponse as InvestigatorRetrievePerformanceResponse, + ) + from didit.types.investigator.retrieve_summary_response import ( + InvestigatorRetrieveSummaryResponse as InvestigatorRetrieveSummaryResponse, + ) + from didit.types.investigator.retrieve_transaction_response import ( + InvestigatorRetrieveTransactionResponse as InvestigatorRetrieveTransactionResponse, + ) + from didit.types.investigator.update_settings_params import ( + InvestigatorUpdateSettingsParams as InvestigatorUpdateSettingsParams, + ) from didit.types.ip.assess_risk_params import IpAssessRiskParams as IpAssessRiskParams from didit.types.kyb.list_shareholders_params import ( KybListShareholdersParams as KybListShareholdersParams, @@ -1396,15 +2399,6 @@ ) from didit.types.kyb.search_params import KybSearchParams as KybSearchParams from didit.types.kyb.search_response import KybSearchResponse as KybSearchResponse - from didit.types.kyb.search_response import ( - KybSearchResponseKybRegistry as KybSearchResponseKybRegistry, - ) - from didit.types.kyb.search_response import ( - KybSearchResponseKybRegistryCompanies as KybSearchResponseKybRegistryCompanies, - ) - from didit.types.kyb.search_response import ( - KybSearchResponseKybRegistryPagination as KybSearchResponseKybRegistryPagination, - ) from didit.types.kyb.select_params import KybSelectParams as KybSelectParams from didit.types.kyb.select_response import KybSelectResponse as KybSelectResponse from didit.types.kyb.select_response import ( @@ -1416,6 +2410,65 @@ from didit.types.kyb.select_response import ( KybSelectResponseKybRegistryOfficers as KybSelectResponseKybRegistryOfficers, ) + from didit.types.lists.create_params import ListsCreateParams as ListsCreateParams + from didit.types.lists.entries.create_params import ( + ListsEntriesCreateParams as ListsEntriesCreateParams, + ) + from didit.types.lists.entries.create_params import ( + ListsEntriesCreateParamsMetadata as ListsEntriesCreateParamsMetadata, + ) + from didit.types.lists.entries.list_response import ( + ListsEntriesListResponse as ListsEntriesListResponse, + ) + from didit.types.lists.entries.upload_face_params import ( + ListsEntriesUploadFaceParams as ListsEntriesUploadFaceParams, + ) + from didit.types.lists.list_response import ListsListResponse as ListsListResponse + from didit.types.lists.update_params import ListsUpdateParams as ListsUpdateParams + from didit.types.lists.update_response import ListsUpdateResponse as ListsUpdateResponse + from didit.types.monitoring_events.create_batch_params import ( + MonitoringEventsCreateBatchParams as MonitoringEventsCreateBatchParams, + ) + from didit.types.monitoring_events.create_batch_response import ( + MonitoringEventsCreateBatchResponse as MonitoringEventsCreateBatchResponse, + ) + from didit.types.monitoring_events.create_batch_response import ( + MonitoringEventsCreateBatchResponseResults as MonitoringEventsCreateBatchResponseResults, + ) + from didit.types.monitoring_events.create_batch_response import ( + MonitoringEventsCreateBatchResponseResultsRuleMatches as MonitoringEventsCreateBatchResponseResultsRuleMatches, + ) + from didit.types.monitoring_events.create_params import ( + MonitoringEventsCreateParams as MonitoringEventsCreateParams, + ) + from didit.types.monitoring_events.create_params import ( + MonitoringEventsCreateParamsBusiness as MonitoringEventsCreateParamsBusiness, + ) + from didit.types.monitoring_events.create_params import ( + MonitoringEventsCreateParamsDeviceContext as MonitoringEventsCreateParamsDeviceContext, + ) + from didit.types.monitoring_events.create_params import ( + MonitoringEventsCreateParamsDeviceContextNetworkContext as MonitoringEventsCreateParamsDeviceContextNetworkContext, + ) + from didit.types.monitoring_events.create_params import ( + MonitoringEventsCreateParamsSession as MonitoringEventsCreateParamsSession, + ) + from didit.types.monitoring_events.create_params import ( + MonitoringEventsCreateParamsSubject as MonitoringEventsCreateParamsSubject, + ) + from didit.types.monitoring_events.create_params import ( + MonitoringEventsCreateParamsTransactionContext as MonitoringEventsCreateParamsTransactionContext, + ) + from didit.types.monitoring_events.create_sdk_token_params import ( + MonitoringEventsCreateSdkTokenParams as MonitoringEventsCreateSdkTokenParams, + ) + from didit.types.monitoring_events.create_sdk_token_response import ( + MonitoringEventsCreateSdkTokenResponse as MonitoringEventsCreateSdkTokenResponse, + ) + from didit.types.monitoring_events.retrieve_enrichment_metrics_response import ( + MonitoringEventsRetrieveEnrichmentMetricsResponse as MonitoringEventsRetrieveEnrichmentMetricsResponse, + ) + from didit.types.networks.add_tag_params import NetworksAddTagParams as NetworksAddTagParams from didit.types.networks.list_members_response import ( NetworksListMembersResponse as NetworksListMembersResponse, ) @@ -1432,8 +2485,8 @@ from didit.types.networks.notes.list_response import ( NetworksNotesListResponse as NetworksNotesListResponse, ) - from didit.types.networks.retrieve_timeline_response import ( - NetworksRetrieveTimelineResponse as NetworksRetrieveTimelineResponse, + from didit.types.networks.remove_tag_params import ( + NetworksRemoveTagParams as NetworksRemoveTagParams, ) from didit.types.passive_liveness.check_params import ( PassiveLivenessCheckParams as PassiveLivenessCheckParams, @@ -1498,9 +2551,6 @@ from didit.types.regulatory_reports.create_params import ( RegulatoryReportsCreateParamsNarrative as RegulatoryReportsCreateParamsNarrative, ) - from didit.types.regulatory_reports.list_response import ( - RegulatoryReportsListResponse as RegulatoryReportsListResponse, - ) from didit.types.regulatory_reports.retrieve_download_url_response import ( RegulatoryReportsRetrieveDownloadUrlResponse as RegulatoryReportsRetrieveDownloadUrlResponse, ) @@ -1525,18 +2575,18 @@ from didit.types.report_templates.update_params import ( ReportTemplatesUpdateParams as ReportTemplatesUpdateParams, ) + from didit.types.sessions.add_images_params import ( + SessionsAddImagesParams as SessionsAddImagesParams, + ) + from didit.types.sessions.add_images_response import ( + SessionsAddImagesResponse as SessionsAddImagesResponse, + ) from didit.types.sessions.batch_delete_params import ( SessionsBatchDeleteParams as SessionsBatchDeleteParams, ) from didit.types.sessions.batch_delete_response import ( SessionsBatchDeleteResponse as SessionsBatchDeleteResponse, ) - from didit.types.sessions.batch_delete_response import ( - SessionsBatchDeleteResponse202 as SessionsBatchDeleteResponse202, - ) - from didit.types.sessions.batch_delete_response import ( - SessionsBatchDeleteResponse202Outcomes as SessionsBatchDeleteResponse202Outcomes, - ) from didit.types.sessions.batch_delete_response import ( SessionsBatchDeleteResponseResults as SessionsBatchDeleteResponseResults, ) @@ -1546,6 +2596,9 @@ from didit.types.sessions.bulk_update_aml_hit_status_params import ( SessionsBulkUpdateAmlHitStatusParamsHitUpdates as SessionsBulkUpdateAmlHitStatusParamsHitUpdates, ) + from didit.types.sessions.bulk_update_aml_hit_status_response import ( + SessionsBulkUpdateAmlHitStatusResponse as SessionsBulkUpdateAmlHitStatusResponse, + ) from didit.types.sessions.create_params import SessionsCreateParams as SessionsCreateParams from didit.types.sessions.create_params import ( SessionsCreateParamsContactDetails as SessionsCreateParamsContactDetails, @@ -1553,6 +2606,9 @@ from didit.types.sessions.create_params import ( SessionsCreateParamsExpectedDetails as SessionsCreateParamsExpectedDetails, ) + from didit.types.sessions.create_params import ( + SessionsCreateParamsSign as SessionsCreateParamsSign, + ) from didit.types.sessions.create_response import ( SessionsCreateResponse as SessionsCreateResponse, ) @@ -1569,51 +2625,21 @@ from didit.types.sessions.imports.list_errors_response import ( SessionsImportsListErrorsResponse as SessionsImportsListErrorsResponse, ) - from didit.types.sessions.list_networks_response import ( - SessionsListNetworksResponse as SessionsListNetworksResponse, - ) - from didit.types.sessions.list_response import SessionsListResponse as SessionsListResponse - from didit.types.sessions.list_response import ( - SessionsListResponseResultsKybSessionRowSessionKindBusiness as SessionsListResponseResultsKybSessionRowSessionKindBusiness, - ) - from didit.types.sessions.list_response import ( - SessionsListResponseResultsKycSessionRowSessionKindUser as SessionsListResponseResultsKycSessionRowSessionKindUser, - ) - from didit.types.sessions.list_response import ( - SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetails as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetails, - ) - from didit.types.sessions.list_response import ( - SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAml as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAml, + from didit.types.sessions.list_aml_screening_history_response import ( + SessionsListAmlScreeningHistoryResponse as SessionsListAmlScreeningHistoryResponse, ) - from didit.types.sessions.list_response import ( - SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAmlScreenedData as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAmlScreenedData, + from didit.types.sessions.list_events_response import ( + SessionsListEventsResponse as SessionsListEventsResponse, ) - from didit.types.sessions.list_response import ( - SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsDatabaseValidation as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsDatabaseValidation, + from didit.types.sessions.list_feature_overrides_response import ( + SessionsListFeatureOverridesResponse as SessionsListFeatureOverridesResponse, ) - from didit.types.sessions.list_response import ( - SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsEmailVerification as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsEmailVerification, - ) - from didit.types.sessions.list_response import ( - SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsFaceMatch as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsFaceMatch, - ) - from didit.types.sessions.list_response import ( - SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsLiveness as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsLiveness, - ) - from didit.types.sessions.list_response import ( - SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPhoneVerification as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPhoneVerification, - ) - from didit.types.sessions.list_response import ( - SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPoa as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPoa, - ) - from didit.types.sessions.list_response import ( - SessionsListResponseResultsKycSessionRowSessionKindUserEmailAddress as SessionsListResponseResultsKycSessionRowSessionKindUserEmailAddress, - ) - from didit.types.sessions.list_response import ( - SessionsListResponseResultsKycSessionRowSessionKindUserFeatures as SessionsListResponseResultsKycSessionRowSessionKindUserFeatures, + from didit.types.sessions.list_response import SessionsListResponse as SessionsListResponse + from didit.types.sessions.preview_decision_email_params import ( + SessionsPreviewDecisionEmailParams as SessionsPreviewDecisionEmailParams, ) - from didit.types.sessions.list_response import ( - SessionsListResponseResultsKycSessionRowSessionKindUserPhoneNumber as SessionsListResponseResultsKycSessionRowSessionKindUserPhoneNumber, + from didit.types.sessions.preview_decision_email_response import ( + SessionsPreviewDecisionEmailResponse as SessionsPreviewDecisionEmailResponse, ) from didit.types.sessions.preview_resubmission_params import ( SessionsPreviewResubmissionParams as SessionsPreviewResubmissionParams, @@ -1627,414 +2653,6 @@ from didit.types.sessions.preview_resubmission_response import ( SessionsPreviewResubmissionResponseNodesToResubmit as SessionsPreviewResubmissionResponseNodesToResubmit, ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponse as SessionsRetrieveDecisionResponse, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreenings as SessionsRetrieveDecisionResponseAmlScreenings, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsHits as SessionsRetrieveDecisionResponseAmlScreeningsHits, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaDetails as SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaDetails, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaMatches as SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaMatches, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsLinkedEntities as SessionsRetrieveDecisionResponseAmlScreeningsHitsLinkedEntities, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsPepMatches as SessionsRetrieveDecisionResponseAmlScreeningsHitsPepMatches, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsProperties as SessionsRetrieveDecisionResponseAmlScreeningsHitsProperties, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskView as SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskView, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCategories as SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCategories, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCountries as SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCountries, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCrimes as SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCrimes, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsSanctionMatches as SessionsRetrieveDecisionResponseAmlScreeningsHitsSanctionMatches, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsScoreBreakdown as SessionsRetrieveDecisionResponseAmlScreeningsHitsScoreBreakdown, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatches as SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatches, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalData as SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalData, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataData as SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataData, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataDataCaseDetails as SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataDataCaseDetails, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsScreenedData as SessionsRetrieveDecisionResponseAmlScreeningsScreenedData, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseAmlScreeningsWarnings as SessionsRetrieveDecisionResponseAmlScreeningsWarnings, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseContactDetails as SessionsRetrieveDecisionResponseContactDetails, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseDatabaseValidations as SessionsRetrieveDecisionResponseDatabaseValidations, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseDatabaseValidationsErrors as SessionsRetrieveDecisionResponseDatabaseValidationsErrors, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseDatabaseValidationsScreenedData as SessionsRetrieveDecisionResponseDatabaseValidationsScreenedData, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseDatabaseValidationsValidations as SessionsRetrieveDecisionResponseDatabaseValidationsValidations, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseDatabaseValidationsValidationsValidation as SessionsRetrieveDecisionResponseDatabaseValidationsValidationsValidation, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseDatabaseValidationsWarnings as SessionsRetrieveDecisionResponseDatabaseValidationsWarnings, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseDocumentAiDocuments as SessionsRetrieveDecisionResponseDocumentAiDocuments, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseDocumentAiDocumentsItems as SessionsRetrieveDecisionResponseDocumentAiDocumentsItems, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseDocumentAiDocumentsItemsFields as SessionsRetrieveDecisionResponseDocumentAiDocumentsItemsFields, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseDocumentVerifications as SessionsRetrieveDecisionResponseDocumentVerifications, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseDocumentVerificationsGroupsValue as SessionsRetrieveDecisionResponseDocumentVerificationsGroupsValue, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseDocumentVerificationsItems as SessionsRetrieveDecisionResponseDocumentVerificationsItems, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata as SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulation as SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulation, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions as SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseEmailVerifications as SessionsRetrieveDecisionResponseEmailVerifications, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseEmailVerificationsBreaches as SessionsRetrieveDecisionResponseEmailVerificationsBreaches, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseEmailVerificationsLifecycle as SessionsRetrieveDecisionResponseEmailVerificationsLifecycle, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseEmailVerificationsLifecycleDetails as SessionsRetrieveDecisionResponseEmailVerificationsLifecycleDetails, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseEmailVerificationsMatches as SessionsRetrieveDecisionResponseEmailVerificationsMatches, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseEmailVerificationsWarnings as SessionsRetrieveDecisionResponseEmailVerificationsWarnings, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseExpectedDetails as SessionsRetrieveDecisionResponseExpectedDetails, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseFaceMatches as SessionsRetrieveDecisionResponseFaceMatches, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseFaceMatchesWarnings as SessionsRetrieveDecisionResponseFaceMatchesWarnings, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerifications as SessionsRetrieveDecisionResponseIdVerifications, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsBackImageQualityScore as SessionsRetrieveDecisionResponseIdVerificationsBackImageQualityScore, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsDocumentLiveness as SessionsRetrieveDecisionResponseIdVerificationsDocumentLiveness, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPortraitReplace as SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPortraitReplace, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPrintedCopy as SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPrintedCopy, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessScreenReplay as SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessScreenReplay, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatches as SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatches, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatchesUserDetails as SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatchesUserDetails, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsExtraFields as SessionsRetrieveDecisionResponseIdVerificationsExtraFields, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsFallbackFrom as SessionsRetrieveDecisionResponseIdVerificationsFallbackFrom, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsFrontImageQualityScore as SessionsRetrieveDecisionResponseIdVerificationsFrontImageQualityScore, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsIdLookup as SessionsRetrieveDecisionResponseIdVerificationsIdLookup, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsIdLookupComparison as SessionsRetrieveDecisionResponseIdVerificationsIdLookupComparison, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsMatches as SessionsRetrieveDecisionResponseIdVerificationsMatches, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsMatchesUserDetails as SessionsRetrieveDecisionResponseIdVerificationsMatchesUserDetails, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsMrz as SessionsRetrieveDecisionResponseIdVerificationsMrz, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddress as SessionsRetrieveDecisionResponseIdVerificationsParsedAddress, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressDocumentLocation as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressDocumentLocation, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResults as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResults, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsAddressComponents as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsAddressComponents, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometry as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometry, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryLocation as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryLocation, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewport as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewport, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportNortheast as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportNortheast, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPoints as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPoints, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPointsLocation as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPointsLocation, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsWalletVerification as SessionsRetrieveDecisionResponseIdVerificationsWalletVerification, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIdVerificationsWarnings as SessionsRetrieveDecisionResponseIdVerificationsWarnings, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIpAnalyses as SessionsRetrieveDecisionResponseIpAnalyses, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIpAnalysesIdDocument as SessionsRetrieveDecisionResponseIpAnalysesIdDocument, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIpAnalysesIdDocumentLocation as SessionsRetrieveDecisionResponseIpAnalysesIdDocumentLocation, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIpAnalysesIp as SessionsRetrieveDecisionResponseIpAnalysesIp, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIpAnalysesIpLocation as SessionsRetrieveDecisionResponseIpAnalysesIpLocation, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIpAnalysesMatches as SessionsRetrieveDecisionResponseIpAnalysesMatches, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIpAnalysesMatchesDeviceInfo as SessionsRetrieveDecisionResponseIpAnalysesMatchesDeviceInfo, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIpAnalysesMatchesLocationInfo as SessionsRetrieveDecisionResponseIpAnalysesMatchesLocationInfo, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIpAnalysesPoaDocument as SessionsRetrieveDecisionResponseIpAnalysesPoaDocument, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIpAnalysesPoaDocumentLocation as SessionsRetrieveDecisionResponseIpAnalysesPoaDocumentLocation, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseIpAnalysesWarnings as SessionsRetrieveDecisionResponseIpAnalysesWarnings, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseKeyPeopleChecks as SessionsRetrieveDecisionResponseKeyPeopleChecks, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseKeyPeopleChecksRegistry as SessionsRetrieveDecisionResponseKeyPeopleChecksRegistry, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners as SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories as SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures as SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers as SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersKycAmlCategories as SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersKycAmlCategories, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures as SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseKeyPeopleChecksSubmitted as SessionsRetrieveDecisionResponseKeyPeopleChecksSubmitted, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties as SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesRoles as SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesRoles, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures as SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseKeyPeopleChecksUboKycSummary as SessionsRetrieveDecisionResponseKeyPeopleChecksUboKycSummary, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseLivenessChecks as SessionsRetrieveDecisionResponseLivenessChecks, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseLivenessChecksMatches as SessionsRetrieveDecisionResponseLivenessChecksMatches, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseLivenessChecksMatchesUserDetails as SessionsRetrieveDecisionResponseLivenessChecksMatchesUserDetails, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseLivenessChecksWarnings as SessionsRetrieveDecisionResponseLivenessChecksWarnings, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseNfcVerifications as SessionsRetrieveDecisionResponseNfcVerifications, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseNfcVerificationsAuthenticity as SessionsRetrieveDecisionResponseNfcVerificationsAuthenticity, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseNfcVerificationsCertificateSummary as SessionsRetrieveDecisionResponseNfcVerificationsCertificateSummary, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseNfcVerificationsChipData as SessionsRetrieveDecisionResponseNfcVerificationsChipData, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseNfcVerificationsWarnings as SessionsRetrieveDecisionResponseNfcVerificationsWarnings, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponsePhoneVerifications as SessionsRetrieveDecisionResponsePhoneVerifications, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponsePhoneVerificationsCarrier as SessionsRetrieveDecisionResponsePhoneVerificationsCarrier, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponsePhoneVerificationsLifecycle as SessionsRetrieveDecisionResponsePhoneVerificationsLifecycle, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponsePhoneVerificationsLifecycleDetails as SessionsRetrieveDecisionResponsePhoneVerificationsLifecycleDetails, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponsePhoneVerificationsMatches as SessionsRetrieveDecisionResponsePhoneVerificationsMatches, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponsePhoneVerificationsWarnings as SessionsRetrieveDecisionResponsePhoneVerificationsWarnings, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponsePoaVerifications as SessionsRetrieveDecisionResponsePoaVerifications, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadata as SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadata, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulation as SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulation, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions as SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponsePoaVerificationsExtraFields as SessionsRetrieveDecisionResponsePoaVerificationsExtraFields, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddress as SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddress, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddressDocumentLocation as SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddressDocumentLocation, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponsePoaVerificationsWarnings as SessionsRetrieveDecisionResponsePoaVerificationsWarnings, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseQuestionnaireResponses as SessionsRetrieveDecisionResponseQuestionnaireResponses, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseQuestionnaireResponsesSections as SessionsRetrieveDecisionResponseQuestionnaireResponsesSections, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItems as SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItems, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsAnswer as SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsAnswer, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsChoices as SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsChoices, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseRegistryChecks as SessionsRetrieveDecisionResponseRegistryChecks, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseRegistryChecksCompany as SessionsRetrieveDecisionResponseRegistryChecksCompany, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseRegistryChecksCompanyBeneficialOwners as SessionsRetrieveDecisionResponseRegistryChecksCompanyBeneficialOwners, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseRegistryChecksCompanyOfficers as SessionsRetrieveDecisionResponseRegistryChecksCompanyOfficers, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseResubmissions as SessionsRetrieveDecisionResponseResubmissions, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseResubmissionsNodes as SessionsRetrieveDecisionResponseResubmissionsNodes, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseResubmissionsRequestedBy as SessionsRetrieveDecisionResponseResubmissionsRequestedBy, - ) - from didit.types.sessions.retrieve_decision_response import ( - SessionsRetrieveDecisionResponseReviews as SessionsRetrieveDecisionResponseReviews, - ) - from didit.types.sessions.retrieve_deletion_job_response import ( - SessionsRetrieveDeletionJobResponse as SessionsRetrieveDeletionJobResponse, - ) - from didit.types.sessions.retrieve_deletion_job_response import ( - SessionsRetrieveDeletionJobResponseOutcomes as SessionsRetrieveDeletionJobResponseOutcomes, - ) from didit.types.sessions.reviews.create_params import ( SessionsReviewsCreateParams as SessionsReviewsCreateParams, ) @@ -2046,9 +2664,27 @@ ) from didit.types.sessions.share_params import SessionsShareParams as SessionsShareParams from didit.types.sessions.share_response import SessionsShareResponse as SessionsShareResponse + from didit.types.sessions.signatures.validate_params import ( + SessionsSignaturesValidateParams as SessionsSignaturesValidateParams, + ) + from didit.types.sessions.signatures.validate_response import ( + SessionsSignaturesValidateResponse as SessionsSignaturesValidateResponse, + ) + from didit.types.sessions.simulate_params import ( + SessionsSimulateParams as SessionsSimulateParams, + ) from didit.types.sessions.update_aml_hit_status_params import ( SessionsUpdateAmlHitStatusParams as SessionsUpdateAmlHitStatusParams, ) + from didit.types.sessions.update_aml_hit_status_response import ( + SessionsUpdateAmlHitStatusResponse as SessionsUpdateAmlHitStatusResponse, + ) + from didit.types.sessions.update_aml_status_params import ( + SessionsUpdateAmlStatusParams as SessionsUpdateAmlStatusParams, + ) + from didit.types.sessions.update_aml_status_response import ( + SessionsUpdateAmlStatusResponse as SessionsUpdateAmlStatusResponse, + ) from didit.types.sessions.update_data_params import ( SessionsUpdateDataParams as SessionsUpdateDataParams, ) @@ -2061,6 +2697,12 @@ from didit.types.sessions.update_data_response import ( SessionsUpdateDataResponseParsedAddress as SessionsUpdateDataResponseParsedAddress, ) + from didit.types.sessions.update_document_ai_data_params import ( + SessionsUpdateDocumentAiDataParams as SessionsUpdateDocumentAiDataParams, + ) + from didit.types.sessions.update_document_ai_data_response import ( + SessionsUpdateDocumentAiDataResponse as SessionsUpdateDocumentAiDataResponse, + ) from didit.types.sessions.update_feature_status_params import ( SessionsUpdateFeatureStatusParams as SessionsUpdateFeatureStatusParams, ) @@ -2151,11 +2793,8 @@ from didit.types.transactions.create_sdk_token_response import ( TransactionsCreateSdkTokenResponse as TransactionsCreateSdkTokenResponse, ) - from didit.types.transactions.list_networks_response import ( - TransactionsListNetworksResponse as TransactionsListNetworksResponse, - ) - from didit.types.transactions.list_party_networks_response import ( - TransactionsListPartyNetworksResponse as TransactionsListPartyNetworksResponse, + from didit.types.transactions.list_related_response import ( + TransactionsListRelatedResponse as TransactionsListRelatedResponse, ) from didit.types.transactions.list_response import ( TransactionsListResponse as TransactionsListResponse, @@ -2166,6 +2805,12 @@ from didit.types.transactions.list_response import ( TransactionsListResponseResultsTags as TransactionsListResponseResultsTags, ) + from didit.types.transactions.notes.create_params import ( + TransactionsNotesCreateParams as TransactionsNotesCreateParams, + ) + from didit.types.transactions.notes.list_response import ( + TransactionsNotesListResponse as TransactionsNotesListResponse, + ) from didit.types.transactions.rules.backtest_params import ( TransactionsRulesBacktestParams as TransactionsRulesBacktestParams, ) @@ -2190,9 +2835,21 @@ from didit.types.transactions.travel_rule.confirm_ownership_params import ( TransactionsTravelRuleConfirmOwnershipParams as TransactionsTravelRuleConfirmOwnershipParams, ) + from didit.types.transactions.travel_rule.create_message_params import ( + TransactionsTravelRuleCreateMessageParams as TransactionsTravelRuleCreateMessageParams, + ) + from didit.types.transactions.travel_rule.trigger_test_vasp_inbound_params import ( + TransactionsTravelRuleTriggerTestVaspInboundParams as TransactionsTravelRuleTriggerTestVaspInboundParams, + ) from didit.types.transactions.travel_rule.update_transfer_params import ( TransactionsTravelRuleUpdateTransferParams as TransactionsTravelRuleUpdateTransferParams, ) + from didit.types.travel_rule.counterparty_contacts.create_params import ( + TravelRuleCounterpartyContactsCreateParams as TravelRuleCounterpartyContactsCreateParams, + ) + from didit.types.travel_rule.counterparty_contacts.list_response import ( + TravelRuleCounterpartyContactsListResponse as TravelRuleCounterpartyContactsListResponse, + ) from didit.types.travel_rule.create_widget_session_params import ( TravelRuleCreateWidgetSessionParams as TravelRuleCreateWidgetSessionParams, ) @@ -2205,9 +2862,27 @@ from didit.types.travel_rule.register_inbound_params import ( TravelRuleRegisterInboundParamsOriginatingVasp as TravelRuleRegisterInboundParamsOriginatingVasp, ) + from didit.types.travel_rule.screen_vasp_params import ( + TravelRuleScreenVaspParams as TravelRuleScreenVaspParams, + ) + from didit.types.travel_rule.send_compliance_email_confirmation_response import ( + TravelRuleSendComplianceEmailConfirmationResponse as TravelRuleSendComplianceEmailConfirmationResponse, + ) + from didit.types.travel_rule.send_vasp_questionnaire_params import ( + TravelRuleSendVaspQuestionnaireParams as TravelRuleSendVaspQuestionnaireParams, + ) + from didit.types.travel_rule.transfers.list_response import ( + TravelRuleTransfersListResponse as TravelRuleTransfersListResponse, + ) from didit.types.travel_rule.update_settings_params import ( TravelRuleUpdateSettingsParams as TravelRuleUpdateSettingsParams, ) + from didit.types.travel_rule.update_vasp_decision_params import ( + TravelRuleUpdateVaspDecisionParams as TravelRuleUpdateVaspDecisionParams, + ) + from didit.types.travel_rule.wallet_addresses.bulk_import_params import ( + TravelRuleWalletAddressesBulkImportParamsVariant1 as TravelRuleWalletAddressesBulkImportParamsVariant1, + ) from didit.types.travel_rule.wallet_addresses.create_params import ( TravelRuleWalletAddressesCreateParams as TravelRuleWalletAddressesCreateParams, ) @@ -2217,6 +2892,15 @@ from didit.types.travel_rule.wallet_addresses.update_params import ( TravelRuleWalletAddressesUpdateParams as TravelRuleWalletAddressesUpdateParams, ) + from didit.types.travel_rule.workspace.claim_params import ( + TravelRuleWorkspaceClaimParams as TravelRuleWorkspaceClaimParams, + ) + from didit.types.travel_rule.workspace.confirm_domain_params import ( + TravelRuleWorkspaceConfirmDomainParams as TravelRuleWorkspaceConfirmDomainParams, + ) + from didit.types.travel_rule.workspace.enable_billing_params import ( + TravelRuleWorkspaceEnableBillingParams as TravelRuleWorkspaceEnableBillingParams, + ) from didit.types.users.batch_delete_params import ( UsersBatchDeleteParams as UsersBatchDeleteParams, ) @@ -2224,73 +2908,27 @@ UsersBatchDeleteResponse as UsersBatchDeleteResponse, ) from didit.types.users.create_params import UsersCreateParams as UsersCreateParams - from didit.types.users.list_networks_response import ( - UsersListNetworksResponse as UsersListNetworksResponse, + from didit.types.users.create_response import UsersCreateResponse as UsersCreateResponse + from didit.types.users.create_response import ( + UsersCreateResponseComments as UsersCreateResponseComments, ) - from didit.types.users.list_response import UsersListResponse as UsersListResponse - from didit.types.users.retrieve_ongoing_monitoring_job_response import ( - UsersRetrieveOngoingMonitoringJobResponse as UsersRetrieveOngoingMonitoringJobResponse, + from didit.types.users.create_response import ( + UsersCreateResponseFeaturesList as UsersCreateResponseFeaturesList, ) - from didit.types.users.retrieve_ongoing_monitoring_job_response import ( - UsersRetrieveOngoingMonitoringJobResponseEstimate as UsersRetrieveOngoingMonitoringJobResponseEstimate, + from didit.types.users.create_response import UsersCreateResponseTags as UsersCreateResponseTags + from didit.types.users.create_response import ( + UsersCreateResponseTagsTag as UsersCreateResponseTagsTag, ) - from didit.types.users.retrieve_ongoing_monitoring_job_response import ( - UsersRetrieveOngoingMonitoringJobResponseOutcomes as UsersRetrieveOngoingMonitoringJobResponseOutcomes, + from didit.types.users.create_response import ( + UsersCreateResponseVerificationLink as UsersCreateResponseVerificationLink, ) + from didit.types.users.list_response import UsersListResponse as UsersListResponse from didit.types.users.set_aml_monitoring_params import ( UsersSetAmlMonitoringParams as UsersSetAmlMonitoringParams, ) - from didit.types.users.set_aml_monitoring_response import ( - UsersSetAmlMonitoringResponse202 as UsersSetAmlMonitoringResponse202, - ) - from didit.types.users.set_aml_monitoring_response import ( - UsersSetAmlMonitoringResponse202Estimate as UsersSetAmlMonitoringResponse202Estimate, - ) - from didit.types.users.set_aml_monitoring_response import ( - UsersSetAmlMonitoringResponse202Outcomes as UsersSetAmlMonitoringResponse202Outcomes, - ) - from didit.types.users.set_aml_monitoring_response import ( - UsersSetAmlMonitoringResponse202Results as UsersSetAmlMonitoringResponse202Results, - ) - from didit.types.users.set_aml_monitoring_response import ( - UsersSetAmlMonitoringResponseVariant1 as UsersSetAmlMonitoringResponseVariant1, - ) - from didit.types.users.set_aml_monitoring_response import ( - UsersSetAmlMonitoringResponseVariant2 as UsersSetAmlMonitoringResponseVariant2, - ) - from didit.types.users.set_aml_monitoring_response import ( - UsersSetAmlMonitoringResponseVariant2Estimate as UsersSetAmlMonitoringResponseVariant2Estimate, - ) - from didit.types.users.set_aml_monitoring_response import ( - UsersSetAmlMonitoringResponseVariant2Outcomes as UsersSetAmlMonitoringResponseVariant2Outcomes, - ) - from didit.types.users.set_aml_monitoring_response import ( - UsersSetAmlMonitoringResponseVariant2Results as UsersSetAmlMonitoringResponseVariant2Results, - ) from didit.types.users.set_ongoing_monitoring_params import ( UsersSetOngoingMonitoringParams as UsersSetOngoingMonitoringParams, ) - from didit.types.users.set_ongoing_monitoring_response import ( - UsersSetOngoingMonitoringResponse202 as UsersSetOngoingMonitoringResponse202, - ) - from didit.types.users.set_ongoing_monitoring_response import ( - UsersSetOngoingMonitoringResponse202Estimate as UsersSetOngoingMonitoringResponse202Estimate, - ) - from didit.types.users.set_ongoing_monitoring_response import ( - UsersSetOngoingMonitoringResponse202Outcomes as UsersSetOngoingMonitoringResponse202Outcomes, - ) - from didit.types.users.set_ongoing_monitoring_response import ( - UsersSetOngoingMonitoringResponseVariant1 as UsersSetOngoingMonitoringResponseVariant1, - ) - from didit.types.users.set_ongoing_monitoring_response import ( - UsersSetOngoingMonitoringResponseVariant2 as UsersSetOngoingMonitoringResponseVariant2, - ) - from didit.types.users.set_ongoing_monitoring_response import ( - UsersSetOngoingMonitoringResponseVariant2Estimate as UsersSetOngoingMonitoringResponseVariant2Estimate, - ) - from didit.types.users.set_ongoing_monitoring_response import ( - UsersSetOngoingMonitoringResponseVariant2Outcomes as UsersSetOngoingMonitoringResponseVariant2Outcomes, - ) from didit.types.users.update_params import UsersUpdateParams as UsersUpdateParams from didit.types.users.update_status_params import ( UsersUpdateStatusParams as UsersUpdateStatusParams, @@ -2326,9 +2964,24 @@ from didit.types.webhooks.destinations.create_response import ( WebhooksDestinationsCreateResponseSummary as WebhooksDestinationsCreateResponseSummary, ) + from didit.types.webhooks.destinations.list_deliveries_response import ( + WebhooksDestinationsListDeliveriesResponse as WebhooksDestinationsListDeliveriesResponse, + ) from didit.types.webhooks.destinations.list_response import ( WebhooksDestinationsListResponseItem as WebhooksDestinationsListResponseItem, ) + from didit.types.webhooks.destinations.list_response import ( + WebhooksDestinationsListResponseItemDailyPoints as WebhooksDestinationsListResponseItemDailyPoints, + ) + from didit.types.webhooks.destinations.retrieve_analytics_response import ( + WebhooksDestinationsRetrieveAnalyticsResponse as WebhooksDestinationsRetrieveAnalyticsResponse, + ) + from didit.types.webhooks.destinations.retrieve_analytics_response import ( + WebhooksDestinationsRetrieveAnalyticsResponsePoints as WebhooksDestinationsRetrieveAnalyticsResponsePoints, + ) + from didit.types.webhooks.destinations.retrieve_analytics_response import ( + WebhooksDestinationsRetrieveAnalyticsResponseSummary as WebhooksDestinationsRetrieveAnalyticsResponseSummary, + ) from didit.types.webhooks.destinations.retrieve_response import ( WebhooksDestinationsRetrieveResponse as WebhooksDestinationsRetrieveResponse, ) @@ -2344,16 +2997,53 @@ from didit.types.webhooks.destinations.update_response import ( WebhooksDestinationsUpdateResponseSummary as WebhooksDestinationsUpdateResponseSummary, ) + from didit.types.webhooks.update_config_params import ( + WebhooksUpdateConfigParams as WebhooksUpdateConfigParams, + ) from didit.types.workflows.create_params import WorkflowsCreateParams as WorkflowsCreateParams from didit.types.workflows.list_response import WorkflowsListResponse as WorkflowsListResponse from didit.types.workflows.update_params import WorkflowsUpdateParams as WorkflowsUpdateParams _MODULES: dict[str, str] = { + "AMLDatasetFreshness": "components.aml_dataset_freshness", + "AMLDatasetFreshnessSources": "components.aml_dataset_freshness", + "AMLDatasetSourceFreshness": "components.aml_dataset_source_freshness", + "AMLMonitoringJob": "components.aml_monitoring_job", + "AMLMonitoringJobEstimate": "components.aml_monitoring_job", + "AMLMonitoringJobOutcomes": "components.aml_monitoring_job", + "AMLMonitoringJobResults": "components.aml_monitoring_job", + "AMLMonitoringToggleResult": "components.aml_monitoring_toggle_result", + "AMLMonitoringToggleResultVariant1": "components.aml_monitoring_toggle_result", + "AMLMonitoringToggleResultVariant2": "components.aml_monitoring_toggle_result", + "AMLMonitoringToggleResultVariant2Estimate": "components.aml_monitoring_toggle_result", + "AMLMonitoringToggleResultVariant2Outcomes": "components.aml_monitoring_toggle_result", + "AMLMonitoringToggleResultVariant2Results": "components.aml_monitoring_toggle_result", + "AMLScreening": "components.aml_screening", + "AMLScreeningHits": "components.aml_screening", + "AMLScreeningHitsAdverseMediaDetails": "components.aml_screening", + "AMLScreeningHitsAdverseMediaMatches": "components.aml_screening", + "AMLScreeningHitsLinkedEntities": "components.aml_screening", + "AMLScreeningHitsPepMatches": "components.aml_screening", + "AMLScreeningHitsProperties": "components.aml_screening", + "AMLScreeningHitsRiskView": "components.aml_screening", + "AMLScreeningHitsRiskViewCategories": "components.aml_screening", + "AMLScreeningHitsRiskViewCountries": "components.aml_screening", + "AMLScreeningHitsRiskViewCrimes": "components.aml_screening", + "AMLScreeningHitsSanctionMatches": "components.aml_screening", + "AMLScreeningHitsScoreBreakdown": "components.aml_screening", + "AMLScreeningHitsWarningMatches": "components.aml_screening", + "AMLScreeningHitsWarningMatchesAdditionalData": "components.aml_screening", + "AMLScreeningHitsWarningMatchesAdditionalDataData": "components.aml_screening", + "AMLScreeningHitsWarningMatchesAdditionalDataDataCaseDetails": "components.aml_screening", + "AMLScreeningRun": "components.aml_screening_run", + "AMLScreeningScreenedData": "components.aml_screening", + "AMLScreeningWarnings": "components.aml_screening", "AccountApplicationsCreateParams": "account.applications.create_params", "AccountApplicationsUpdateParams": "account.applications.update_params", "AccountLoginParams": "account.login_params", "AccountRegisterParams": "account.register_params", "AccountResendOtpParams": "account.resend_otp_params", + "AccountResendOtpResponse": "account.resend_otp_response", "AccountVerifyEmailParams": "account.verify_email_params", "AgeEstimationEstimateParams": "age_estimation.estimate_params", "AgeEstimationEstimateResponse": "age_estimation.estimate_response", @@ -2364,6 +3054,7 @@ "AmlMonitoringRetrieveJobResponse": "aml_monitoring.retrieve_job_response", "AmlMonitoringRetrieveJobResponseOutcomes": "aml_monitoring.retrieve_job_response", "AmlMonitoringRetrieveJobResponseResults": "aml_monitoring.retrieve_job_response", + "AmlRetrieveDatasetFreshnessResponse": "aml.retrieve_dataset_freshness_response", "AmlScreenParams": "aml.screen_params", "AmlScreenResponse": "aml.screen_response", "AmlScreenResponseAml": "aml.screen_response", @@ -2375,7 +3066,41 @@ "AmlScreenResponseAmlScreenedData": "aml.screen_response", "AmlScreenResponseAmlWarnings": "aml.screen_response", "Application": "components.application", + "ApplicationAnalyticsCountryCount": "components.application_analytics_country_count", + "ApplicationAnalyticsDataPoint": "components.application_analytics_data_point", + "ApplicationAnalyticsDemographicPoint": "components.application_analytics_demographic_point", + "ApplicationAnalyticsDemographicStats": "components.application_analytics_demographic_stats", + "ApplicationAnalyticsDemographicStatsAgeDistribution": "components.application_analytics_demographic_stats", + "ApplicationAnalyticsDeviceDetails": "components.application_analytics_device_details", + "ApplicationAnalyticsDeviceDetailsBrowsers": "components.application_analytics_device_details", + "ApplicationAnalyticsDeviceDetailsDeviceModels": "components.application_analytics_device_details", + "ApplicationAnalyticsDeviceDetailsOperatingSystems": "components.application_analytics_device_details", + "ApplicationAnalyticsDevicePoint": "components.application_analytics_device_point", + "ApplicationAnalyticsLocationStats": "components.application_analytics_location_stats", + "ApplicationAnalyticsLocationStatsCities": "components.application_analytics_location_stats", + "ApplicationAnalyticsLocationStatsConversionByCountry": "components.application_analytics_location_stats", + "ApplicationAnalyticsOriginalStatusPoint": "components.application_analytics_original_status_point", + "ApplicationAnalyticsOriginalStatusStats": "components.application_analytics_original_status_stats", + "ApplicationAnalyticsRequestBreakdown": "components.application_analytics_request_breakdown", + "ApplicationAnalyticsResubmissionPoint": "components.application_analytics_resubmission_point", + "ApplicationAnalyticsResubmissionStats": "components.application_analytics_resubmission_stats", + "ApplicationAnalyticsSummary": "components.application_analytics_summary", + "ApplicationAnalyticsVerificationTimePoint": "components.application_analytics_verification_time_point", + "ApplicationAnalyticsVerificationTimeStats": "components.application_analytics_verification_time_stats", + "ApplicationAnalyticsVerificationTimeStatsPerFeatureValue": "components.application_analytics_verification_time_stats", + "ApplicationAnalyticsVerificationTimeStatsPerSubStepValueValue": "components.application_analytics_verification_time_stats", + "ApplicationAnalyticsWarningStats": "components.application_analytics_warning_stats", + "ApplicationAnalyticsWarningStatsValue": "components.application_analytics_warning_stats", + "ApplicationAnalyticsWarningStatsValueRisksValue": "components.application_analytics_warning_stats", + "ApplicationAnalyticsWidget": "components.application_analytics_widget", + "ApplicationAnalyticsWidgetResponse": "components.application_analytics_widget_response", + "ApplicationAnalyticsWidgetSearch": "components.application_analytics_widget", + "ApplicationAnalyticsWidgetThresholds": "components.application_analytics_widget", + "ApplicationAnalyticsWorkflowFunnel": "components.application_analytics_workflow_funnel", + "ApplicationAnalyticsWorkflowFunnelValue": "components.application_analytics_workflow_funnel", + "ApplicationAnalyticsWorkflowFunnelValueStepsValue": "components.application_analytics_workflow_funnel", "ApplicationCredentialsSummary": "components.application_credentials_summary", + "ApplicationProduct": "components.application_product", "BillingRetrieveBalanceResponse": "billing.retrieve_balance_response", "BillingTopUpParams": "billing.top_up_params", "BillingTopUpResponse": "billing.top_up_response", @@ -2387,49 +3112,25 @@ "BiometricTemplatesListResponse": "biometric_templates.list_response", "BiometricTemplatesListResponseResults": "biometric_templates.list_response", "BiometricTemplatesRetrieveResponse": "biometric_templates.retrieve_response", + "BlocklistAddParams": "blocklist.add_params", + "BlocklistAddResponse": "blocklist.add_response", + "BlocklistListResponse": "blocklist.list_response", + "BlocklistRemoveParams": "blocklist.remove_params", + "BlocklistRemoveResponse": "blocklist.remove_response", + "BlocklistedItem": "components.blocklisted_item", "BrandingCustomization": "components.branding_customization", "BusinessDetailItem": "components.business_detail_item", "BusinessListItem": "components.business_list_item", "BusinessListItemFeaturesList": "components.business_list_item", "BusinessListItemTags": "components.business_list_item", - "BusinessSessionsListNetworksResponse": "business_sessions.list_networks_response", + "BusinessSessionListItem": "components.business_session_list_item", "BusinessesBatchDeleteParams": "businesses.batch_delete_params", "BusinessesBatchDeleteResponse": "businesses.batch_delete_response", "BusinessesCreateParams": "businesses.create_params", - "BusinessesListNetworksResponse": "businesses.list_networks_response", "BusinessesListResponse": "businesses.list_response", - "BusinessesRetrieveOngoingMonitoringJobResponse": "businesses.retrieve_ongoing_monitoring_job_response", - "BusinessesRetrieveOngoingMonitoringJobResponseEstimate": "businesses.retrieve_ongoing_monitoring_job_response", - "BusinessesRetrieveOngoingMonitoringJobResponseOutcomes": "businesses.retrieve_ongoing_monitoring_job_response", - "BusinessesRetrieveSessionOngoingMonitoringJobResponse": "businesses.retrieve_session_ongoing_monitoring_job_response", - "BusinessesRetrieveSessionOngoingMonitoringJobResponseEstimate": "businesses.retrieve_session_ongoing_monitoring_job_response", - "BusinessesRetrieveSessionOngoingMonitoringJobResponseOutcomes": "businesses.retrieve_session_ongoing_monitoring_job_response", "BusinessesSetAmlMonitoringParams": "businesses.set_aml_monitoring_params", - "BusinessesSetAmlMonitoringResponse202": "businesses.set_aml_monitoring_response", - "BusinessesSetAmlMonitoringResponse202Estimate": "businesses.set_aml_monitoring_response", - "BusinessesSetAmlMonitoringResponse202Outcomes": "businesses.set_aml_monitoring_response", - "BusinessesSetAmlMonitoringResponse202Results": "businesses.set_aml_monitoring_response", - "BusinessesSetAmlMonitoringResponseVariant1": "businesses.set_aml_monitoring_response", - "BusinessesSetAmlMonitoringResponseVariant2": "businesses.set_aml_monitoring_response", - "BusinessesSetAmlMonitoringResponseVariant2Estimate": "businesses.set_aml_monitoring_response", - "BusinessesSetAmlMonitoringResponseVariant2Outcomes": "businesses.set_aml_monitoring_response", - "BusinessesSetAmlMonitoringResponseVariant2Results": "businesses.set_aml_monitoring_response", "BusinessesSetOngoingMonitoringParams": "businesses.set_ongoing_monitoring_params", - "BusinessesSetOngoingMonitoringResponse202": "businesses.set_ongoing_monitoring_response", - "BusinessesSetOngoingMonitoringResponse202Estimate": "businesses.set_ongoing_monitoring_response", - "BusinessesSetOngoingMonitoringResponse202Outcomes": "businesses.set_ongoing_monitoring_response", - "BusinessesSetOngoingMonitoringResponseVariant1": "businesses.set_ongoing_monitoring_response", - "BusinessesSetOngoingMonitoringResponseVariant2": "businesses.set_ongoing_monitoring_response", - "BusinessesSetOngoingMonitoringResponseVariant2Estimate": "businesses.set_ongoing_monitoring_response", - "BusinessesSetOngoingMonitoringResponseVariant2Outcomes": "businesses.set_ongoing_monitoring_response", "BusinessesSetSessionOngoingMonitoringParams": "businesses.set_session_ongoing_monitoring_params", - "BusinessesSetSessionOngoingMonitoringResponse202": "businesses.set_session_ongoing_monitoring_response", - "BusinessesSetSessionOngoingMonitoringResponse202Estimate": "businesses.set_session_ongoing_monitoring_response", - "BusinessesSetSessionOngoingMonitoringResponse202Outcomes": "businesses.set_session_ongoing_monitoring_response", - "BusinessesSetSessionOngoingMonitoringResponseVariant1": "businesses.set_session_ongoing_monitoring_response", - "BusinessesSetSessionOngoingMonitoringResponseVariant2": "businesses.set_session_ongoing_monitoring_response", - "BusinessesSetSessionOngoingMonitoringResponseVariant2Estimate": "businesses.set_session_ongoing_monitoring_response", - "BusinessesSetSessionOngoingMonitoringResponseVariant2Outcomes": "businesses.set_session_ongoing_monitoring_response", "BusinessesUpdateParams": "businesses.update_params", "BusinessesUpdateStatusParams": "businesses.update_status_params", "CaseActionActionEnum": "components.case_action_action_enum", @@ -2451,7 +3152,11 @@ "CaseBlueprintDetailCreationSources": "components.case_blueprint_detail", "CaseBlueprintDetailReceivesEscalationFrom": "components.case_blueprint_detail", "CaseBlueprintListItem": "components.case_blueprint_list_item", + "CaseBlueprintPresetInstallResult": "components.case_blueprint_preset_install_result", "CaseBlueprintSummary": "components.case_blueprint_summary", + "CaseBulkUpdateResult": "components.case_bulk_update_result", + "CaseBulkUpdateResultFailures": "components.case_bulk_update_result", + "CaseChecker": "components.case_checker", "CaseChecklistItem": "components.case_checklist_item", "CaseCollaborationCaseNote": "components.case_collaboration_case_note", "CaseCollaborationCaseNoteReply": "components.case_collaboration_case_note_reply", @@ -2486,6 +3191,7 @@ "CaseEvent": "components.case_event", "CaseLink": "components.case_link", "CaseListItem": "components.case_list_item", + "CaseListItemPendingResolution": "components.case_list_item", "CaseNote": "components.case_note", "CaseNoteAttachments": "components.case_note", "CasePublicationAssignmentMethodEnum": "components.case_publication_assignment_method_enum", @@ -2505,12 +3211,12 @@ "CaseRoutingRule": "components.case_routing_rule", "CaseRoutingRuleCreate": "components.case_routing_rule_create", "CaseSubject": "components.case_subject", + "CaseTeamSummary": "components.case_team_summary", "CasesApplyEntityActionParams": "cases.apply_entity_action_params", "CasesAssignParams": "cases.assign_params", "CasesBlueprintsCreateParams": "cases.blueprints.create_params", "CasesBlueprintsDiscardDraftParams": "cases.blueprints.discard_draft_params", "CasesBlueprintsInstallPresetsParams": "cases.blueprints.install_presets_params", - "CasesBlueprintsInstallPresetsResponse": "cases.blueprints.install_presets_response", "CasesBlueprintsListPresetsResponse": "cases.blueprints.list_presets_response", "CasesBlueprintsListPresetsResponseResults": "cases.blueprints.list_presets_response", "CasesBlueprintsListResponse": "cases.blueprints.list_response", @@ -2518,11 +3224,7 @@ "CasesBlueprintsSaveDraftParams": "cases.blueprints.save_draft_params", "CasesBlueprintsUpdateParams": "cases.blueprints.update_params", "CasesBulkAssignParams": "cases.bulk_assign_params", - "CasesBulkAssignResponse": "cases.bulk_assign_response", - "CasesBulkAssignResponseFailures": "cases.bulk_assign_response", "CasesBulkUpdateStatusParams": "cases.bulk_update_status_params", - "CasesBulkUpdateStatusResponse": "cases.bulk_update_status_response", - "CasesBulkUpdateStatusResponseFailures": "cases.bulk_update_status_response", "CasesChecklistToggleItemParams": "cases.checklist.toggle_item_params", "CasesCreateParams": "cases.create_params", "CasesCreateParamsSubject": "cases.create_params", @@ -2540,10 +3242,10 @@ "CasesPrintResponse": "cases.print_response", "CasesRegulatoryReportsCreateParams": "cases.regulatory_reports.create_params", "CasesRegulatoryReportsCreateParamsNarrative": "cases.regulatory_reports.create_params", - "CasesRegulatoryReportsListResponse": "cases.regulatory_reports.list_response", "CasesRejectResolutionParams": "cases.reject_resolution_params", "CasesRequestsCreateParams": "cases.requests.create_params", "CasesRequestsPreviewParams": "cases.requests.preview_params", + "CasesRequestsRemindResponse": "cases.requests.remind_response", "CasesResolveParams": "cases.resolve_params", "CasesRetrieveStatisticsResponse": "cases.retrieve_statistics_response", "CasesRetrieveStatisticsResponseQueue": "cases.retrieve_statistics_response", @@ -2566,6 +3268,9 @@ "CasesTeamsUpdateParams": "cases.teams.update_params", "CasesTransferParams": "cases.transfer_params", "CasesUpdateParams": "cases.update_params", + "ComplianceList": "components.compliance_list", + "ComplianceListEntry": "components.compliance_list_entry", + "ComplianceListEntryMetadata": "components.compliance_list_entry", "CustomerMessagingCaseRequest": "components.customer_messaging_case_request", "CustomerMessagingCaseRequestContact": "components.customer_messaging_case_request_contact", "CustomerMessagingCaseRequestOptions": "components.customer_messaging_case_request_options", @@ -2639,6 +3344,7 @@ "EmailVerificationCheckResponseEmailWarnings": "components.email_verification_check_response", "EmailVerificationSendResponse": "components.email_verification_send_response", "EntityNetworkMembership": "components.entity_network_membership", + "EntityNetworkMembershipList": "components.entity_network_membership_list", "FaceMatchCompareParams": "face_match.compare_params", "FaceMatchCompareResponse": "face_match.compare_response", "FaceMatchCompareResponseFaceMatch": "face_match.compare_response", @@ -2655,6 +3361,8 @@ "FaceSearchSearchResponseFaceSearchUserImage": "face_search.search_response", "FaceSearchSearchResponseFaceSearchUserImageEntities": "face_search.search_response", "FaceSearchSearchResponseFaceSearchWarnings": "face_search.search_response", + "FeatureStatusOverride": "components.feature_status_override", + "FeatureStatusOverrideRequestedBy": "components.feature_status_override", "FraudAvailabilityEnum": "components.fraud_availability_enum", "FraudBandEnum": "components.fraud_band_enum", "FraudCheckAttributesRequest": "components.fraud_check_attributes_request", @@ -2716,11 +3424,39 @@ "IdWalletsStartSmartIdParams": "id_wallets.start_smart_id_params", "IdWalletsStartUaePassParams": "id_wallets.start_uae_pass_params", "IdWalletsStartVippsParams": "id_wallets.start_vipps_params", + "InvestigatorAcceptRunParams": "investigator.accept_run_params", + "InvestigatorBatchAcceptResponse": "components.investigator_batch_accept_response", + "InvestigatorBatchAcceptResponseResults": "components.investigator_batch_accept_response", + "InvestigatorBatchAcceptRunsParams": "investigator.batch_accept_runs_params", + "InvestigatorBlock": "components.investigator_block", + "InvestigatorDecision": "components.investigator_decision", + "InvestigatorDecisionOriginalOutcome": "components.investigator_decision", + "InvestigatorDecisionOverrides": "components.investigator_decision", + "InvestigatorListActivityResponse": "investigator.list_activity_response", + "InvestigatorListGuardrailsResponse": "investigator.list_guardrails_response", + "InvestigatorListGuardrailsResponseResults": "investigator.list_guardrails_response", + "InvestigatorListQueueResponse": "investigator.list_queue_response", + "InvestigatorOverrideRunParams": "investigator.override_run_params", + "InvestigatorQueueRow": "components.investigator_queue_row", + "InvestigatorRetrieveDashboardResponse": "investigator.retrieve_dashboard_response", + "InvestigatorRetrievePerformanceResponse": "investigator.retrieve_performance_response", + "InvestigatorRetrieveSummaryResponse": "investigator.retrieve_summary_response", + "InvestigatorRetrieveTransactionResponse": "investigator.retrieve_transaction_response", + "InvestigatorRunDetail": "components.investigator_run_detail", + "InvestigatorSettings": "components.investigator_settings", + "InvestigatorSettingsSpendLimit": "components.investigator_settings", + "InvestigatorUpdateSettingsParams": "investigator.update_settings_params", "IpAssessRiskParams": "ip.assess_risk_params", + "KYBKeyPeopleParty": "components.kyb_key_people_party", + "KYBKeyPeoplePartyRoles": "components.kyb_key_people_party", + "KYBKeyPeoplePartyVerificationWorkflowFeatures": "components.kyb_key_people_party", "KYBRegistryAddress": "components.kyb_registry_address", "KYBRegistryIndustry": "components.kyb_registry_industry", "KYBRegistryOwnership": "components.kyb_registry_ownership", "KYBRegistryOwnershipUnresolved": "components.kyb_registry_ownership", + "KYBRegistrySearchResults": "components.kyb_registry_search_results", + "KYBRegistrySearchResultsCompanies": "components.kyb_registry_search_results", + "KYBRegistrySearchResultsPagination": "components.kyb_registry_search_results", "KYBRegistryShareholding": "components.kyb_registry_shareholding", "KybListShareholdersParams": "kyb.list_shareholders_params", "KybListShareholdersResponse": "kyb.list_shareholders_response", @@ -2742,20 +3478,66 @@ "KybListUbosResponseKybRegistryOfficers": "kyb.list_ubos_response", "KybSearchParams": "kyb.search_params", "KybSearchResponse": "kyb.search_response", - "KybSearchResponseKybRegistry": "kyb.search_response", - "KybSearchResponseKybRegistryCompanies": "kyb.search_response", - "KybSearchResponseKybRegistryPagination": "kyb.search_response", "KybSelectParams": "kyb.select_params", "KybSelectResponse": "kyb.select_response", "KybSelectResponseKybRegistry": "kyb.select_response", "KybSelectResponseKybRegistryBeneficialOwners": "kyb.select_response", "KybSelectResponseKybRegistryOfficers": "kyb.select_response", + "ListsCreateParams": "lists.create_params", + "ListsEntriesCreateParams": "lists.entries.create_params", + "ListsEntriesCreateParamsMetadata": "lists.entries.create_params", + "ListsEntriesListResponse": "lists.entries.list_response", + "ListsEntriesUploadFaceParams": "lists.entries.upload_face_params", + "ListsListResponse": "lists.list_response", + "ListsUpdateParams": "lists.update_params", + "ListsUpdateResponse": "lists.update_response", "LoginResponse": "components.login_response", + "MonitoringEvent": "components.monitoring_event", + "MonitoringEventCreateRequest": "components.monitoring_event_create_request", + "MonitoringEventCreateRequestBusiness": "components.monitoring_event_create_request", + "MonitoringEventCreateRequestDeviceContext": "components.monitoring_event_create_request", + "MonitoringEventCreateRequestDeviceContextNetworkContext": "components.monitoring_event_create_request", + "MonitoringEventCreateRequestSession": "components.monitoring_event_create_request", + "MonitoringEventCreateRequestSubject": "components.monitoring_event_create_request", + "MonitoringEventCreateRequestTransactionContext": "components.monitoring_event_create_request", + "MonitoringEventDetail": "components.monitoring_event_detail", + "MonitoringEventDetailIpEnrichment": "components.monitoring_event_detail", + "MonitoringEventDetailSubjectProfile": "components.monitoring_event_detail", + "MonitoringEventIngestResult": "components.monitoring_event_ingest_result", + "MonitoringEventIngestResultRuleMatches": "components.monitoring_event_ingest_result", + "MonitoringEventPage": "components.monitoring_event_page", + "MonitoringEventRuleRun": "components.monitoring_event_rule_run", + "MonitoringEventsCreateBatchParams": "monitoring_events.create_batch_params", + "MonitoringEventsCreateBatchResponse": "monitoring_events.create_batch_response", + "MonitoringEventsCreateBatchResponseResults": "monitoring_events.create_batch_response", + "MonitoringEventsCreateBatchResponseResultsRuleMatches": "monitoring_events.create_batch_response", + "MonitoringEventsCreateParams": "monitoring_events.create_params", + "MonitoringEventsCreateParamsBusiness": "monitoring_events.create_params", + "MonitoringEventsCreateParamsDeviceContext": "monitoring_events.create_params", + "MonitoringEventsCreateParamsDeviceContextNetworkContext": "monitoring_events.create_params", + "MonitoringEventsCreateParamsSession": "monitoring_events.create_params", + "MonitoringEventsCreateParamsSubject": "monitoring_events.create_params", + "MonitoringEventsCreateParamsTransactionContext": "monitoring_events.create_params", + "MonitoringEventsCreateSdkTokenParams": "monitoring_events.create_sdk_token_params", + "MonitoringEventsCreateSdkTokenResponse": "monitoring_events.create_sdk_token_response", + "MonitoringEventsRetrieveEnrichmentMetricsResponse": "monitoring_events.retrieve_enrichment_metrics_response", "NetworkDetail": "components.network_detail", + "NetworkDetailResponse": "components.network_detail_response", "NetworkGraph": "components.network_graph", "NetworkGraphEdge": "components.network_graph_edge", "NetworkGraphMetrics": "components.network_graph", "NetworkGraphNode": "components.network_graph_node", + "NetworkListCounts": "components.network_list_counts", + "NetworkListCountsFacets": "components.network_list_counts", + "NetworkListCountsFacetsAssignee": "components.network_list_counts", + "NetworkListCountsFacetsAssigneeUsers": "components.network_list_counts", + "NetworkListCountsFacetsLastActivityDays": "components.network_list_counts", + "NetworkListCountsFacetsMinSize": "components.network_list_counts", + "NetworkListCountsFacetsPriority": "components.network_list_counts", + "NetworkListCountsFacetsRiskMin": "components.network_list_counts", + "NetworkListCountsFacetsSignalTypes": "components.network_list_counts", + "NetworkListCountsScopes": "components.network_list_counts", + "NetworkListCountsViews": "components.network_list_counts", "NetworkListItem": "components.network_list_item", "NetworkMap": "components.network_map", "NetworkMapPoints": "components.network_map", @@ -2763,18 +3545,45 @@ "NetworkMemberProvenance": "components.network_member", "NetworkNodeTag": "components.network_node_tag", "NetworkNote": "components.network_note", + "NetworkOutcomes": "components.network_outcomes", + "NetworkPatternScore": "components.network_pattern_score", "NetworkRiskDisplay": "components.network_risk_display", "NetworkSignal": "components.network_signal", + "NetworkTag": "components.network_tag", + "NetworkTimeline": "components.network_timeline", + "NetworkTimelineAccount": "components.network_timeline_account", + "NetworkTimelineAmount": "components.network_timeline_amount", + "NetworkTimelineAttempt": "components.network_timeline_attempt", + "NetworkTimelineCaseLinked": "components.network_timeline_case_linked", + "NetworkTimelineDeclineReason": "components.network_timeline_decline_reason", + "NetworkTimelineDevice": "components.network_timeline_device", "NetworkTimelineEvent": "components.network_timeline_event", + "NetworkTimelineIP": "components.network_timeline_ip", + "NetworkTimelineLogin": "components.network_timeline_login", + "NetworkTimelineNoteAdded": "components.network_timeline_note_added", + "NetworkTimelinePage": "components.network_timeline_page", + "NetworkTimelineRoute": "components.network_timeline_route", + "NetworkTimelineRuleHit": "components.network_timeline_rule_hit", + "NetworkTimelineSourceRef": "components.network_timeline_source_ref", + "NetworkTimelineTransaction": "components.network_timeline_transaction", + "NetworkTypedTimelineEvent": "components.network_typed_timeline_event", + "NetworksAddTagParams": "networks.add_tag_params", "NetworksListMembersResponse": "networks.list_members_response", "NetworksListResponse": "networks.list_response", "NetworksListResponseKpis": "networks.list_response", "NetworksListSignalsResponse": "networks.list_signals_response", "NetworksNotesCreateParams": "networks.notes.create_params", "NetworksNotesListResponse": "networks.notes.list_response", - "NetworksRetrieveTimelineResponse": "networks.retrieve_timeline_response", + "NetworksRemoveTagParams": "networks.remove_tag_params", + "OngoingMonitoringJob": "components.ongoing_monitoring_job", + "OngoingMonitoringJobEstimate": "components.ongoing_monitoring_job", + "OngoingMonitoringJobOutcomes": "components.ongoing_monitoring_job", + "OngoingMonitoringToggleResult": "components.ongoing_monitoring_toggle_result", + "OngoingMonitoringToggleResultVariant1": "components.ongoing_monitoring_toggle_result", "Organization": "components.organization", "OrganizationSummary": "components.organization_summary", + "PaginatedApplicationList": "components.paginated_application_list", + "PaginatedOrganizationList": "components.paginated_organization_list", "PassiveLivenessCheckParams": "passive_liveness.check_params", "PassiveLivenessCheckResponse": "passive_liveness.check_response", "PassiveLivenessCheckResponseLiveness": "passive_liveness.check_response", @@ -2815,9 +3624,9 @@ "RegulatoryReportDetail": "components.regulatory_report_detail", "RegulatoryReportDetailNarrative": "components.regulatory_report_detail", "RegulatoryReportDetailValidationErrors": "components.regulatory_report_detail", + "RegulatoryReportList": "components.regulatory_report_list", "RegulatoryReportsCreateParams": "regulatory_reports.create_params", "RegulatoryReportsCreateParamsNarrative": "regulatory_reports.create_params", - "RegulatoryReportsListResponse": "regulatory_reports.list_response", "RegulatoryReportsRetrieveDownloadUrlResponse": "regulatory_reports.retrieve_download_url_response", "RegulatoryReportsUpdateParams": "regulatory_reports.update_params", "RegulatoryReportsUpdateParamsNarrative": "regulatory_reports.update_params", @@ -2839,190 +3648,178 @@ "RoutingPublicationPolicyDraftResponse": "components.routing_publication_policy_draft_response", "RoutingPublicationPolicyRevision": "components.routing_publication_policy_revision", "RoutingPublicationRoutingCondition": "components.routing_publication_routing_condition", + "SandboxSimulateResponse": "components.sandbox_simulate_response", + "SessionDecision": "components.session_decision", + "SessionDecisionContactDetails": "components.session_decision", + "SessionDecisionDatabaseValidations": "components.session_decision", + "SessionDecisionDatabaseValidationsErrors": "components.session_decision", + "SessionDecisionDatabaseValidationsScreenedData": "components.session_decision", + "SessionDecisionDatabaseValidationsValidations": "components.session_decision", + "SessionDecisionDatabaseValidationsValidationsValidation": "components.session_decision", + "SessionDecisionDatabaseValidationsWarnings": "components.session_decision", + "SessionDecisionDocumentAiDocuments": "components.session_decision", + "SessionDecisionDocumentAiDocumentsItems": "components.session_decision", + "SessionDecisionDocumentAiDocumentsItemsFields": "components.session_decision", + "SessionDecisionDocumentVerifications": "components.session_decision", + "SessionDecisionDocumentVerificationsGroupsValue": "components.session_decision", + "SessionDecisionDocumentVerificationsItems": "components.session_decision", + "SessionDecisionDocumentVerificationsItemsDocumentMetadata": "components.session_decision", + "SessionDecisionDocumentVerificationsItemsDocumentMetadataOverlayManipulation": "components.session_decision", + "SessionDecisionDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions": "components.session_decision", + "SessionDecisionEmailVerifications": "components.session_decision", + "SessionDecisionEmailVerificationsBreaches": "components.session_decision", + "SessionDecisionEmailVerificationsLifecycle": "components.session_decision", + "SessionDecisionEmailVerificationsLifecycleDetails": "components.session_decision", + "SessionDecisionEmailVerificationsMatches": "components.session_decision", + "SessionDecisionEmailVerificationsWarnings": "components.session_decision", + "SessionDecisionExpectedDetails": "components.session_decision", + "SessionDecisionFaceMatches": "components.session_decision", + "SessionDecisionFaceMatchesWarnings": "components.session_decision", + "SessionDecisionIdVerifications": "components.session_decision", + "SessionDecisionIdVerificationsBackImageQualityScore": "components.session_decision", + "SessionDecisionIdVerificationsDocumentLiveness": "components.session_decision", + "SessionDecisionIdVerificationsDocumentLivenessPortraitReplace": "components.session_decision", + "SessionDecisionIdVerificationsDocumentLivenessPrintedCopy": "components.session_decision", + "SessionDecisionIdVerificationsDocumentLivenessScreenReplay": "components.session_decision", + "SessionDecisionIdVerificationsDocumentSelfieFaceMatches": "components.session_decision", + "SessionDecisionIdVerificationsDocumentSelfieFaceMatchesUserDetails": "components.session_decision", + "SessionDecisionIdVerificationsExtraFields": "components.session_decision", + "SessionDecisionIdVerificationsFallbackFrom": "components.session_decision", + "SessionDecisionIdVerificationsFrontImageQualityScore": "components.session_decision", + "SessionDecisionIdVerificationsIdLookup": "components.session_decision", + "SessionDecisionIdVerificationsIdLookupComparison": "components.session_decision", + "SessionDecisionIdVerificationsMatches": "components.session_decision", + "SessionDecisionIdVerificationsMatchesUserDetails": "components.session_decision", + "SessionDecisionIdVerificationsMrz": "components.session_decision", + "SessionDecisionIdVerificationsParsedAddress": "components.session_decision", + "SessionDecisionIdVerificationsParsedAddressDocumentLocation": "components.session_decision", + "SessionDecisionIdVerificationsParsedAddressRawResults": "components.session_decision", + "SessionDecisionIdVerificationsParsedAddressRawResultsAddressComponents": "components.session_decision", + "SessionDecisionIdVerificationsParsedAddressRawResultsGeometry": "components.session_decision", + "SessionDecisionIdVerificationsParsedAddressRawResultsGeometryLocation": "components.session_decision", + "SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewport": "components.session_decision", + "SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewportNortheast": "components.session_decision", + "SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest": "components.session_decision", + "SessionDecisionIdVerificationsParsedAddressRawResultsNavigationPoints": "components.session_decision", + "SessionDecisionIdVerificationsParsedAddressRawResultsNavigationPointsLocation": "components.session_decision", + "SessionDecisionIdVerificationsWalletVerification": "components.session_decision", + "SessionDecisionIdVerificationsWarnings": "components.session_decision", + "SessionDecisionIpAnalyses": "components.session_decision", + "SessionDecisionIpAnalysesIdDocument": "components.session_decision", + "SessionDecisionIpAnalysesIdDocumentLocation": "components.session_decision", + "SessionDecisionIpAnalysesIp": "components.session_decision", + "SessionDecisionIpAnalysesIpLocation": "components.session_decision", + "SessionDecisionIpAnalysesMatches": "components.session_decision", + "SessionDecisionIpAnalysesMatchesDeviceInfo": "components.session_decision", + "SessionDecisionIpAnalysesMatchesLocationInfo": "components.session_decision", + "SessionDecisionIpAnalysesPoaDocument": "components.session_decision", + "SessionDecisionIpAnalysesPoaDocumentLocation": "components.session_decision", + "SessionDecisionIpAnalysesWarnings": "components.session_decision", + "SessionDecisionKeyPeopleChecks": "components.session_decision", + "SessionDecisionKeyPeopleChecksRegistry": "components.session_decision", + "SessionDecisionKeyPeopleChecksRegistryBeneficialOwners": "components.session_decision", + "SessionDecisionKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories": "components.session_decision", + "SessionDecisionKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures": "components.session_decision", + "SessionDecisionKeyPeopleChecksRegistryOfficers": "components.session_decision", + "SessionDecisionKeyPeopleChecksRegistryOfficersKycAmlCategories": "components.session_decision", + "SessionDecisionKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures": "components.session_decision", + "SessionDecisionKeyPeopleChecksSubmitted": "components.session_decision", + "SessionDecisionKeyPeopleChecksSubmittedParties": "components.session_decision", + "SessionDecisionKeyPeopleChecksSubmittedPartiesRoles": "components.session_decision", + "SessionDecisionKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures": "components.session_decision", + "SessionDecisionKeyPeopleChecksUboKycSummary": "components.session_decision", + "SessionDecisionLivenessChecks": "components.session_decision", + "SessionDecisionLivenessChecksMatches": "components.session_decision", + "SessionDecisionLivenessChecksMatchesUserDetails": "components.session_decision", + "SessionDecisionLivenessChecksWarnings": "components.session_decision", + "SessionDecisionNfcVerifications": "components.session_decision", + "SessionDecisionNfcVerificationsAuthenticity": "components.session_decision", + "SessionDecisionNfcVerificationsCertificateSummary": "components.session_decision", + "SessionDecisionNfcVerificationsChipData": "components.session_decision", + "SessionDecisionNfcVerificationsJpki": "components.session_decision", + "SessionDecisionNfcVerificationsJpkiAttributes": "components.session_decision", + "SessionDecisionNfcVerificationsJpkiCertificates": "components.session_decision", + "SessionDecisionNfcVerificationsWarnings": "components.session_decision", + "SessionDecisionPhoneVerifications": "components.session_decision", + "SessionDecisionPhoneVerificationsCarrier": "components.session_decision", + "SessionDecisionPhoneVerificationsLifecycle": "components.session_decision", + "SessionDecisionPhoneVerificationsLifecycleDetails": "components.session_decision", + "SessionDecisionPhoneVerificationsMatches": "components.session_decision", + "SessionDecisionPhoneVerificationsWarnings": "components.session_decision", + "SessionDecisionPoaVerifications": "components.session_decision", + "SessionDecisionPoaVerificationsDocumentMetadata": "components.session_decision", + "SessionDecisionPoaVerificationsDocumentMetadataOverlayManipulation": "components.session_decision", + "SessionDecisionPoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions": "components.session_decision", + "SessionDecisionPoaVerificationsExtraFields": "components.session_decision", + "SessionDecisionPoaVerificationsPoaParsedAddress": "components.session_decision", + "SessionDecisionPoaVerificationsPoaParsedAddressDocumentLocation": "components.session_decision", + "SessionDecisionPoaVerificationsWarnings": "components.session_decision", + "SessionDecisionQuestionnaireResponses": "components.session_decision", + "SessionDecisionQuestionnaireResponsesSections": "components.session_decision", + "SessionDecisionQuestionnaireResponsesSectionsItems": "components.session_decision", + "SessionDecisionQuestionnaireResponsesSectionsItemsAnswer": "components.session_decision", + "SessionDecisionQuestionnaireResponsesSectionsItemsChoices": "components.session_decision", + "SessionDecisionRegistryChecks": "components.session_decision", + "SessionDecisionRegistryChecksCompany": "components.session_decision", + "SessionDecisionRegistryChecksCompanyBeneficialOwners": "components.session_decision", + "SessionDecisionRegistryChecksCompanyOfficers": "components.session_decision", + "SessionDecisionResubmissions": "components.session_decision", + "SessionDecisionResubmissionsNodes": "components.session_decision", + "SessionDecisionResubmissionsRequestedBy": "components.session_decision", + "SessionDecisionReviews": "components.session_decision", + "SessionDeletionJob": "components.session_deletion_job", + "SessionDeletionJobOutcomes": "components.session_deletion_job", + "SessionEvent": "components.session_event", "SessionImportError": "components.session_import_error", "SessionImportJob": "components.session_import_job", + "SessionSignDocument": "components.session_sign_document", + "SessionWebhookDelivery": "components.session_webhook_delivery", + "SessionsAddImagesParams": "sessions.add_images_params", + "SessionsAddImagesResponse": "sessions.add_images_response", "SessionsBatchDeleteParams": "sessions.batch_delete_params", "SessionsBatchDeleteResponse": "sessions.batch_delete_response", - "SessionsBatchDeleteResponse202": "sessions.batch_delete_response", - "SessionsBatchDeleteResponse202Outcomes": "sessions.batch_delete_response", "SessionsBatchDeleteResponseResults": "sessions.batch_delete_response", "SessionsBulkUpdateAmlHitStatusParams": "sessions.bulk_update_aml_hit_status_params", "SessionsBulkUpdateAmlHitStatusParamsHitUpdates": "sessions.bulk_update_aml_hit_status_params", + "SessionsBulkUpdateAmlHitStatusResponse": "sessions.bulk_update_aml_hit_status_response", "SessionsCreateParams": "sessions.create_params", "SessionsCreateParamsContactDetails": "sessions.create_params", "SessionsCreateParamsExpectedDetails": "sessions.create_params", + "SessionsCreateParamsSign": "sessions.create_params", "SessionsCreateResponse": "sessions.create_response", "SessionsDeleteParams": "sessions.delete_params", "SessionsDeleteResponse": "sessions.delete_response", "SessionsImportSharedParams": "sessions.import_shared_params", "SessionsImportsCreateParams": "sessions.imports.create_params", "SessionsImportsListErrorsResponse": "sessions.imports.list_errors_response", - "SessionsListNetworksResponse": "sessions.list_networks_response", + "SessionsListAmlScreeningHistoryResponse": "sessions.list_aml_screening_history_response", + "SessionsListEventsResponse": "sessions.list_events_response", + "SessionsListFeatureOverridesResponse": "sessions.list_feature_overrides_response", "SessionsListResponse": "sessions.list_response", - "SessionsListResponseResultsKybSessionRowSessionKindBusiness": "sessions.list_response", - "SessionsListResponseResultsKycSessionRowSessionKindUser": "sessions.list_response", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetails": "sessions.list_response", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAml": "sessions.list_response", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAmlScreenedData": "sessions.list_response", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsDatabaseValidation": "sessions.list_response", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsEmailVerification": "sessions.list_response", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsFaceMatch": "sessions.list_response", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsLiveness": "sessions.list_response", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPhoneVerification": "sessions.list_response", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPoa": "sessions.list_response", - "SessionsListResponseResultsKycSessionRowSessionKindUserEmailAddress": "sessions.list_response", - "SessionsListResponseResultsKycSessionRowSessionKindUserFeatures": "sessions.list_response", - "SessionsListResponseResultsKycSessionRowSessionKindUserPhoneNumber": "sessions.list_response", + "SessionsPreviewDecisionEmailParams": "sessions.preview_decision_email_params", + "SessionsPreviewDecisionEmailResponse": "sessions.preview_decision_email_response", "SessionsPreviewResubmissionParams": "sessions.preview_resubmission_params", "SessionsPreviewResubmissionParamsNodesToResubmit": "sessions.preview_resubmission_params", "SessionsPreviewResubmissionResponse": "sessions.preview_resubmission_response", "SessionsPreviewResubmissionResponseNodesToResubmit": "sessions.preview_resubmission_response", - "SessionsRetrieveDecisionResponse": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreenings": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsHits": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaDetails": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaMatches": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsLinkedEntities": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsPepMatches": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsProperties": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskView": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCategories": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCountries": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCrimes": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsSanctionMatches": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsScoreBreakdown": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatches": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalData": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataData": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataDataCaseDetails": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsScreenedData": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseAmlScreeningsWarnings": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseContactDetails": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseDatabaseValidations": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseDatabaseValidationsErrors": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseDatabaseValidationsScreenedData": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseDatabaseValidationsValidations": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseDatabaseValidationsValidationsValidation": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseDatabaseValidationsWarnings": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseDocumentAiDocuments": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseDocumentAiDocumentsItems": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseDocumentAiDocumentsItemsFields": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseDocumentVerifications": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseDocumentVerificationsGroupsValue": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseDocumentVerificationsItems": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulation": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseEmailVerifications": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseEmailVerificationsBreaches": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseEmailVerificationsLifecycle": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseEmailVerificationsLifecycleDetails": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseEmailVerificationsMatches": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseEmailVerificationsWarnings": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseExpectedDetails": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseFaceMatches": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseFaceMatchesWarnings": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerifications": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsBackImageQualityScore": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentLiveness": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPortraitReplace": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPrintedCopy": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessScreenReplay": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatches": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatchesUserDetails": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsExtraFields": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsFallbackFrom": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsFrontImageQualityScore": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsIdLookup": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsIdLookupComparison": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsMatches": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsMatchesUserDetails": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsMrz": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddress": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressDocumentLocation": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResults": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsAddressComponents": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometry": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryLocation": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewport": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportNortheast": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPoints": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPointsLocation": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsWalletVerification": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIdVerificationsWarnings": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIpAnalyses": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIpAnalysesIdDocument": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIpAnalysesIdDocumentLocation": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIpAnalysesIp": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIpAnalysesIpLocation": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIpAnalysesMatches": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIpAnalysesMatchesDeviceInfo": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIpAnalysesMatchesLocationInfo": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIpAnalysesPoaDocument": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIpAnalysesPoaDocumentLocation": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseIpAnalysesWarnings": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseKeyPeopleChecks": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistry": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersKycAmlCategories": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmitted": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesRoles": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseKeyPeopleChecksUboKycSummary": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseLivenessChecks": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseLivenessChecksMatches": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseLivenessChecksMatchesUserDetails": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseLivenessChecksWarnings": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseNfcVerifications": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseNfcVerificationsAuthenticity": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseNfcVerificationsCertificateSummary": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseNfcVerificationsChipData": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseNfcVerificationsWarnings": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponsePhoneVerifications": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponsePhoneVerificationsCarrier": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponsePhoneVerificationsLifecycle": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponsePhoneVerificationsLifecycleDetails": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponsePhoneVerificationsMatches": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponsePhoneVerificationsWarnings": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponsePoaVerifications": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadata": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulation": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponsePoaVerificationsExtraFields": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddress": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddressDocumentLocation": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponsePoaVerificationsWarnings": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseQuestionnaireResponses": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseQuestionnaireResponsesSections": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItems": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsAnswer": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsChoices": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseRegistryChecks": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseRegistryChecksCompany": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseRegistryChecksCompanyBeneficialOwners": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseRegistryChecksCompanyOfficers": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseResubmissions": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseResubmissionsNodes": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseResubmissionsRequestedBy": "sessions.retrieve_decision_response", - "SessionsRetrieveDecisionResponseReviews": "sessions.retrieve_decision_response", - "SessionsRetrieveDeletionJobResponse": "sessions.retrieve_deletion_job_response", - "SessionsRetrieveDeletionJobResponseOutcomes": "sessions.retrieve_deletion_job_response", "SessionsReviewsCreateParams": "sessions.reviews.create_params", "SessionsReviewsCreateResponse": "sessions.reviews.create_response", "SessionsReviewsListResponse": "sessions.reviews.list_response", "SessionsShareParams": "sessions.share_params", "SessionsShareResponse": "sessions.share_response", + "SessionsSignaturesValidateParams": "sessions.signatures.validate_params", + "SessionsSignaturesValidateResponse": "sessions.signatures.validate_response", + "SessionsSimulateParams": "sessions.simulate_params", "SessionsUpdateAmlHitStatusParams": "sessions.update_aml_hit_status_params", + "SessionsUpdateAmlHitStatusResponse": "sessions.update_aml_hit_status_response", + "SessionsUpdateAmlStatusParams": "sessions.update_aml_status_params", + "SessionsUpdateAmlStatusResponse": "sessions.update_aml_status_response", "SessionsUpdateDataParams": "sessions.update_data_params", "SessionsUpdateDataParamsParsedAddress": "sessions.update_data_params", "SessionsUpdateDataResponse": "sessions.update_data_response", "SessionsUpdateDataResponseParsedAddress": "sessions.update_data_response", + "SessionsUpdateDocumentAiDataParams": "sessions.update_document_ai_data_params", + "SessionsUpdateDocumentAiDataResponse": "sessions.update_document_ai_data_response", "SessionsUpdateFeatureStatusParams": "sessions.update_feature_status_params", "SessionsUpdateFeatureStatusResponse": "sessions.update_feature_status_response", "SessionsUpdateKybCompanyDataParams": "sessions.update_kyb_company_data_params", @@ -3036,6 +3833,25 @@ "SessionsUpdateStatusParams": "sessions.update_status_params", "SessionsUpdateStatusParamsNodesToResubmit": "sessions.update_status_params", "SessionsUpdateStatusResponse": "sessions.update_status_response", + "SignatureField": "components.signature_field", + "SignatureFieldAnchor": "components.signature_field_anchor", + "SignatureFieldResult": "components.signature_field_result", + "SignatureFieldValidation": "components.signature_field_validation", + "SignatureResult": "components.signature_result", + "SignatureResultAdoptedSignature": "components.signature_result", + "SignatureResultCertificate": "components.signature_result", + "SignatureResultConfirmation": "components.signature_result", + "SignatureResultDocuments": "components.signature_result", + "SignatureResultDocumentsConversion": "components.signature_result", + "SignatureResultIdentityUsed": "components.signature_result", + "SignatureResultSigner": "components.signature_result", + "SignatureResultTimestamp": "components.signature_result", + "SignatureResultValidation": "components.signature_result", + "SignatureResultValidationDocuments": "components.signature_result", + "SignatureResultValidationDocumentsCertificate": "components.signature_result", + "SignatureResultValidationDocumentsTimestamp": "components.signature_result", + "SignatureResultValidationDocumentsTrustedList": "components.signature_result", + "SignatureResultWarnings": "components.signature_result", "SimpleQuestionnaireElement": "components.simple_questionnaire_element", "SimpleQuestionnaireElementOptions": "components.simple_questionnaire_element", "SimpleQuestionnaireElementValidation": "components.simple_questionnaire_element", @@ -3044,8 +3860,6 @@ "SocialFootprintPlatform": "components.social_footprint_platform", "SocialFootprintProfile": "components.social_footprint_profile", "TransactionDetail": "components.transaction_detail", - "TransactionDetailActionRequiredVerificationSession": "components.transaction_detail", - "TransactionDetailActionRequiredWalletOwnership": "components.transaction_detail", "TransactionDetailActivities": "components.transaction_detail", "TransactionDetailAlerts": "components.transaction_detail", "TransactionDetailDocuments": "components.transaction_detail", @@ -3059,21 +3873,22 @@ "TransactionDetailRemediation": "components.transaction_detail", "TransactionDetailRuleRuns": "components.transaction_detail", "TransactionDetailTags": "components.transaction_detail", - "TransactionDetailTravelRule": "components.transaction_detail", + "TransactionNote": "components.transaction_note", + "TransactionNoteMetadata": "components.transaction_note", "TransactionRuleAction": "components.transaction_rule_action", + "TransactionRuleActionAddNote": "components.transaction_rule_action_add_note", + "TransactionRuleActionAddNoteParam": "components.transaction_rule_action_add_note", + "TransactionRuleActionAddScore": "components.transaction_rule_action_add_score", + "TransactionRuleActionAddScoreParam": "components.transaction_rule_action_add_score", + "TransactionRuleActionAddTags": "components.transaction_rule_action_add_tags", + "TransactionRuleActionAddTagsParam": "components.transaction_rule_action_add_tags", + "TransactionRuleActionAddToList": "components.transaction_rule_action_add_to_list", + "TransactionRuleActionAddToListParam": "components.transaction_rule_action_add_to_list", + "TransactionRuleActionChangeStatus": "components.transaction_rule_action_change_status", + "TransactionRuleActionChangeStatusParam": "components.transaction_rule_action_change_status", + "TransactionRuleActionOpenCase": "components.transaction_rule_action_open_case", + "TransactionRuleActionOpenCaseParam": "components.transaction_rule_action_open_case", "TransactionRuleActionParam": "components.transaction_rule_action", - "TransactionRuleActionVariant1": "components.transaction_rule_action", - "TransactionRuleActionVariant1Param": "components.transaction_rule_action", - "TransactionRuleActionVariant2": "components.transaction_rule_action", - "TransactionRuleActionVariant2Param": "components.transaction_rule_action", - "TransactionRuleActionVariant3": "components.transaction_rule_action", - "TransactionRuleActionVariant3Param": "components.transaction_rule_action", - "TransactionRuleActionVariant4": "components.transaction_rule_action", - "TransactionRuleActionVariant4Param": "components.transaction_rule_action", - "TransactionRuleActionVariant5": "components.transaction_rule_action", - "TransactionRuleActionVariant5Param": "components.transaction_rule_action", - "TransactionRuleActionVariant6": "components.transaction_rule_action", - "TransactionRuleActionVariant6Param": "components.transaction_rule_action", "TransactionRuleAggregation": "components.transaction_rule_aggregation", "TransactionRuleAggregationParam": "components.transaction_rule_aggregation", "TransactionRuleBacktestResult": "components.transaction_rule_backtest_result", @@ -3084,6 +3899,13 @@ "TransactionRuleLibraryEntry": "components.transaction_rule_library_entry", "TransactionRuleScope": "components.transaction_rule_scope", "TransactionRuleScopeParam": "components.transaction_rule_scope", + "TransactionSummary": "components.transaction_summary", + "TransactionSummaryTags": "components.transaction_summary", + "TransactionTravelRule": "components.transaction_travel_rule", + "TransactionTravelRuleCounterpartyVasp": "components.transaction_travel_rule", + "TransactionTravelRuleCounterpartyVaspDdScoreBreakdownValue": "components.transaction_travel_rule", + "TransactionVerificationSessionAction": "components.transaction_verification_session_action", + "TransactionWalletOwnershipAction": "components.transaction_wallet_ownership_action", "TransactionsCreateParams": "transactions.create_params", "TransactionsCreateParamsCounterparty": "transactions.create_params", "TransactionsCreateParamsCounterpartyDeviceContext": "transactions.create_params", @@ -3101,11 +3923,12 @@ "TransactionsCreateParamsTravelRuleDetails": "transactions.create_params", "TransactionsCreateSdkTokenParams": "transactions.create_sdk_token_params", "TransactionsCreateSdkTokenResponse": "transactions.create_sdk_token_response", - "TransactionsListNetworksResponse": "transactions.list_networks_response", - "TransactionsListPartyNetworksResponse": "transactions.list_party_networks_response", + "TransactionsListRelatedResponse": "transactions.list_related_response", "TransactionsListResponse": "transactions.list_response", "TransactionsListResponseResults": "transactions.list_response", "TransactionsListResponseResultsTags": "transactions.list_response", + "TransactionsNotesCreateParams": "transactions.notes.create_params", + "TransactionsNotesListResponse": "transactions.notes.list_response", "TransactionsRulesBacktestParams": "transactions.rules.backtest_params", "TransactionsRulesCreateParams": "transactions.rules.create_params", "TransactionsRulesInstallLibraryParams": "transactions.rules.install_library_params", @@ -3114,65 +3937,132 @@ "TransactionsRulesUninstallLibraryParams": "transactions.rules.uninstall_library_params", "TransactionsRulesUpdateParams": "transactions.rules.update_params", "TransactionsTravelRuleConfirmOwnershipParams": "transactions.travel_rule.confirm_ownership_params", + "TransactionsTravelRuleCreateMessageParams": "transactions.travel_rule.create_message_params", + "TransactionsTravelRuleTriggerTestVaspInboundParams": "transactions.travel_rule.trigger_test_vasp_inbound_params", "TransactionsTravelRuleUpdateTransferParams": "transactions.travel_rule.update_transfer_params", + "TravelRuleChainDetails": "components.travel_rule_chain_details", + "TravelRuleChainDetailsUtxo": "components.travel_rule_chain_details", + "TravelRuleChainSide": "components.travel_rule_chain_side", + "TravelRuleCounterpartyContact": "components.travel_rule_counterparty_contact", + "TravelRuleCounterpartyContactsCreateParams": "travel_rule.counterparty_contacts.create_params", + "TravelRuleCounterpartyContactsListResponse": "travel_rule.counterparty_contacts.list_response", + "TravelRuleCounterpartyDecision": "components.travel_rule_counterparty_decision", + "TravelRuleCounterpartyDecisionInheritedFrom": "components.travel_rule_counterparty_decision", + "TravelRuleCounterpartyDecisionResponse": "components.travel_rule_counterparty_decision_response", + "TravelRuleCounterpartyQuestionnaire": "components.travel_rule_counterparty_questionnaire", + "TravelRuleCounterpartyQuestionnaireList": "components.travel_rule_counterparty_questionnaire_list", + "TravelRuleCounterpartyScreening": "components.travel_rule_counterparty_screening", + "TravelRuleCounterpartyScreeningResult": "components.travel_rule_counterparty_screening_result", + "TravelRuleCounterpartyScreeningStatus": "components.travel_rule_counterparty_screening_status", "TravelRuleCreateWidgetSessionParams": "travel_rule.create_widget_session_params", + "TravelRuleDataMatch": "components.travel_rule_data_match", + "TravelRuleDataMatchFieldsValue": "components.travel_rule_data_match", + "TravelRuleDataSharingFields": "components.travel_rule_data_sharing_fields", + "TravelRuleDataSharingFieldsParam": "components.travel_rule_data_sharing_fields", + "TravelRuleDataUnmasked": "components.travel_rule_data_unmasked", + "TravelRuleDelivery": "components.travel_rule_delivery", + "TravelRuleDiscoveryStep": "components.travel_rule_discovery_step", "TravelRuleInboundResponse": "components.travel_rule_inbound_response", "TravelRuleListVaspsResponse": "travel_rule.list_vasps_response", + "TravelRuleMatchingRules": "components.travel_rule_matching_rules", + "TravelRuleMatchingRulesCompany": "components.travel_rule_matching_rules", + "TravelRuleMatchingRulesCompanyParam": "components.travel_rule_matching_rules", + "TravelRuleMatchingRulesIndividual": "components.travel_rule_matching_rules", + "TravelRuleMatchingRulesIndividualParam": "components.travel_rule_matching_rules", + "TravelRuleMatchingRulesParam": "components.travel_rule_matching_rules", + "TravelRuleMessage": "components.travel_rule_message", + "TravelRuleMessageAttachment": "components.travel_rule_message_attachment", + "TravelRuleMessageAttachmentFiles": "components.travel_rule_message_attachment", + "TravelRuleNameMatch": "components.travel_rule_name_match", + "TravelRuleNextAction": "components.travel_rule_next_action", "TravelRuleRegisterInboundParams": "travel_rule.register_inbound_params", "TravelRuleRegisterInboundParamsOriginatingVasp": "travel_rule.register_inbound_params", + "TravelRuleScreenVaspParams": "travel_rule.screen_vasp_params", + "TravelRuleSendComplianceEmailConfirmationResponse": "travel_rule.send_compliance_email_confirmation_response", + "TravelRuleSendVaspQuestionnaireParams": "travel_rule.send_vasp_questionnaire_params", "TravelRuleSettingsDetail": "components.travel_rule_settings_detail", + "TravelRuleSettingsDetailDiditNetwork": "components.travel_rule_settings_detail", "TravelRuleSettingsDetailNetworks": "components.travel_rule_settings_detail", "TravelRuleSettingsDetailNetworksGtr": "components.travel_rule_settings_detail", "TravelRuleSettingsDetailNetworksSygna": "components.travel_rule_settings_detail", "TravelRuleSettingsDetailNetworksTrust": "components.travel_rule_settings_detail", "TravelRuleSettingsDetailNetworksVerifyvasp": "components.travel_rule_settings_detail", + "TravelRuleTestVaspInboundResponse": "components.travel_rule_test_vasp_inbound_response", "TravelRuleTransferDetail": "components.travel_rule_transfer_detail", "TravelRuleTransferDetailCounterpartyVasp": "components.travel_rule_transfer_detail", + "TravelRuleTransferDetailCounterpartyVaspDdScoreBreakdownValue": "components.travel_rule_transfer_detail", "TravelRuleTransferDetailTimeline": "components.travel_rule_transfer_detail", + "TravelRuleTransferRow": "components.travel_rule_transfer_row", + "TravelRuleTransfersListResponse": "travel_rule.transfers.list_response", "TravelRuleUpdateSettingsParams": "travel_rule.update_settings_params", + "TravelRuleUpdateVaspDecisionParams": "travel_rule.update_vasp_decision_params", + "TravelRuleUtxo": "components.travel_rule_utxo", + "TravelRuleUtxoEntry": "components.travel_rule_utxo_entry", + "TravelRuleUtxoEntryParam": "components.travel_rule_utxo_entry", + "TravelRuleWalletAddressesBulkImportParamsVariant1": "travel_rule.wallet_addresses.bulk_import_params", "TravelRuleWalletAddressesCreateParams": "travel_rule.wallet_addresses.create_params", "TravelRuleWalletAddressesListResponse": "travel_rule.wallet_addresses.list_response", "TravelRuleWalletAddressesUpdateParams": "travel_rule.wallet_addresses.update_params", "TravelRuleWidgetSessionResponse": "components.travel_rule_widget_session_response", + "TravelRuleWorkspace": "components.travel_rule_workspace", + "TravelRuleWorkspaceClaim": "components.travel_rule_workspace", + "TravelRuleWorkspaceClaimParams": "travel_rule.workspace.claim_params", + "TravelRuleWorkspaceConfirmDomainParams": "travel_rule.workspace.confirm_domain_params", + "TravelRuleWorkspaceDomain": "components.travel_rule_workspace", + "TravelRuleWorkspaceDomainCodeResponse": "components.travel_rule_workspace_domain_code_response", + "TravelRuleWorkspaceEnableBillingParams": "travel_rule.workspace.enable_billing_params", + "TravelRuleWorkspaceTxtRecord": "components.travel_rule_workspace_txt_record", "UserDetailItem": "components.user_detail_item", "UserDetailItemComments": "components.user_detail_item", "UserListItem": "components.user_list_item", "UserListItemFeaturesList": "components.user_list_item", "UserListItemTags": "components.user_list_item", + "UserSessionListItem": "components.user_session_list_item", + "UserSessionListItemAdditionalDetails": "components.user_session_list_item", + "UserSessionListItemAdditionalDetailsAml": "components.user_session_list_item", + "UserSessionListItemAdditionalDetailsAmlScreenedData": "components.user_session_list_item", + "UserSessionListItemAdditionalDetailsDatabaseValidation": "components.user_session_list_item", + "UserSessionListItemAdditionalDetailsEmailVerification": "components.user_session_list_item", + "UserSessionListItemAdditionalDetailsFaceMatch": "components.user_session_list_item", + "UserSessionListItemAdditionalDetailsLiveness": "components.user_session_list_item", + "UserSessionListItemAdditionalDetailsPhoneVerification": "components.user_session_list_item", + "UserSessionListItemAdditionalDetailsPoa": "components.user_session_list_item", + "UserSessionListItemEmailAddress": "components.user_session_list_item", + "UserSessionListItemFeatures": "components.user_session_list_item", + "UserSessionListItemPhoneNumber": "components.user_session_list_item", "UsersBatchDeleteParams": "users.batch_delete_params", "UsersBatchDeleteResponse": "users.batch_delete_response", "UsersCreateParams": "users.create_params", - "UsersListNetworksResponse": "users.list_networks_response", + "UsersCreateResponse": "users.create_response", + "UsersCreateResponseComments": "users.create_response", + "UsersCreateResponseFeaturesList": "users.create_response", + "UsersCreateResponseTags": "users.create_response", + "UsersCreateResponseTagsTag": "users.create_response", + "UsersCreateResponseVerificationLink": "users.create_response", "UsersListResponse": "users.list_response", - "UsersRetrieveOngoingMonitoringJobResponse": "users.retrieve_ongoing_monitoring_job_response", - "UsersRetrieveOngoingMonitoringJobResponseEstimate": "users.retrieve_ongoing_monitoring_job_response", - "UsersRetrieveOngoingMonitoringJobResponseOutcomes": "users.retrieve_ongoing_monitoring_job_response", "UsersSetAmlMonitoringParams": "users.set_aml_monitoring_params", - "UsersSetAmlMonitoringResponse202": "users.set_aml_monitoring_response", - "UsersSetAmlMonitoringResponse202Estimate": "users.set_aml_monitoring_response", - "UsersSetAmlMonitoringResponse202Outcomes": "users.set_aml_monitoring_response", - "UsersSetAmlMonitoringResponse202Results": "users.set_aml_monitoring_response", - "UsersSetAmlMonitoringResponseVariant1": "users.set_aml_monitoring_response", - "UsersSetAmlMonitoringResponseVariant2": "users.set_aml_monitoring_response", - "UsersSetAmlMonitoringResponseVariant2Estimate": "users.set_aml_monitoring_response", - "UsersSetAmlMonitoringResponseVariant2Outcomes": "users.set_aml_monitoring_response", - "UsersSetAmlMonitoringResponseVariant2Results": "users.set_aml_monitoring_response", "UsersSetOngoingMonitoringParams": "users.set_ongoing_monitoring_params", - "UsersSetOngoingMonitoringResponse202": "users.set_ongoing_monitoring_response", - "UsersSetOngoingMonitoringResponse202Estimate": "users.set_ongoing_monitoring_response", - "UsersSetOngoingMonitoringResponse202Outcomes": "users.set_ongoing_monitoring_response", - "UsersSetOngoingMonitoringResponseVariant1": "users.set_ongoing_monitoring_response", - "UsersSetOngoingMonitoringResponseVariant2": "users.set_ongoing_monitoring_response", - "UsersSetOngoingMonitoringResponseVariant2Estimate": "users.set_ongoing_monitoring_response", - "UsersSetOngoingMonitoringResponseVariant2Outcomes": "users.set_ongoing_monitoring_response", "UsersUpdateParams": "users.update_params", "UsersUpdateStatusParams": "users.update_status_params", "UsersUploadFaceParams": "users.upload_face_params", "UsersUploadFaceResponse": "users.upload_face_response", "VaspDirectoryEntry": "components.vasp_directory_entry", + "VaspDirectoryEntryAliases": "components.vasp_directory_entry", + "VaspDirectoryEntryContacts": "components.vasp_directory_entry", + "VaspDirectoryEntryDdScoreBreakdownValue": "components.vasp_directory_entry", + "VaspDirectoryEntryEntities": "components.vasp_directory_entry", + "VaspDirectoryEntryEssentialChecks": "components.vasp_directory_entry", + "VaspDirectoryEntryEssentialChecksChecks": "components.vasp_directory_entry", + "VaspDirectoryEntryNetworks": "components.vasp_directory_entry", + "VaspDirectoryEntryResponsiveness": "components.vasp_directory_entry", + "VaspDirectoryEntryRoutes": "components.vasp_directory_entry", + "VaspDirectoryEntryUmbrella": "components.vasp_directory_entry", "VerifyEmailResponse": "components.verify_email_response", "WalletAddressEntry": "components.wallet_address_entry", "WalletAddressEntryUpdate": "components.wallet_address_entry_update", + "WalletAddressImportReport": "components.wallet_address_import_report", + "WalletAddressImportReportRows": "components.wallet_address_import_report", + "WalletAddressImportRow": "components.wallet_address_import_row", "WalletScreeningResult": "components.wallet_screening_result", "WalletScreeningResultCounterpartyConnections": "components.wallet_screening_result", "WalletScreeningResultDestinationOfFunds": "components.wallet_screening_result", @@ -3184,18 +4074,30 @@ "WalletScreeningScreenPdfParamsDestinationOfFunds": "wallet_screening.screen_pdf_params", "WalletScreeningScreenPdfParamsRiskFactors": "wallet_screening.screen_pdf_params", "WalletScreeningScreenPdfParamsSourceOfFunds": "wallet_screening.screen_pdf_params", + "WebhookConfig": "components.webhook_config", + "WebhookConfigAvailableEvents": "components.webhook_config", + "WebhookConfigDestination": "components.webhook_config_destination", + "WebhookDestinationDelivery": "components.webhook_destination_delivery", + "WebhookDestinationDeliveryDetail": "components.webhook_destination_delivery_detail", "WebhookEvent": "components.webhook_event", "WebhookSubscribedEvents": "components.webhook_subscribed_events", "WebhookSubscribedEventsParam": "components.webhook_subscribed_events", "WebhooksDestinationsCreateParams": "webhooks.destinations.create_params", "WebhooksDestinationsCreateResponse": "webhooks.destinations.create_response", "WebhooksDestinationsCreateResponseSummary": "webhooks.destinations.create_response", + "WebhooksDestinationsListDeliveriesResponse": "webhooks.destinations.list_deliveries_response", "WebhooksDestinationsListResponseItem": "webhooks.destinations.list_response", + "WebhooksDestinationsListResponseItemDailyPoints": "webhooks.destinations.list_response", + "WebhooksDestinationsRetrieveAnalyticsResponse": "webhooks.destinations.retrieve_analytics_response", + "WebhooksDestinationsRetrieveAnalyticsResponsePoints": "webhooks.destinations.retrieve_analytics_response", + "WebhooksDestinationsRetrieveAnalyticsResponseSummary": "webhooks.destinations.retrieve_analytics_response", "WebhooksDestinationsRetrieveResponse": "webhooks.destinations.retrieve_response", "WebhooksDestinationsRetrieveResponseSummary": "webhooks.destinations.retrieve_response", "WebhooksDestinationsUpdateParams": "webhooks.destinations.update_params", "WebhooksDestinationsUpdateResponse": "webhooks.destinations.update_response", "WebhooksDestinationsUpdateResponseSummary": "webhooks.destinations.update_response", + "WebhooksUpdateConfigParams": "webhooks.update_config_params", + "WorkflowDetail": "components.workflow_detail", "WorkflowListItem": "components.workflow_list_item", "WorkflowListItemCreatedBy": "components.workflow_list_item", "WorkflowsCreateParams": "workflows.create_params", @@ -3204,11 +4106,45 @@ } __all__ = [ + "AMLDatasetFreshness", + "AMLDatasetFreshnessSources", + "AMLDatasetSourceFreshness", + "AMLMonitoringJob", + "AMLMonitoringJobEstimate", + "AMLMonitoringJobOutcomes", + "AMLMonitoringJobResults", + "AMLMonitoringToggleResult", + "AMLMonitoringToggleResultVariant1", + "AMLMonitoringToggleResultVariant2", + "AMLMonitoringToggleResultVariant2Estimate", + "AMLMonitoringToggleResultVariant2Outcomes", + "AMLMonitoringToggleResultVariant2Results", + "AMLScreening", + "AMLScreeningHits", + "AMLScreeningHitsAdverseMediaDetails", + "AMLScreeningHitsAdverseMediaMatches", + "AMLScreeningHitsLinkedEntities", + "AMLScreeningHitsPepMatches", + "AMLScreeningHitsProperties", + "AMLScreeningHitsRiskView", + "AMLScreeningHitsRiskViewCategories", + "AMLScreeningHitsRiskViewCountries", + "AMLScreeningHitsRiskViewCrimes", + "AMLScreeningHitsSanctionMatches", + "AMLScreeningHitsScoreBreakdown", + "AMLScreeningHitsWarningMatches", + "AMLScreeningHitsWarningMatchesAdditionalData", + "AMLScreeningHitsWarningMatchesAdditionalDataData", + "AMLScreeningHitsWarningMatchesAdditionalDataDataCaseDetails", + "AMLScreeningRun", + "AMLScreeningScreenedData", + "AMLScreeningWarnings", "AccountApplicationsCreateParams", "AccountApplicationsUpdateParams", "AccountLoginParams", "AccountRegisterParams", "AccountResendOtpParams", + "AccountResendOtpResponse", "AccountVerifyEmailParams", "AgeEstimationEstimateParams", "AgeEstimationEstimateResponse", @@ -3219,6 +4155,7 @@ "AmlMonitoringRetrieveJobResponse", "AmlMonitoringRetrieveJobResponseOutcomes", "AmlMonitoringRetrieveJobResponseResults", + "AmlRetrieveDatasetFreshnessResponse", "AmlScreenParams", "AmlScreenResponse", "AmlScreenResponseAml", @@ -3230,7 +4167,41 @@ "AmlScreenResponseAmlScreenedData", "AmlScreenResponseAmlWarnings", "Application", + "ApplicationAnalyticsCountryCount", + "ApplicationAnalyticsDataPoint", + "ApplicationAnalyticsDemographicPoint", + "ApplicationAnalyticsDemographicStats", + "ApplicationAnalyticsDemographicStatsAgeDistribution", + "ApplicationAnalyticsDeviceDetails", + "ApplicationAnalyticsDeviceDetailsBrowsers", + "ApplicationAnalyticsDeviceDetailsDeviceModels", + "ApplicationAnalyticsDeviceDetailsOperatingSystems", + "ApplicationAnalyticsDevicePoint", + "ApplicationAnalyticsLocationStats", + "ApplicationAnalyticsLocationStatsCities", + "ApplicationAnalyticsLocationStatsConversionByCountry", + "ApplicationAnalyticsOriginalStatusPoint", + "ApplicationAnalyticsOriginalStatusStats", + "ApplicationAnalyticsRequestBreakdown", + "ApplicationAnalyticsResubmissionPoint", + "ApplicationAnalyticsResubmissionStats", + "ApplicationAnalyticsSummary", + "ApplicationAnalyticsVerificationTimePoint", + "ApplicationAnalyticsVerificationTimeStats", + "ApplicationAnalyticsVerificationTimeStatsPerFeatureValue", + "ApplicationAnalyticsVerificationTimeStatsPerSubStepValueValue", + "ApplicationAnalyticsWarningStats", + "ApplicationAnalyticsWarningStatsValue", + "ApplicationAnalyticsWarningStatsValueRisksValue", + "ApplicationAnalyticsWidget", + "ApplicationAnalyticsWidgetResponse", + "ApplicationAnalyticsWidgetSearch", + "ApplicationAnalyticsWidgetThresholds", + "ApplicationAnalyticsWorkflowFunnel", + "ApplicationAnalyticsWorkflowFunnelValue", + "ApplicationAnalyticsWorkflowFunnelValueStepsValue", "ApplicationCredentialsSummary", + "ApplicationProduct", "BillingRetrieveBalanceResponse", "BillingTopUpParams", "BillingTopUpResponse", @@ -3242,49 +4213,25 @@ "BiometricTemplatesListResponse", "BiometricTemplatesListResponseResults", "BiometricTemplatesRetrieveResponse", + "BlocklistAddParams", + "BlocklistAddResponse", + "BlocklistListResponse", + "BlocklistRemoveParams", + "BlocklistRemoveResponse", + "BlocklistedItem", "BrandingCustomization", "BusinessDetailItem", "BusinessListItem", "BusinessListItemFeaturesList", "BusinessListItemTags", - "BusinessSessionsListNetworksResponse", + "BusinessSessionListItem", "BusinessesBatchDeleteParams", "BusinessesBatchDeleteResponse", "BusinessesCreateParams", - "BusinessesListNetworksResponse", "BusinessesListResponse", - "BusinessesRetrieveOngoingMonitoringJobResponse", - "BusinessesRetrieveOngoingMonitoringJobResponseEstimate", - "BusinessesRetrieveOngoingMonitoringJobResponseOutcomes", - "BusinessesRetrieveSessionOngoingMonitoringJobResponse", - "BusinessesRetrieveSessionOngoingMonitoringJobResponseEstimate", - "BusinessesRetrieveSessionOngoingMonitoringJobResponseOutcomes", "BusinessesSetAmlMonitoringParams", - "BusinessesSetAmlMonitoringResponse202", - "BusinessesSetAmlMonitoringResponse202Estimate", - "BusinessesSetAmlMonitoringResponse202Outcomes", - "BusinessesSetAmlMonitoringResponse202Results", - "BusinessesSetAmlMonitoringResponseVariant1", - "BusinessesSetAmlMonitoringResponseVariant2", - "BusinessesSetAmlMonitoringResponseVariant2Estimate", - "BusinessesSetAmlMonitoringResponseVariant2Outcomes", - "BusinessesSetAmlMonitoringResponseVariant2Results", "BusinessesSetOngoingMonitoringParams", - "BusinessesSetOngoingMonitoringResponse202", - "BusinessesSetOngoingMonitoringResponse202Estimate", - "BusinessesSetOngoingMonitoringResponse202Outcomes", - "BusinessesSetOngoingMonitoringResponseVariant1", - "BusinessesSetOngoingMonitoringResponseVariant2", - "BusinessesSetOngoingMonitoringResponseVariant2Estimate", - "BusinessesSetOngoingMonitoringResponseVariant2Outcomes", "BusinessesSetSessionOngoingMonitoringParams", - "BusinessesSetSessionOngoingMonitoringResponse202", - "BusinessesSetSessionOngoingMonitoringResponse202Estimate", - "BusinessesSetSessionOngoingMonitoringResponse202Outcomes", - "BusinessesSetSessionOngoingMonitoringResponseVariant1", - "BusinessesSetSessionOngoingMonitoringResponseVariant2", - "BusinessesSetSessionOngoingMonitoringResponseVariant2Estimate", - "BusinessesSetSessionOngoingMonitoringResponseVariant2Outcomes", "BusinessesUpdateParams", "BusinessesUpdateStatusParams", "CaseActionActionEnum", @@ -3306,7 +4253,11 @@ "CaseBlueprintDetailCreationSources", "CaseBlueprintDetailReceivesEscalationFrom", "CaseBlueprintListItem", + "CaseBlueprintPresetInstallResult", "CaseBlueprintSummary", + "CaseBulkUpdateResult", + "CaseBulkUpdateResultFailures", + "CaseChecker", "CaseChecklistItem", "CaseCollaborationCaseNote", "CaseCollaborationCaseNoteReply", @@ -3341,6 +4292,7 @@ "CaseEvent", "CaseLink", "CaseListItem", + "CaseListItemPendingResolution", "CaseNote", "CaseNoteAttachments", "CasePublicationAssignmentMethodEnum", @@ -3360,12 +4312,12 @@ "CaseRoutingRule", "CaseRoutingRuleCreate", "CaseSubject", + "CaseTeamSummary", "CasesApplyEntityActionParams", "CasesAssignParams", "CasesBlueprintsCreateParams", "CasesBlueprintsDiscardDraftParams", "CasesBlueprintsInstallPresetsParams", - "CasesBlueprintsInstallPresetsResponse", "CasesBlueprintsListPresetsResponse", "CasesBlueprintsListPresetsResponseResults", "CasesBlueprintsListResponse", @@ -3373,11 +4325,7 @@ "CasesBlueprintsSaveDraftParams", "CasesBlueprintsUpdateParams", "CasesBulkAssignParams", - "CasesBulkAssignResponse", - "CasesBulkAssignResponseFailures", "CasesBulkUpdateStatusParams", - "CasesBulkUpdateStatusResponse", - "CasesBulkUpdateStatusResponseFailures", "CasesChecklistToggleItemParams", "CasesCreateParams", "CasesCreateParamsSubject", @@ -3395,10 +4343,10 @@ "CasesPrintResponse", "CasesRegulatoryReportsCreateParams", "CasesRegulatoryReportsCreateParamsNarrative", - "CasesRegulatoryReportsListResponse", "CasesRejectResolutionParams", "CasesRequestsCreateParams", "CasesRequestsPreviewParams", + "CasesRequestsRemindResponse", "CasesResolveParams", "CasesRetrieveStatisticsResponse", "CasesRetrieveStatisticsResponseQueue", @@ -3421,6 +4369,9 @@ "CasesTeamsUpdateParams", "CasesTransferParams", "CasesUpdateParams", + "ComplianceList", + "ComplianceListEntry", + "ComplianceListEntryMetadata", "CustomerMessagingCaseRequest", "CustomerMessagingCaseRequestContact", "CustomerMessagingCaseRequestOptions", @@ -3494,6 +4445,7 @@ "EmailVerificationCheckResponseEmailWarnings", "EmailVerificationSendResponse", "EntityNetworkMembership", + "EntityNetworkMembershipList", "FaceMatchCompareParams", "FaceMatchCompareResponse", "FaceMatchCompareResponseFaceMatch", @@ -3510,6 +4462,8 @@ "FaceSearchSearchResponseFaceSearchUserImage", "FaceSearchSearchResponseFaceSearchUserImageEntities", "FaceSearchSearchResponseFaceSearchWarnings", + "FeatureStatusOverride", + "FeatureStatusOverrideRequestedBy", "FraudAvailabilityEnum", "FraudBandEnum", "FraudCheckAttributesRequest", @@ -3571,11 +4525,39 @@ "IdWalletsStartSmartIdParams", "IdWalletsStartUaePassParams", "IdWalletsStartVippsParams", + "InvestigatorAcceptRunParams", + "InvestigatorBatchAcceptResponse", + "InvestigatorBatchAcceptResponseResults", + "InvestigatorBatchAcceptRunsParams", + "InvestigatorBlock", + "InvestigatorDecision", + "InvestigatorDecisionOriginalOutcome", + "InvestigatorDecisionOverrides", + "InvestigatorListActivityResponse", + "InvestigatorListGuardrailsResponse", + "InvestigatorListGuardrailsResponseResults", + "InvestigatorListQueueResponse", + "InvestigatorOverrideRunParams", + "InvestigatorQueueRow", + "InvestigatorRetrieveDashboardResponse", + "InvestigatorRetrievePerformanceResponse", + "InvestigatorRetrieveSummaryResponse", + "InvestigatorRetrieveTransactionResponse", + "InvestigatorRunDetail", + "InvestigatorSettings", + "InvestigatorSettingsSpendLimit", + "InvestigatorUpdateSettingsParams", "IpAssessRiskParams", + "KYBKeyPeopleParty", + "KYBKeyPeoplePartyRoles", + "KYBKeyPeoplePartyVerificationWorkflowFeatures", "KYBRegistryAddress", "KYBRegistryIndustry", "KYBRegistryOwnership", "KYBRegistryOwnershipUnresolved", + "KYBRegistrySearchResults", + "KYBRegistrySearchResultsCompanies", + "KYBRegistrySearchResultsPagination", "KYBRegistryShareholding", "KybListShareholdersParams", "KybListShareholdersResponse", @@ -3597,20 +4579,66 @@ "KybListUbosResponseKybRegistryOfficers", "KybSearchParams", "KybSearchResponse", - "KybSearchResponseKybRegistry", - "KybSearchResponseKybRegistryCompanies", - "KybSearchResponseKybRegistryPagination", "KybSelectParams", "KybSelectResponse", "KybSelectResponseKybRegistry", "KybSelectResponseKybRegistryBeneficialOwners", "KybSelectResponseKybRegistryOfficers", + "ListsCreateParams", + "ListsEntriesCreateParams", + "ListsEntriesCreateParamsMetadata", + "ListsEntriesListResponse", + "ListsEntriesUploadFaceParams", + "ListsListResponse", + "ListsUpdateParams", + "ListsUpdateResponse", "LoginResponse", + "MonitoringEvent", + "MonitoringEventCreateRequest", + "MonitoringEventCreateRequestBusiness", + "MonitoringEventCreateRequestDeviceContext", + "MonitoringEventCreateRequestDeviceContextNetworkContext", + "MonitoringEventCreateRequestSession", + "MonitoringEventCreateRequestSubject", + "MonitoringEventCreateRequestTransactionContext", + "MonitoringEventDetail", + "MonitoringEventDetailIpEnrichment", + "MonitoringEventDetailSubjectProfile", + "MonitoringEventIngestResult", + "MonitoringEventIngestResultRuleMatches", + "MonitoringEventPage", + "MonitoringEventRuleRun", + "MonitoringEventsCreateBatchParams", + "MonitoringEventsCreateBatchResponse", + "MonitoringEventsCreateBatchResponseResults", + "MonitoringEventsCreateBatchResponseResultsRuleMatches", + "MonitoringEventsCreateParams", + "MonitoringEventsCreateParamsBusiness", + "MonitoringEventsCreateParamsDeviceContext", + "MonitoringEventsCreateParamsDeviceContextNetworkContext", + "MonitoringEventsCreateParamsSession", + "MonitoringEventsCreateParamsSubject", + "MonitoringEventsCreateParamsTransactionContext", + "MonitoringEventsCreateSdkTokenParams", + "MonitoringEventsCreateSdkTokenResponse", + "MonitoringEventsRetrieveEnrichmentMetricsResponse", "NetworkDetail", + "NetworkDetailResponse", "NetworkGraph", "NetworkGraphEdge", "NetworkGraphMetrics", "NetworkGraphNode", + "NetworkListCounts", + "NetworkListCountsFacets", + "NetworkListCountsFacetsAssignee", + "NetworkListCountsFacetsAssigneeUsers", + "NetworkListCountsFacetsLastActivityDays", + "NetworkListCountsFacetsMinSize", + "NetworkListCountsFacetsPriority", + "NetworkListCountsFacetsRiskMin", + "NetworkListCountsFacetsSignalTypes", + "NetworkListCountsScopes", + "NetworkListCountsViews", "NetworkListItem", "NetworkMap", "NetworkMapPoints", @@ -3618,18 +4646,45 @@ "NetworkMemberProvenance", "NetworkNodeTag", "NetworkNote", + "NetworkOutcomes", + "NetworkPatternScore", "NetworkRiskDisplay", "NetworkSignal", + "NetworkTag", + "NetworkTimeline", + "NetworkTimelineAccount", + "NetworkTimelineAmount", + "NetworkTimelineAttempt", + "NetworkTimelineCaseLinked", + "NetworkTimelineDeclineReason", + "NetworkTimelineDevice", "NetworkTimelineEvent", + "NetworkTimelineIP", + "NetworkTimelineLogin", + "NetworkTimelineNoteAdded", + "NetworkTimelinePage", + "NetworkTimelineRoute", + "NetworkTimelineRuleHit", + "NetworkTimelineSourceRef", + "NetworkTimelineTransaction", + "NetworkTypedTimelineEvent", + "NetworksAddTagParams", "NetworksListMembersResponse", "NetworksListResponse", "NetworksListResponseKpis", "NetworksListSignalsResponse", "NetworksNotesCreateParams", "NetworksNotesListResponse", - "NetworksRetrieveTimelineResponse", + "NetworksRemoveTagParams", + "OngoingMonitoringJob", + "OngoingMonitoringJobEstimate", + "OngoingMonitoringJobOutcomes", + "OngoingMonitoringToggleResult", + "OngoingMonitoringToggleResultVariant1", "Organization", "OrganizationSummary", + "PaginatedApplicationList", + "PaginatedOrganizationList", "PassiveLivenessCheckParams", "PassiveLivenessCheckResponse", "PassiveLivenessCheckResponseLiveness", @@ -3670,9 +4725,9 @@ "RegulatoryReportDetail", "RegulatoryReportDetailNarrative", "RegulatoryReportDetailValidationErrors", + "RegulatoryReportList", "RegulatoryReportsCreateParams", "RegulatoryReportsCreateParamsNarrative", - "RegulatoryReportsListResponse", "RegulatoryReportsRetrieveDownloadUrlResponse", "RegulatoryReportsUpdateParams", "RegulatoryReportsUpdateParamsNarrative", @@ -3694,190 +4749,178 @@ "RoutingPublicationPolicyDraftResponse", "RoutingPublicationPolicyRevision", "RoutingPublicationRoutingCondition", + "SandboxSimulateResponse", + "SessionDecision", + "SessionDecisionContactDetails", + "SessionDecisionDatabaseValidations", + "SessionDecisionDatabaseValidationsErrors", + "SessionDecisionDatabaseValidationsScreenedData", + "SessionDecisionDatabaseValidationsValidations", + "SessionDecisionDatabaseValidationsValidationsValidation", + "SessionDecisionDatabaseValidationsWarnings", + "SessionDecisionDocumentAiDocuments", + "SessionDecisionDocumentAiDocumentsItems", + "SessionDecisionDocumentAiDocumentsItemsFields", + "SessionDecisionDocumentVerifications", + "SessionDecisionDocumentVerificationsGroupsValue", + "SessionDecisionDocumentVerificationsItems", + "SessionDecisionDocumentVerificationsItemsDocumentMetadata", + "SessionDecisionDocumentVerificationsItemsDocumentMetadataOverlayManipulation", + "SessionDecisionDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions", + "SessionDecisionEmailVerifications", + "SessionDecisionEmailVerificationsBreaches", + "SessionDecisionEmailVerificationsLifecycle", + "SessionDecisionEmailVerificationsLifecycleDetails", + "SessionDecisionEmailVerificationsMatches", + "SessionDecisionEmailVerificationsWarnings", + "SessionDecisionExpectedDetails", + "SessionDecisionFaceMatches", + "SessionDecisionFaceMatchesWarnings", + "SessionDecisionIdVerifications", + "SessionDecisionIdVerificationsBackImageQualityScore", + "SessionDecisionIdVerificationsDocumentLiveness", + "SessionDecisionIdVerificationsDocumentLivenessPortraitReplace", + "SessionDecisionIdVerificationsDocumentLivenessPrintedCopy", + "SessionDecisionIdVerificationsDocumentLivenessScreenReplay", + "SessionDecisionIdVerificationsDocumentSelfieFaceMatches", + "SessionDecisionIdVerificationsDocumentSelfieFaceMatchesUserDetails", + "SessionDecisionIdVerificationsExtraFields", + "SessionDecisionIdVerificationsFallbackFrom", + "SessionDecisionIdVerificationsFrontImageQualityScore", + "SessionDecisionIdVerificationsIdLookup", + "SessionDecisionIdVerificationsIdLookupComparison", + "SessionDecisionIdVerificationsMatches", + "SessionDecisionIdVerificationsMatchesUserDetails", + "SessionDecisionIdVerificationsMrz", + "SessionDecisionIdVerificationsParsedAddress", + "SessionDecisionIdVerificationsParsedAddressDocumentLocation", + "SessionDecisionIdVerificationsParsedAddressRawResults", + "SessionDecisionIdVerificationsParsedAddressRawResultsAddressComponents", + "SessionDecisionIdVerificationsParsedAddressRawResultsGeometry", + "SessionDecisionIdVerificationsParsedAddressRawResultsGeometryLocation", + "SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewport", + "SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewportNortheast", + "SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest", + "SessionDecisionIdVerificationsParsedAddressRawResultsNavigationPoints", + "SessionDecisionIdVerificationsParsedAddressRawResultsNavigationPointsLocation", + "SessionDecisionIdVerificationsWalletVerification", + "SessionDecisionIdVerificationsWarnings", + "SessionDecisionIpAnalyses", + "SessionDecisionIpAnalysesIdDocument", + "SessionDecisionIpAnalysesIdDocumentLocation", + "SessionDecisionIpAnalysesIp", + "SessionDecisionIpAnalysesIpLocation", + "SessionDecisionIpAnalysesMatches", + "SessionDecisionIpAnalysesMatchesDeviceInfo", + "SessionDecisionIpAnalysesMatchesLocationInfo", + "SessionDecisionIpAnalysesPoaDocument", + "SessionDecisionIpAnalysesPoaDocumentLocation", + "SessionDecisionIpAnalysesWarnings", + "SessionDecisionKeyPeopleChecks", + "SessionDecisionKeyPeopleChecksRegistry", + "SessionDecisionKeyPeopleChecksRegistryBeneficialOwners", + "SessionDecisionKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories", + "SessionDecisionKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures", + "SessionDecisionKeyPeopleChecksRegistryOfficers", + "SessionDecisionKeyPeopleChecksRegistryOfficersKycAmlCategories", + "SessionDecisionKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures", + "SessionDecisionKeyPeopleChecksSubmitted", + "SessionDecisionKeyPeopleChecksSubmittedParties", + "SessionDecisionKeyPeopleChecksSubmittedPartiesRoles", + "SessionDecisionKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures", + "SessionDecisionKeyPeopleChecksUboKycSummary", + "SessionDecisionLivenessChecks", + "SessionDecisionLivenessChecksMatches", + "SessionDecisionLivenessChecksMatchesUserDetails", + "SessionDecisionLivenessChecksWarnings", + "SessionDecisionNfcVerifications", + "SessionDecisionNfcVerificationsAuthenticity", + "SessionDecisionNfcVerificationsCertificateSummary", + "SessionDecisionNfcVerificationsChipData", + "SessionDecisionNfcVerificationsJpki", + "SessionDecisionNfcVerificationsJpkiAttributes", + "SessionDecisionNfcVerificationsJpkiCertificates", + "SessionDecisionNfcVerificationsWarnings", + "SessionDecisionPhoneVerifications", + "SessionDecisionPhoneVerificationsCarrier", + "SessionDecisionPhoneVerificationsLifecycle", + "SessionDecisionPhoneVerificationsLifecycleDetails", + "SessionDecisionPhoneVerificationsMatches", + "SessionDecisionPhoneVerificationsWarnings", + "SessionDecisionPoaVerifications", + "SessionDecisionPoaVerificationsDocumentMetadata", + "SessionDecisionPoaVerificationsDocumentMetadataOverlayManipulation", + "SessionDecisionPoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions", + "SessionDecisionPoaVerificationsExtraFields", + "SessionDecisionPoaVerificationsPoaParsedAddress", + "SessionDecisionPoaVerificationsPoaParsedAddressDocumentLocation", + "SessionDecisionPoaVerificationsWarnings", + "SessionDecisionQuestionnaireResponses", + "SessionDecisionQuestionnaireResponsesSections", + "SessionDecisionQuestionnaireResponsesSectionsItems", + "SessionDecisionQuestionnaireResponsesSectionsItemsAnswer", + "SessionDecisionQuestionnaireResponsesSectionsItemsChoices", + "SessionDecisionRegistryChecks", + "SessionDecisionRegistryChecksCompany", + "SessionDecisionRegistryChecksCompanyBeneficialOwners", + "SessionDecisionRegistryChecksCompanyOfficers", + "SessionDecisionResubmissions", + "SessionDecisionResubmissionsNodes", + "SessionDecisionResubmissionsRequestedBy", + "SessionDecisionReviews", + "SessionDeletionJob", + "SessionDeletionJobOutcomes", + "SessionEvent", "SessionImportError", "SessionImportJob", + "SessionSignDocument", + "SessionWebhookDelivery", + "SessionsAddImagesParams", + "SessionsAddImagesResponse", "SessionsBatchDeleteParams", "SessionsBatchDeleteResponse", - "SessionsBatchDeleteResponse202", - "SessionsBatchDeleteResponse202Outcomes", "SessionsBatchDeleteResponseResults", "SessionsBulkUpdateAmlHitStatusParams", "SessionsBulkUpdateAmlHitStatusParamsHitUpdates", + "SessionsBulkUpdateAmlHitStatusResponse", "SessionsCreateParams", "SessionsCreateParamsContactDetails", "SessionsCreateParamsExpectedDetails", + "SessionsCreateParamsSign", "SessionsCreateResponse", "SessionsDeleteParams", "SessionsDeleteResponse", "SessionsImportSharedParams", "SessionsImportsCreateParams", "SessionsImportsListErrorsResponse", - "SessionsListNetworksResponse", + "SessionsListAmlScreeningHistoryResponse", + "SessionsListEventsResponse", + "SessionsListFeatureOverridesResponse", "SessionsListResponse", - "SessionsListResponseResultsKybSessionRowSessionKindBusiness", - "SessionsListResponseResultsKycSessionRowSessionKindUser", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetails", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAml", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAmlScreenedData", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsDatabaseValidation", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsEmailVerification", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsFaceMatch", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsLiveness", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPhoneVerification", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPoa", - "SessionsListResponseResultsKycSessionRowSessionKindUserEmailAddress", - "SessionsListResponseResultsKycSessionRowSessionKindUserFeatures", - "SessionsListResponseResultsKycSessionRowSessionKindUserPhoneNumber", + "SessionsPreviewDecisionEmailParams", + "SessionsPreviewDecisionEmailResponse", "SessionsPreviewResubmissionParams", "SessionsPreviewResubmissionParamsNodesToResubmit", "SessionsPreviewResubmissionResponse", "SessionsPreviewResubmissionResponseNodesToResubmit", - "SessionsRetrieveDecisionResponse", - "SessionsRetrieveDecisionResponseAmlScreenings", - "SessionsRetrieveDecisionResponseAmlScreeningsHits", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaDetails", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaMatches", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsLinkedEntities", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsPepMatches", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsProperties", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskView", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCategories", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCountries", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCrimes", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsSanctionMatches", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsScoreBreakdown", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatches", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalData", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataData", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataDataCaseDetails", - "SessionsRetrieveDecisionResponseAmlScreeningsScreenedData", - "SessionsRetrieveDecisionResponseAmlScreeningsWarnings", - "SessionsRetrieveDecisionResponseContactDetails", - "SessionsRetrieveDecisionResponseDatabaseValidations", - "SessionsRetrieveDecisionResponseDatabaseValidationsErrors", - "SessionsRetrieveDecisionResponseDatabaseValidationsScreenedData", - "SessionsRetrieveDecisionResponseDatabaseValidationsValidations", - "SessionsRetrieveDecisionResponseDatabaseValidationsValidationsValidation", - "SessionsRetrieveDecisionResponseDatabaseValidationsWarnings", - "SessionsRetrieveDecisionResponseDocumentAiDocuments", - "SessionsRetrieveDecisionResponseDocumentAiDocumentsItems", - "SessionsRetrieveDecisionResponseDocumentAiDocumentsItemsFields", - "SessionsRetrieveDecisionResponseDocumentVerifications", - "SessionsRetrieveDecisionResponseDocumentVerificationsGroupsValue", - "SessionsRetrieveDecisionResponseDocumentVerificationsItems", - "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata", - "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulation", - "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions", - "SessionsRetrieveDecisionResponseEmailVerifications", - "SessionsRetrieveDecisionResponseEmailVerificationsBreaches", - "SessionsRetrieveDecisionResponseEmailVerificationsLifecycle", - "SessionsRetrieveDecisionResponseEmailVerificationsLifecycleDetails", - "SessionsRetrieveDecisionResponseEmailVerificationsMatches", - "SessionsRetrieveDecisionResponseEmailVerificationsWarnings", - "SessionsRetrieveDecisionResponseExpectedDetails", - "SessionsRetrieveDecisionResponseFaceMatches", - "SessionsRetrieveDecisionResponseFaceMatchesWarnings", - "SessionsRetrieveDecisionResponseIdVerifications", - "SessionsRetrieveDecisionResponseIdVerificationsBackImageQualityScore", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentLiveness", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPortraitReplace", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPrintedCopy", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessScreenReplay", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatches", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatchesUserDetails", - "SessionsRetrieveDecisionResponseIdVerificationsExtraFields", - "SessionsRetrieveDecisionResponseIdVerificationsFallbackFrom", - "SessionsRetrieveDecisionResponseIdVerificationsFrontImageQualityScore", - "SessionsRetrieveDecisionResponseIdVerificationsIdLookup", - "SessionsRetrieveDecisionResponseIdVerificationsIdLookupComparison", - "SessionsRetrieveDecisionResponseIdVerificationsMatches", - "SessionsRetrieveDecisionResponseIdVerificationsMatchesUserDetails", - "SessionsRetrieveDecisionResponseIdVerificationsMrz", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddress", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressDocumentLocation", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResults", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsAddressComponents", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometry", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryLocation", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewport", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportNortheast", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPoints", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPointsLocation", - "SessionsRetrieveDecisionResponseIdVerificationsWalletVerification", - "SessionsRetrieveDecisionResponseIdVerificationsWarnings", - "SessionsRetrieveDecisionResponseIpAnalyses", - "SessionsRetrieveDecisionResponseIpAnalysesIdDocument", - "SessionsRetrieveDecisionResponseIpAnalysesIdDocumentLocation", - "SessionsRetrieveDecisionResponseIpAnalysesIp", - "SessionsRetrieveDecisionResponseIpAnalysesIpLocation", - "SessionsRetrieveDecisionResponseIpAnalysesMatches", - "SessionsRetrieveDecisionResponseIpAnalysesMatchesDeviceInfo", - "SessionsRetrieveDecisionResponseIpAnalysesMatchesLocationInfo", - "SessionsRetrieveDecisionResponseIpAnalysesPoaDocument", - "SessionsRetrieveDecisionResponseIpAnalysesPoaDocumentLocation", - "SessionsRetrieveDecisionResponseIpAnalysesWarnings", - "SessionsRetrieveDecisionResponseKeyPeopleChecks", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistry", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersKycAmlCategories", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures", - "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmitted", - "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties", - "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesRoles", - "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures", - "SessionsRetrieveDecisionResponseKeyPeopleChecksUboKycSummary", - "SessionsRetrieveDecisionResponseLivenessChecks", - "SessionsRetrieveDecisionResponseLivenessChecksMatches", - "SessionsRetrieveDecisionResponseLivenessChecksMatchesUserDetails", - "SessionsRetrieveDecisionResponseLivenessChecksWarnings", - "SessionsRetrieveDecisionResponseNfcVerifications", - "SessionsRetrieveDecisionResponseNfcVerificationsAuthenticity", - "SessionsRetrieveDecisionResponseNfcVerificationsCertificateSummary", - "SessionsRetrieveDecisionResponseNfcVerificationsChipData", - "SessionsRetrieveDecisionResponseNfcVerificationsWarnings", - "SessionsRetrieveDecisionResponsePhoneVerifications", - "SessionsRetrieveDecisionResponsePhoneVerificationsCarrier", - "SessionsRetrieveDecisionResponsePhoneVerificationsLifecycle", - "SessionsRetrieveDecisionResponsePhoneVerificationsLifecycleDetails", - "SessionsRetrieveDecisionResponsePhoneVerificationsMatches", - "SessionsRetrieveDecisionResponsePhoneVerificationsWarnings", - "SessionsRetrieveDecisionResponsePoaVerifications", - "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadata", - "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulation", - "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions", - "SessionsRetrieveDecisionResponsePoaVerificationsExtraFields", - "SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddress", - "SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddressDocumentLocation", - "SessionsRetrieveDecisionResponsePoaVerificationsWarnings", - "SessionsRetrieveDecisionResponseQuestionnaireResponses", - "SessionsRetrieveDecisionResponseQuestionnaireResponsesSections", - "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItems", - "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsAnswer", - "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsChoices", - "SessionsRetrieveDecisionResponseRegistryChecks", - "SessionsRetrieveDecisionResponseRegistryChecksCompany", - "SessionsRetrieveDecisionResponseRegistryChecksCompanyBeneficialOwners", - "SessionsRetrieveDecisionResponseRegistryChecksCompanyOfficers", - "SessionsRetrieveDecisionResponseResubmissions", - "SessionsRetrieveDecisionResponseResubmissionsNodes", - "SessionsRetrieveDecisionResponseResubmissionsRequestedBy", - "SessionsRetrieveDecisionResponseReviews", - "SessionsRetrieveDeletionJobResponse", - "SessionsRetrieveDeletionJobResponseOutcomes", "SessionsReviewsCreateParams", "SessionsReviewsCreateResponse", "SessionsReviewsListResponse", "SessionsShareParams", "SessionsShareResponse", + "SessionsSignaturesValidateParams", + "SessionsSignaturesValidateResponse", + "SessionsSimulateParams", "SessionsUpdateAmlHitStatusParams", + "SessionsUpdateAmlHitStatusResponse", + "SessionsUpdateAmlStatusParams", + "SessionsUpdateAmlStatusResponse", "SessionsUpdateDataParams", "SessionsUpdateDataParamsParsedAddress", "SessionsUpdateDataResponse", "SessionsUpdateDataResponseParsedAddress", + "SessionsUpdateDocumentAiDataParams", + "SessionsUpdateDocumentAiDataResponse", "SessionsUpdateFeatureStatusParams", "SessionsUpdateFeatureStatusResponse", "SessionsUpdateKybCompanyDataParams", @@ -3891,6 +4934,25 @@ "SessionsUpdateStatusParams", "SessionsUpdateStatusParamsNodesToResubmit", "SessionsUpdateStatusResponse", + "SignatureField", + "SignatureFieldAnchor", + "SignatureFieldResult", + "SignatureFieldValidation", + "SignatureResult", + "SignatureResultAdoptedSignature", + "SignatureResultCertificate", + "SignatureResultConfirmation", + "SignatureResultDocuments", + "SignatureResultDocumentsConversion", + "SignatureResultIdentityUsed", + "SignatureResultSigner", + "SignatureResultTimestamp", + "SignatureResultValidation", + "SignatureResultValidationDocuments", + "SignatureResultValidationDocumentsCertificate", + "SignatureResultValidationDocumentsTimestamp", + "SignatureResultValidationDocumentsTrustedList", + "SignatureResultWarnings", "SimpleQuestionnaireElement", "SimpleQuestionnaireElementOptions", "SimpleQuestionnaireElementValidation", @@ -3899,8 +4961,6 @@ "SocialFootprintPlatform", "SocialFootprintProfile", "TransactionDetail", - "TransactionDetailActionRequiredVerificationSession", - "TransactionDetailActionRequiredWalletOwnership", "TransactionDetailActivities", "TransactionDetailAlerts", "TransactionDetailDocuments", @@ -3914,21 +4974,22 @@ "TransactionDetailRemediation", "TransactionDetailRuleRuns", "TransactionDetailTags", - "TransactionDetailTravelRule", + "TransactionNote", + "TransactionNoteMetadata", "TransactionRuleAction", + "TransactionRuleActionAddNote", + "TransactionRuleActionAddNoteParam", + "TransactionRuleActionAddScore", + "TransactionRuleActionAddScoreParam", + "TransactionRuleActionAddTags", + "TransactionRuleActionAddTagsParam", + "TransactionRuleActionAddToList", + "TransactionRuleActionAddToListParam", + "TransactionRuleActionChangeStatus", + "TransactionRuleActionChangeStatusParam", + "TransactionRuleActionOpenCase", + "TransactionRuleActionOpenCaseParam", "TransactionRuleActionParam", - "TransactionRuleActionVariant1", - "TransactionRuleActionVariant1Param", - "TransactionRuleActionVariant2", - "TransactionRuleActionVariant2Param", - "TransactionRuleActionVariant3", - "TransactionRuleActionVariant3Param", - "TransactionRuleActionVariant4", - "TransactionRuleActionVariant4Param", - "TransactionRuleActionVariant5", - "TransactionRuleActionVariant5Param", - "TransactionRuleActionVariant6", - "TransactionRuleActionVariant6Param", "TransactionRuleAggregation", "TransactionRuleAggregationParam", "TransactionRuleBacktestResult", @@ -3939,6 +5000,13 @@ "TransactionRuleLibraryEntry", "TransactionRuleScope", "TransactionRuleScopeParam", + "TransactionSummary", + "TransactionSummaryTags", + "TransactionTravelRule", + "TransactionTravelRuleCounterpartyVasp", + "TransactionTravelRuleCounterpartyVaspDdScoreBreakdownValue", + "TransactionVerificationSessionAction", + "TransactionWalletOwnershipAction", "TransactionsCreateParams", "TransactionsCreateParamsCounterparty", "TransactionsCreateParamsCounterpartyDeviceContext", @@ -3956,11 +5024,12 @@ "TransactionsCreateParamsTravelRuleDetails", "TransactionsCreateSdkTokenParams", "TransactionsCreateSdkTokenResponse", - "TransactionsListNetworksResponse", - "TransactionsListPartyNetworksResponse", + "TransactionsListRelatedResponse", "TransactionsListResponse", "TransactionsListResponseResults", "TransactionsListResponseResultsTags", + "TransactionsNotesCreateParams", + "TransactionsNotesListResponse", "TransactionsRulesBacktestParams", "TransactionsRulesCreateParams", "TransactionsRulesInstallLibraryParams", @@ -3969,65 +5038,132 @@ "TransactionsRulesUninstallLibraryParams", "TransactionsRulesUpdateParams", "TransactionsTravelRuleConfirmOwnershipParams", + "TransactionsTravelRuleCreateMessageParams", + "TransactionsTravelRuleTriggerTestVaspInboundParams", "TransactionsTravelRuleUpdateTransferParams", + "TravelRuleChainDetails", + "TravelRuleChainDetailsUtxo", + "TravelRuleChainSide", + "TravelRuleCounterpartyContact", + "TravelRuleCounterpartyContactsCreateParams", + "TravelRuleCounterpartyContactsListResponse", + "TravelRuleCounterpartyDecision", + "TravelRuleCounterpartyDecisionInheritedFrom", + "TravelRuleCounterpartyDecisionResponse", + "TravelRuleCounterpartyQuestionnaire", + "TravelRuleCounterpartyQuestionnaireList", + "TravelRuleCounterpartyScreening", + "TravelRuleCounterpartyScreeningResult", + "TravelRuleCounterpartyScreeningStatus", "TravelRuleCreateWidgetSessionParams", + "TravelRuleDataMatch", + "TravelRuleDataMatchFieldsValue", + "TravelRuleDataSharingFields", + "TravelRuleDataSharingFieldsParam", + "TravelRuleDataUnmasked", + "TravelRuleDelivery", + "TravelRuleDiscoveryStep", "TravelRuleInboundResponse", "TravelRuleListVaspsResponse", + "TravelRuleMatchingRules", + "TravelRuleMatchingRulesCompany", + "TravelRuleMatchingRulesCompanyParam", + "TravelRuleMatchingRulesIndividual", + "TravelRuleMatchingRulesIndividualParam", + "TravelRuleMatchingRulesParam", + "TravelRuleMessage", + "TravelRuleMessageAttachment", + "TravelRuleMessageAttachmentFiles", + "TravelRuleNameMatch", + "TravelRuleNextAction", "TravelRuleRegisterInboundParams", "TravelRuleRegisterInboundParamsOriginatingVasp", + "TravelRuleScreenVaspParams", + "TravelRuleSendComplianceEmailConfirmationResponse", + "TravelRuleSendVaspQuestionnaireParams", "TravelRuleSettingsDetail", + "TravelRuleSettingsDetailDiditNetwork", "TravelRuleSettingsDetailNetworks", "TravelRuleSettingsDetailNetworksGtr", "TravelRuleSettingsDetailNetworksSygna", "TravelRuleSettingsDetailNetworksTrust", "TravelRuleSettingsDetailNetworksVerifyvasp", + "TravelRuleTestVaspInboundResponse", "TravelRuleTransferDetail", "TravelRuleTransferDetailCounterpartyVasp", + "TravelRuleTransferDetailCounterpartyVaspDdScoreBreakdownValue", "TravelRuleTransferDetailTimeline", + "TravelRuleTransferRow", + "TravelRuleTransfersListResponse", "TravelRuleUpdateSettingsParams", + "TravelRuleUpdateVaspDecisionParams", + "TravelRuleUtxo", + "TravelRuleUtxoEntry", + "TravelRuleUtxoEntryParam", + "TravelRuleWalletAddressesBulkImportParamsVariant1", "TravelRuleWalletAddressesCreateParams", "TravelRuleWalletAddressesListResponse", "TravelRuleWalletAddressesUpdateParams", "TravelRuleWidgetSessionResponse", + "TravelRuleWorkspace", + "TravelRuleWorkspaceClaim", + "TravelRuleWorkspaceClaimParams", + "TravelRuleWorkspaceConfirmDomainParams", + "TravelRuleWorkspaceDomain", + "TravelRuleWorkspaceDomainCodeResponse", + "TravelRuleWorkspaceEnableBillingParams", + "TravelRuleWorkspaceTxtRecord", "UserDetailItem", "UserDetailItemComments", "UserListItem", "UserListItemFeaturesList", "UserListItemTags", + "UserSessionListItem", + "UserSessionListItemAdditionalDetails", + "UserSessionListItemAdditionalDetailsAml", + "UserSessionListItemAdditionalDetailsAmlScreenedData", + "UserSessionListItemAdditionalDetailsDatabaseValidation", + "UserSessionListItemAdditionalDetailsEmailVerification", + "UserSessionListItemAdditionalDetailsFaceMatch", + "UserSessionListItemAdditionalDetailsLiveness", + "UserSessionListItemAdditionalDetailsPhoneVerification", + "UserSessionListItemAdditionalDetailsPoa", + "UserSessionListItemEmailAddress", + "UserSessionListItemFeatures", + "UserSessionListItemPhoneNumber", "UsersBatchDeleteParams", "UsersBatchDeleteResponse", "UsersCreateParams", - "UsersListNetworksResponse", + "UsersCreateResponse", + "UsersCreateResponseComments", + "UsersCreateResponseFeaturesList", + "UsersCreateResponseTags", + "UsersCreateResponseTagsTag", + "UsersCreateResponseVerificationLink", "UsersListResponse", - "UsersRetrieveOngoingMonitoringJobResponse", - "UsersRetrieveOngoingMonitoringJobResponseEstimate", - "UsersRetrieveOngoingMonitoringJobResponseOutcomes", "UsersSetAmlMonitoringParams", - "UsersSetAmlMonitoringResponse202", - "UsersSetAmlMonitoringResponse202Estimate", - "UsersSetAmlMonitoringResponse202Outcomes", - "UsersSetAmlMonitoringResponse202Results", - "UsersSetAmlMonitoringResponseVariant1", - "UsersSetAmlMonitoringResponseVariant2", - "UsersSetAmlMonitoringResponseVariant2Estimate", - "UsersSetAmlMonitoringResponseVariant2Outcomes", - "UsersSetAmlMonitoringResponseVariant2Results", "UsersSetOngoingMonitoringParams", - "UsersSetOngoingMonitoringResponse202", - "UsersSetOngoingMonitoringResponse202Estimate", - "UsersSetOngoingMonitoringResponse202Outcomes", - "UsersSetOngoingMonitoringResponseVariant1", - "UsersSetOngoingMonitoringResponseVariant2", - "UsersSetOngoingMonitoringResponseVariant2Estimate", - "UsersSetOngoingMonitoringResponseVariant2Outcomes", "UsersUpdateParams", "UsersUpdateStatusParams", "UsersUploadFaceParams", "UsersUploadFaceResponse", "VaspDirectoryEntry", + "VaspDirectoryEntryAliases", + "VaspDirectoryEntryContacts", + "VaspDirectoryEntryDdScoreBreakdownValue", + "VaspDirectoryEntryEntities", + "VaspDirectoryEntryEssentialChecks", + "VaspDirectoryEntryEssentialChecksChecks", + "VaspDirectoryEntryNetworks", + "VaspDirectoryEntryResponsiveness", + "VaspDirectoryEntryRoutes", + "VaspDirectoryEntryUmbrella", "VerifyEmailResponse", "WalletAddressEntry", "WalletAddressEntryUpdate", + "WalletAddressImportReport", + "WalletAddressImportReportRows", + "WalletAddressImportRow", "WalletScreeningResult", "WalletScreeningResultCounterpartyConnections", "WalletScreeningResultDestinationOfFunds", @@ -4039,18 +5175,30 @@ "WalletScreeningScreenPdfParamsDestinationOfFunds", "WalletScreeningScreenPdfParamsRiskFactors", "WalletScreeningScreenPdfParamsSourceOfFunds", + "WebhookConfig", + "WebhookConfigAvailableEvents", + "WebhookConfigDestination", + "WebhookDestinationDelivery", + "WebhookDestinationDeliveryDetail", "WebhookEvent", "WebhookSubscribedEvents", "WebhookSubscribedEventsParam", "WebhooksDestinationsCreateParams", "WebhooksDestinationsCreateResponse", "WebhooksDestinationsCreateResponseSummary", + "WebhooksDestinationsListDeliveriesResponse", "WebhooksDestinationsListResponseItem", + "WebhooksDestinationsListResponseItemDailyPoints", + "WebhooksDestinationsRetrieveAnalyticsResponse", + "WebhooksDestinationsRetrieveAnalyticsResponsePoints", + "WebhooksDestinationsRetrieveAnalyticsResponseSummary", "WebhooksDestinationsRetrieveResponse", "WebhooksDestinationsRetrieveResponseSummary", "WebhooksDestinationsUpdateParams", "WebhooksDestinationsUpdateResponse", "WebhooksDestinationsUpdateResponseSummary", + "WebhooksUpdateConfigParams", + "WorkflowDetail", "WorkflowListItem", "WorkflowListItemCreatedBy", "WorkflowsCreateParams", diff --git a/src/didit/types/account/applications/update_params.py b/src/didit/types/account/applications/update_params.py index 690fdcc..5c166a6 100644 --- a/src/didit/types/account/applications/update_params.py +++ b/src/didit/types/account/applications/update_params.py @@ -9,7 +9,7 @@ class AccountApplicationsUpdateParams(TypedDict, total=False): - """PATCH body for application metadata. Send only the fields you want to change; omitted fields are preserved. `uuid`, `client_id`, and `api_key` cannot be updated.""" + """PATCH body for application metadata. Send only the fields you want to change; omitted fields are preserved. `uuid`, `client_id`, and `api_key` cannot be updated. `products` is read-only: a `products` key in the body is ignored.""" name: str """Application display name.""" diff --git a/src/didit/types/account/resend_otp_response.py b/src/didit/types/account/resend_otp_response.py new file mode 100644 index 0000000..0c68d5e --- /dev/null +++ b/src/didit/types/account/resend_otp_response.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["AccountResendOtpResponse"] + + +class AccountResendOtpResponse(BaseModel): + message: str + """Always `A new verification code has been sent to your email.`""" + + email: str + """The address the code was sent to, normalized: lowercased, with any `+tag` removed from the local part (`Dev+test@Example.com` becomes `dev@example.com`).""" diff --git a/src/didit/types/aml/retrieve_dataset_freshness_response.py b/src/didit/types/aml/retrieve_dataset_freshness_response.py new file mode 100644 index 0000000..ec16fd9 --- /dev/null +++ b/src/didit/types/aml/retrieve_dataset_freshness_response.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.aml_dataset_freshness import AMLDatasetFreshness + +__all__ = ["AmlRetrieveDatasetFreshnessResponse"] + + +class AmlRetrieveDatasetFreshnessResponse(BaseModel): + dataset_freshness: AMLDatasetFreshness | None + """The latest reading, or `null` when `availability` is not `available`.""" + + availability: Literal["available", "not_configured", "not_yet_collected"] | str + """`available` when a reading exists; `not_configured` when dataset dates are not collected in this environment; `not_yet_collected` when collection is set up but no reading exists yet.""" diff --git a/src/didit/types/billing/retrieve_balance_response.py b/src/didit/types/billing/retrieve_balance_response.py index d8445b3..fce164e 100644 --- a/src/didit/types/billing/retrieve_balance_response.py +++ b/src/didit/types/billing/retrieve_balance_response.py @@ -11,7 +11,7 @@ class BillingRetrieveBalanceResponse(BaseModel): """Current pre-paid credit balance in USD, as a fixed-point decimal string. Decreases as verifications are billed and increases when a top-up settles.""" auto_refill_enabled: bool - """When `true`, Didit will automatically charge the saved Stripe payment method to add credits once the balance crosses `auto_refill_threshold`. Configured from Console → Billing.""" + """When `true`, Didit will automatically charge the saved payment method to add credits once the balance crosses `auto_refill_threshold`. Configured from Console → Billing.""" auto_refill_amount: str | None = None """USD amount that will be charged on each auto-refill. `null` when auto-refill is disabled or unset.""" diff --git a/src/didit/types/billing/top_up_params.py b/src/didit/types/billing/top_up_params.py index 1e94f69..7acd3ef 100644 --- a/src/didit/types/billing/top_up_params.py +++ b/src/didit/types/billing/top_up_params.py @@ -11,7 +11,7 @@ class BillingTopUpParams(TypedDict, total=False): """Amount to charge, in USD. **Must be at least 50.** Tax is added automatically at checkout based on the customer's billing address.""" success_url: str - """URL Stripe redirects the customer to after a successful payment. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. Append the literal placeholder `{CHECKOUT_SESSION_ID}` if you want Stripe to substitute the session ID.""" + """URL the hosted checkout redirects the customer to after a successful payment. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. Append the literal placeholder `{CHECKOUT_SESSION_ID}` if you want the hosted checkout to substitute the session ID.""" cancel_url: str - """URL Stripe redirects the customer to when they abort or close the Checkout window. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable.""" + """URL the hosted checkout redirects the customer to when they abort or close the Checkout window. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable.""" diff --git a/src/didit/types/billing/top_up_response.py b/src/didit/types/billing/top_up_response.py index db88ee7..24fbdf3 100644 --- a/src/didit/types/billing/top_up_response.py +++ b/src/didit/types/billing/top_up_response.py @@ -8,7 +8,7 @@ class BillingTopUpResponse(BaseModel): checkout_session_id: str - """Stripe Checkout Session identifier (e.g. `cs_test_a1B2c3...`). Useful for server-side reconciliation.""" + """Hosted checkout session identifier (e.g. `cs_test_a1B2c3...`). Useful for server-side reconciliation.""" checkout_session_url: str - """Single-use Stripe-hosted payment page URL. Redirect the customer here to complete payment.""" + """Single-use hosted payment page URL. Redirect the customer here to complete payment.""" diff --git a/src/didit/types/business_sessions/__init__.py b/src/didit/types/blocklist/__init__.py similarity index 100% rename from src/didit/types/business_sessions/__init__.py rename to src/didit/types/blocklist/__init__.py diff --git a/src/didit/types/blocklist/add_params.py b/src/didit/types/blocklist/add_params.py new file mode 100644 index 0000000..31c9b32 --- /dev/null +++ b/src/didit/types/blocklist/add_params.py @@ -0,0 +1,25 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["BlocklistAddParams"] + + +class BlocklistAddParams(TypedDict, total=False): + """At least one `blocklist_*` flag must be `true`.""" + + session_id: Required[str] + """A person verification session of your application. Deleted sessions are refused.""" + + blocklist_face: bool + """Blocklist the session's face.""" + + blocklist_document: bool + """Blocklist the session's identity document. The document needs its issuing state, document type, document number and full name.""" + + blocklist_phone: bool + """Blocklist the session's phone number.""" + + blocklist_email: bool + """Blocklist the session's email address.""" diff --git a/src/didit/types/blocklist/add_response.py b/src/didit/types/blocklist/add_response.py new file mode 100644 index 0000000..a2d565c --- /dev/null +++ b/src/didit/types/blocklist/add_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["BlocklistAddResponse"] + + +class BlocklistAddResponse(BaseModel): + message: str + """Always `Successfully updated blocklist.`""" diff --git a/src/didit/types/blocklist/list_response.py b/src/didit/types/blocklist/list_response.py new file mode 100644 index 0000000..42b338b --- /dev/null +++ b/src/didit/types/blocklist/list_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.blocklisted_item import BlocklistedItem + +__all__ = ["BlocklistListResponse"] + + +class BlocklistListResponse(BaseModel): + count: int + """Total number of blocklisted items matching the filters.""" + + next: str | None + """Absolute URL of the next page, or `null` on the last page.""" + + previous: str | None + """Absolute URL of the previous page, or `null` on the first page.""" + + results: list[BlocklistedItem] diff --git a/src/didit/types/blocklist/remove_params.py b/src/didit/types/blocklist/remove_params.py new file mode 100644 index 0000000..07a43a8 --- /dev/null +++ b/src/didit/types/blocklist/remove_params.py @@ -0,0 +1,25 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["BlocklistRemoveParams"] + + +class BlocklistRemoveParams(TypedDict, total=False): + """At least one `unblock_*` flag must be `true`.""" + + session_id: Required[str] + """A person verification session of your application. Deleted sessions are refused.""" + + unblock_face: bool + """Lift the block on the session's face.""" + + unblock_document: bool + """Lift the block on the session's identity document.""" + + unblock_phone: bool + """Lift the block on the session's phone number.""" + + unblock_email: bool + """Lift the block on the session's email address.""" diff --git a/src/didit/types/blocklist/remove_response.py b/src/didit/types/blocklist/remove_response.py new file mode 100644 index 0000000..22a6256 --- /dev/null +++ b/src/didit/types/blocklist/remove_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["BlocklistRemoveResponse"] + + +class BlocklistRemoveResponse(BaseModel): + message: str + """Always `Successfully removed from blocklist.`""" diff --git a/src/didit/types/business_sessions/list_networks_response.py b/src/didit/types/business_sessions/list_networks_response.py deleted file mode 100644 index eb740ad..0000000 --- a/src/didit/types/business_sessions/list_networks_response.py +++ /dev/null @@ -1,11 +0,0 @@ -# Code generated by codegen from openapi/*.json. DO NOT EDIT. -from __future__ import annotations - -from didit._models import BaseModel -from didit.types.components.entity_network_membership import EntityNetworkMembership - -__all__ = ["BusinessSessionsListNetworksResponse"] - - -class BusinessSessionsListNetworksResponse(BaseModel): - results: list[EntityNetworkMembership] | None = None diff --git a/src/didit/types/businesses/list_networks_response.py b/src/didit/types/businesses/list_networks_response.py deleted file mode 100644 index bd24e5e..0000000 --- a/src/didit/types/businesses/list_networks_response.py +++ /dev/null @@ -1,11 +0,0 @@ -# Code generated by codegen from openapi/*.json. DO NOT EDIT. -from __future__ import annotations - -from didit._models import BaseModel -from didit.types.components.entity_network_membership import EntityNetworkMembership - -__all__ = ["BusinessesListNetworksResponse"] - - -class BusinessesListNetworksResponse(BaseModel): - results: list[EntityNetworkMembership] | None = None diff --git a/src/didit/types/businesses/retrieve_ongoing_monitoring_job_response.py b/src/didit/types/businesses/retrieve_ongoing_monitoring_job_response.py deleted file mode 100644 index 313c473..0000000 --- a/src/didit/types/businesses/retrieve_ongoing_monitoring_job_response.py +++ /dev/null @@ -1,88 +0,0 @@ -# Code generated by codegen from openapi/*.json. DO NOT EDIT. -from __future__ import annotations - -from typing import Literal - -from didit._models import BaseModel - -__all__ = [ - "BusinessesRetrieveOngoingMonitoringJobResponse", - "BusinessesRetrieveOngoingMonitoringJobResponseEstimate", - "BusinessesRetrieveOngoingMonitoringJobResponseOutcomes", -] - - -class BusinessesRetrieveOngoingMonitoringJobResponseEstimate(BaseModel): - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" - - users_total: int | None = None - """Entities in the selection.""" - - already_enabled: int | None = None - - with_existing_aml: int | None = None - """Entities whose existing AML screening will be switched to monitored.""" - - needing_screening: int | None = None - """Entities without any AML screening that will be screened first (billed).""" - - skipped_no_identity_data: int | None = None - - skipped_blocked: int | None = None - - estimated_screening_cost_usd: float | None = None - - estimated_annual_monitoring_cost_usd: float | None = None - - balance_sufficient: bool | None = None - - -class BusinessesRetrieveOngoingMonitoringJobResponseOutcomes(BaseModel): - """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" - - enabled: int | None = None - """Monitoring enabled on an existing AML screening.""" - - screened_and_enabled: int | None = None - """A new AML screening was created (billed) and monitoring enabled.""" - - already_enabled: int | None = None - """Monitoring was already active (legacy duplicate rows are consolidated).""" - - disabled: int | None = None - """Monitoring disabled on every AML row of the entity.""" - - skipped_no_identity_data: int | None = None - """Skipped: no full name + date of birth + country available to screen.""" - - skipped_blocked: int | None = None - """Skipped: the user is blocked.""" - - skipped_sandbox: int | None = None - """Skipped: sandbox application.""" - - failed_provider: int | None = None - """The screening provider call failed.""" - - failed_insufficient_balance: int | None = None - """Balance ran out mid-job.""" - - -class BusinessesRetrieveOngoingMonitoringJobResponse(BaseModel): - job_id: str | None = None - - status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None - - is_enabled: bool | None = None - - total: int | None = None - - processed: int | None = None - - outcomes: BusinessesRetrieveOngoingMonitoringJobResponseOutcomes | None = None - """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" - - error: str | None = None - - estimate: BusinessesRetrieveOngoingMonitoringJobResponseEstimate | None = None - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" diff --git a/src/didit/types/businesses/retrieve_session_ongoing_monitoring_job_response.py b/src/didit/types/businesses/retrieve_session_ongoing_monitoring_job_response.py deleted file mode 100644 index 2832d8c..0000000 --- a/src/didit/types/businesses/retrieve_session_ongoing_monitoring_job_response.py +++ /dev/null @@ -1,88 +0,0 @@ -# Code generated by codegen from openapi/*.json. DO NOT EDIT. -from __future__ import annotations - -from typing import Literal - -from didit._models import BaseModel - -__all__ = [ - "BusinessesRetrieveSessionOngoingMonitoringJobResponse", - "BusinessesRetrieveSessionOngoingMonitoringJobResponseEstimate", - "BusinessesRetrieveSessionOngoingMonitoringJobResponseOutcomes", -] - - -class BusinessesRetrieveSessionOngoingMonitoringJobResponseEstimate(BaseModel): - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" - - users_total: int | None = None - """Entities in the selection.""" - - already_enabled: int | None = None - - with_existing_aml: int | None = None - """Entities whose existing AML screening will be switched to monitored.""" - - needing_screening: int | None = None - """Entities without any AML screening that will be screened first (billed).""" - - skipped_no_identity_data: int | None = None - - skipped_blocked: int | None = None - - estimated_screening_cost_usd: float | None = None - - estimated_annual_monitoring_cost_usd: float | None = None - - balance_sufficient: bool | None = None - - -class BusinessesRetrieveSessionOngoingMonitoringJobResponseOutcomes(BaseModel): - """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" - - enabled: int | None = None - """Monitoring enabled on an existing AML screening.""" - - screened_and_enabled: int | None = None - """A new AML screening was created (billed) and monitoring enabled.""" - - already_enabled: int | None = None - """Monitoring was already active (legacy duplicate rows are consolidated).""" - - disabled: int | None = None - """Monitoring disabled on every AML row of the entity.""" - - skipped_no_identity_data: int | None = None - """Skipped: no full name + date of birth + country available to screen.""" - - skipped_blocked: int | None = None - """Skipped: the user is blocked.""" - - skipped_sandbox: int | None = None - """Skipped: sandbox application.""" - - failed_provider: int | None = None - """The screening provider call failed.""" - - failed_insufficient_balance: int | None = None - """Balance ran out mid-job.""" - - -class BusinessesRetrieveSessionOngoingMonitoringJobResponse(BaseModel): - job_id: str | None = None - - status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None - - is_enabled: bool | None = None - - total: int | None = None - - processed: int | None = None - - outcomes: BusinessesRetrieveSessionOngoingMonitoringJobResponseOutcomes | None = None - """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" - - error: str | None = None - - estimate: BusinessesRetrieveSessionOngoingMonitoringJobResponseEstimate | None = None - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" diff --git a/src/didit/types/businesses/set_aml_monitoring_response.py b/src/didit/types/businesses/set_aml_monitoring_response.py deleted file mode 100644 index 31395bb..0000000 --- a/src/didit/types/businesses/set_aml_monitoring_response.py +++ /dev/null @@ -1,299 +0,0 @@ -# Code generated by codegen from openapi/*.json. DO NOT EDIT. -from __future__ import annotations - -import datetime as _dt -from typing import Literal - -from didit._models import BaseModel - -__all__ = [ - "BusinessesSetAmlMonitoringResponse202", - "BusinessesSetAmlMonitoringResponse202Estimate", - "BusinessesSetAmlMonitoringResponse202Outcomes", - "BusinessesSetAmlMonitoringResponse202Results", - "BusinessesSetAmlMonitoringResponseVariant1", - "BusinessesSetAmlMonitoringResponseVariant2", - "BusinessesSetAmlMonitoringResponseVariant2Estimate", - "BusinessesSetAmlMonitoringResponseVariant2Outcomes", - "BusinessesSetAmlMonitoringResponseVariant2Results", -] - - -class BusinessesSetAmlMonitoringResponseVariant1(BaseModel): - """Cost preview. Returned directly when estimate_only is true (nothing is written).""" - - users_total: int - """Profiles matched by the selection.""" - - already_enabled: int - """Profiles already monitored (nothing to do when enabling; these are what disabling turns off).""" - - with_existing_aml: int - """Profiles whose existing AML screening will be switched to monitored (no new screening).""" - - needing_screening: int - """Profiles without any AML screening that will be screened first (billed as a regular AML screening).""" - - skipped_no_identity_data: int - """Profiles that cannot be screened because identity data is missing.""" - - skipped_blocked: int - """Blocked users that will be skipped (always 0 for businesses).""" - - estimated_screening_cost_usd: float - """One-time cost of the new screenings.""" - - estimated_annual_monitoring_cost_usd: float - """Yearly monitoring cost of the profiles that will become monitored.""" - - balance_sufficient: bool - """Whether the organization balance covers the screening plus the first year of monitoring.""" - - unmatched: list[str] - """Identifiers of the request that matched no profile in this application, exactly as sent.""" - - -class BusinessesSetAmlMonitoringResponseVariant2Estimate(BaseModel): - """Cost preview. Returned directly when estimate_only is true (nothing is written).""" - - users_total: int | None = None - """Profiles matched by the selection.""" - - already_enabled: int | None = None - """Profiles already monitored (nothing to do when enabling; these are what disabling turns off).""" - - with_existing_aml: int | None = None - """Profiles whose existing AML screening will be switched to monitored (no new screening).""" - - needing_screening: int | None = None - """Profiles without any AML screening that will be screened first (billed as a regular AML screening).""" - - skipped_no_identity_data: int | None = None - """Profiles that cannot be screened because identity data is missing.""" - - skipped_blocked: int | None = None - """Blocked users that will be skipped (always 0 for businesses).""" - - estimated_screening_cost_usd: float | None = None - """One-time cost of the new screenings.""" - - estimated_annual_monitoring_cost_usd: float | None = None - """Yearly monitoring cost of the profiles that will become monitored.""" - - balance_sufficient: bool | None = None - """Whether the organization balance covers the screening plus the first year of monitoring.""" - - -class BusinessesSetAmlMonitoringResponseVariant2Outcomes(BaseModel): - """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" - - enabled: int | None = None - """Monitoring enabled on an existing AML screening.""" - - screened_and_enabled: int | None = None - """A new AML screening was created (billed) and monitoring enabled on it.""" - - already_enabled: int | None = None - """Monitoring was already active; nothing changed and nothing was billed.""" - - disabled: int | None = None - """Monitoring turned off across every AML screening of the profile.""" - - skipped_no_identity_data: int | None = None - """Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen.""" - - skipped_blocked: int | None = None - """Skipped: the user is blocked.""" - - skipped_sandbox: int | None = None - """Skipped: sandbox application, no screening is created.""" - - failed_provider: int | None = None - """The screening could not be completed; retry later.""" - - failed_insufficient_balance: int | None = None - """Balance ran out while the job was running.""" - - -class BusinessesSetAmlMonitoringResponseVariant2Results(BaseModel): - didit_internal_id: str | None = None - """Didit internal id of the profile.""" - - vendor_data: str | None = None - """Your own identifier for the profile.""" - - outcome: ( - Literal[ - "enabled", - "screened_and_enabled", - "already_enabled", - "disabled", - "skipped_no_identity_data", - "skipped_blocked", - "skipped_sandbox", - "failed_provider", - "failed_insufficient_balance", - ] - | str - | None - ) = None - - -class BusinessesSetAmlMonitoringResponseVariant2(BaseModel): - job_id: str - - status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str - - is_enabled: bool - """true when the job enables monitoring, false when it disables it.""" - - target_type: Literal["user", "business"] | str - """Which profile family the job toggles.""" - - total: int - """Profiles the job processes.""" - - processed: int - """Profiles processed so far.""" - - outcomes: BusinessesSetAmlMonitoringResponseVariant2Outcomes - """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" - - results: list[BusinessesSetAmlMonitoringResponseVariant2Results] - """One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API.""" - - error: str | None - """Generic failure message when status is FAILED; the detailed cause is kept on the platform side.""" - - created_at: _dt.datetime - - completed_at: _dt.datetime | None - - estimate: BusinessesSetAmlMonitoringResponseVariant2Estimate - """Cost preview. Returned directly when estimate_only is true (nothing is written).""" - - unmatched: list[str] - """Identifiers of the request that matched no profile in this application, exactly as sent. They are not part of the job.""" - - -class BusinessesSetAmlMonitoringResponse202Estimate(BaseModel): - """Cost preview. Returned directly when estimate_only is true (nothing is written).""" - - users_total: int | None = None - """Profiles matched by the selection.""" - - already_enabled: int | None = None - """Profiles already monitored (nothing to do when enabling; these are what disabling turns off).""" - - with_existing_aml: int | None = None - """Profiles whose existing AML screening will be switched to monitored (no new screening).""" - - needing_screening: int | None = None - """Profiles without any AML screening that will be screened first (billed as a regular AML screening).""" - - skipped_no_identity_data: int | None = None - """Profiles that cannot be screened because identity data is missing.""" - - skipped_blocked: int | None = None - """Blocked users that will be skipped (always 0 for businesses).""" - - estimated_screening_cost_usd: float | None = None - """One-time cost of the new screenings.""" - - estimated_annual_monitoring_cost_usd: float | None = None - """Yearly monitoring cost of the profiles that will become monitored.""" - - balance_sufficient: bool | None = None - """Whether the organization balance covers the screening plus the first year of monitoring.""" - - -class BusinessesSetAmlMonitoringResponse202Outcomes(BaseModel): - """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" - - enabled: int | None = None - """Monitoring enabled on an existing AML screening.""" - - screened_and_enabled: int | None = None - """A new AML screening was created (billed) and monitoring enabled on it.""" - - already_enabled: int | None = None - """Monitoring was already active; nothing changed and nothing was billed.""" - - disabled: int | None = None - """Monitoring turned off across every AML screening of the profile.""" - - skipped_no_identity_data: int | None = None - """Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen.""" - - skipped_blocked: int | None = None - """Skipped: the user is blocked.""" - - skipped_sandbox: int | None = None - """Skipped: sandbox application, no screening is created.""" - - failed_provider: int | None = None - """The screening could not be completed; retry later.""" - - failed_insufficient_balance: int | None = None - """Balance ran out while the job was running.""" - - -class BusinessesSetAmlMonitoringResponse202Results(BaseModel): - didit_internal_id: str | None = None - """Didit internal id of the profile.""" - - vendor_data: str | None = None - """Your own identifier for the profile.""" - - outcome: ( - Literal[ - "enabled", - "screened_and_enabled", - "already_enabled", - "disabled", - "skipped_no_identity_data", - "skipped_blocked", - "skipped_sandbox", - "failed_provider", - "failed_insufficient_balance", - ] - | str - | None - ) = None - - -class BusinessesSetAmlMonitoringResponse202(BaseModel): - job_id: str | None = None - - status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None - - is_enabled: bool | None = None - """true when the job enables monitoring, false when it disables it.""" - - target_type: Literal["user", "business"] | str | None = None - """Which profile family the job toggles.""" - - total: int | None = None - """Profiles the job processes.""" - - processed: int | None = None - """Profiles processed so far.""" - - outcomes: BusinessesSetAmlMonitoringResponse202Outcomes | None = None - """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" - - results: list[BusinessesSetAmlMonitoringResponse202Results] | None = None - """One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API.""" - - error: str | None = None - """Generic failure message when status is FAILED; the detailed cause is kept on the platform side.""" - - created_at: _dt.datetime | None = None - - completed_at: _dt.datetime | None = None - - estimate: BusinessesSetAmlMonitoringResponse202Estimate | None = None - """Cost preview. Returned directly when estimate_only is true (nothing is written).""" - - unmatched: list[str] | None = None - """Identifiers of the request that matched no profile in this application, exactly as sent. They are not part of the job.""" diff --git a/src/didit/types/businesses/set_ongoing_monitoring_response.py b/src/didit/types/businesses/set_ongoing_monitoring_response.py deleted file mode 100644 index fdad7de..0000000 --- a/src/didit/types/businesses/set_ongoing_monitoring_response.py +++ /dev/null @@ -1,193 +0,0 @@ -# Code generated by codegen from openapi/*.json. DO NOT EDIT. -from __future__ import annotations - -from typing import Literal - -from didit._models import BaseModel - -__all__ = [ - "BusinessesSetOngoingMonitoringResponse202", - "BusinessesSetOngoingMonitoringResponse202Estimate", - "BusinessesSetOngoingMonitoringResponse202Outcomes", - "BusinessesSetOngoingMonitoringResponseVariant1", - "BusinessesSetOngoingMonitoringResponseVariant2", - "BusinessesSetOngoingMonitoringResponseVariant2Estimate", - "BusinessesSetOngoingMonitoringResponseVariant2Outcomes", -] - - -class BusinessesSetOngoingMonitoringResponseVariant1(BaseModel): - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" - - users_total: int | None = None - """Entities in the selection.""" - - already_enabled: int | None = None - - with_existing_aml: int | None = None - """Entities whose existing AML screening will be switched to monitored.""" - - needing_screening: int | None = None - """Entities without any AML screening that will be screened first (billed).""" - - skipped_no_identity_data: int | None = None - - skipped_blocked: int | None = None - - estimated_screening_cost_usd: float | None = None - - estimated_annual_monitoring_cost_usd: float | None = None - - balance_sufficient: bool | None = None - - -class BusinessesSetOngoingMonitoringResponseVariant2Estimate(BaseModel): - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" - - users_total: int | None = None - """Entities in the selection.""" - - already_enabled: int | None = None - - with_existing_aml: int | None = None - """Entities whose existing AML screening will be switched to monitored.""" - - needing_screening: int | None = None - """Entities without any AML screening that will be screened first (billed).""" - - skipped_no_identity_data: int | None = None - - skipped_blocked: int | None = None - - estimated_screening_cost_usd: float | None = None - - estimated_annual_monitoring_cost_usd: float | None = None - - balance_sufficient: bool | None = None - - -class BusinessesSetOngoingMonitoringResponseVariant2Outcomes(BaseModel): - """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" - - enabled: int | None = None - """Monitoring enabled on an existing AML screening.""" - - screened_and_enabled: int | None = None - """A new AML screening was created (billed) and monitoring enabled.""" - - already_enabled: int | None = None - """Monitoring was already active (legacy duplicate rows are consolidated).""" - - disabled: int | None = None - """Monitoring disabled on every AML row of the entity.""" - - skipped_no_identity_data: int | None = None - """Skipped: no full name + date of birth + country available to screen.""" - - skipped_blocked: int | None = None - """Skipped: the user is blocked.""" - - skipped_sandbox: int | None = None - """Skipped: sandbox application.""" - - failed_provider: int | None = None - """The screening provider call failed.""" - - failed_insufficient_balance: int | None = None - """Balance ran out mid-job.""" - - -class BusinessesSetOngoingMonitoringResponseVariant2(BaseModel): - job_id: str | None = None - - status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None - - is_enabled: bool | None = None - - total: int | None = None - - processed: int | None = None - - outcomes: BusinessesSetOngoingMonitoringResponseVariant2Outcomes | None = None - """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" - - error: str | None = None - - estimate: BusinessesSetOngoingMonitoringResponseVariant2Estimate | None = None - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" - - -class BusinessesSetOngoingMonitoringResponse202Estimate(BaseModel): - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" - - users_total: int | None = None - """Entities in the selection.""" - - already_enabled: int | None = None - - with_existing_aml: int | None = None - """Entities whose existing AML screening will be switched to monitored.""" - - needing_screening: int | None = None - """Entities without any AML screening that will be screened first (billed).""" - - skipped_no_identity_data: int | None = None - - skipped_blocked: int | None = None - - estimated_screening_cost_usd: float | None = None - - estimated_annual_monitoring_cost_usd: float | None = None - - balance_sufficient: bool | None = None - - -class BusinessesSetOngoingMonitoringResponse202Outcomes(BaseModel): - """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" - - enabled: int | None = None - """Monitoring enabled on an existing AML screening.""" - - screened_and_enabled: int | None = None - """A new AML screening was created (billed) and monitoring enabled.""" - - already_enabled: int | None = None - """Monitoring was already active (legacy duplicate rows are consolidated).""" - - disabled: int | None = None - """Monitoring disabled on every AML row of the entity.""" - - skipped_no_identity_data: int | None = None - """Skipped: no full name + date of birth + country available to screen.""" - - skipped_blocked: int | None = None - """Skipped: the user is blocked.""" - - skipped_sandbox: int | None = None - """Skipped: sandbox application.""" - - failed_provider: int | None = None - """The screening provider call failed.""" - - failed_insufficient_balance: int | None = None - """Balance ran out mid-job.""" - - -class BusinessesSetOngoingMonitoringResponse202(BaseModel): - job_id: str | None = None - - status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None - - is_enabled: bool | None = None - - total: int | None = None - - processed: int | None = None - - outcomes: BusinessesSetOngoingMonitoringResponse202Outcomes | None = None - """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" - - error: str | None = None - - estimate: BusinessesSetOngoingMonitoringResponse202Estimate | None = None - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" diff --git a/src/didit/types/businesses/set_session_ongoing_monitoring_response.py b/src/didit/types/businesses/set_session_ongoing_monitoring_response.py deleted file mode 100644 index 28fc663..0000000 --- a/src/didit/types/businesses/set_session_ongoing_monitoring_response.py +++ /dev/null @@ -1,193 +0,0 @@ -# Code generated by codegen from openapi/*.json. DO NOT EDIT. -from __future__ import annotations - -from typing import Literal - -from didit._models import BaseModel - -__all__ = [ - "BusinessesSetSessionOngoingMonitoringResponse202", - "BusinessesSetSessionOngoingMonitoringResponse202Estimate", - "BusinessesSetSessionOngoingMonitoringResponse202Outcomes", - "BusinessesSetSessionOngoingMonitoringResponseVariant1", - "BusinessesSetSessionOngoingMonitoringResponseVariant2", - "BusinessesSetSessionOngoingMonitoringResponseVariant2Estimate", - "BusinessesSetSessionOngoingMonitoringResponseVariant2Outcomes", -] - - -class BusinessesSetSessionOngoingMonitoringResponseVariant1(BaseModel): - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" - - users_total: int | None = None - """Entities in the selection.""" - - already_enabled: int | None = None - - with_existing_aml: int | None = None - """Entities whose existing AML screening will be switched to monitored.""" - - needing_screening: int | None = None - """Entities without any AML screening that will be screened first (billed).""" - - skipped_no_identity_data: int | None = None - - skipped_blocked: int | None = None - - estimated_screening_cost_usd: float | None = None - - estimated_annual_monitoring_cost_usd: float | None = None - - balance_sufficient: bool | None = None - - -class BusinessesSetSessionOngoingMonitoringResponseVariant2Estimate(BaseModel): - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" - - users_total: int | None = None - """Entities in the selection.""" - - already_enabled: int | None = None - - with_existing_aml: int | None = None - """Entities whose existing AML screening will be switched to monitored.""" - - needing_screening: int | None = None - """Entities without any AML screening that will be screened first (billed).""" - - skipped_no_identity_data: int | None = None - - skipped_blocked: int | None = None - - estimated_screening_cost_usd: float | None = None - - estimated_annual_monitoring_cost_usd: float | None = None - - balance_sufficient: bool | None = None - - -class BusinessesSetSessionOngoingMonitoringResponseVariant2Outcomes(BaseModel): - """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" - - enabled: int | None = None - """Monitoring enabled on an existing AML screening.""" - - screened_and_enabled: int | None = None - """A new AML screening was created (billed) and monitoring enabled.""" - - already_enabled: int | None = None - """Monitoring was already active (legacy duplicate rows are consolidated).""" - - disabled: int | None = None - """Monitoring disabled on every AML row of the entity.""" - - skipped_no_identity_data: int | None = None - """Skipped: no full name + date of birth + country available to screen.""" - - skipped_blocked: int | None = None - """Skipped: the user is blocked.""" - - skipped_sandbox: int | None = None - """Skipped: sandbox application.""" - - failed_provider: int | None = None - """The screening provider call failed.""" - - failed_insufficient_balance: int | None = None - """Balance ran out mid-job.""" - - -class BusinessesSetSessionOngoingMonitoringResponseVariant2(BaseModel): - job_id: str | None = None - - status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None - - is_enabled: bool | None = None - - total: int | None = None - - processed: int | None = None - - outcomes: BusinessesSetSessionOngoingMonitoringResponseVariant2Outcomes | None = None - """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" - - error: str | None = None - - estimate: BusinessesSetSessionOngoingMonitoringResponseVariant2Estimate | None = None - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" - - -class BusinessesSetSessionOngoingMonitoringResponse202Estimate(BaseModel): - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" - - users_total: int | None = None - """Entities in the selection.""" - - already_enabled: int | None = None - - with_existing_aml: int | None = None - """Entities whose existing AML screening will be switched to monitored.""" - - needing_screening: int | None = None - """Entities without any AML screening that will be screened first (billed).""" - - skipped_no_identity_data: int | None = None - - skipped_blocked: int | None = None - - estimated_screening_cost_usd: float | None = None - - estimated_annual_monitoring_cost_usd: float | None = None - - balance_sufficient: bool | None = None - - -class BusinessesSetSessionOngoingMonitoringResponse202Outcomes(BaseModel): - """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" - - enabled: int | None = None - """Monitoring enabled on an existing AML screening.""" - - screened_and_enabled: int | None = None - """A new AML screening was created (billed) and monitoring enabled.""" - - already_enabled: int | None = None - """Monitoring was already active (legacy duplicate rows are consolidated).""" - - disabled: int | None = None - """Monitoring disabled on every AML row of the entity.""" - - skipped_no_identity_data: int | None = None - """Skipped: no full name + date of birth + country available to screen.""" - - skipped_blocked: int | None = None - """Skipped: the user is blocked.""" - - skipped_sandbox: int | None = None - """Skipped: sandbox application.""" - - failed_provider: int | None = None - """The screening provider call failed.""" - - failed_insufficient_balance: int | None = None - """Balance ran out mid-job.""" - - -class BusinessesSetSessionOngoingMonitoringResponse202(BaseModel): - job_id: str | None = None - - status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None - - is_enabled: bool | None = None - - total: int | None = None - - processed: int | None = None - - outcomes: BusinessesSetSessionOngoingMonitoringResponse202Outcomes | None = None - """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" - - error: str | None = None - - estimate: BusinessesSetSessionOngoingMonitoringResponse202Estimate | None = None - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" diff --git a/src/didit/types/businesses/update_params.py b/src/didit/types/businesses/update_params.py index 145a780..2581eda 100644 --- a/src/didit/types/businesses/update_params.py +++ b/src/didit/types/businesses/update_params.py @@ -28,3 +28,6 @@ class BusinessesUpdateParams(TypedDict, total=False): metadata: Mapping[str, object] | None """Arbitrary JSON object you attach to the business. **Fully replaces** the existing metadata on update - merge client-side if you only want to add keys.""" + + reason: str | None + """Why the status changed. Recorded on the status-change entry of the company's **Activity** in the Business Console, only when the status actually changes. Webhooks do not carry it.""" diff --git a/src/didit/types/businesses/update_status_params.py b/src/didit/types/businesses/update_status_params.py index 69986cd..32b7c6d 100644 --- a/src/didit/types/businesses/update_status_params.py +++ b/src/didit/types/businesses/update_status_params.py @@ -11,3 +11,6 @@ class BusinessesUpdateStatusParams(TypedDict, total=False): status: Required[Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str] """New lifecycle status. `BLOCKED` also adds the `vendor_data` to the system blocklist.""" + + reason: str | None + """Why the status changed. Recorded on the status-change entry of the company's **Activity** in the Business Console, only when the status actually changes. Webhooks do not carry it.""" diff --git a/src/didit/types/cases/bulk_update_status_params.py b/src/didit/types/cases/bulk_update_status_params.py index 98ccdb5..4034ae1 100644 --- a/src/didit/types/cases/bulk_update_status_params.py +++ b/src/didit/types/cases/bulk_update_status_params.py @@ -13,7 +13,16 @@ class CasesBulkUpdateStatusParams(TypedDict, total=False): status: Required[Literal["OPEN", "AWAITING_USER", "RESOLVED"] | str] - resolution: Literal["FALSE_POSITIVE", "VALID_THREAT"] | str + resolution: ( + Literal[ + "FALSE_POSITIVE", + "VALID_THREAT", + "INCONCLUSIVE", + "REQUIREMENTS_NOT_MET", + "REQUIREMENTS_SATISFIED", + ] + | str + ) """Required when status is RESOLVED.""" note: str | None diff --git a/src/didit/types/cases/bulk_update_status_response.py b/src/didit/types/cases/bulk_update_status_response.py deleted file mode 100644 index 0fc39ce..0000000 --- a/src/didit/types/cases/bulk_update_status_response.py +++ /dev/null @@ -1,18 +0,0 @@ -# Code generated by codegen from openapi/*.json. DO NOT EDIT. -from __future__ import annotations - -from didit._models import BaseModel - -__all__ = ["CasesBulkUpdateStatusResponse", "CasesBulkUpdateStatusResponseFailures"] - - -class CasesBulkUpdateStatusResponseFailures(BaseModel): - case_uuid: str | None = None - - detail: str | None = None - - -class CasesBulkUpdateStatusResponse(BaseModel): - updated: int | None = None - - failures: list[CasesBulkUpdateStatusResponseFailures] | None = None diff --git a/src/didit/types/cases/regulatory_reports/list_response.py b/src/didit/types/cases/regulatory_reports/list_response.py deleted file mode 100644 index 5b34f59..0000000 --- a/src/didit/types/cases/regulatory_reports/list_response.py +++ /dev/null @@ -1,18 +0,0 @@ -# Code generated by codegen from openapi/*.json. DO NOT EDIT. -from __future__ import annotations - -from didit._models import BaseModel -from didit.types.components.regulatory_report_detail import RegulatoryReportDetail - -__all__ = ["CasesRegulatoryReportsListResponse"] - - -class CasesRegulatoryReportsListResponse(BaseModel): - count: int | None = None - """Number of matching rows, capped at 100 for performance.""" - - next: str | None = None - - previous: str | None = None - - results: list[RegulatoryReportDetail] | None = None diff --git a/src/didit/types/cases/requests/remind_response.py b/src/didit/types/cases/requests/remind_response.py new file mode 100644 index 0000000..692a1bb --- /dev/null +++ b/src/didit/types/cases/requests/remind_response.py @@ -0,0 +1,23 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.customer_messaging_case_request import CustomerMessagingCaseRequest + +__all__ = ["CasesRequestsRemindResponse"] + + +class CasesRequestsRemindResponse(BaseModel): + request: CustomerMessagingCaseRequest + + reminded_at: _dt.datetime + """When this reminder was recorded.""" + + delivery_status: Literal["SENT", "SIMULATED", "UNKNOWN"] | str + """How this reminder's delivery ended. SIMULATED: the email was not sent. UNKNOWN: the email might have been accepted; it is not sent again automatically.""" + + next_reminder_at: _dt.datetime + """When you can remind this request again: `reminded_at` plus 24 hours.""" diff --git a/src/didit/types/cases/resolve_params.py b/src/didit/types/cases/resolve_params.py index 4a1ee1a..145dc3c 100644 --- a/src/didit/types/cases/resolve_params.py +++ b/src/didit/types/cases/resolve_params.py @@ -9,7 +9,19 @@ class CasesResolveParams(TypedDict, total=False): - resolution: Required[Literal["FALSE_POSITIVE", "VALID_THREAT"] | str] + resolution: Required[ + Literal[ + "FALSE_POSITIVE", + "VALID_THREAT", + "INCONCLUSIVE", + "REQUIREMENTS_NOT_MET", + "REQUIREMENTS_SATISFIED", + ] + | str + ] note: str | None """Required when the blueprint sets require_resolution_note.""" + + checker: str | None + """Four-eyes blueprints only. The user uuid of the member who must approve this resolution (the same id as `assigned_to.uuid` on a case, or `members[].user_id` on a review team). The member must have case access to this application and cannot be the officer who submits the resolution. Omit it or send null to leave the resolution to the checker queue.""" diff --git a/src/didit/types/cases/update_params.py b/src/didit/types/cases/update_params.py index b4f98aa..87676aa 100644 --- a/src/didit/types/cases/update_params.py +++ b/src/didit/types/cases/update_params.py @@ -11,9 +11,15 @@ class CasesUpdateParams(TypedDict, total=False): title: str + """New case title.""" priority: Literal["LOW", "MEDIUM", "HIGH"] | str due_at: str | _dt.datetime | None + """New deadline. Send null to clear it.""" tags: Sequence[str] + """The complete tag list. It replaces the current tags.""" + + team: str | None + """UUID of the review team that owns the case. It must be an active team of this application. Send null to clear the team.""" diff --git a/src/didit/types/components/aml_dataset_freshness.py b/src/didit/types/components/aml_dataset_freshness.py new file mode 100644 index 0000000..b442c6b --- /dev/null +++ b/src/didit/types/components/aml_dataset_freshness.py @@ -0,0 +1,41 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.aml_dataset_source_freshness import AMLDatasetSourceFreshness + +__all__ = ["AMLDatasetFreshness", "AMLDatasetFreshnessSources"] + + +class AMLDatasetFreshnessSources(BaseModel): + """Freshness per dataset family. A family whose datasets reported no update date is absent rather than filled with `null`.""" + + sanctions: AMLDatasetSourceFreshness | None = None + """Freshness of one family of AML datasets.""" + + pep: AMLDatasetSourceFreshness | None = None + """Freshness of one family of AML datasets.""" + + warnings: AMLDatasetSourceFreshness | None = None + """Freshness of one family of AML datasets.""" + + adverse_media: AMLDatasetSourceFreshness | None = None + """Freshness of one family of AML datasets.""" + + +class AMLDatasetFreshness(BaseModel): + """How current the AML datasets were when a screening ran. Dates are never estimated.""" + + captured_at: _dt.datetime + """When Didit read the dataset catalog this freshness comes from.""" + + provider: str + """Opaque identifier of the AML data source. Treat it as an opaque string; do not branch on it.""" + + catalog_version: str | None + """Version identifier of the dataset catalog, when the source reports one.""" + + sources: AMLDatasetFreshnessSources + """Freshness per dataset family. A family whose datasets reported no update date is absent rather than filled with `null`.""" diff --git a/src/didit/types/components/aml_dataset_source_freshness.py b/src/didit/types/components/aml_dataset_source_freshness.py new file mode 100644 index 0000000..70a2ff4 --- /dev/null +++ b/src/didit/types/components/aml_dataset_source_freshness.py @@ -0,0 +1,24 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["AMLDatasetSourceFreshness"] + + +class AMLDatasetSourceFreshness(BaseModel): + """Freshness of one family of AML datasets.""" + + last_updated_at: _dt.datetime + """Most recent update of any dataset in the family.""" + + oldest_updated_at: _dt.datetime | None + """Staleness bound: every dataset in the family is at least this current. `null` when at least one dataset in the family reported no update date, because the bound is then unknown.""" + + dataset_count: int + """Number of datasets in the family.""" + + dated_dataset_count: int + """Number of those datasets that reported an update date.""" diff --git a/src/didit/types/components/aml_monitoring_job.py b/src/didit/types/components/aml_monitoring_job.py new file mode 100644 index 0000000..9f195e0 --- /dev/null +++ b/src/didit/types/components/aml_monitoring_job.py @@ -0,0 +1,137 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "AMLMonitoringJob", + "AMLMonitoringJobEstimate", + "AMLMonitoringJobOutcomes", + "AMLMonitoringJobResults", +] + + +class AMLMonitoringJobEstimate(BaseModel): + """Cost preview. Returned directly when estimate_only is true (nothing is written).""" + + users_total: int | None = None + """Profiles matched by the selection.""" + + already_enabled: int | None = None + """Profiles already monitored (nothing to do when enabling; these are what disabling turns off).""" + + with_existing_aml: int | None = None + """Profiles whose existing AML screening will be switched to monitored (no new screening).""" + + needing_screening: int | None = None + """Profiles without any AML screening that will be screened first (billed as a regular AML screening).""" + + skipped_no_identity_data: int | None = None + """Profiles that cannot be screened because identity data is missing.""" + + skipped_blocked: int | None = None + """Blocked users that will be skipped (always 0 for businesses).""" + + estimated_screening_cost_usd: float | None = None + """One-time cost of the new screenings.""" + + estimated_annual_monitoring_cost_usd: float | None = None + """Yearly monitoring cost of the profiles that will become monitored.""" + + balance_sufficient: bool | None = None + """Whether the organization balance covers the screening plus the first year of monitoring.""" + + +class AMLMonitoringJobOutcomes(BaseModel): + """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" + + enabled: int | None = None + """Monitoring enabled on an existing AML screening.""" + + screened_and_enabled: int | None = None + """A new AML screening was created (billed) and monitoring enabled on it.""" + + already_enabled: int | None = None + """Monitoring was already active; nothing changed and nothing was billed.""" + + disabled: int | None = None + """Monitoring turned off across every AML screening of the profile.""" + + skipped_no_identity_data: int | None = None + """Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen.""" + + skipped_blocked: int | None = None + """Skipped: the user is blocked.""" + + skipped_sandbox: int | None = None + """Skipped: sandbox application, no screening is created.""" + + failed_provider: int | None = None + """The screening could not be completed; retry later.""" + + failed_insufficient_balance: int | None = None + """Balance ran out while the job was running.""" + + +class AMLMonitoringJobResults(BaseModel): + didit_internal_id: str | None = None + """Didit internal id of the profile.""" + + vendor_data: str | None = None + """Your own identifier for the profile.""" + + outcome: ( + Literal[ + "enabled", + "screened_and_enabled", + "already_enabled", + "disabled", + "skipped_no_identity_data", + "skipped_blocked", + "skipped_sandbox", + "failed_provider", + "failed_insufficient_balance", + ] + | str + | None + ) = None + + +class AMLMonitoringJob(BaseModel): + job_id: str | None = None + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None + + is_enabled: bool | None = None + """true when the job enables monitoring, false when it disables it.""" + + target_type: Literal["user", "business"] | str | None = None + """Which profile family the job toggles.""" + + total: int | None = None + """Profiles the job processes.""" + + processed: int | None = None + """Profiles processed so far.""" + + outcomes: AMLMonitoringJobOutcomes | None = None + """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" + + results: list[AMLMonitoringJobResults] | None = None + """One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API.""" + + error: str | None = None + """Generic failure message when status is FAILED; the detailed cause is kept on the platform side.""" + + created_at: _dt.datetime | None = None + + completed_at: _dt.datetime | None = None + + estimate: AMLMonitoringJobEstimate | None = None + """Cost preview. Returned directly when estimate_only is true (nothing is written).""" + + unmatched: list[str] | None = None + """Identifiers of the request that matched no profile in this application, exactly as sent. They are not part of the job.""" diff --git a/src/didit/types/components/aml_monitoring_toggle_result.py b/src/didit/types/components/aml_monitoring_toggle_result.py new file mode 100644 index 0000000..60d833f --- /dev/null +++ b/src/didit/types/components/aml_monitoring_toggle_result.py @@ -0,0 +1,180 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from typing_extensions import TypeAlias + +from didit._models import BaseModel + +__all__ = [ + "AMLMonitoringToggleResult", + "AMLMonitoringToggleResultVariant1", + "AMLMonitoringToggleResultVariant2", + "AMLMonitoringToggleResultVariant2Estimate", + "AMLMonitoringToggleResultVariant2Outcomes", + "AMLMonitoringToggleResultVariant2Results", +] + + +class AMLMonitoringToggleResultVariant1(BaseModel): + """Cost preview. Returned directly when estimate_only is true (nothing is written).""" + + users_total: int + """Profiles matched by the selection.""" + + already_enabled: int + """Profiles already monitored (nothing to do when enabling; these are what disabling turns off).""" + + with_existing_aml: int + """Profiles whose existing AML screening will be switched to monitored (no new screening).""" + + needing_screening: int + """Profiles without any AML screening that will be screened first (billed as a regular AML screening).""" + + skipped_no_identity_data: int + """Profiles that cannot be screened because identity data is missing.""" + + skipped_blocked: int + """Blocked users that will be skipped (always 0 for businesses).""" + + estimated_screening_cost_usd: float + """One-time cost of the new screenings.""" + + estimated_annual_monitoring_cost_usd: float + """Yearly monitoring cost of the profiles that will become monitored.""" + + balance_sufficient: bool + """Whether the organization balance covers the screening plus the first year of monitoring.""" + + unmatched: list[str] + """Identifiers of the request that matched no profile in this application, exactly as sent.""" + + +class AMLMonitoringToggleResultVariant2Estimate(BaseModel): + """Cost preview. Returned directly when estimate_only is true (nothing is written).""" + + users_total: int | None = None + """Profiles matched by the selection.""" + + already_enabled: int | None = None + """Profiles already monitored (nothing to do when enabling; these are what disabling turns off).""" + + with_existing_aml: int | None = None + """Profiles whose existing AML screening will be switched to monitored (no new screening).""" + + needing_screening: int | None = None + """Profiles without any AML screening that will be screened first (billed as a regular AML screening).""" + + skipped_no_identity_data: int | None = None + """Profiles that cannot be screened because identity data is missing.""" + + skipped_blocked: int | None = None + """Blocked users that will be skipped (always 0 for businesses).""" + + estimated_screening_cost_usd: float | None = None + """One-time cost of the new screenings.""" + + estimated_annual_monitoring_cost_usd: float | None = None + """Yearly monitoring cost of the profiles that will become monitored.""" + + balance_sufficient: bool | None = None + """Whether the organization balance covers the screening plus the first year of monitoring.""" + + +class AMLMonitoringToggleResultVariant2Outcomes(BaseModel): + """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" + + enabled: int | None = None + """Monitoring enabled on an existing AML screening.""" + + screened_and_enabled: int | None = None + """A new AML screening was created (billed) and monitoring enabled on it.""" + + already_enabled: int | None = None + """Monitoring was already active; nothing changed and nothing was billed.""" + + disabled: int | None = None + """Monitoring turned off across every AML screening of the profile.""" + + skipped_no_identity_data: int | None = None + """Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen.""" + + skipped_blocked: int | None = None + """Skipped: the user is blocked.""" + + skipped_sandbox: int | None = None + """Skipped: sandbox application, no screening is created.""" + + failed_provider: int | None = None + """The screening could not be completed; retry later.""" + + failed_insufficient_balance: int | None = None + """Balance ran out while the job was running.""" + + +class AMLMonitoringToggleResultVariant2Results(BaseModel): + didit_internal_id: str | None = None + """Didit internal id of the profile.""" + + vendor_data: str | None = None + """Your own identifier for the profile.""" + + outcome: ( + Literal[ + "enabled", + "screened_and_enabled", + "already_enabled", + "disabled", + "skipped_no_identity_data", + "skipped_blocked", + "skipped_sandbox", + "failed_provider", + "failed_insufficient_balance", + ] + | str + | None + ) = None + + +class AMLMonitoringToggleResultVariant2(BaseModel): + job_id: str + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str + + is_enabled: bool + """true when the job enables monitoring, false when it disables it.""" + + target_type: Literal["user", "business"] | str + """Which profile family the job toggles.""" + + total: int + """Profiles the job processes.""" + + processed: int + """Profiles processed so far.""" + + outcomes: AMLMonitoringToggleResultVariant2Outcomes + """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" + + results: list[AMLMonitoringToggleResultVariant2Results] + """One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API.""" + + error: str | None + """Generic failure message when status is FAILED; the detailed cause is kept on the platform side.""" + + created_at: _dt.datetime + + completed_at: _dt.datetime | None + + estimate: AMLMonitoringToggleResultVariant2Estimate + """Cost preview. Returned directly when estimate_only is true (nothing is written).""" + + unmatched: list[str] + """Identifiers of the request that matched no profile in this application, exactly as sent. They are not part of the job.""" + + +AMLMonitoringToggleResult: TypeAlias = ( + AMLMonitoringToggleResultVariant1 | AMLMonitoringToggleResultVariant2 +) diff --git a/src/didit/types/components/aml_screening.py b/src/didit/types/components/aml_screening.py new file mode 100644 index 0000000..8a17d76 --- /dev/null +++ b/src/didit/types/components/aml_screening.py @@ -0,0 +1,526 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "AMLScreening", + "AMLScreeningHits", + "AMLScreeningHitsAdverseMediaDetails", + "AMLScreeningHitsAdverseMediaMatches", + "AMLScreeningHitsLinkedEntities", + "AMLScreeningHitsPepMatches", + "AMLScreeningHitsProperties", + "AMLScreeningHitsRiskView", + "AMLScreeningHitsRiskViewCategories", + "AMLScreeningHitsRiskViewCountries", + "AMLScreeningHitsRiskViewCrimes", + "AMLScreeningHitsSanctionMatches", + "AMLScreeningHitsScoreBreakdown", + "AMLScreeningHitsWarningMatches", + "AMLScreeningHitsWarningMatchesAdditionalData", + "AMLScreeningHitsWarningMatchesAdditionalDataData", + "AMLScreeningHitsWarningMatchesAdditionalDataDataCaseDetails", + "AMLScreeningScreenedData", + "AMLScreeningWarnings", +] + + +class AMLScreeningHitsAdverseMediaDetails(BaseModel): + sentiment: str | None = None + + entity_type: str | None = None + + sentiment_score: int | None = None + + adverse_keywords: dict[str, int] | None = None + + +class AMLScreeningHitsAdverseMediaMatches(BaseModel): + country: str | None = None + + summary: str | None = None + + headline: str | None = None + + sentiment: str | None = None + + thumbnail: str | None = None + + source_url: str | None = None + + author_name: str | None = None + + other_sources: list[str] | None = None + + adverse_keywords: list[str] | None = None + + sentiment_score: int | None = None + + publication_date: _dt.datetime | None = None + + +class AMLScreeningHitsLinkedEntities(BaseModel): + name: list[str] | None = None + + active: list[str] | None = None + + status: list[str] | None = None + + details: list[str] | None = None + + relation: list[str] | None = None + + +class AMLScreeningHitsPepMatches(BaseModel): + aliases: list[str] | None = None + + education: list[str] | None = None + + list_name: str | None = None + + publisher: str | None = None + + source_url: str | None = None + + description: str | None = None + + matched_name: str | None = None + + pep_position: str | None = None + + date_of_birth: str | None = None + + other_sources: list[str] | None = None + + place_of_birth: str | None = None + + +class AMLScreeningHitsProperties(BaseModel): + name: list[str] | None = None + + alias: list[str] | None = None + + notes: list[str] | None = None + + title: str | None = None + + gender: list[str] | None = None + + height: str | None = None + + topics: str | None = None + + weight: str | None = None + + address: str | None = None + + country: str | None = None + + website: str | None = None + + eyeColor: str | None = None + + keywords: str | None = None + + lastName: list[str] | None = None + + position: str | None = None + + religion: str | None = None + + birthDate: list[str] | None = None + + education: list[str] | None = None + + ethnicity: str | None = None + + firstName: list[str] | None = None + + hairColor: str | None = None + + weakAlias: str | None = None + + birthPlace: list[str] | None = None + + modifiedAt: str | None = None + + wikidataId: str | None = None + + citizenship: str | None = None + + nationality: str | None = None + + +class AMLScreeningHitsRiskViewCategories(BaseModel): + score: int | None = None + + weightage: int | None = None + + risk_level: str | None = None + + risk_scores: dict[str, object] | None = None + + +class AMLScreeningHitsRiskViewCountries(BaseModel): + score: int | None = None + + weightage: int | None = None + + risk_level: str | None = None + + risk_scores: dict[str, object] | None = None + + +class AMLScreeningHitsRiskViewCrimes(BaseModel): + score: int | None = None + + weightage: int | None = None + + risk_level: str | None = None + + risk_scores: dict[str, object] | None = None + + +class AMLScreeningHitsRiskView(BaseModel): + crimes: AMLScreeningHitsRiskViewCrimes | None = None + + countries: AMLScreeningHitsRiskViewCountries | None = None + + categories: AMLScreeningHitsRiskViewCategories | None = None + + custom_list: dict[str, object] | None = None + + +class AMLScreeningHitsSanctionMatches(BaseModel): + list_name: list[str] | None = None + """Name(s) of the sanction list(s)""" + + matched_name: str | None = None + """Name matched in the sanction list""" + + description: str | None = None + """Description of the sanction entry""" + + reason: str | None = None + """Free-text reason why the entity was sanctioned. Joined from the sanction list's reason entries. Examples: 'Executive Order 13224 (Terrorism)', 'Support for actions undermining Ukraine's sovereignty', 'Listed due to national security concerns.'""" + + source_url: str | None = None + """URL to the source of the sanction""" + + other_sources: list[str] | None = None + """Additional source URLs""" + + details: list[str] | None = None + """Additional details about the sanction""" + + legal_basis: list[str] | None = None + """Legal basis for the sanction""" + + listed_on: list[str] | None = None + """Date(s) when the entity was listed""" + + sanction_list: list[str] | None = None + """Sanction list identifier(s)""" + + sanction_program: list[str] | None = None + """Sanction program(s) the entity is listed under""" + + sanctioning_authority: list[str] | None = None + """Authority/authorities that imposed the sanction""" + + updated_on: list[str] | None = None + """Date(s) when the sanction entry was last updated""" + + additional_data: dict[str, object] | None = None + """Additional data from the sanction source not captured in the main fields""" + + +class AMLScreeningHitsScoreBreakdown(BaseModel): + """Detailed breakdown of how the match_score was calculated.""" + + name_score: int | None = None + """Raw name similarity percentage (0-100).""" + + name_weight: int | None = None + """Original weight for name from config (0-100).""" + + name_weight_normalized: float | None = None + """Normalized weight for name after re-weighting for missing data (0-100).""" + + name_contribution: float | None = None + """Points contributed by name to the total score.""" + + dob_score: int | None = None + """DOB match score (-100 to 100).""" + + dob_weight: int | None = None + """Original weight for DOB from config (0-100).""" + + dob_weight_normalized: float | None = None + """Normalized weight for DOB after re-weighting for missing data (0-100).""" + + dob_contribution: float | None = None + """Points contributed by DOB to the total score.""" + + country_score: int | None = None + """Country match score (-100 to 100).""" + + country_weight: int | None = None + """Original weight for country from config (0-100).""" + + country_weight_normalized: float | None = None + """Normalized weight for country after re-weighting for missing data (0-100).""" + + country_contribution: float | None = None + """Points contributed by country to the total score.""" + + document_number_match_type: Literal["MATCH", "NEUTRAL", "HARD_MISMATCH"] | str | None = None + """Result of document number 'Golden Key' evaluation.""" + + document_number_effect: str | None = None + """Human-readable description of the document number effect on the score.""" + + total_score: int | None = None + """Final weighted match score (0-100).""" + + +class AMLScreeningHitsWarningMatchesAdditionalDataDataCaseDetails(BaseModel): + case_type: ( + Literal[ + "Accusation", + "Arrest", + "Bribery", + "Criminal", + "Fraud", + "Illegal Fundraising", + "Non-conviction Terror", + "Proclaimed Offender", + "Regulatory Enforcement", + "Terrorism", + "Terrorism-related", + "Terrorism-related listing", + ] + | str + | None + ) = None + """Type of the warning case""" + + +class AMLScreeningHitsWarningMatchesAdditionalDataData(BaseModel): + case_details: list[AMLScreeningHitsWarningMatchesAdditionalDataDataCaseDetails] | None = None + """Details of warning cases associated with this match""" + + +class AMLScreeningHitsWarningMatchesAdditionalData(BaseModel): + """Additional data from the warning source. May contain a `data` object with `case_details` array.""" + + data: AMLScreeningHitsWarningMatchesAdditionalDataData | None = None + + +class AMLScreeningHitsWarningMatches(BaseModel): + list_name: str | None = None + """Name of the warning list publisher""" + + matched_name: str | None = None + """Name matched in the warning list""" + + description: str | None = None + """Description of the warning entry""" + + publisher: str | None = None + """Publisher or source of the warning""" + + source_url: str | None = None + """URL to the source of the warning""" + + other_sources: list[str] | None = None + """Additional source URLs""" + + countries: list[str] | None = None + """Countries associated with the warning""" + + created_at: str | None = None + """Date the warning entry was created""" + + updated_at: str | None = None + """Date the warning entry was last updated""" + + additional_data: AMLScreeningHitsWarningMatchesAdditionalData | None = None + """Additional data from the warning source. May contain a `data` object with `case_details` array.""" + + +class AMLScreeningHits(BaseModel): + id: str | None = None + """Unique identifier of the match""" + + url: str | None = None + """Source URL for the match""" + + match: bool | None = None + """Whether this is a confirmed match""" + + score: float | None = None + """Deprecated. Deprecated weighted match score in the 0-1 range (e.g. `0.73`). Use `match_score` (0-100) instead.""" + + target: bool | None = None + """Whether the matched entity is the direct target of the listing (as opposed to a related party). `null` when the source does not specify it.""" + + caption: str | None = None + """Caption or title of the match""" + + datasets: ( + list[ + Literal[ + "PEP", + "PEP Level 1", + "PEP Level 2", + "PEP Level 3", + "PEP Level 4", + "Sanctions", + "Adverse Media", + "Warnings and Regulatory Enforcement", + "Fitness and Probity", + "SIP", + "SIE", + "Insolvency", + ] + | str + ] + | None + ) = None + """List of dataset categories where the match was found. Indicates whether the entity appeared in PEP lists (with level), sanctions databases, adverse media, warnings/regulatory enforcement, or other compliance datasets.""" + + features: dict[str, object] | None = None + """Additional features of the match""" + + rca_name: str | None = None + """RCA name if applicable""" + + last_seen: _dt.datetime | None = None + """Date when match was last seen""" + + risk_view: AMLScreeningHitsRiskView | None = None + + first_seen: _dt.datetime | None = None + """Date when match was first seen""" + + properties: AMLScreeningHitsProperties | None = None + + match_score: float | None = None + """Weighted identity confidence score (0-100) calculated from name similarity, date of birth, and country matching. Used to determine if hit is a False Positive or Possible Match.""" + + risk_score: float | None = None + """Entity risk score (0-100). Used to determine final AML status (Approved/In Review/Declined) based on the highest risk_score among non-False-Positive hits.""" + + review_status: ( + Literal["Unreviewed", "Confirmed Match", "False Positive", "Inconclusive"] | str | None + ) = None + """Review status for this hit. Can be 'Unreviewed', 'Confirmed Match', 'False Positive', or 'Inconclusive'.""" + + score_breakdown: AMLScreeningHitsScoreBreakdown | None = None + """Detailed breakdown of how the match_score was calculated.""" + + pep_matches: list[AMLScreeningHitsPepMatches] | None = None + + linked_entities: list[AMLScreeningHitsLinkedEntities] | None = None + + warning_matches: list[AMLScreeningHitsWarningMatches] | None = None + """List of warning and regulatory enforcement matches. Includes entries from categories: Warning, SIP, Warnings and Regulatory Enforcement, Fitness and Probity, SIE, and Insolvency.""" + + sanction_matches: list[AMLScreeningHitsSanctionMatches] | None = None + """List of sanction list matches found for this entity.""" + + adverse_media_details: AMLScreeningHitsAdverseMediaDetails | None = None + + adverse_media_matches: list[AMLScreeningHitsAdverseMediaMatches] | None = None + + additional_information: dict[str, object] | None = None + + +class AMLScreeningScreenedData(BaseModel): + full_name: str | None = None + + nationality: str | None = None + + date_of_birth: _dt.date | None = None + + document_number: str | None = None + + +class AMLScreeningWarnings(BaseModel): + feature: ( + Literal[ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + ] + | str + | None + ) = None + """The feature that generated this warning. The value identifies the feature block the warning belongs to. Within an AML screening this is typically `AML`, but the enum above is authoritative across all feature warnings on the decision payload.""" + + risk: str | None = None + + additional_data: dict[str, object] | None = None + + log_type: str | None = None + + short_description: str | None = None + + long_description: str | None = None + + node_id: str | None = None + """The workflow graph node ID that generated this warning""" + + +class AMLScreening(BaseModel): + node_id: str | None = None + """Unique identifier for this AML step in the workflow graph.""" + + status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str | None + ) = None + """Feature-level status of this AML screening. Independent of the top-level session `status` - a session can reach its overall decision while an individual feature stays `In Review`.""" + + total_hits: int | None = None + """Total number of matches found""" + + entity_type: str | None = None + """Type of entity screened: person or company""" + + hits: list[AMLScreeningHits] | None = None + + score: float | None = None + """Overall AML screening score - the highest risk score across all hits (float, 0-100). `null` until computed.""" + + screened_data: AMLScreeningScreenedData | None = None + + is_ongoing_monitoring_enabled: bool | None = None + """Whether AML ongoing monitoring is enabled for this screening. When `true`, the entity is re-screened periodically and any new hits update the report.""" + + next_ongoing_monitoring_bill_date: _dt.date | None = None + """Date (YYYY-MM-DD) when this ongoing-monitoring subscription will next be billed. Computed as the date of the last ongoing-monitoring billing plus 365 days when `is_ongoing_monitoring_enabled = true`; otherwise `null`. This is a billing date, not a screening date - use `last_ongoing_screening_at` for that.""" + + last_ongoing_screening_at: _dt.datetime | None = None + """When ongoing monitoring last completed a re-screening of this record, whether or not the result changed. `null` when the record has never been re-screened; it is never back-filled from the enablement or billing date.""" + + last_ongoing_screening_datasets_updated_at: _dt.datetime | None = None + """The most recent AML dataset update date the provider reported at the time of that screening. `null` when the provider reported no dataset dates; never estimated.""" + + warnings: list[AMLScreeningWarnings] | None = None diff --git a/src/didit/types/components/aml_screening_run.py b/src/didit/types/components/aml_screening_run.py new file mode 100644 index 0000000..f0c2396 --- /dev/null +++ b/src/didit/types/components/aml_screening_run.py @@ -0,0 +1,46 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.aml_dataset_freshness import AMLDatasetFreshness + +__all__ = ["AMLScreeningRun"] + + +class AMLScreeningRun(BaseModel): + """One completed ongoing-monitoring re-screen of an AML check.""" + + uuid: str + """Identifier of this screening run.""" + + completed_at: _dt.datetime + """When this subject's re-screen finished. Results are ordered by it, newest first.""" + + started_at: _dt.datetime + """When the monitoring batch this re-screen belongs to started.""" + + outcome: Literal["NO_CHANGE", "CHANGED", "FAILED"] | str + """`NO_CHANGE`: completed and the result did not move. `CHANGED`: completed and the score, status, hit count or hits changed. `FAILED`: no usable answer for this subject, so the check was left as it was.""" + + trigger: Literal["ONGOING_MONITORING"] | str + """What started the screening. Always `ONGOING_MONITORING` today.""" + + provider: str + """Opaque identifier of the AML data source that answered. Treat it as an opaque string; do not branch on it.""" + + status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str | None + ) + """AML check status this run left the check in.""" + + score: float | None + """AML score this run left the check in.""" + + total_hits: int | None + """Number of hits this run left the check in.""" + + dataset_freshness: AMLDatasetFreshness | None + """Dataset freshness behind this run, or `null` when no dates were available.""" diff --git a/src/didit/types/components/application.py b/src/didit/types/components/application.py index e4983ff..8a4912d 100644 --- a/src/didit/types/components/application.py +++ b/src/didit/types/components/application.py @@ -4,6 +4,7 @@ import datetime as _dt from didit._models import BaseModel +from didit.types.components.application_product import ApplicationProduct __all__ = ["Application"] @@ -21,7 +22,7 @@ class Application(BaseModel): """Public client identifier, safe to embed in OAuth-style flows.""" api_key: str | None = None - """Long-lived secret (also called `client_secret`). Use as the `x-api-key` header for every call to `https://verification.didit.me/v3/...` (sessions, workflows, AML, etc.). Treat as a credential; never expose client-side.""" + """Long-lived secret (also called `client_secret`). Use as the `x-api-key` header for every call to `https://verification.didit.me/v3/...` (sessions, workflows, AML, etc.). Treat as a credential; never expose client-side. New keys start with `sk_live_` (live application) or `sk_test_` (sandbox application), followed by 43 URL-safe characters (letters, digits, `-` and `_`). Keys created before this format have no prefix and keep working unchanged.""" website_url: str | None = None """Website or app URL associated with this application.""" @@ -39,3 +40,6 @@ class Application(BaseModel): """Internal description for the application (not shown to end users).""" created_at: _dt.datetime | None = None + + products: list[ApplicationProduct] | None = None + """The application's products. Always all three, in this order: `identity`, `transactions`, `authentication`. A new application has `identity` enabled with every module on, and `transactions` and `authentication` disabled. Read-only: a `products` key in a create or update body is ignored.""" diff --git a/src/didit/types/components/application_analytics_country_count.py b/src/didit/types/components/application_analytics_country_count.py new file mode 100644 index 0000000..6ece740 --- /dev/null +++ b/src/didit/types/components/application_analytics_country_count.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["ApplicationAnalyticsCountryCount"] + + +class ApplicationAnalyticsCountryCount(BaseModel): + country: str | None = None + """Country name.""" + + country_code: str | None = None + """Two-letter country code.""" + + session_count: int | None = None + """Sessions from that country.""" diff --git a/src/didit/types/components/application_analytics_data_point.py b/src/didit/types/components/application_analytics_data_point.py new file mode 100644 index 0000000..8270ac0 --- /dev/null +++ b/src/didit/types/components/application_analytics_data_point.py @@ -0,0 +1,51 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["ApplicationAnalyticsDataPoint"] + + +class ApplicationAnalyticsDataPoint(BaseModel): + """Session counts for one day, by creation date (UTC). Each count reflects the session's current status. Points of the previous period carry at least the required fields.""" + + rounded_date: _dt.date + """The day, `YYYY-MM-DD`.""" + + total: int + """Sessions created that day.""" + + approved: int + """Of those, sessions `Approved` or `Kyc Expired`.""" + + declined: int + """Sessions `Declined`.""" + + canceled: int + """Sessions `Expired` or `Abandoned`.""" + + pending: int + """Sessions `Not Started`, `In Progress` or `In Review`.""" + + in_review: int | None = None + """Sessions `In Review`.""" + + expired: int | None = None + """Sessions `Expired`.""" + + abandoned: int | None = None + """Sessions `Abandoned`.""" + + in_progress: int | None = None + """Sessions `In Progress`.""" + + not_started: int | None = None + """Sessions `Not Started`.""" + + manually_approved: int | None = None + """Sessions a reviewer approved after the workflow gave another verdict.""" + + manually_declined: int | None = None + """Sessions a reviewer declined after the workflow gave another verdict.""" diff --git a/src/didit/types/components/application_analytics_demographic_point.py b/src/didit/types/components/application_analytics_demographic_point.py new file mode 100644 index 0000000..3d1b123 --- /dev/null +++ b/src/didit/types/components/application_analytics_demographic_point.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["ApplicationAnalyticsDemographicPoint"] + + +class ApplicationAnalyticsDemographicPoint(BaseModel): + date: str | None = None + """Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`.""" + + age_distribution: dict[str, int] | None = None + + gender_distribution: dict[str, int] | None = None diff --git a/src/didit/types/components/application_analytics_demographic_stats.py b/src/didit/types/components/application_analytics_demographic_stats.py new file mode 100644 index 0000000..218188d --- /dev/null +++ b/src/didit/types/components/application_analytics_demographic_stats.py @@ -0,0 +1,35 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from pydantic import Field + +from didit._models import BaseModel + +__all__ = [ + "ApplicationAnalyticsDemographicStats", + "ApplicationAnalyticsDemographicStatsAgeDistribution", +] + + +class ApplicationAnalyticsDemographicStatsAgeDistribution(BaseModel): + """Sessions per age band from the ID document. Empty when there is no data.""" + + under_18: int | None = None + + v_18_24: int | None = Field(default=None, alias="18_24") + + v_25_34: int | None = Field(default=None, alias="25_34") + + v_35_44: int | None = Field(default=None, alias="35_44") + + v_45_64: int | None = Field(default=None, alias="45_64") + + v_65_plus: int | None = Field(default=None, alias="65_plus") + + +class ApplicationAnalyticsDemographicStats(BaseModel): + age_distribution: ApplicationAnalyticsDemographicStatsAgeDistribution | None = None + """Sessions per age band from the ID document. Empty when there is no data.""" + + gender_distribution: dict[str, int] | None = None + """Sessions per document gender code: `M`, `F` or `U` (unknown).""" diff --git a/src/didit/types/components/application_analytics_device_details.py b/src/didit/types/components/application_analytics_device_details.py new file mode 100644 index 0000000..70072cf --- /dev/null +++ b/src/didit/types/components/application_analytics_device_details.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = [ + "ApplicationAnalyticsDeviceDetails", + "ApplicationAnalyticsDeviceDetailsBrowsers", + "ApplicationAnalyticsDeviceDetailsDeviceModels", + "ApplicationAnalyticsDeviceDetailsOperatingSystems", +] + + +class ApplicationAnalyticsDeviceDetailsBrowsers(BaseModel): + browser: str | None = None + + count: int | None = None + + +class ApplicationAnalyticsDeviceDetailsDeviceModels(BaseModel): + device: str | None = None + + count: int | None = None + + +class ApplicationAnalyticsDeviceDetailsOperatingSystems(BaseModel): + os: str | None = None + + count: int | None = None + + +class ApplicationAnalyticsDeviceDetails(BaseModel): + """Top five of each, most sessions first.""" + + device_models: list[ApplicationAnalyticsDeviceDetailsDeviceModels] | None = None + """Device brands: `Apple`, `Android`, `LG`, everything else grouped as `Other`.""" + + browsers: list[ApplicationAnalyticsDeviceDetailsBrowsers] | None = None + """Browsers: `Chrome`, `Safari`, `Didit App`, `Firefox`, everything else grouped as `Other`.""" + + operating_systems: list[ApplicationAnalyticsDeviceDetailsOperatingSystems] | None = None + """Operating systems.""" diff --git a/src/didit/types/components/application_analytics_device_point.py b/src/didit/types/components/application_analytics_device_point.py new file mode 100644 index 0000000..0e6d63a --- /dev/null +++ b/src/didit/types/components/application_analytics_device_point.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["ApplicationAnalyticsDevicePoint"] + + +class ApplicationAnalyticsDevicePoint(BaseModel): + date: str | None = None + """Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`.""" + + devices: dict[str, int] | None = None + + browsers: dict[str, int] | None = None + + operating_systems: dict[str, int] | None = None diff --git a/src/didit/types/components/application_analytics_location_stats.py b/src/didit/types/components/application_analytics_location_stats.py new file mode 100644 index 0000000..b8f7919 --- /dev/null +++ b/src/didit/types/components/application_analytics_location_stats.py @@ -0,0 +1,51 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.application_analytics_country_count import ( + ApplicationAnalyticsCountryCount, +) + +__all__ = [ + "ApplicationAnalyticsLocationStats", + "ApplicationAnalyticsLocationStatsCities", + "ApplicationAnalyticsLocationStatsConversionByCountry", +] + + +class ApplicationAnalyticsLocationStatsCities(BaseModel): + location: str | None = None + """`City, Country`.""" + + session_count: int | None = None + + latitude: float | None = None + + longitude: float | None = None + + +class ApplicationAnalyticsLocationStatsConversionByCountry(BaseModel): + country: str | None = None + + country_code: str | None = None + + finished: int | None = None + + approved: int | None = None + + +class ApplicationAnalyticsLocationStats(BaseModel): + ip_countries: list[ApplicationAnalyticsCountryCount] | None = None + """Countries of the users' IP addresses, most sessions first.""" + + countries: list[ApplicationAnalyticsCountryCount] | None = None + """Same list as `ip_countries`.""" + + id_countries: list[ApplicationAnalyticsCountryCount] | None = None + """Issuing countries of the verified ID documents, most sessions first.""" + + conversion_by_country: list[ApplicationAnalyticsLocationStatsConversionByCountry] | None = None + """Per document issuing country: finished sessions (approved, declined or in review) and how many were approved. Most finished first.""" + + cities: list[ApplicationAnalyticsLocationStatsCities] | None = None + """Up to 100 IP cities, most sessions first.""" diff --git a/src/didit/types/components/application_analytics_original_status_point.py b/src/didit/types/components/application_analytics_original_status_point.py new file mode 100644 index 0000000..3dd38db --- /dev/null +++ b/src/didit/types/components/application_analytics_original_status_point.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["ApplicationAnalyticsOriginalStatusPoint"] + + +class ApplicationAnalyticsOriginalStatusPoint(BaseModel): + date: str | None = None + """Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`.""" + + approved: int | None = None + + in_review: int | None = None + + declined: int | None = None diff --git a/src/didit/types/components/application_analytics_original_status_stats.py b/src/didit/types/components/application_analytics_original_status_stats.py new file mode 100644 index 0000000..aecb7c7 --- /dev/null +++ b/src/didit/types/components/application_analytics_original_status_stats.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["ApplicationAnalyticsOriginalStatusStats"] + + +class ApplicationAnalyticsOriginalStatusStats(BaseModel): + """The workflow's automatic verdict, before any reviewer changed it.""" + + approved: int | None = None + + in_review: int | None = None + + declined: int | None = None diff --git a/src/didit/types/components/application_analytics_request_breakdown.py b/src/didit/types/components/application_analytics_request_breakdown.py new file mode 100644 index 0000000..4c2507b --- /dev/null +++ b/src/didit/types/components/application_analytics_request_breakdown.py @@ -0,0 +1,43 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["ApplicationAnalyticsRequestBreakdown"] + + +class ApplicationAnalyticsRequestBreakdown(BaseModel): + """Status totals for the range. The previous-period breakdown carries at least the required fields.""" + + total_approved: int + """Sessions `Approved` or `Kyc Expired`.""" + + total_in_review: int + """Sessions `In Review`.""" + + total_declined: int + """Sessions `Declined`.""" + + total_canceled: int + """Sessions `Expired` or `Abandoned`.""" + + total_pending: int + """Sessions `Not Started`, `In Progress` or `In Review` (includes `total_in_review`).""" + + total_expired: int | None = None + """Sessions `Expired`.""" + + total_abandoned: int | None = None + """Sessions `Abandoned`.""" + + total_in_progress: int | None = None + """Sessions `In Progress`.""" + + total_not_started: int | None = None + """Sessions `Not Started`.""" + + total_manually_approved: int | None = None + """Approved by a reviewer.""" + + total_manually_declined: int | None = None + """Declined by a reviewer.""" diff --git a/src/didit/types/components/application_analytics_resubmission_point.py b/src/didit/types/components/application_analytics_resubmission_point.py new file mode 100644 index 0000000..6a3ecfb --- /dev/null +++ b/src/didit/types/components/application_analytics_resubmission_point.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["ApplicationAnalyticsResubmissionPoint"] + + +class ApplicationAnalyticsResubmissionPoint(BaseModel): + date: str | None = None + """Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`.""" + + first_pass: int | None = None + + second: int | None = None + + third_plus: int | None = None + + abandoned: int | None = None diff --git a/src/didit/types/components/application_analytics_resubmission_stats.py b/src/didit/types/components/application_analytics_resubmission_stats.py new file mode 100644 index 0000000..9c37f56 --- /dev/null +++ b/src/didit/types/components/application_analytics_resubmission_stats.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["ApplicationAnalyticsResubmissionStats"] + + +class ApplicationAnalyticsResubmissionStats(BaseModel): + first_pass: int | None = None + """Finished sessions with no resubmission.""" + + second: int | None = None + """Finished sessions resubmitted once.""" + + third_plus: int | None = None + """Finished sessions resubmitted two or more times.""" + + abandoned: int | None = None + """Abandoned sessions.""" diff --git a/src/didit/types/components/application_analytics_summary.py b/src/didit/types/components/application_analytics_summary.py new file mode 100644 index 0000000..faffc63 --- /dev/null +++ b/src/didit/types/components/application_analytics_summary.py @@ -0,0 +1,104 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.application_analytics_data_point import ApplicationAnalyticsDataPoint +from didit.types.components.application_analytics_demographic_point import ( + ApplicationAnalyticsDemographicPoint, +) +from didit.types.components.application_analytics_demographic_stats import ( + ApplicationAnalyticsDemographicStats, +) +from didit.types.components.application_analytics_device_details import ( + ApplicationAnalyticsDeviceDetails, +) +from didit.types.components.application_analytics_device_point import ( + ApplicationAnalyticsDevicePoint, +) +from didit.types.components.application_analytics_location_stats import ( + ApplicationAnalyticsLocationStats, +) +from didit.types.components.application_analytics_original_status_point import ( + ApplicationAnalyticsOriginalStatusPoint, +) +from didit.types.components.application_analytics_original_status_stats import ( + ApplicationAnalyticsOriginalStatusStats, +) +from didit.types.components.application_analytics_request_breakdown import ( + ApplicationAnalyticsRequestBreakdown, +) +from didit.types.components.application_analytics_resubmission_point import ( + ApplicationAnalyticsResubmissionPoint, +) +from didit.types.components.application_analytics_resubmission_stats import ( + ApplicationAnalyticsResubmissionStats, +) +from didit.types.components.application_analytics_verification_time_point import ( + ApplicationAnalyticsVerificationTimePoint, +) +from didit.types.components.application_analytics_verification_time_stats import ( + ApplicationAnalyticsVerificationTimeStats, +) +from didit.types.components.application_analytics_warning_stats import ( + ApplicationAnalyticsWarningStats, +) +from didit.types.components.application_analytics_workflow_funnel import ( + ApplicationAnalyticsWorkflowFunnel, +) + +__all__ = ["ApplicationAnalyticsSummary"] + + +class ApplicationAnalyticsSummary(BaseModel): + """User verification (KYC) summary, returned when neither `scope` nor `widget_keys` is sent.""" + + data_points: list[ApplicationAnalyticsDataPoint] + """One point per day, from the first day with sessions in the range to `end_date`. Always daily, whatever `grouping_mode` says.""" + + request_breakdown: ApplicationAnalyticsRequestBreakdown + """Status totals for the range. The previous-period breakdown carries at least the required fields.""" + + previous_data_points: list[ApplicationAnalyticsDataPoint] + """Daily points for the period of the same length right before `start_date`.""" + + previous_request_breakdown: ApplicationAnalyticsRequestBreakdown + """Status totals for the range. The previous-period breakdown carries at least the required fields.""" + + conversion_rate: float | None + """`total_approved / (total_approved + total_declined + total_in_review) x 100`, one decimal. `null` when nothing finished.""" + + location_stats: ApplicationAnalyticsLocationStats + + demographic_stats: ApplicationAnalyticsDemographicStats + + device_details: ApplicationAnalyticsDeviceDetails + """Top five of each, most sessions first.""" + + workflow_funnel_stats: ApplicationAnalyticsWorkflowFunnel + """Keyed by workflow ID. `total` is the sessions started on that workflow; `steps` counts the sessions that reached each step (`OCR`, `LIVENESS`, `FACE_MATCH`, `AML`, `POA`, `PHONE`, `EMAIL`, `NFC`, `DATABASE_VALIDATION`, `QUESTIONNAIRE`, `IP_ANALYSIS`, `AGE_ESTIMATION`, `KYB_REGISTRY`, `KYB_DOCUMENTS`, `KYB_KEY_PEOPLE`). Reaching a step is not passing it.""" + + verification_time_stats: ApplicationAnalyticsVerificationTimeStats + """Active time users spent verifying, ignoring idle gaps. An empty object when there is no data.""" + + resubmission_stats: ApplicationAnalyticsResubmissionStats + + warning_stats: ApplicationAnalyticsWarningStats + """Keyed by feature (`ID_VERIFICATION`, `LIVENESS`, `FACEMATCH`, `AML`, `PROOF_OF_ADDRESS`, `PHONE`, `EMAIL`, `NFC`, `LOCATION`, `DATABASE_VALIDATION`, `QUESTIONNAIRE` and the KYB features). `total` is the sessions that reached the feature; `risks` counts sessions per warning code and level.""" + + original_status_stats: ApplicationAnalyticsOriginalStatusStats + """The workflow's automatic verdict, before any reviewer changed it.""" + + grouping_mode: Literal["day", "week", "month"] | str + """How the `*_time_series` lists are grouped: `day` for up to 14 days, `week` up to 90, `month` beyond. Measured from the first day with sessions in the range.""" + + demographic_time_series: list[ApplicationAnalyticsDemographicPoint] + + device_time_series: list[ApplicationAnalyticsDevicePoint] + + verification_time_time_series: list[ApplicationAnalyticsVerificationTimePoint] + + resubmission_time_series: list[ApplicationAnalyticsResubmissionPoint] + + original_status_time_series: list[ApplicationAnalyticsOriginalStatusPoint] diff --git a/src/didit/types/components/application_analytics_verification_time_point.py b/src/didit/types/components/application_analytics_verification_time_point.py new file mode 100644 index 0000000..c931819 --- /dev/null +++ b/src/didit/types/components/application_analytics_verification_time_point.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["ApplicationAnalyticsVerificationTimePoint"] + + +class ApplicationAnalyticsVerificationTimePoint(BaseModel): + date: str | None = None + """Period key: `YYYY-MM-DD` for a day, the Monday of a week, or `YYYY-MM` for a month, following `grouping_mode`.""" + + overall_avg: float | None = None + """Average active seconds per session.""" + + per_feature: dict[str, float] | None = None + """Average active seconds per feature.""" diff --git a/src/didit/types/components/application_analytics_verification_time_stats.py b/src/didit/types/components/application_analytics_verification_time_stats.py new file mode 100644 index 0000000..29e6e9a --- /dev/null +++ b/src/didit/types/components/application_analytics_verification_time_stats.py @@ -0,0 +1,44 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = [ + "ApplicationAnalyticsVerificationTimeStats", + "ApplicationAnalyticsVerificationTimeStatsPerFeatureValue", + "ApplicationAnalyticsVerificationTimeStatsPerSubStepValueValue", +] + + +class ApplicationAnalyticsVerificationTimeStatsPerFeatureValue(BaseModel): + avg_seconds: float | None = None + + total_seconds: float | None = None + + count: int | None = None + + +class ApplicationAnalyticsVerificationTimeStatsPerSubStepValueValue(BaseModel): + avg_seconds: float | None = None + + total_seconds: float | None = None + + count: int | None = None + + +class ApplicationAnalyticsVerificationTimeStats(BaseModel): + """Active time users spent verifying, ignoring idle gaps. An empty object when there is no data.""" + + overall_avg_seconds: float | None = None + """Average per session, one decimal.""" + + overall_count: int | None = None + """Sessions measured.""" + + per_feature: dict[str, ApplicationAnalyticsVerificationTimeStatsPerFeatureValue] | None = None + """Keyed by feature, for example `OCR` or `LIVENESS`.""" + + per_sub_step: ( + dict[str, dict[str, ApplicationAnalyticsVerificationTimeStatsPerSubStepValueValue]] | None + ) = None + """Keyed by feature, then by sub-step, for example `front_id_photo`.""" diff --git a/src/didit/types/components/application_analytics_warning_stats.py b/src/didit/types/components/application_analytics_warning_stats.py new file mode 100644 index 0000000..99be77c --- /dev/null +++ b/src/didit/types/components/application_analytics_warning_stats.py @@ -0,0 +1,30 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import TypeAlias + +from didit._models import BaseModel + +__all__ = [ + "ApplicationAnalyticsWarningStats", + "ApplicationAnalyticsWarningStatsValue", + "ApplicationAnalyticsWarningStatsValueRisksValue", +] + + +class ApplicationAnalyticsWarningStatsValueRisksValue(BaseModel): + error: int | None = None + + warning: int | None = None + + information: int | None = None + + +class ApplicationAnalyticsWarningStatsValue(BaseModel): + total: int | None = None + + risks: dict[str, ApplicationAnalyticsWarningStatsValueRisksValue] | None = None + + +ApplicationAnalyticsWarningStats: TypeAlias = dict[str, ApplicationAnalyticsWarningStatsValue] +"""Keyed by feature (`ID_VERIFICATION`, `LIVENESS`, `FACEMATCH`, `AML`, `PROOF_OF_ADDRESS`, `PHONE`, `EMAIL`, `NFC`, `LOCATION`, `DATABASE_VALIDATION`, `QUESTIONNAIRE` and the KYB features). `total` is the sessions that reached the feature; `risks` counts sessions per warning code and level.""" diff --git a/src/didit/types/components/application_analytics_widget.py b/src/didit/types/components/application_analytics_widget.py new file mode 100644 index 0000000..6510bfa --- /dev/null +++ b/src/didit/types/components/application_analytics_widget.py @@ -0,0 +1,166 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.application_analytics_data_point import ApplicationAnalyticsDataPoint +from didit.types.components.application_analytics_demographic_point import ( + ApplicationAnalyticsDemographicPoint, +) +from didit.types.components.application_analytics_demographic_stats import ( + ApplicationAnalyticsDemographicStats, +) +from didit.types.components.application_analytics_device_details import ( + ApplicationAnalyticsDeviceDetails, +) +from didit.types.components.application_analytics_device_point import ( + ApplicationAnalyticsDevicePoint, +) +from didit.types.components.application_analytics_original_status_point import ( + ApplicationAnalyticsOriginalStatusPoint, +) +from didit.types.components.application_analytics_original_status_stats import ( + ApplicationAnalyticsOriginalStatusStats, +) +from didit.types.components.application_analytics_request_breakdown import ( + ApplicationAnalyticsRequestBreakdown, +) +from didit.types.components.application_analytics_resubmission_point import ( + ApplicationAnalyticsResubmissionPoint, +) +from didit.types.components.application_analytics_resubmission_stats import ( + ApplicationAnalyticsResubmissionStats, +) +from didit.types.components.application_analytics_verification_time_point import ( + ApplicationAnalyticsVerificationTimePoint, +) +from didit.types.components.application_analytics_verification_time_stats import ( + ApplicationAnalyticsVerificationTimeStats, +) +from didit.types.components.application_analytics_warning_stats import ( + ApplicationAnalyticsWarningStats, +) +from didit.types.components.application_analytics_workflow_funnel import ( + ApplicationAnalyticsWorkflowFunnel, +) + +__all__ = [ + "ApplicationAnalyticsWidget", + "ApplicationAnalyticsWidgetSearch", + "ApplicationAnalyticsWidgetThresholds", +] + + +class ApplicationAnalyticsWidgetSearch(BaseModel): + """`kyb_registry_coverage` widget: registry search totals.""" + + searches_total: int | None = None + + searches_with_no_candidates: int | None = None + + +class ApplicationAnalyticsWidgetThresholds(BaseModel): + """`histogram` widget: the application's review and decline score thresholds.""" + + review: int | None = None + + decline: int | None = None + + +class ApplicationAnalyticsWidget(BaseModel): + """One dashboard widget. Every widget has `kind` and `title`; the other fields depend on `kind`. The session widgets reuse the summary objects (`data_points`, `request_breakdown`, `workflow_funnel_stats` and so on); list widgets put their entries in `items`; transaction metrics use `value`.""" + + kind: ( + Literal[ + "sessions_volume", + "workflow_funnel", + "average_verification_time", + "country_distribution", + "warning_insights", + "conversion_rate", + "conversion_by_country", + "resubmission_insights", + "original_status_distribution", + "database_validation_coverage", + "database_validation_latency", + "demographics", + "device_details", + "horizontal_bar", + "kyb_registry_coverage", + "kyb_registry_data_completeness", + "metric", + "time_series", + "breakdown", + "status_time_series", + "leaderboard", + "histogram", + ] + | str + ) + """Widget type.""" + + title: str + """Display title, for example `Verification volume`.""" + + grouping_mode: Literal["day", "week", "month"] | str | None = None + """How the `*_time_series` lists are grouped: `day` for up to 14 days, `week` up to 90, `month` beyond. Measured from the first day with sessions in the range.""" + + data_points: list[ApplicationAnalyticsDataPoint] | None = None + + request_breakdown: ApplicationAnalyticsRequestBreakdown | None = None + """Status totals for the range. The previous-period breakdown carries at least the required fields.""" + + previous_data_points: list[ApplicationAnalyticsDataPoint] | None = None + + previous_request_breakdown: ApplicationAnalyticsRequestBreakdown | None = None + """Status totals for the range. The previous-period breakdown carries at least the required fields.""" + + conversion_rate: float | None = None + + workflow_funnel_stats: ApplicationAnalyticsWorkflowFunnel | None = None + """Keyed by workflow ID. `total` is the sessions started on that workflow; `steps` counts the sessions that reached each step (`OCR`, `LIVENESS`, `FACE_MATCH`, `AML`, `POA`, `PHONE`, `EMAIL`, `NFC`, `DATABASE_VALIDATION`, `QUESTIONNAIRE`, `IP_ANALYSIS`, `AGE_ESTIMATION`, `KYB_REGISTRY`, `KYB_DOCUMENTS`, `KYB_KEY_PEOPLE`). Reaching a step is not passing it.""" + + verification_time_stats: ApplicationAnalyticsVerificationTimeStats | None = None + """Active time users spent verifying, ignoring idle gaps. An empty object when there is no data.""" + + verification_time_time_series: list[ApplicationAnalyticsVerificationTimePoint] | None = None + + warning_stats: ApplicationAnalyticsWarningStats | None = None + """Keyed by feature (`ID_VERIFICATION`, `LIVENESS`, `FACEMATCH`, `AML`, `PROOF_OF_ADDRESS`, `PHONE`, `EMAIL`, `NFC`, `LOCATION`, `DATABASE_VALIDATION`, `QUESTIONNAIRE` and the KYB features). `total` is the sessions that reached the feature; `risks` counts sessions per warning code and level.""" + + resubmission_stats: ApplicationAnalyticsResubmissionStats | None = None + + resubmission_time_series: list[ApplicationAnalyticsResubmissionPoint] | None = None + + original_status_stats: ApplicationAnalyticsOriginalStatusStats | None = None + """The workflow's automatic verdict, before any reviewer changed it.""" + + original_status_time_series: list[ApplicationAnalyticsOriginalStatusPoint] | None = None + + demographic_stats: ApplicationAnalyticsDemographicStats | None = None + + demographic_time_series: list[ApplicationAnalyticsDemographicPoint] | None = None + + device_details: ApplicationAnalyticsDeviceDetails | None = None + """Top five of each, most sessions first.""" + + device_time_series: list[ApplicationAnalyticsDevicePoint] | None = None + + items: list[dict[str, object]] | None = None + """Entries of a list widget. `country_distribution` (KYC): `country`, `country_code`, `session_count`. `conversion_by_country`: `country`, `country_code`, `finished`, `approved`. `time_series`: `date`, `value`. `status_time_series`: `date`, `approved`, `declined`, `flagged`. `breakdown`: `transaction_type` or `action_type` with `count` and `volume`, or `tag_id`, `tag_name`, `count`. `leaderboard`: `rule_title`, `trigger_count` or `assignee`, `handled_count`, `average_processing_seconds`, each with `approved_percentage`, `declined_percentage`, `flagged_percentage`. `histogram`: `score`, `count`.""" + + previous_items: list[dict[str, object]] | None = None + """Transaction time series for the period of the same length right before `start_date`.""" + + value: float | None = None + """`metric` widgets: the figure for the range.""" + + currency: str | None = None + """Transaction volume and amount widgets: the application's default transaction currency.""" + + thresholds: ApplicationAnalyticsWidgetThresholds | None = None + """`histogram` widget: the application's review and decline score thresholds.""" + + search: ApplicationAnalyticsWidgetSearch | None = None + """`kyb_registry_coverage` widget: registry search totals.""" diff --git a/src/didit/types/components/application_analytics_widget_response.py b/src/didit/types/components/application_analytics_widget_response.py new file mode 100644 index 0000000..eac2a56 --- /dev/null +++ b/src/didit/types/components/application_analytics_widget_response.py @@ -0,0 +1,25 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.application_analytics_widget import ApplicationAnalyticsWidget + +__all__ = ["ApplicationAnalyticsWidgetResponse"] + + +class ApplicationAnalyticsWidgetResponse(BaseModel): + """Dashboard widgets, returned when `scope` or `widget_keys` is sent.""" + + scope: Literal["home", "kyc", "kyb", "transactions"] | str + """Dashboard tab.""" + + available_widget_keys: list[str] + """Every widget key this scope accepts.""" + + requested_widget_keys: list[str] + """The widget keys answered, in order: your valid `widget_keys`, or the scope's defaults.""" + + widgets: dict[str, ApplicationAnalyticsWidget] + """Keyed by widget key, in the order of `requested_widget_keys`.""" diff --git a/src/didit/types/components/application_analytics_workflow_funnel.py b/src/didit/types/components/application_analytics_workflow_funnel.py new file mode 100644 index 0000000..33f2a3f --- /dev/null +++ b/src/didit/types/components/application_analytics_workflow_funnel.py @@ -0,0 +1,26 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import TypeAlias + +from didit._models import BaseModel + +__all__ = [ + "ApplicationAnalyticsWorkflowFunnel", + "ApplicationAnalyticsWorkflowFunnelValue", + "ApplicationAnalyticsWorkflowFunnelValueStepsValue", +] + + +class ApplicationAnalyticsWorkflowFunnelValueStepsValue(BaseModel): + reached: int | None = None + + +class ApplicationAnalyticsWorkflowFunnelValue(BaseModel): + total: int | None = None + + steps: dict[str, ApplicationAnalyticsWorkflowFunnelValueStepsValue] | None = None + + +ApplicationAnalyticsWorkflowFunnel: TypeAlias = dict[str, ApplicationAnalyticsWorkflowFunnelValue] +"""Keyed by workflow ID. `total` is the sessions started on that workflow; `steps` counts the sessions that reached each step (`OCR`, `LIVENESS`, `FACE_MATCH`, `AML`, `POA`, `PHONE`, `EMAIL`, `NFC`, `DATABASE_VALIDATION`, `QUESTIONNAIRE`, `IP_ANALYSIS`, `AGE_ESTIMATION`, `KYB_REGISTRY`, `KYB_DOCUMENTS`, `KYB_KEY_PEOPLE`). Reaching a step is not passing it.""" diff --git a/src/didit/types/components/application_credentials_summary.py b/src/didit/types/components/application_credentials_summary.py index 3ec85b0..2092267 100644 --- a/src/didit/types/components/application_credentials_summary.py +++ b/src/didit/types/components/application_credentials_summary.py @@ -16,4 +16,4 @@ class ApplicationCredentialsSummary(BaseModel): client_id: str | None = None api_key: str | None = None - """Use this as `x-api-key` on every verification API call.""" + """Use this as `x-api-key` on every verification API call. New keys start with `sk_live_` (live application) or `sk_test_` (sandbox application), followed by 43 URL-safe characters (letters, digits, `-` and `_`). Keys created before this format have no prefix and keep working unchanged.""" diff --git a/src/didit/types/components/application_product.py b/src/didit/types/components/application_product.py new file mode 100644 index 0000000..6e52eeb --- /dev/null +++ b/src/didit/types/components/application_product.py @@ -0,0 +1,21 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["ApplicationProduct"] + + +class ApplicationProduct(BaseModel): + """One product of an application and its state. Read-only: the application endpoints return it and ignore it in a request body.""" + + product: Literal["identity", "transactions", "authentication"] | str + """`identity` (Identity verification), `transactions` (Transaction monitoring) or `authentication` (Authentication).""" + + status: Literal["enabled", "early_access", "disabled"] | str + """Whether the product is on for this application. `early_access` applies only to `authentication`, which Didit turns on for you.""" + + modules: dict[str, bool] + """Module switches of the product, each `true` or `false`. `identity` has `user_verification`, `business_verification` and `case_management`. `transactions` has `travel_rule` and `case_management`. `authentication` has none (`{}`). `case_management` is one setting shown under both `identity` and `transactions`, so it has the same value on both.""" diff --git a/src/didit/types/components/blocklisted_item.py b/src/didit/types/components/blocklisted_item.py new file mode 100644 index 0000000..dd09ed1 --- /dev/null +++ b/src/didit/types/components/blocklisted_item.py @@ -0,0 +1,46 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["BlocklistedItem"] + + +class BlocklistedItem(BaseModel): + """One blocklisted face, identity document, phone number or email address of a verification session. Fields that do not apply to the item's `type` are `null`.""" + + type: Literal["face", "document", "phone", "email"] | str + """What was blocklisted.""" + + session_id: str + """Verification session the item belongs to.""" + + session_number: int | None + """Application-scoped number of that session, as shown in the console.""" + + blocklisted_at: _dt.datetime + """When the item was last blocklisted.""" + + face_image: str | None + """Face items: short-lived signed URL, issued again on every call; `null` for a scoped API key without `media: read`. Also `null` when no reference image is stored.""" + + document_type: str | None + """Document items: document type code, for example `P` (passport), `ID` (national ID), `DL` (driver's license) or `RP` (residence permit).""" + + document_number: str | None + """Document items: the document number, or the personal number when the document has none.""" + + front_image: str | None + """Document items: front image of the document, short-lived signed URL, issued again on every call; `null` for a scoped API key without `media: read`.""" + + phone_number: str | None + """Phone items: the phone number in E.164.""" + + country_code: str | None + """Phone items: ISO 3166-1 alpha-2 country of the number.""" + + email: str | None + """Email items: the email address.""" diff --git a/src/didit/types/components/branding_customization.py b/src/didit/types/components/branding_customization.py index 5de11db..46e7301 100644 --- a/src/didit/types/components/branding_customization.py +++ b/src/didit/types/components/branding_customization.py @@ -52,6 +52,9 @@ class BrandingCustomization(BaseModel): logo_rectangular: str | None = None """Read-only URL of the rectangular logo. Upload via `image_rectangular`.""" + logo_rectangular_dark: str | None = None + """Read-only. Temporary signed URL of the rectangular logo for dark mode, shown on a dark background. `null` when no dark logo is set or rectangular logos are not shown for your account. Upload it with `image_rectangular_dark`.""" + favicon: str | None = None """Read-only URL of the favicon. Upload via `image_favicon`.""" diff --git a/src/didit/types/components/business_session_list_item.py b/src/didit/types/components/business_session_list_item.py new file mode 100644 index 0000000..95d4b0e --- /dev/null +++ b/src/didit/types/components/business_session_list_item.py @@ -0,0 +1,63 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["BusinessSessionListItem"] + + +class BusinessSessionListItem(BaseModel): + """Summary row for a Business Verification (KYB) session. Returned only when `session_kind` is `business` or `all`.""" + + session_id: str + """Unique identifier of the business verification session.""" + + session_kind: Literal["business"] | str + """Row discriminator. Always `"business"` for KYB rows.""" + + session_number: int + """Human-friendly sequential number of the business session within your application. Numbered independently from KYC sessions.""" + + status: ( + Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + ) + """Overall session status. See the [Verification Statuses](/integration/verification-statuses) page.""" + + vendor_data: str | None + """Your own identifier for the business, echoed back exactly as sent at session creation.""" + + created_at: _dt.datetime + """UTC timestamp when the session was created.""" + + workflow_id: str | None + """Identifier of the workflow this session was created with.""" + + workflow_type: str | None + """Workflow type. `kyb` for business workflows.""" + + workflow_label: str | None + """Display name of the workflow.""" + + company_name: str | None + """Name of the company being verified, from the registry company linked to the session. `null` until a company is selected.""" + + registration_number: str | None + """Company registration number from the registry company linked to the session.""" + + country: str | None + """Country code of the registry company linked to the session.""" diff --git a/src/didit/types/cases/blueprints/install_presets_response.py b/src/didit/types/components/case_blueprint_preset_install_result.py similarity index 61% rename from src/didit/types/cases/blueprints/install_presets_response.py rename to src/didit/types/components/case_blueprint_preset_install_result.py index a1cfc1c..d27f654 100644 --- a/src/didit/types/cases/blueprints/install_presets_response.py +++ b/src/didit/types/components/case_blueprint_preset_install_result.py @@ -3,8 +3,8 @@ from didit._models import BaseModel -__all__ = ["CasesBlueprintsInstallPresetsResponse"] +__all__ = ["CaseBlueprintPresetInstallResult"] -class CasesBlueprintsInstallPresetsResponse(BaseModel): +class CaseBlueprintPresetInstallResult(BaseModel): created: list[str] | None = None diff --git a/src/didit/types/cases/bulk_assign_response.py b/src/didit/types/components/case_bulk_update_result.py similarity index 50% rename from src/didit/types/cases/bulk_assign_response.py rename to src/didit/types/components/case_bulk_update_result.py index 22646fd..425b1bc 100644 --- a/src/didit/types/cases/bulk_assign_response.py +++ b/src/didit/types/components/case_bulk_update_result.py @@ -3,16 +3,16 @@ from didit._models import BaseModel -__all__ = ["CasesBulkAssignResponse", "CasesBulkAssignResponseFailures"] +__all__ = ["CaseBulkUpdateResult", "CaseBulkUpdateResultFailures"] -class CasesBulkAssignResponseFailures(BaseModel): +class CaseBulkUpdateResultFailures(BaseModel): case_uuid: str | None = None detail: str | None = None -class CasesBulkAssignResponse(BaseModel): +class CaseBulkUpdateResult(BaseModel): updated: int | None = None - failures: list[CasesBulkAssignResponseFailures] | None = None + failures: list[CaseBulkUpdateResultFailures] | None = None diff --git a/src/didit/types/components/case_checker.py b/src/didit/types/components/case_checker.py new file mode 100644 index 0000000..e94e49f --- /dev/null +++ b/src/didit/types/components/case_checker.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["CaseChecker"] + + +class CaseChecker(BaseModel): + """The member named to approve or reject the pending resolution. Set only while a four-eyes resolution waits on a named checker. Null when nothing is pending, while the pending resolution waits on the checker queue, and once the resolution is approved, rejected, or returned for a new review because the evidence changed.""" + + member_uuid: str | None = None + """The member's user uuid: the value you pass as `checker` when you resolve the case.""" + + name: str | None = None + """The member's full name, or null when it is not known.""" + + email: str | None = None diff --git a/src/didit/types/components/case_detail.py b/src/didit/types/components/case_detail.py index 3d5a248..5197c82 100644 --- a/src/didit/types/components/case_detail.py +++ b/src/didit/types/components/case_detail.py @@ -7,11 +7,13 @@ from didit._models import BaseModel from didit.types.components.case_assignee import CaseAssignee from didit.types.components.case_blueprint_detail import CaseBlueprintDetail +from didit.types.components.case_checker import CaseChecker from didit.types.components.case_checklist_item import CaseChecklistItem from didit.types.components.case_event import CaseEvent from didit.types.components.case_link import CaseLink from didit.types.components.case_note import CaseNote from didit.types.components.case_subject import CaseSubject +from didit.types.components.case_team_summary import CaseTeamSummary __all__ = ["CaseDetail", "CaseDetailPendingResolution"] @@ -19,14 +21,28 @@ class CaseDetailPendingResolution(BaseModel): """Set only while a 4-eyes resolution is staged on the checker blueprint, awaiting approve or reject-approval.""" - resolution: Literal["FALSE_POSITIVE", "VALID_THREAT"] | str | None = None + resolution: ( + Literal[ + "FALSE_POSITIVE", + "VALID_THREAT", + "INCONCLUSIVE", + "REQUIREMENTS_NOT_MET", + "REQUIREMENTS_SATISFIED", + ] + | str + | None + ) = None resolution_notes: str | None = None maker_email: str | None = None + """Email of the officer who submitted the resolution. Null when an API key submitted it: that resolution has no recorded maker, so approve and reject-approval return 403 for every caller.""" maker_name: str | None = None + maker_user_id: str | None = None + """Member user uuid of the officer who submitted the resolution. Null when an API key submitted it. That member cannot be named as `checker`.""" + maker_blueprint_id: str | None = None staged_at: _dt.datetime | None = None @@ -41,7 +57,17 @@ class CaseDetail(BaseModel): status: Literal["OPEN", "AWAITING_USER", "RESOLVED"] | str | None = None - resolution: Literal["FALSE_POSITIVE", "VALID_THREAT"] | str | None = None + resolution: ( + Literal[ + "FALSE_POSITIVE", + "VALID_THREAT", + "INCONCLUSIVE", + "REQUIREMENTS_NOT_MET", + "REQUIREMENTS_SATISFIED", + ] + | str + | None + ) = None resolution_notes: str | None = None @@ -60,6 +86,9 @@ class CaseDetail(BaseModel): assigned_at: _dt.datetime | None = None + team: CaseTeamSummary | None = None + """The review team that owns the case, or null when no team owns it. Routing sets it when a routing rule places the case with one of its teams. Change or clear it with `PATCH .../cases/{case_uuid}/` (`team`).""" + due_at: _dt.datetime | None = None resolved_at: _dt.datetime | None = None @@ -69,6 +98,9 @@ class CaseDetail(BaseModel): pending_resolution: CaseDetailPendingResolution | None = None """Set only while a 4-eyes resolution is staged on the checker blueprint, awaiting approve or reject-approval.""" + checker: CaseChecker | None = None + """The member named to approve or reject the pending resolution. Set only while a four-eyes resolution waits on a named checker. Null when nothing is pending, while the pending resolution waits on the checker queue, and once the resolution is approved, rejected, or returned for a new review because the evidence changed.""" + metadata: dict[str, object] | None = None blueprint: CaseBlueprintDetail | None = None diff --git a/src/didit/types/components/case_event.py b/src/didit/types/components/case_event.py index 306b091..2f2d353 100644 --- a/src/didit/types/components/case_event.py +++ b/src/didit/types/components/case_event.py @@ -23,6 +23,8 @@ class CaseEvent(BaseModel): "LINK_ADDED", "LINK_REMOVED", "NOTE_ADDED", + "NOTE_PINNED", + "NOTE_UNPINNED", "PRIORITY_CHANGED", "DUE_DATE_CHANGED", "TAG_ADDED", @@ -37,6 +39,11 @@ class CaseEvent(BaseModel): "APPROVAL_REJECTED", "ENTITY_ACTION", "APPROVAL_INVALIDATED", + "CUSTOMER_REQUEST", + "STAGE_CHANGED", + "TEAM_CHANGED", + "CHECKER_ASSIGNED", + "CHECKER_UNASSIGNED", ] | str | None diff --git a/src/didit/types/components/case_list_item.py b/src/didit/types/components/case_list_item.py index cace29d..cc94be1 100644 --- a/src/didit/types/components/case_list_item.py +++ b/src/didit/types/components/case_list_item.py @@ -7,9 +7,41 @@ from didit._models import BaseModel from didit.types.components.case_assignee import CaseAssignee from didit.types.components.case_blueprint_summary import CaseBlueprintSummary +from didit.types.components.case_checker import CaseChecker from didit.types.components.case_subject import CaseSubject +from didit.types.components.case_team_summary import CaseTeamSummary -__all__ = ["CaseListItem"] +__all__ = ["CaseListItem", "CaseListItemPendingResolution"] + + +class CaseListItemPendingResolution(BaseModel): + """Set only while a 4-eyes resolution is staged on the checker blueprint, awaiting approve or reject-approval.""" + + resolution: ( + Literal[ + "FALSE_POSITIVE", + "VALID_THREAT", + "INCONCLUSIVE", + "REQUIREMENTS_NOT_MET", + "REQUIREMENTS_SATISFIED", + ] + | str + | None + ) = None + + resolution_notes: str | None = None + + maker_email: str | None = None + """Email of the officer who submitted the resolution. Null when an API key submitted it: that resolution has no recorded maker, so approve and reject-approval return 403 for every caller.""" + + maker_name: str | None = None + + maker_user_id: str | None = None + """Member user uuid of the officer who submitted the resolution. Null when an API key submitted it. That member cannot be named as `checker`.""" + + maker_blueprint_id: str | None = None + + staged_at: _dt.datetime | None = None class CaseListItem(BaseModel): @@ -22,7 +54,23 @@ class CaseListItem(BaseModel): status: Literal["OPEN", "AWAITING_USER", "RESOLVED"] | str | None = None - resolution: Literal["FALSE_POSITIVE", "VALID_THREAT"] | str | None = None + resolution: ( + Literal[ + "FALSE_POSITIVE", + "VALID_THREAT", + "INCONCLUSIVE", + "REQUIREMENTS_NOT_MET", + "REQUIREMENTS_SATISFIED", + ] + | str + | None + ) = None + + pending_resolution: CaseListItemPendingResolution | None = None + """Set only while a 4-eyes resolution is staged on the checker blueprint, awaiting approve or reject-approval.""" + + checker: CaseChecker | None = None + """The member named to approve or reject the pending resolution. Set only while a four-eyes resolution waits on a named checker. Null when nothing is pending, while the pending resolution waits on the checker queue, and once the resolution is approved, rejected, or returned for a new review because the evidence changed.""" priority: Literal["LOW", "MEDIUM", "HIGH"] | str | None = None @@ -39,6 +87,9 @@ class CaseListItem(BaseModel): assigned_at: _dt.datetime | None = None + team: CaseTeamSummary | None = None + """The review team that owns the case, or null when no team owns it. Routing sets it when a routing rule places the case with one of its teams. Change or clear it with `PATCH .../cases/{case_uuid}/` (`team`).""" + due_at: _dt.datetime | None = None resolved_at: _dt.datetime | None = None diff --git a/src/didit/types/components/case_subject.py b/src/didit/types/components/case_subject.py index 0b9b366..dc7a52b 100644 --- a/src/didit/types/components/case_subject.py +++ b/src/didit/types/components/case_subject.py @@ -19,3 +19,9 @@ class CaseSubject(BaseModel): external_id: str | None = None """The subject's vendor_data.""" + + country: str | None = None + """Three-letter country code of the subject (ISO 3166-1 alpha-3), or null when it is not known. For a business, its country of incorporation. For a person, the issuing country of an identity document from their verifications; a person with documents from several countries shows one of them, and ICAO document codes such as `XXA` can appear.""" + + portrait: str | None = None + """Short-lived signed URL of the person's portrait image. Always null for a business, and null for a person without a portrait. The URL expires, so download the image when you read the case instead of storing the URL.""" diff --git a/src/didit/types/components/case_team_summary.py b/src/didit/types/components/case_team_summary.py new file mode 100644 index 0000000..751a5c7 --- /dev/null +++ b/src/didit/types/components/case_team_summary.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["CaseTeamSummary"] + + +class CaseTeamSummary(BaseModel): + """The review team that owns the case, or null when no team owns it. Routing sets it when a routing rule places the case with one of its teams. Change or clear it with `PATCH .../cases/{case_uuid}/` (`team`).""" + + uuid: str | None = None + + name: str | None = None diff --git a/src/didit/types/components/compliance_list.py b/src/didit/types/components/compliance_list.py new file mode 100644 index 0000000..c65be2c --- /dev/null +++ b/src/didit/types/components/compliance_list.py @@ -0,0 +1,59 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["ComplianceList"] + + +class ComplianceList(BaseModel): + """A blocklist, allowlist or custom list of one application.""" + + uuid: str + """List identifier. Use it as `list_uuid` in the list and entry endpoints, and as the value of a workflow or transaction rule condition with `value_type: "list"`.""" + + name: str + """List name, unique among the application's lists.""" + + description: str + """Free-text description. Empty string when not set.""" + + list_type: Literal["blocklist", "allowlist", "custom"] | str + """`blocklist`: a match declines the session or transaction (system lists only, one per entry type). `allowlist`: matching items are trusted, for example a face allowlist skips the duplicated-face action; never overrides a blocklist match. `custom`: used in workflow and transaction rule conditions (`value_type: "list"`).""" + + entry_type: ( + Literal[ + "face", + "document", + "phone", + "email", + "ip_address", + "device_fingerprint", + "wallet_address", + "bank_account", + "country", + "user", + "business", + "key", + ] + | str + ) + """What the list holds: `face`, `document` (document number), `phone`, `email`, `ip_address` (IP address or CIDR range), `device_fingerprint`, `wallet_address`, `bank_account`, `country` (ISO 3166-1 alpha-3), `user` or `business` (your `vendor_data`), or `key` (any string). It decides how entry values are validated and matched.""" + + is_system: bool + """`true` for the lists Didit provisions: one blocklist per entry type (`face`, `document`, `phone`, `email`, `ip_address`, `device_fingerprint`, `wallet_address`, `bank_account`, `user`, `business`), the **High Risk Countries** custom list, and system allowlists. System lists cannot be renamed, edited or deleted; you can add and delete their entries.""" + + entry_count: int + """Number of entries currently in the list.""" + + last_entry_added_at: _dt.datetime | None + """When the newest current entry was added. `null` for an empty list.""" + + created_at: _dt.datetime + """When the list was created.""" + + updated_at: _dt.datetime + """When the list was last updated.""" diff --git a/src/didit/types/components/compliance_list_entry.py b/src/didit/types/components/compliance_list_entry.py new file mode 100644 index 0000000..906ad58 --- /dev/null +++ b/src/didit/types/components/compliance_list_entry.py @@ -0,0 +1,93 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["ComplianceListEntry", "ComplianceListEntryMetadata"] + + +class ComplianceListEntryMetadata(BaseModel): + """Extra context. Holds the keys Didit sets (listed here) merged with any `metadata` you sent when creating the entry, returned as stored. `null` when there is none.""" + + reference_image_url: str | None = None + """Face entries: short-lived signed URL, issued again on every call; `null` for a scoped API key without `media: read`. Absent when no image belongs to the entry.""" + + front_image_url: str | None = None + """Document entries that carry a document front image: short-lived signed URL, issued again on every call; `null` for a scoped API key without `media: read`.""" + + full_name: str | None = None + """Document entries taken from a session: holder's full name.""" + + issuing_state: str | None = None + """Document entries taken from a session: ISO 3166-1 alpha-3 issuing country.""" + + document_type: str | None = None + """Document entries taken from a session: document type code, for example `P`, `ID` or `DL`.""" + + region: str | None = None + """Document entries taken from a session: issuing state or province of a regionally issued document, empty when not applicable. Not set by Add session items to the blocklist.""" + + document_number: str | None = None + """Document entries taken from a session: the document number.""" + + face_image_source: ( + Literal["front_image_camera_front", "back_image_camera_front"] | str | None + ) = None + """Face entries taken from a document-capture selfie: which selfie. Absent for liveness faces.""" + + reference_node_id: str | None = None + """Face entries taken from a document-capture selfie: the ID verification node the selfie came from.""" + + fingerprint_tier: ( + Literal["persistent_id", "composite_hash", "legacy_fp", "unknown"] | str | None + ) = None + """Device fingerprint entries taken from a session: how the fingerprint was derived. `persistent_id` identifies one device; `composite_hash` and `legacy_fp` can be shared by devices of the same model.""" + + sandbox: bool | None = None + """`true` on a face entry uploaded in a sandbox application; such an entry matches nothing.""" + + reference_type: Literal["transaction", "vendor_user", "vendor_business"] | str | None = None + """Set by you: what `reference_object_uuid` points to, so the console links back to it.""" + + reference_id: str | None = None + """Set by you: identifier of the source record.""" + + +class ComplianceListEntry(BaseModel): + """One value in a list.""" + + uuid: str + """Entry identifier. Use it as `entry_uuid` in Delete a list entry.""" + + value: str + """The listed value: a phone number in E.164, an email address, a document number, an IP address or CIDR range, a device fingerprint, a wallet address, a bank account, an ISO 3166-1 alpha-3 country code, a `vendor_data`, any string on a `key` list, or the UUID of the stored face on a face list. Values you send are stored in lower case, except phone numbers and country codes. Matching is case-insensitive.""" + + display_label: str | None + """Human-readable label. Didit fills it for entries taken from a session (for example the holder's name and document number) when you send none.""" + + comment: str + """Free-text note. Empty string when not set.""" + + added_by: str | None + """Who added the entry: `api` (an API key), a console user's email address or `console`, `system` or `migration` (entries Didit created), or `rule` (a transaction or monitoring rule action).""" + + reference_session_id: str | None + """Verification session the entry was taken from. `null` when the entry was added by value.""" + + reference_session_number: int | None + """Application-scoped number of that session, as shown in the console.""" + + reference_session_type: Literal["kyc", "kyb"] | str | None + """`kyc` for a person verification session, `kyb` for a business verification session, `null` when there is no session.""" + + reference_object_uuid: str | None + """The record the entry stands for: the face, document, phone or email record read from a session, the face created by Upload a face to a list, or the UUID you sent (for example a transaction UUID or a user's `didit_internal_id`).""" + + metadata: ComplianceListEntryMetadata | None + """Extra context. Holds the keys Didit sets (listed here) merged with any `metadata` you sent when creating the entry, returned as stored. `null` when there is none.""" + + created_at: _dt.datetime + """When the entry was added.""" diff --git a/src/didit/types/users/list_networks_response.py b/src/didit/types/components/entity_network_membership_list.py similarity index 76% rename from src/didit/types/users/list_networks_response.py rename to src/didit/types/components/entity_network_membership_list.py index c7d2954..78a83bb 100644 --- a/src/didit/types/users/list_networks_response.py +++ b/src/didit/types/components/entity_network_membership_list.py @@ -4,8 +4,8 @@ from didit._models import BaseModel from didit.types.components.entity_network_membership import EntityNetworkMembership -__all__ = ["UsersListNetworksResponse"] +__all__ = ["EntityNetworkMembershipList"] -class UsersListNetworksResponse(BaseModel): +class EntityNetworkMembershipList(BaseModel): results: list[EntityNetworkMembership] | None = None diff --git a/src/didit/types/components/feature_status_override.py b/src/didit/types/components/feature_status_override.py new file mode 100644 index 0000000..c5aed4b --- /dev/null +++ b/src/didit/types/components/feature_status_override.py @@ -0,0 +1,72 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["FeatureStatusOverride", "FeatureStatusOverrideRequestedBy"] + + +class FeatureStatusOverrideRequestedBy(BaseModel): + """Who requested the override, or `null` when that is not known.""" + + email: str | None = None + """Email of the console user, or `null` for API keys and the system.""" + + name: str | None = None + """Display name of the requester, or `null`.""" + + type: Literal["CONSOLE_USER", "API_KEY", "SYSTEM"] | str | None = None + """Who made the request: a console user, an API key, or Didit itself.""" + + +class FeatureStatusOverride(BaseModel): + """One override of a check (feature) status made with `PATCH /v3/session/{sessionId}/features/{nodeId}/update-status/`: what it changes, whether it applied or still waits for the person to finish the flow, who asked and why.""" + + id: str + """Identifier of the override. Pass it to the cancel endpoint.""" + + node_id: str + """The workflow node the request named (`default` for a step without a node).""" + + feature_type: str + """The `feature_type` the request passed, or an empty string when it passed none.""" + + feature_model: str + """The kind of check that was overridden, for example `KYC`, `Face`, `AML` or `GeolocationCheck`.""" + + previous_status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str + ) + """The check's status when the override was requested.""" + + new_status: Literal["Approved", "Declined", "In Review"] | str + """The status the override sets.""" + + state: Literal["applied", "pending_until_flow_finishes", "cancelled"] | str + """`applied`: the check has the new status. `pending_until_flow_finishes`: the override waits and applies when the person finishes the flow. `cancelled`: it waited and never applied (see `cancel_reason`).""" + + forced: bool + """`true` when the override applied at once while the person was still in the flow.""" + + reason: str | None + """The `comment` sent with the request, or `null`.""" + + requested_by: FeatureStatusOverrideRequestedBy | None + """Who requested the override, or `null` when that is not known.""" + + requested_at: _dt.datetime + """When the override was requested.""" + + applied_at: _dt.datetime | None + """When the check took the new status, or `null` while it waits or once it is cancelled.""" + + cancelled_at: _dt.datetime | None + """When a waiting override was cancelled, or `null`.""" + + cancel_reason: ( + Literal["cancelled_by_reviewer", "replaced", "check_changed", "session_closed"] | str | None + ) + """Why a waiting override never applied, or `null`. `cancelled_by_reviewer`: cancelled with the cancel endpoint. `replaced`: a newer override on the same check replaced it. `check_changed`: the person redid the check before finishing the flow. `session_closed`: the session left the flow before the person finished it (abandoned, expired, or decided mid-flow).""" diff --git a/src/didit/types/components/investigator_batch_accept_response.py b/src/didit/types/components/investigator_batch_accept_response.py new file mode 100644 index 0000000..18849b7 --- /dev/null +++ b/src/didit/types/components/investigator_batch_accept_response.py @@ -0,0 +1,34 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["InvestigatorBatchAcceptResponse", "InvestigatorBatchAcceptResponseResults"] + + +class InvestigatorBatchAcceptResponseResults(BaseModel): + run_id: str | None = None + + result: Literal["accepted", "skipped"] | str | None = None + + reason: ( + Literal["not_found", "forbidden", "already_decided", "not_ready", "guardrail", "invalid"] + | str + | None + ) = None + + detail: object = None + + action: str | None = None + + final_status: str | None = None + + +class InvestigatorBatchAcceptResponse(BaseModel): + accepted: int | None = None + + skipped: int | None = None + + results: list[InvestigatorBatchAcceptResponseResults] | None = None diff --git a/src/didit/types/components/investigator_block.py b/src/didit/types/components/investigator_block.py new file mode 100644 index 0000000..1e4c021 --- /dev/null +++ b/src/didit/types/components/investigator_block.py @@ -0,0 +1,43 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.investigator_decision import InvestigatorDecision + +__all__ = ["InvestigatorBlock"] + + +class InvestigatorBlock(BaseModel): + """The item's latest Investigator review, or null when it never had one. Carried by the V3 session and business session decision (and so by `status.updated` webhooks).""" + + run_id: str | None = None + + status: Literal["running", "completed", "failed"] | str | None = None + + stage: Literal["shadow", "live"] | str | None = None + + queue: Literal["kyc", "kyb", "aml", "transactions", "cases"] | str | None = None + + is_open: bool | None = None + + recommendation: Literal["approve", "decline", "escalate", "request"] | str | None = None + + recommendation_detail: str | None = None + + confidence: Literal["high", "moderate", "low"] | str | None = None + + summary: str | None = None + + ready_to_accept: bool | None = None + + prepared_at: _dt.datetime | None = None + + automated: bool | None = None + """The Investigator decided on its own, even if the decision was later overridden.""" + + decided_by: Literal["investigator", "member"] | str | None = None + + decision: InvestigatorDecision | None = None diff --git a/src/didit/types/components/investigator_decision.py b/src/didit/types/components/investigator_decision.py new file mode 100644 index 0000000..db8e07e --- /dev/null +++ b/src/didit/types/components/investigator_decision.py @@ -0,0 +1,92 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "InvestigatorDecision", + "InvestigatorDecisionOriginalOutcome", + "InvestigatorDecisionOverrides", +] + + +class InvestigatorDecisionOriginalOutcome(BaseModel): + """The decision the Investigator took on its own, unchanged by later overrides. Null for a decision the team took.""" + + action: str | None = None + + final_status: str | None = None + + decided_at: _dt.datetime | None = None + + +class InvestigatorDecisionOverrides(BaseModel): + action: str | None = None + + actor_email: str | None = None + + actor_name: str | None = None + + override_category: str | None = None + + override_reason: str | None = None + + previous_status: str | None = None + + final_status: str | None = None + + agreed: bool | None = None + + overridden_at: _dt.datetime | None = None + + +class InvestigatorDecision(BaseModel): + """How the item left the Investigator queue. `decided_by`, `original_outcome` and `overrides` are returned by the accept and override endpoints and the report; the `investigator` block of a session decision carries the other fields.""" + + outcome: ( + Literal["accepted", "edited", "overridden", "decided_elsewhere", "automated"] | str | None + ) = None + """`accepted` or `edited` took the recommendation (`edited` with a rewritten narrative), `overridden` took another action, `decided_elsewhere` is a decision taken outside the Investigator while the report was open, `automated` is a decision the Investigator took on its own.""" + + action: Literal["approve", "decline", "escalate", "request"] | str | None = None + + actor_email: str | None = None + + actor_name: str | None = None + + override_category: ( + Literal[ + "missing_context", + "needs_information", + "policy_differs", + "evidence_misread", + "other", + "", + ] + | str + | None + ) = None + + override_reason: str | None = None + + final_status: str | None = None + """The item's status after the decision.""" + + agreed: bool | None = None + """Whether the decision matched the recommendation.""" + + decided_at: _dt.datetime | None = None + + billed: bool | None = None + """The review was charged.""" + + decided_by: Literal["investigator", "member"] | str | None = None + + original_outcome: InvestigatorDecisionOriginalOutcome | None = None + """The decision the Investigator took on its own, unchanged by later overrides. Null for a decision the team took.""" + + overrides: list[InvestigatorDecisionOverrides] | None = None + """Every override of a decision the Investigator took on its own, oldest first.""" diff --git a/src/didit/types/components/investigator_queue_row.py b/src/didit/types/components/investigator_queue_row.py new file mode 100644 index 0000000..31413da --- /dev/null +++ b/src/didit/types/components/investigator_queue_row.py @@ -0,0 +1,43 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["InvestigatorQueueRow"] + + +class InvestigatorQueueRow(BaseModel): + run_id: str | None = None + + kind: Literal["session", "business_session", "transaction", "case"] | str | None = None + + queue: Literal["kyc", "kyb", "aml", "transactions", "cases"] | str | None = None + + stage: Literal["shadow", "live"] | str | None = None + + subject: dict[str, object] | None = None + """The item under review: `id`, `number`, `status`, `name` and, depending on the kind, `country`, `workflow`, `vendor_data`, `txn_id`, `amount`, `currency`, `direction`, `title` or `priority`.""" + + recommendation: Literal["approve", "decline", "escalate", "request"] | str | None = None + + recommendation_detail: str | None = None + + confidence: Literal["high", "moderate", "low"] | str | None = None + + summary: str | None = None + + sources_count: int | None = None + + ready_to_accept: bool | None = None + + guardrails: object = None + """The locked guardrails that hold this item, if any.""" + + waiting_since: _dt.datetime | None = None + + due_at: _dt.datetime | None = None + + prepared_at: _dt.datetime | None = None diff --git a/src/didit/types/components/investigator_run_detail.py b/src/didit/types/components/investigator_run_detail.py new file mode 100644 index 0000000..6d8e8c4 --- /dev/null +++ b/src/didit/types/components/investigator_run_detail.py @@ -0,0 +1,33 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit.types.components.investigator_decision import InvestigatorDecision +from didit.types.components.investigator_queue_row import InvestigatorQueueRow + +__all__ = ["InvestigatorRunDetail"] + + +class InvestigatorRunDetail(InvestigatorQueueRow): + status: str | None = None + + is_open: bool | None = None + + lookups: int | None = None + """How many sections and linked records it read.""" + + duration_ms: int | None = None + + agent_version: str | None = None + + prepared_before_opened: bool | None = None + + report: dict[str, object] | None = None + """The report: reasoning, evidence points with their verdict (`ok`, `risk`, `look`, `explained`, `context`) and cited sources, screening hit verdicts (`same`, `not_same`, `unclear`) and suggested next steps.""" + + trail: object = None + """What I checked: the overview, sections and records it read, in order.""" + + decision: InvestigatorDecision | None = None + + autonomy: dict[str, object] | None = None + """What autonomy made of the report (`result`, `reason`, `tier`, `policy_version`, `rule`, `agreement`, `decisions`, `threshold`, `at`). Null when the queue is not automated.""" diff --git a/src/didit/types/components/investigator_settings.py b/src/didit/types/components/investigator_settings.py new file mode 100644 index 0000000..d4ce4bb --- /dev/null +++ b/src/didit/types/components/investigator_settings.py @@ -0,0 +1,40 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["InvestigatorSettings", "InvestigatorSettingsSpendLimit"] + + +class InvestigatorSettingsSpendLimit(BaseModel): + enabled: bool | None = None + + limit: str | None = None + + spent: str | None = None + + reviews: int | None = None + + reserved: str | None = None + """The price of every open live report, held back because accepting it charges it.""" + + currency: str | None = None + + paused: bool | None = None + """At the limit: no new review starts.""" + + +class InvestigatorSettings(BaseModel): + stage: Literal["none", "shadow", "live"] | str | None = None + + stage_changed_at: _dt.datetime | None = None + + stage_changed_by: str | None = None + + monthly_spend_limit: str | None = None + """Monthly limit in your billing currency, 0.01 to 1,000,000. Null turns it off.""" + + spend_limit: InvestigatorSettingsSpendLimit | None = None diff --git a/src/didit/types/components/kyb_key_people_party.py b/src/didit/types/components/kyb_key_people_party.py new file mode 100644 index 0000000..9fd50e8 --- /dev/null +++ b/src/didit/types/components/kyb_key_people_party.py @@ -0,0 +1,195 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "KYBKeyPeopleParty", + "KYBKeyPeoplePartyRoles", + "KYBKeyPeoplePartyVerificationWorkflowFeatures", +] + + +class KYBKeyPeoplePartyRoles(BaseModel): + role: ( + Literal[ + "director", + "non_executive_director", + "secretary", + "chairman", + "ubo", + "shareholder", + "representative", + "founder", + "legal_advisor", + "authorized_signatory", + "trustee", + "beneficiary", + "settlor", + "protector", + "company_officer", + "investor", + "other", + ] + | str + ) + """Role code.""" + + ownership_percent: float | None + """Ownership percentage for this role, when known.""" + + voting_percent: float | None + """Voting-rights percentage for this role, when known.""" + + +class KYBKeyPeoplePartyVerificationWorkflowFeatures(BaseModel): + feature: str + """Feature code, for example `ID_VERIFICATION` or `LIVENESS`.""" + + status: str + """Feature status, `Not Finished` until it runs.""" + + +class KYBKeyPeopleParty(BaseModel): + """A Key People party of a Business Verification (KYB) session, with its child verification. The same party appears in `key_people_checks[].submitted.parties[]` of the business session decision.""" + + uuid: str + """Party identifier.""" + + entity_type: Literal["person", "company"] | str + """Whether the party is an individual or a company.""" + + source: Literal["REGISTRY", "USER"] | str + """`REGISTRY`: taken from the company registry. `USER`: entered in the Key People screen.""" + + name: str + """Display name of the party.""" + + first_name: str | None = None + """First name (persons).""" + + last_name: str | None = None + """Last name (persons).""" + + company_name: str | None = None + """Company name (company parties).""" + + registration_number: str | None = None + """Company registration number (company parties).""" + + email: str | None = None + """Email the verification link is sent to.""" + + nationality: str | None = None + """Nationality for persons, country of incorporation for companies.""" + + phone_number: str | None = None + """Phone number collected for the party.""" + + date_of_birth: _dt.date | None = None + """Date of birth (persons).""" + + position: str | None = None + """Free-text job title (persons).""" + + custom_fields: dict[str, object] | None = None + """Workflow-defined custom fields collected for the party, keyed by field key.""" + + is_active: bool + """Whether the party is active.""" + + is_skipped: bool + """Whether the party is exempt from verification. Regenerating clears it.""" + + requires_verification: bool + """Whether the role and ownership rules require a verification for this party.""" + + roles: list[KYBKeyPeoplePartyRoles] + """Roles the party holds in the company.""" + + kyc_session_id: str | None = None + """Child individual KYC session (persons).""" + + kyc_session_status: ( + Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + | None + ) = None + """Status of the child KYC session. A deleted session keeps the status it had when it was deleted.""" + + kyc_session_url: str | None = None + """Hosted link of the child KYC session. `null` when there is none or it was deleted, and for a scoped API key without `sessions: write`.""" + + kyc_session_deleted: bool + """The child KYC session was deleted, so it can never settle.""" + + kyb_sub_session_id: str | None = None + """Child KYB session (company parties).""" + + kyb_sub_session_status: ( + Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + | None + ) = None + """Status of the child KYB session.""" + + kyb_sub_session_url: str | None = None + """Hosted link of the child KYB session, with the same `null` rules as `kyc_session_url`.""" + + kyb_sub_session_deleted: bool + """Company-party counterpart of `kyc_session_deleted`.""" + + verification_workflow_type: ( + Literal[ + "kyc", + "kyb", + "adaptive_age_verification", + "biometric_authentication", + "address_verification", + "questionnaire_verification", + "email_verification", + "phone_verification", + ] + | str + | None + ) = None + """Type of the workflow the child session runs.""" + + verification_workflow_id: str | None = None + """Workflow the child session runs.""" + + verification_workflow_label: str | None = None + """Label of that workflow.""" + + verification_workflow_features: list[KYBKeyPeoplePartyVerificationWorkflowFeatures] | None = ( + None + ) + """Features of the child session's workflow with their current status.""" + + didit_internal_id: str | None = None + """Didit's identifier of the verified person, once known.""" diff --git a/src/didit/types/components/kyb_registry_search_results.py b/src/didit/types/components/kyb_registry_search_results.py new file mode 100644 index 0000000..57a327a --- /dev/null +++ b/src/didit/types/components/kyb_registry_search_results.py @@ -0,0 +1,60 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "KYBRegistrySearchResults", + "KYBRegistrySearchResultsCompanies", + "KYBRegistrySearchResultsPagination", +] + + +class KYBRegistrySearchResultsCompanies(BaseModel): + kyb_response_id: str | None = None + """Handle for this candidate - pass it to `POST /v3/kyb/select/` to retrieve the full profile. Ephemeral and scoped to this search: do not store it long-term; re-search to get a fresh one.""" + + name: str | None = None + """Registered company name.""" + + registration_number: str | None = None + """Registry registration number.""" + + status: str | None = None + """Normalized registry status of the company (e.g. `active`, `dissolved`, `inactive`, `struck off`).""" + + type: str | None = None + """Legal form as reported by the registry.""" + + registered_address: str | None = None + """Registered address of the candidate, when the registry returns one. Use it, with the registration number, to tell same-named companies apart: in the United States a candidate can be listed under the state where the company operates rather than the state you searched.""" + + risk_level: str | None = None + """Registry-reported risk level, when available. Usually `null` at search time.""" + + fetch_status: str | None = None + """Always `pending` for search candidates - the full company profile is only fetched (and billed) when you select the candidate.""" + + +class KYBRegistrySearchResultsPagination(BaseModel): + total: int | None = None + """Number of candidates returned.""" + + page: int | None = None + + per_page: int | None = None + + +class KYBRegistrySearchResults(BaseModel): + companies: list[KYBRegistrySearchResultsCompanies] | None = None + """Deduplicated candidate list. Empty while `search_resolved=false` (webhook mode) or when nothing matched.""" + + pagination: KYBRegistrySearchResultsPagination | None = None + + search_status: Literal["pending", "resolved"] | str | None = None + """Registry search lifecycle status.""" + + search_resolved: bool | None = None + """`true` once the candidate list is final.""" diff --git a/src/didit/types/components/monitoring_event.py b/src/didit/types/components/monitoring_event.py new file mode 100644 index 0000000..0611c8d --- /dev/null +++ b/src/didit/types/components/monitoring_event.py @@ -0,0 +1,110 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["MonitoringEvent"] + + +class MonitoringEvent(BaseModel): + """One stored monitoring event as returned by the list and subject-timeline endpoints. Restricted evidence (payload, device context, rule evidence) is only returned by `GET /v3/events/{event_uuid}/`.""" + + event_uuid: str + """Didit identifier of the event. Use it as `{event_uuid}` in the retrieve and delete calls.""" + + event_id: str + """Your own identifier, the idempotency key you sent.""" + + event_category: ( + Literal[ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom", + ] + | str + ) + """Event category.""" + + action_type: str + """What happened within the category, as you sent it.""" + + occurred_at: _dt.datetime + """When the event happened on your side.""" + + received_at: _dt.datetime + """When Didit accepted the event.""" + + status: Literal["ACCEPTED", "APPROVED", "IN_REVIEW", "DECLINED"] | str + """`ACCEPTED`: stored, no matched rule changed its standing (the normal answer for most events, and always the answer in a sandbox application, where no rules run). `APPROVED`: a rule matched and the score stayed under your review threshold. `IN_REVIEW`: a rule pushed the score over the review threshold or set the status. `DECLINED`: a rule declined it or the score crossed your decline threshold.""" + + score: int + """Accumulated rule score. Starts at 0; rule `add_score` actions move it (negative deltas are allowed).""" + + subject_vendor_data: str + """`subject.vendor_data` of the event: your id for the user or company.""" + + subject_entity_type: str + """`individual` or `company`: which kind of subject `subject_vendor_data` names.""" + + subject_uuid: str | None + """UUID of the user (individual) or company the event resolved to, or null when that record no longer exists.""" + + counterparty_vendor_data: str | None + """The other player of a P2P event (promoted from `payload.counterparty_vendor_data`).""" + + session_id: str | None + """`session.session_id` sent with the event.""" + + device_fingerprint: str | None + """`device_context.fingerprint` sent with the event.""" + + ip_address: str | None + """IP address of the event. On the server lane this is `device_context.network_context.ip_address` as you sent it; on the device lane it is the IP of the request.""" + + ip_country: str | None + """Country code of `ip_address` from IP enrichment, or null when the address was not enriched (missing, private, or low-value telemetry whose address was not already known).""" + + amount: str | None + """Amount as a decimal string with 18 decimal places, from `transaction_context.amount`, `payload.amount` or `payload.stake_amount`, in that order.""" + + currency: str | None + """Currency from `transaction_context.currency` or `payload.currency`.""" + + game_id: str | None + """Promoted from `payload.game_id`.""" + + round_id: str | None + """Promoted from `payload.round_id`.""" + + bonus_id: str | None + """Promoted from `payload.bonus_id`.""" + + affiliate_code: str | None + """Promoted from `payload.affiliate_code`.""" + + payment_reference_id: str | None + """Payment reference from `transaction_context.payment_reference_id` or `payload.payment_reference_id`.""" + + retention_class: ( + Literal["low_value", "standard", "risk_relevant", "monetary_or_compliance"] | str + ) + """Retention class. Payment and dispute events are `monetary_or_compliance`; security and responsible-gambling events are `risk_relevant`; `session.page_view`, `session.sdk_heartbeat`, `session.sdk_loaded`, `session.device_seen`, `session.bot_signal_observed`, `bonus.promotion_viewed` and `affiliate.referral_clicked` are `low_value`; everything else is `standard`. An event an active rule matched is promoted to `risk_relevant` (never demoted).""" + + rules_matched_count: int + """Number of rules that matched the event, including rules in `TEST` mode (which record evidence but apply no actions).""" + + correlation_pending: bool + """`true` while an asynchronous rule (cross-subject correlation, or a sequence longer than 24 hours) still has to run for this event. It resolves within minutes and `status`, `score` and `rules_matched_count` update in place.""" + + is_droppable: bool + """Whether an SDK under backpressure may drop events of this kind. `false` for every `security`, `payment`, `dispute` and `responsible_gambling` event and for `gameplay.bet_placed`, `gameplay.bet_settled` and `gameplay.p2p_settlement`.""" diff --git a/src/didit/types/components/monitoring_event_create_request.py b/src/didit/types/components/monitoring_event_create_request.py new file mode 100644 index 0000000..c975370 --- /dev/null +++ b/src/didit/types/components/monitoring_event_create_request.py @@ -0,0 +1,152 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Mapping + +from typing_extensions import Required, TypedDict + +__all__ = [ + "MonitoringEventCreateRequest", + "MonitoringEventCreateRequestBusiness", + "MonitoringEventCreateRequestDeviceContext", + "MonitoringEventCreateRequestDeviceContextNetworkContext", + "MonitoringEventCreateRequestSession", + "MonitoringEventCreateRequestSubject", + "MonitoringEventCreateRequestTransactionContext", +] + + +class MonitoringEventCreateRequestBusiness(TypedDict, total=False): + """The business the event belongs to, when it differs from the subject. Only `vendor_data` is read.""" + + vendor_data: str + """Your id for the business. Filterable as `business_vendor_data`.""" + + +class MonitoringEventCreateRequestDeviceContextNetworkContext(TypedDict, total=False): + ip_address: str + """End user's IP address. Public addresses are enriched once per unique address.""" + + +class MonitoringEventCreateRequestDeviceContext(TypedDict, total=False): + """Device and network context, stored as sent and returned by `GET /v3/events/{event_uuid}/`. On this server lane Didit does not see the end user's connection, so the IP enriched and used by rules is `network_context.ip_address` as you send it.""" + + fingerprint: str + """Device fingerprint. Filterable as `device_fingerprint`.""" + + persistent_device_id: str + """Your persistent device identifier.""" + + bot_score: int + """Bot score from your own detection, as an integer.""" + + network_context: MonitoringEventCreateRequestDeviceContextNetworkContext + + +class MonitoringEventCreateRequestSession(TypedDict, total=False): + """Your session context for the event.""" + + session_id: str | None + """Your session identifier. Filterable and returned as `session_id`.""" + + sequence: int | None + """Position of the event within the session. Returned as `session_sequence`.""" + + page_url: str | None + """Validated; not stored on the event.""" + + referrer: str | None + """Validated; not stored on the event.""" + + +class MonitoringEventCreateRequestSubject(TypedDict, total=False): + """The user or company the event belongs to. It resolves to the user (or company) with this `vendor_data` in your application; one is created when none exists.""" + + vendor_data: Required[str] + """Your id for the subject.""" + + entity_type: str + """`company` resolves the subject to a company; any other value (default `individual`) resolves it to an individual. The same `vendor_data` can name one of each.""" + + full_name: str | None + """Display name, used only when this event creates the user or company.""" + + email: str | None + """Validated as an email address; not stored on the event.""" + + phone: str | None + """Validated; not stored on the event.""" + + country: str | None + """Validated; not stored on the event.""" + + +class MonitoringEventCreateRequestTransactionContext(TypedDict, total=False): + """Money context. Non-empty values here also satisfy the required payload keys of the action.""" + + amount: str | None + """Amount as a decimal string (a number is also accepted).""" + + currency: str | None + """Currency code.""" + + direction: str | None + """Direction of the money movement, for example `inbound` or `outbound`.""" + + payment_reference_id: str | None + """Your payment reference. Filterable as `payment_reference_id`.""" + + +class MonitoringEventCreateRequest(TypedDict, total=False): + """The event envelope, the same for every category. camelCase aliases are accepted: `eventId`, `eventCategory` (or `category`), `actionType`, `occurredAt`, `timeZone` (or `zoneId`), `deviceContext` (or `device`), `transactionContext`, `customValues` (or `props`); inside `subject`: `vendorData`, `entityType`, `fullName`; inside `session`: `sessionId`, `pageUrl`; inside `transaction_context`: `paymentReferenceId`.""" + + event_id: Required[str] + """Your identifier for the event, unique per application. It is the idempotency key: resending the same `event_category`, `action_type`, `occurred_at`, `subject.vendor_data` and `payload` under it is a replay, anything different is a conflict.""" + + event_category: Required[ + Literal[ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom", + ] + | str + ] + """Event category.""" + + action_type: Required[str] + """What happened within the category. Open vocabulary; the managed actions listed in [Event ingestion](https://docs.didit.me/transaction-monitoring/events) (for example `login`, `deposit_completed`, `chargeback_opened`) unlock typed validation and managed rule fields. Leading and trailing whitespace is trimmed.""" + + occurred_at: Required[str | _dt.datetime] + """When the event happened on your side, ISO 8601 (a value without an offset is read as UTC). Must be no more than 7 days in the past and no more than 5 minutes in the future.""" + + time_zone: str | None + """Time zone of the occurrence on your side, for example `Europe/Madrid`. Stored with the event; not returned by the read endpoints.""" + + subject: Required[MonitoringEventCreateRequestSubject] + """The user or company the event belongs to. It resolves to the user (or company) with this `vendor_data` in your application; one is created when none exists.""" + + business: MonitoringEventCreateRequestBusiness + """The business the event belongs to, when it differs from the subject. Only `vendor_data` is read.""" + + session: MonitoringEventCreateRequestSession + """Your session context for the event.""" + + device_context: MonitoringEventCreateRequestDeviceContext + """Device and network context, stored as sent and returned by `GET /v3/events/{event_uuid}/`. On this server lane Didit does not see the end user's connection, so the IP enriched and used by rules is `network_context.ip_address` as you send it.""" + + transaction_context: MonitoringEventCreateRequestTransactionContext + """Money context. Non-empty values here also satisfy the required payload keys of the action.""" + + payload: Mapping[str, object] + """The category's domain fields, at most 16384 bytes once JSON-encoded. Some actions require keys (in `payload` or `transaction_context`): `gameplay.p2p_settlement` needs `game_id`, `counterparty_vendor_data`, `stake_amount`, `net_result_amount`, `role` and one of `hand_id`, `round_id`, `match_id`; `gameplay.bet_placed` and `gameplay.bet_settled` need `game_id`; `payment.deposit_initiated`, `payment.deposit_completed`, `payment.withdrawal_requested` and `payment.withdrawal_completed` need `payment_reference_id`, `amount`, `currency`, `payment_method`, `status`; `payment.withdrawal_address_changed` needs `withdrawal_address_hash`; `dispute.chargeback_opened`, `dispute.chargeback_won` and `dispute.chargeback_lost` need `payment_reference_id` and one of `dispute_id`, `chargeback_id`; `session.bot_signal_observed` needs `telemetry_schema_version` and one of `bot_score`, `actions_per_minute`, `timing_variance_ms`, `pointer_entropy`, `keystroke_entropy`, `reaction_time_ms_p50`; `responsible_gambling.self_exclusion_match` needs `registry_source`, `exclusion_status`, `identity_match_ref`. Every `security` action ending in `_changed` needs `changed_field`, plus `previous_reference_hash` or `new_reference_hash` when the field is `password`, `mfa_method`, `mfa`, `email`, `phone`, `withdrawal_address`, `payout_method` or `api_key`. Send hashes and references, never a card number, a full withdrawal address, a password hash or an MFA secret.""" + + custom_values: Mapping[str, str | float | bool | None] + """Up to 100 keys of your own, addressable from rules as `custom_values.`. Values must be scalars (string, number, boolean or null); nested objects and arrays are rejected.""" diff --git a/src/didit/types/components/monitoring_event_detail.py b/src/didit/types/components/monitoring_event_detail.py new file mode 100644 index 0000000..0c81adf --- /dev/null +++ b/src/didit/types/components/monitoring_event_detail.py @@ -0,0 +1,168 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.monitoring_event import MonitoringEvent +from didit.types.components.monitoring_event_rule_run import MonitoringEventRuleRun + +__all__ = [ + "MonitoringEventDetail", + "MonitoringEventDetailIpEnrichment", + "MonitoringEventDetailSubjectProfile", +] + + +class MonitoringEventDetailIpEnrichment(BaseModel): + """Enrichment of the event's IP address, or null when the address was not enriched. See [IP enrichment](https://docs.didit.me/transaction-monitoring/ip-enrichment).""" + + ip_country_code: str | None + """Country code of the address.""" + + ip_city: str | None + """City of the address.""" + + asn_number: int | None + """Autonomous system number.""" + + asn_organization: str | None + """Organization that operates the autonomous system.""" + + is_vpn_or_tor: bool + """Whether the address is a known VPN or Tor exit.""" + + is_data_center: bool + """Whether the address belongs to a hosting or data-centre network.""" + + proxy_type: str | None + """Proxy classification of the address as an opaque string, or null.""" + + last_enriched_at: _dt.datetime | None + """When the address was last enriched.""" + + +class MonitoringEventDetailSubjectProfile(BaseModel): + """Cached email and phone risk signals of the subject, or null when no profile exists for the subject.""" + + email_risk_score: int | None + """Email risk score.""" + + email_deliverability: str | None + """Email deliverability verdict.""" + + email_is_disposable: bool | None + """Whether the email address is disposable.""" + + email_is_breached: bool | None + """Whether the email address appears in known data breaches.""" + + email_social_count: int | None + """Number of online accounts linked to the email address.""" + + email_social_providers: list[str] + """Names of the online services where an account linked to the email address was found.""" + + phone_risk_score: int | None + """Phone risk score.""" + + phone_line_type: str | None + """Phone line type.""" + + phone_carrier: str | None + """Phone carrier name.""" + + phone_social_count: int | None + """Number of online accounts linked to the phone number.""" + + phone_social_providers: list[str] + """Names of the online services where an account linked to the phone number was found.""" + + source_provider: str | None + """Opaque identifier of the source of the profile.""" + + last_enriched_at: _dt.datetime | None + """When the profile was last refreshed.""" + + +class MonitoringEventDetail(MonitoringEvent): + """One monitoring event with its full envelope, enrichment and rule evidence. Every field of `MonitoringEvent` plus the fields below.""" + + payload: dict[str, object] + """The category fields you sent in `payload`.""" + + custom_values: dict[str, object] + """The scalar `custom_values` you sent. Didit adds `vpn_or_proxy_detected: true` or `is_data_center: true` when IP enrichment flags the address and you did not set them.""" + + device_context: dict[str, object] + """The device context as stored: your `device_context` (including `fingerprint`, `persistent_device_id` and `bot_score` when sent) with `network_context` completed by IP enrichment (`ip_country`, `ip_country_code`, `ip_city`, `asn_number`, `asn_organization`, `is_vpn_or_tor`, `is_data_center`, `proxy_type`, `connection_type`) for keys you left empty.""" + + field_sources: dict[str, Literal["customer", "sdk", "server"] | str] + """Field path to provenance: `customer` (sent by your backend), `sdk` (sent through the device lane) or `server` (derived by Didit, such as `received_at` and enriched network fields). Check it before acting on a match.""" + + session_sequence: int | None + """`session.sequence` sent with the event.""" + + table_id: str | None + """Promoted from `payload.table_id`.""" + + hand_id: str | None + """Promoted from `payload.hand_id`.""" + + match_id: str | None + """Promoted from `payload.match_id`.""" + + market_id: str | None + """Promoted from `payload.market_id`.""" + + side: str | None + """Promoted from `payload.side`.""" + + role: str | None + """Promoted from `payload.role`.""" + + net_result_amount: str | None + """Promoted from `payload.net_result_amount`, as a decimal string with 18 decimal places.""" + + payment_fingerprint: str | None + """Promoted from `payload.payment_fingerprint`.""" + + instrument_reference_hash: str | None + """Promoted from `payload.instrument_reference_hash`.""" + + withdrawal_address_hash: str | None + """Promoted from `payload.withdrawal_address_hash` (or `payload.destination_reference_hash`).""" + + dispute_id: str | None + """Promoted from `payload.dispute_id`.""" + + chargeback_id: str | None + """Promoted from `payload.chargeback_id`.""" + + changed_field: str | None + """Promoted from `payload.changed_field` (security change events).""" + + identity_match_ref: str | None + """Promoted from `payload.identity_match_ref` (responsible-gambling registry matches).""" + + schema_version: int + """Version of the event envelope schema. Currently `1`.""" + + expires_at: _dt.datetime | None + """When the event expires under its retention class and is deleted: `occurred_at` plus the class period (14, 30 or 90 days by default, adjustable per application). Null for `monetary_or_compliance` events, which follow transaction retention.""" + + rules_evaluated_count: int + """Number of event rules evaluated for this event.""" + + rule_runs: list[MonitoringEventRuleRun] + """Every rule that matched the event, newest first.""" + + ip_enrichment: MonitoringEventDetailIpEnrichment | None + """Enrichment of the event's IP address, or null when the address was not enriched. See [IP enrichment](https://docs.didit.me/transaction-monitoring/ip-enrichment).""" + + subject_profile: MonitoringEventDetailSubjectProfile | None + """Cached email and phone risk signals of the subject, or null when no profile exists for the subject.""" + + tags: list[str] + """Names of the tags attached to the event (rule `add_tags` actions).""" diff --git a/src/didit/types/components/monitoring_event_ingest_result.py b/src/didit/types/components/monitoring_event_ingest_result.py new file mode 100644 index 0000000..a508f87 --- /dev/null +++ b/src/didit/types/components/monitoring_event_ingest_result.py @@ -0,0 +1,62 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["MonitoringEventIngestResult", "MonitoringEventIngestResultRuleMatches"] + + +class MonitoringEventIngestResultRuleMatches(BaseModel): + rule_id: str + """UUID of the rule.""" + + library_key: str | None + """Library key when the rule was installed from the rules library, otherwise null.""" + + mode: Literal["ACTIVE", "TEST"] | str + """`TEST` rules record the match but apply no actions.""" + + is_async: bool + """`true` when the match came from the asynchronous pass.""" + + +class MonitoringEventIngestResult(BaseModel): + """Outcome of ingesting one event. The same shape is returned for a new event and for an idempotent replay.""" + + event_uuid: str + """Didit identifier of the stored event.""" + + event_id: str + """Your `event_id`.""" + + status: Literal["ACCEPTED", "APPROVED", "IN_REVIEW", "DECLINED"] | str + """Status after the real-time rules ran. See `MonitoringEvent.status`.""" + + score: int + """Score after the real-time rules ran.""" + + rules_evaluated_count: int + """Number of event rules evaluated in the request. 0 in a sandbox application, where no rules run.""" + + rules_matched_count: int + """Number of rules that matched, including `TEST` mode rules.""" + + correlation_pending: bool + """`true` when an asynchronous rule still has to run. Read `GET /v3/events/{event_uuid}/` a few minutes later for the final `status` and `score`.""" + + retention_class: ( + Literal["low_value", "standard", "risk_relevant", "monetary_or_compliance"] | str + ) + """Retention class, after any promotion by a matched rule.""" + + expires_at: _dt.datetime | None + """When the event expires under its retention class and is deleted. Null for `monetary_or_compliance` (payment and dispute) events.""" + + idempotent_replay: bool + """`true` when an event with this `event_id` and the same content was already stored; the values are those of the stored event and no rule ran again.""" + + rule_matches: list[MonitoringEventIngestResultRuleMatches] + """Rules that matched the event.""" diff --git a/src/didit/types/components/monitoring_event_page.py b/src/didit/types/components/monitoring_event_page.py new file mode 100644 index 0000000..b034ce8 --- /dev/null +++ b/src/didit/types/components/monitoring_event_page.py @@ -0,0 +1,22 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.monitoring_event import MonitoringEvent + +__all__ = ["MonitoringEventPage"] + + +class MonitoringEventPage(BaseModel): + """One page of monitoring events, newest `occurred_at` first.""" + + count: int + """Number of matching events, exact up to 100 and capped at 100 above that. Do not compute page counts from it; follow `next` until it is null.""" + + next: str | None + """URL of the next page, or null on the last page.""" + + previous: str | None + """URL of the previous page, or null on the first page.""" + + results: list[MonitoringEvent] diff --git a/src/didit/types/components/monitoring_event_rule_run.py b/src/didit/types/components/monitoring_event_rule_run.py new file mode 100644 index 0000000..3275a45 --- /dev/null +++ b/src/didit/types/components/monitoring_event_rule_run.py @@ -0,0 +1,44 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["MonitoringEventRuleRun"] + + +class MonitoringEventRuleRun(BaseModel): + rule_uuid: str + """UUID of the transaction monitoring rule.""" + + rule_title: str + """Title of the rule.""" + + library_key: str | None + """Library key when the rule was installed from the rules library, otherwise null.""" + + matched: bool + """Always `true`: only matching runs are recorded.""" + + mode: Literal["ACTIVE", "TEST"] | str + """Mode of the rule when it ran. A `TEST` run records evidence but applied no actions.""" + + is_async: bool + """`true` when the run came from the asynchronous correlation pass rather than the ingest request.""" + + severity: Literal["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"] | str | None + """Severity of the rule. Null for `TEST` runs and for rules without a severity.""" + + score_delta: int + """How much this run changed the event's score (0 for `TEST` runs).""" + + action_summary: str | None + """Short summary of the actions applied, for example `score +40; add_tag:VPN_LOGIN`. Null when nothing was applied.""" + + matched_payload: list[dict[str, object]] + """The conditions and aggregations that matched. Condition entries carry `field`, `operator`, `expected_value`, `current_value`, `matched`, `group_index`, `group_logic` and `field_source` (`server`, `sdk` or `customer`); aggregation entries carry `metric`, `field`, `operator`, `expected_value`, `computed_value`, `window`, `filters`, `matched` and `sample_references`.""" + + created_at: _dt.datetime + """When the rule run was recorded.""" diff --git a/src/didit/types/components/network_detail_response.py b/src/didit/types/components/network_detail_response.py new file mode 100644 index 0000000..b264dbf --- /dev/null +++ b/src/didit/types/components/network_detail_response.py @@ -0,0 +1,106 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.network_outcomes import NetworkOutcomes +from didit.types.components.network_pattern_score import NetworkPatternScore +from didit.types.components.network_risk_display import NetworkRiskDisplay +from didit.types.components.network_tag import NetworkTag + +__all__ = ["NetworkDetailResponse"] + + +class NetworkDetailResponse(BaseModel): + """The network detail, plus the organization's sharing mode, whether cross-organization signals apply to it, your application's tags on the network, member outcomes and pattern scores.""" + + uuid: str | None = None + + network_id: str | None = None + """Human-readable per-organization sequential id, e.g. N-0001.""" + + name: str | None = None + + status: Literal["active", "in_review", "resolved", "dismissed"] | str | None = None + """Network lifecycle status. Distinct from the per-applicant outcome_status inside a network (approved/in_review/declined/not_completed).""" + + risk: NetworkRiskDisplay | None = None + + pattern_types: ( + list[ + Literal[ + "exact_same_device", + "similar_device", + "same_ip_address", + "same_address", + "similar_selfie_backgrounds", + "similar_poa_documents", + "same_document_number", + "same_phone_number", + "same_email", + ] + | str + ] + | None + ) = None + """The clustering patterns detected in this network.""" + + signal_types: ( + list[ + Literal["ip_address", "device", "face", "document_number", "phone", "email", "address"] + | str + ] + | None + ) = None + """The distinct signal types present in this network.""" + + member_counts: dict[str, object] | None = None + """Counts of members by category (e.g. total, per outcome_status).""" + + member_status: dict[str, object] | None = None + """Breakdown of members by outcome_status.""" + + signal_coverage: dict[str, object] | None = None + """Per signal_type coverage of this network's members.""" + + first_activity_at: _dt.datetime | None = None + + last_activity_at: _dt.datetime | None = None + + stats: dict[str, object] | None = None + """Aggregate stats for the network (e.g. member totals).""" + + fraud_tags: list[dict[str, object]] | None = None + """Fraud-pattern tags surfaced on the network.""" + + risk_signals: dict[str, object] | None = None + """Risk-signal summaries: residential_proxy_reuse, email_intelligence, phone_intelligence.""" + + reviewed_at: _dt.datetime | None = None + + reviewed_by: str | None = None + + review_reason: str | None = None + + notes_count: int | None = None + """Number of private analyst notes in this investigation.""" + + sharing_mode: Literal["off", "private", "global"] | str | None = None + """The organization's cross-organization sharing mode. Only `global` compares networks across organizations; `private` and `off` keep them to the organization's own history.""" + + cross_org_available: bool | None = None + """Whether cross-organization signals apply to this organization. They need both the Identity Enterprise plan and the `global` sharing mode.""" + + cross_org_unavailable_reason: Literal["plan", "sharing_mode"] | str | None = None + """Why cross-organization signals do not apply, checked in this order. `plan`: the organization's Identity plan does not include the cross-organization fraud network (it needs Identity Enterprise), whatever the sharing mode. `sharing_mode`: the plan includes it, but the sharing mode is `private` or `off`. Null when `cross_org_available` is `true`.""" + + tags: list[NetworkTag] | None = None + """Your application's tags on the network, oldest first. Each application sees only its own tags. `fraud_tags` is separate and unchanged: the tags the network's members carry.""" + + outcomes: NetworkOutcomes | None = None + """Where the network's people and businesses stand. Each member you may see counts once, by the status of their most recent verification that reached a decision. Transaction parties are not counted.""" + + patterns: list[NetworkPatternScore] | None = None + """Pattern scores over the whole network. Today one item, `background`.""" diff --git a/src/didit/types/components/network_list_counts.py b/src/didit/types/components/network_list_counts.py new file mode 100644 index 0000000..2bdadfd --- /dev/null +++ b/src/didit/types/components/network_list_counts.py @@ -0,0 +1,171 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from pydantic import Field + +from didit._models import BaseModel + +__all__ = [ + "NetworkListCounts", + "NetworkListCountsFacets", + "NetworkListCountsFacetsAssignee", + "NetworkListCountsFacetsAssigneeUsers", + "NetworkListCountsFacetsLastActivityDays", + "NetworkListCountsFacetsMinSize", + "NetworkListCountsFacetsPriority", + "NetworkListCountsFacetsRiskMin", + "NetworkListCountsFacetsSignalTypes", + "NetworkListCountsScopes", + "NetworkListCountsViews", +] + + +class NetworkListCountsFacetsAssigneeUsers(BaseModel): + uuid: str | None = None + + full_name: str | None = None + """The assignee's name, or their email when no name is set.""" + + email: str | None = None + + count: int | None = None + + +class NetworkListCountsFacetsAssignee(BaseModel): + """Networks by assignee.""" + + me: int | None = None + """Assigned to the calling console member. Always 0 for an API key.""" + + unassigned: int | None = None + """Assigned to nobody.""" + + users: list[NetworkListCountsFacetsAssigneeUsers] | None = None + """One entry per assignee, most networks first.""" + + +class NetworkListCountsFacetsLastActivityDays(BaseModel): + """Networks with activity in the last 7, 30 and 90 days, keyed by the number of days.""" + + v_7: int | None = Field(default=None, alias="7") + + v_30: int | None = Field(default=None, alias="30") + + v_90: int | None = Field(default=None, alias="90") + + +class NetworkListCountsFacetsMinSize(BaseModel): + """Networks with at least 10 members, keyed by the minimum size.""" + + v_10: int | None = Field(default=None, alias="10") + + +class NetworkListCountsFacetsPriority(BaseModel): + """Networks by priority. `none` counts networks without a priority.""" + + low: int | None = None + + medium: int | None = None + + high: int | None = None + + none: int | None = None + + +class NetworkListCountsFacetsRiskMin(BaseModel): + """Networks with a risk score of at least 50 and at least 80, keyed by the threshold.""" + + v_50: int | None = Field(default=None, alias="50") + + v_80: int | None = Field(default=None, alias="80") + + +class NetworkListCountsFacetsSignalTypes(BaseModel): + """Networks with at least one signal of each type.""" + + ip_address: int | None = None + + device: int | None = None + + face: int | None = None + + document_number: int | None = None + + phone: int | None = None + + email: int | None = None + + address: int | None = None + + +class NetworkListCountsFacets(BaseModel): + """Counts for each filter option, inside the selected `scope` and status view.""" + + total: int | None = None + """Networks in the selected `scope` and status view.""" + + last_activity_days: NetworkListCountsFacetsLastActivityDays | None = None + """Networks with activity in the last 7, 30 and 90 days, keyed by the number of days.""" + + risk_min: NetworkListCountsFacetsRiskMin | None = None + """Networks with a risk score of at least 50 and at least 80, keyed by the threshold.""" + + signal_types: NetworkListCountsFacetsSignalTypes | None = None + """Networks with at least one signal of each type.""" + + priority: NetworkListCountsFacetsPriority | None = None + """Networks by priority. `none` counts networks without a priority.""" + + assignee: NetworkListCountsFacetsAssignee | None = None + """Networks by assignee.""" + + has_case: int | None = None + """Networks this application opened a case for.""" + + min_size: NetworkListCountsFacetsMinSize | None = None + """Networks with at least 10 members, keyed by the minimum size.""" + + +class NetworkListCountsScopes(BaseModel): + """Visible networks by product.""" + + all: int | None = None + + identity: int | None = None + """Every other network.""" + + transactions: int | None = None + """Networks whose members are all transaction parties.""" + + +class NetworkListCountsViews(BaseModel): + """Visible networks inside the selected `scope`, by status, plus two saved views.""" + + all: int | None = None + + active: int | None = None + + in_review: int | None = None + + resolved: int | None = None + + dismissed: int | None = None + + mine: int | None = None + """Networks assigned to the calling console member. Always 0 for an API key.""" + + high_risk: int | None = None + """Networks with a risk score of 80 or more that are not dismissed.""" + + +class NetworkListCounts(BaseModel): + """Network counts for the list's views and filter options, each network counted once. Returned only when `summary` is not `false`. `scopes` count every network visible to the application. `views` count the visible networks inside the selected `scope`. `facets` count the networks in the selected `scope` and `status` view (dismissed networks are left out unless `status` includes them). The other list filters do not change these counts.""" + + scopes: NetworkListCountsScopes | None = None + """Visible networks by product.""" + + views: NetworkListCountsViews | None = None + """Visible networks inside the selected `scope`, by status, plus two saved views.""" + + facets: NetworkListCountsFacets | None = None + """Counts for each filter option, inside the selected `scope` and status view.""" diff --git a/src/didit/types/components/network_outcomes.py b/src/didit/types/components/network_outcomes.py new file mode 100644 index 0000000..c9042c3 --- /dev/null +++ b/src/didit/types/components/network_outcomes.py @@ -0,0 +1,22 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["NetworkOutcomes"] + + +class NetworkOutcomes(BaseModel): + """Where the network's people and businesses stand. Each member you may see counts once, by the status of their most recent verification that reached a decision. Transaction parties are not counted.""" + + approved: int | None = None + """Members whose most recent decided verification was approved.""" + + declined: int | None = None + """Members whose most recent decided verification was declined.""" + + in_review: int | None = None + """Members whose most recent decided verification is in review.""" + + total: int | None = None + """Members with at least one verification, decided or not. A member whose verifications are all undecided (not started, in progress, expired or abandoned) counts here and in no other key.""" diff --git a/src/didit/types/components/network_pattern_score.py b/src/didit/types/components/network_pattern_score.py new file mode 100644 index 0000000..332b515 --- /dev/null +++ b/src/didit/types/components/network_pattern_score.py @@ -0,0 +1,28 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["NetworkPatternScore"] + + +class NetworkPatternScore(BaseModel): + """How alike one kind of the members' data is across the whole network. A pattern is not a signal: no shared value links the members.""" + + type: Literal["background"] | str | None = None + """`background`: the mean similarity of every pair of member selfie backgrounds, with the person masked out. It reads the newest selfie of each person, at most the 50 newest, and leaves out age assurance sessions. It covers the whole network, not only the members of your applications.""" + + score: float | None = None + """From 0 to 1, rounded to 4 decimals. Null when `reason` is set.""" + + members: int | None = None + """How many members' selfies the score compared. A selfie that cannot be read, or shows too little background, is not compared. Null while `computing`; 0 or 1 with `not_enough_selfies`.""" + + reason: Literal["computing", "not_enough_selfies"] | str | None = None + """Why there is no score. `computing`: the score for the current members is being computed; read the network again later. `not_enough_selfies`: fewer than two members have a usable selfie. Null when there is a score.""" + + computed_at: _dt.datetime | None = None + """When the score was computed. Null while `computing`.""" diff --git a/src/didit/types/components/network_tag.py b/src/didit/types/components/network_tag.py new file mode 100644 index 0000000..343b0e5 --- /dev/null +++ b/src/didit/types/components/network_tag.py @@ -0,0 +1,29 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["NetworkTag"] + + +class NetworkTag(BaseModel): + """One of your application's tags on the network.""" + + uuid: str | None = None + """The tag's UUID, the same value you pass as `tag_uuid`.""" + + name: str | None = None + + color: str | None = None + """Hex color, for example `#6366F1`.""" + + source: Literal["didit", "custom"] | str | None = None + """`didit` for a Didit preset tag, `custom` for a tag your team created.""" + + added_by: str | None = None + """Email of the console member who added the tag. Null when an API key added it.""" + + added_at: _dt.datetime | None = None diff --git a/src/didit/types/networks/retrieve_timeline_response.py b/src/didit/types/components/network_timeline.py similarity index 64% rename from src/didit/types/networks/retrieve_timeline_response.py rename to src/didit/types/components/network_timeline.py index 6fbf7c5..6d4ca71 100644 --- a/src/didit/types/networks/retrieve_timeline_response.py +++ b/src/didit/types/components/network_timeline.py @@ -4,8 +4,10 @@ from didit._models import BaseModel from didit.types.components.network_timeline_event import NetworkTimelineEvent -__all__ = ["NetworksRetrieveTimelineResponse"] +__all__ = ["NetworkTimeline"] -class NetworksRetrieveTimelineResponse(BaseModel): +class NetworkTimeline(BaseModel): + """Without `contract`: the network's own events, oldest first, not paginated.""" + results: list[NetworkTimelineEvent] | None = None diff --git a/src/didit/types/components/network_timeline_account.py b/src/didit/types/components/network_timeline_account.py new file mode 100644 index 0000000..7e28e61 --- /dev/null +++ b/src/didit/types/components/network_timeline_account.py @@ -0,0 +1,30 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.network_timeline_route import NetworkTimelineRoute + +__all__ = ["NetworkTimelineAccount"] + + +class NetworkTimelineAccount(BaseModel): + """The member account the event belongs to.""" + + node_id: str | None = None + """`:`, where kind is `vendor_user`, `vendor_business` or `transaction_party`.""" + + node_type: Literal["account", "business_account", "transaction_party"] | str | None = None + + uuid: str | None = None + + vendor_data: str | None = None + + display_name: str | None = None + + ownership: Literal["owned", "organization"] | str | None = None + """`owned` when the record belongs to the application you called (for a console member, to an application they were granted). `organization` when it belongs to another application of your organization.""" + + route: NetworkTimelineRoute | None = None + """Where the record opens: its kind, its UUID and the application it belongs to.""" diff --git a/src/didit/types/components/network_timeline_amount.py b/src/didit/types/components/network_timeline_amount.py new file mode 100644 index 0000000..64eacad --- /dev/null +++ b/src/didit/types/components/network_timeline_amount.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["NetworkTimelineAmount"] + + +class NetworkTimelineAmount(BaseModel): + value: str | None = None + """Decimal amount as a string.""" + + currency: str | None = None + + direction: str | None = None diff --git a/src/didit/types/components/network_timeline_attempt.py b/src/didit/types/components/network_timeline_attempt.py new file mode 100644 index 0000000..e0796ec --- /dev/null +++ b/src/didit/types/components/network_timeline_attempt.py @@ -0,0 +1,32 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.network_timeline_decline_reason import NetworkTimelineDeclineReason + +__all__ = ["NetworkTimelineAttempt"] + + +class NetworkTimelineAttempt(BaseModel): + """One verification session of the account.""" + + session_id: str | None = None + """The session UUID, or the business session UUID for a company.""" + + session_number: int | None = None + + attempt: int | None = None + """This session's position among the account's sessions, in creation order.""" + + attempt_count: int | None = None + """How many sessions the account has.""" + + status: str | None = None + """The session status, for example `Approved` or `Declined`.""" + + status_changed_at: _dt.datetime | None = None + + decline_reasons: list[NetworkTimelineDeclineReason] | None = None + """For a declined session only: its warnings, oldest first, once per warning. Empty otherwise.""" diff --git a/src/didit/types/components/network_timeline_case_linked.py b/src/didit/types/components/network_timeline_case_linked.py new file mode 100644 index 0000000..bc9ec86 --- /dev/null +++ b/src/didit/types/components/network_timeline_case_linked.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["NetworkTimelineCaseLinked"] + + +class NetworkTimelineCaseLinked(BaseModel): + """A case opened for the network in your organization. `source_ref.ownership` says whether it belongs to the application you called.""" + + case_id: str | None = None + + case_number: str | None = None + + status: str | None = None + + author_email: str | None = None + + author_name: str | None = None diff --git a/src/didit/types/components/network_timeline_decline_reason.py b/src/didit/types/components/network_timeline_decline_reason.py new file mode 100644 index 0000000..1eff1ac --- /dev/null +++ b/src/didit/types/components/network_timeline_decline_reason.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["NetworkTimelineDeclineReason"] + + +class NetworkTimelineDeclineReason(BaseModel): + risk: str | None = None + """The warning code.""" + + feature: str | None = None + """The feature that raised the warning.""" + + short_description: str | None = None diff --git a/src/didit/types/components/network_timeline_device.py b/src/didit/types/components/network_timeline_device.py new file mode 100644 index 0000000..8ba5dda --- /dev/null +++ b/src/didit/types/components/network_timeline_device.py @@ -0,0 +1,13 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["NetworkTimelineDevice"] + + +class NetworkTimelineDevice(BaseModel): + fingerprint: str | None = None + + label: str | None = None + """A readable device label built from the event's device context.""" diff --git a/src/didit/types/components/network_timeline_ip.py b/src/didit/types/components/network_timeline_ip.py new file mode 100644 index 0000000..adb44c3 --- /dev/null +++ b/src/didit/types/components/network_timeline_ip.py @@ -0,0 +1,21 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["NetworkTimelineIP"] + + +class NetworkTimelineIP(BaseModel): + address: str | None = None + + country_code: str | None = None + + city: str | None = None + + connection_type: str | None = None + """The connection type label for the address, as free text.""" + + is_vpn_or_tor: bool | None = None + + is_data_center: bool | None = None diff --git a/src/didit/types/components/network_timeline_login.py b/src/didit/types/components/network_timeline_login.py new file mode 100644 index 0000000..e7ad03e --- /dev/null +++ b/src/didit/types/components/network_timeline_login.py @@ -0,0 +1,32 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.network_timeline_device import NetworkTimelineDevice +from didit.types.components.network_timeline_ip import NetworkTimelineIP + +__all__ = ["NetworkTimelineLogin"] + + +class NetworkTimelineLogin(BaseModel): + """A sign-in event you posted for a person through the events API.""" + + action: str | None = None + """`.`: `auth.login` or `security.login_succeeded` for a sign-in, `auth.login_failed` or `security.login_failed` for a failed one.""" + + succeeded: bool | None = None + + status: str | None = None + """The event's status.""" + + ip: NetworkTimelineIP | None = None + """Null when the event has no IP address.""" + + device: NetworkTimelineDevice | None = None + """Null when the event has no device fingerprint or context.""" + + is_first_login: bool | None = None + """The account's first successful sign-in on record.""" + + is_new_device: bool | None = None + """A successful sign-in from a device none of the account's earlier successful sign-ins came from. Never true on the first sign-in.""" diff --git a/src/didit/types/components/network_timeline_note_added.py b/src/didit/types/components/network_timeline_note_added.py new file mode 100644 index 0000000..b122a19 --- /dev/null +++ b/src/didit/types/components/network_timeline_note_added.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["NetworkTimelineNoteAdded"] + + +class NetworkTimelineNoteAdded(BaseModel): + """A note left on the network. The note text is not included.""" + + note_id: str | None = None + + author_email: str | None = None diff --git a/src/didit/types/components/network_timeline_page.py b/src/didit/types/components/network_timeline_page.py new file mode 100644 index 0000000..2e33b80 --- /dev/null +++ b/src/didit/types/components/network_timeline_page.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.network_typed_timeline_event import NetworkTypedTimelineEvent + +__all__ = ["NetworkTimelinePage"] + + +class NetworkTimelinePage(BaseModel): + """With `contract=2`: one page of typed events, newest first.""" + + results: list[NetworkTypedTimelineEvent] | None = None + + next_cursor: str | None = None + """Pass as `cursor` to read the next page. Null on the last page.""" diff --git a/src/didit/types/components/network_timeline_route.py b/src/didit/types/components/network_timeline_route.py new file mode 100644 index 0000000..030bfc9 --- /dev/null +++ b/src/didit/types/components/network_timeline_route.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["NetworkTimelineRoute"] + + +class NetworkTimelineRoute(BaseModel): + """Where the record opens: its kind, its UUID and the application it belongs to.""" + + kind: str | None = None + """`user`, `business` or `transaction_party` for an account; `session`, `business_session` or `transaction` for a source record.""" + + id: str | None = None + + application_id: str | None = None diff --git a/src/didit/types/components/network_timeline_rule_hit.py b/src/didit/types/components/network_timeline_rule_hit.py new file mode 100644 index 0000000..813b5a7 --- /dev/null +++ b/src/didit/types/components/network_timeline_rule_hit.py @@ -0,0 +1,26 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["NetworkTimelineRuleHit"] + + +class NetworkTimelineRuleHit(BaseModel): + """A live rule that matched a member's transaction or event. Test-mode matches are not included.""" + + rule_id: str | None = None + + rule_title: str | None = None + + rule_category: str | None = None + + rule_source: str | None = None + + severity: str | None = None + + score_delta: int | None = None + + status_target: str | None = None + + action_summary: str | None = None diff --git a/src/didit/types/components/network_timeline_source_ref.py b/src/didit/types/components/network_timeline_source_ref.py new file mode 100644 index 0000000..e5ebf54 --- /dev/null +++ b/src/didit/types/components/network_timeline_source_ref.py @@ -0,0 +1,27 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.network_timeline_route import NetworkTimelineRoute + +__all__ = ["NetworkTimelineSourceRef"] + + +class NetworkTimelineSourceRef(BaseModel): + """The record the event was read from.""" + + kind: ( + Literal["session", "business_session", "transaction", "monitoring_event", "case"] + | str + | None + ) = None + + id: str | None = None + + ownership: Literal["owned", "organization"] | str | None = None + """`owned` when the record belongs to the application you called (for a console member, to an application they were granted). `organization` when it belongs to another application of your organization.""" + + route: NetworkTimelineRoute | None = None + """Absent for `monitoring_event` and `case`.""" diff --git a/src/didit/types/components/network_timeline_transaction.py b/src/didit/types/components/network_timeline_transaction.py new file mode 100644 index 0000000..5d10afb --- /dev/null +++ b/src/didit/types/components/network_timeline_transaction.py @@ -0,0 +1,31 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.network_timeline_amount import NetworkTimelineAmount + +__all__ = ["NetworkTimelineTransaction"] + + +class NetworkTimelineTransaction(BaseModel): + """A transaction a member account took part in, dated by its transaction date.""" + + transaction_id: str | None = None + + transaction_number: int | None = None + + transaction_type: str | None = None + + action_type: str | None = None + + amount: NetworkTimelineAmount | None = None + """Null when the transaction has no amount.""" + + status: str | None = None + + decision_reason_code: str | None = None + + decision_reason_label: str | None = None + + party_role: str | None = None + """The account's role in the transaction: `APPLICANT`, `REMITTER`, `BENEFICIARY` or `COUNTERPARTY`.""" diff --git a/src/didit/types/components/network_typed_timeline_event.py b/src/didit/types/components/network_typed_timeline_event.py new file mode 100644 index 0000000..2c5fd25 --- /dev/null +++ b/src/didit/types/components/network_typed_timeline_event.py @@ -0,0 +1,79 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.network_timeline_account import NetworkTimelineAccount +from didit.types.components.network_timeline_attempt import NetworkTimelineAttempt +from didit.types.components.network_timeline_case_linked import NetworkTimelineCaseLinked +from didit.types.components.network_timeline_login import NetworkTimelineLogin +from didit.types.components.network_timeline_note_added import NetworkTimelineNoteAdded +from didit.types.components.network_timeline_rule_hit import NetworkTimelineRuleHit +from didit.types.components.network_timeline_source_ref import NetworkTimelineSourceRef +from didit.types.components.network_timeline_transaction import NetworkTimelineTransaction + +__all__ = ["NetworkTypedTimelineEvent"] + + +class NetworkTypedTimelineEvent(BaseModel): + """One typed timeline event: the four default fields, plus its id, when Didit stored it, the member account and record it belongs to, and the detail block of its event type. The other detail blocks are null.""" + + id: str | None = None + """`:`, unique within the timeline.""" + + event_type: ( + Literal[ + "network_detected", + "signal_observed", + "network_active", + "network_in_review", + "network_resolved", + "network_dismissed", + "verification_attempt", + "transaction", + "login", + "rule_hit", + "case_linked", + "note_added", + ] + | str + | None + ) = None + + occurred_at: _dt.datetime | None = None + """When it happened.""" + + ingested_at: _dt.datetime | None = None + """When Didit stored it.""" + + label: str | None = None + """A short English label, for example `Verification attempt 2 of 3`.""" + + metadata: dict[str, object] | None = None + """For the network's own events, the same metadata as the default timeline. Empty otherwise.""" + + account: NetworkTimelineAccount | None = None + """The member account. Null for the network's own events, `case_linked` and `note_added`.""" + + source_ref: NetworkTimelineSourceRef | None = None + """The source record. Null for the network's own events and `note_added`.""" + + verification_attempt: NetworkTimelineAttempt | None = None + """Set on `verification_attempt` events.""" + + transaction: NetworkTimelineTransaction | None = None + """Set on `transaction` events.""" + + login: NetworkTimelineLogin | None = None + """Set on `login` events.""" + + rule_hit: NetworkTimelineRuleHit | None = None + """Set on `rule_hit` events.""" + + case_linked: NetworkTimelineCaseLinked | None = None + """Set on `case_linked` events.""" + + note_added: NetworkTimelineNoteAdded | None = None + """Set on `note_added` events. Never returned to an API key.""" diff --git a/src/didit/types/users/retrieve_ongoing_monitoring_job_response.py b/src/didit/types/components/ongoing_monitoring_job.py similarity index 81% rename from src/didit/types/users/retrieve_ongoing_monitoring_job_response.py rename to src/didit/types/components/ongoing_monitoring_job.py index 8b5d4b3..990c056 100644 --- a/src/didit/types/users/retrieve_ongoing_monitoring_job_response.py +++ b/src/didit/types/components/ongoing_monitoring_job.py @@ -5,14 +5,10 @@ from didit._models import BaseModel -__all__ = [ - "UsersRetrieveOngoingMonitoringJobResponse", - "UsersRetrieveOngoingMonitoringJobResponseEstimate", - "UsersRetrieveOngoingMonitoringJobResponseOutcomes", -] +__all__ = ["OngoingMonitoringJob", "OngoingMonitoringJobEstimate", "OngoingMonitoringJobOutcomes"] -class UsersRetrieveOngoingMonitoringJobResponseEstimate(BaseModel): +class OngoingMonitoringJobEstimate(BaseModel): """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" users_total: int | None = None @@ -37,7 +33,7 @@ class UsersRetrieveOngoingMonitoringJobResponseEstimate(BaseModel): balance_sufficient: bool | None = None -class UsersRetrieveOngoingMonitoringJobResponseOutcomes(BaseModel): +class OngoingMonitoringJobOutcomes(BaseModel): """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" enabled: int | None = None @@ -68,7 +64,7 @@ class UsersRetrieveOngoingMonitoringJobResponseOutcomes(BaseModel): """Balance ran out mid-job.""" -class UsersRetrieveOngoingMonitoringJobResponse(BaseModel): +class OngoingMonitoringJob(BaseModel): job_id: str | None = None status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None @@ -79,10 +75,10 @@ class UsersRetrieveOngoingMonitoringJobResponse(BaseModel): processed: int | None = None - outcomes: UsersRetrieveOngoingMonitoringJobResponseOutcomes | None = None + outcomes: OngoingMonitoringJobOutcomes | None = None """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" error: str | None = None - estimate: UsersRetrieveOngoingMonitoringJobResponseEstimate | None = None + estimate: OngoingMonitoringJobEstimate | None = None """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" diff --git a/src/didit/types/components/ongoing_monitoring_toggle_result.py b/src/didit/types/components/ongoing_monitoring_toggle_result.py new file mode 100644 index 0000000..c94dedd --- /dev/null +++ b/src/didit/types/components/ongoing_monitoring_toggle_result.py @@ -0,0 +1,39 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import TypeAlias + +from didit._models import BaseModel +from didit.types.components.ongoing_monitoring_job import OngoingMonitoringJob + +__all__ = ["OngoingMonitoringToggleResult", "OngoingMonitoringToggleResultVariant1"] + + +class OngoingMonitoringToggleResultVariant1(BaseModel): + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" + + users_total: int | None = None + """Entities in the selection.""" + + already_enabled: int | None = None + + with_existing_aml: int | None = None + """Entities whose existing AML screening will be switched to monitored.""" + + needing_screening: int | None = None + """Entities without any AML screening that will be screened first (billed).""" + + skipped_no_identity_data: int | None = None + + skipped_blocked: int | None = None + + estimated_screening_cost_usd: float | None = None + + estimated_annual_monitoring_cost_usd: float | None = None + + balance_sufficient: bool | None = None + + +OngoingMonitoringToggleResult: TypeAlias = ( + OngoingMonitoringToggleResultVariant1 | OngoingMonitoringJob +) diff --git a/src/didit/types/components/paginated_application_list.py b/src/didit/types/components/paginated_application_list.py new file mode 100644 index 0000000..6dab8f5 --- /dev/null +++ b/src/didit/types/components/paginated_application_list.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.application import Application + +__all__ = ["PaginatedApplicationList"] + + +class PaginatedApplicationList(BaseModel): + count: int + """Total number of applications.""" + + next: str | None + """URL of the next page, or `null` on the last page.""" + + previous: str | None + """URL of the previous page, or `null` on the first page.""" + + results: list[Application] diff --git a/src/didit/types/components/paginated_organization_list.py b/src/didit/types/components/paginated_organization_list.py new file mode 100644 index 0000000..add866f --- /dev/null +++ b/src/didit/types/components/paginated_organization_list.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.organization import Organization + +__all__ = ["PaginatedOrganizationList"] + + +class PaginatedOrganizationList(BaseModel): + count: int + """Total number of organizations.""" + + next: str | None + """URL of the next page, or `null` on the last page.""" + + previous: str | None + """URL of the previous page, or `null` on the first page.""" + + results: list[Organization] diff --git a/src/didit/types/regulatory_reports/list_response.py b/src/didit/types/components/regulatory_report_list.py similarity index 82% rename from src/didit/types/regulatory_reports/list_response.py rename to src/didit/types/components/regulatory_report_list.py index c6e6585..b9c19ca 100644 --- a/src/didit/types/regulatory_reports/list_response.py +++ b/src/didit/types/components/regulatory_report_list.py @@ -4,10 +4,10 @@ from didit._models import BaseModel from didit.types.components.regulatory_report_detail import RegulatoryReportDetail -__all__ = ["RegulatoryReportsListResponse"] +__all__ = ["RegulatoryReportList"] -class RegulatoryReportsListResponse(BaseModel): +class RegulatoryReportList(BaseModel): count: int | None = None """Number of matching rows, capped at 100 for performance.""" diff --git a/src/didit/types/components/review_item.py b/src/didit/types/components/review_item.py index a98352e..348704f 100644 --- a/src/didit/types/components/review_item.py +++ b/src/didit/types/components/review_item.py @@ -30,11 +30,14 @@ class ReviewItem(BaseModel): "KYB_DOCUMENT_UPLOADED", "TAG_ADDED", "TAG_REMOVED", + "FEATURE_OVERRIDE_PENDING", + "FEATURE_OVERRIDE_APPLIED", + "FEATURE_OVERRIDE_CANCELLED", ] | str | None ) = None - """What kind of activity this entry records. `STATUS_UPDATED` rows come from manual decisions (`PATCH /v3/session/{session_id}/update-status/`) and from entries added via `POST …/reviews/` (which default to this type even when no status is recorded). `COMMENT` rows come from Console reviewers; Console detail-page views are also stored as `COMMENT` rows flagged with `metadata.system_event = "DETAIL_VIEWED"`. The remaining types track data edits, file changes, tags, and AML monitoring activity. STATUS_UPDATED rows are also written by system actors (e.g. AML ongoing monitoring with actor_type SYSTEM, and automatic feature-status updates), not only by the update-status endpoint and manual review posts.""" + """What kind of activity this entry records. `STATUS_UPDATED` rows come from manual decisions (`PATCH /v3/session/{session_id}/update-status/`) and from entries added via `POST …/reviews/` (which default to this type even when no status is recorded). `COMMENT` rows come from Console reviewers; Console detail-page views are also stored as `COMMENT` rows flagged with `metadata.system_event = "DETAIL_VIEWED"`. The remaining types track data edits, file changes, tags, and AML monitoring activity. STATUS_UPDATED rows are also written by system actors (e.g. AML ongoing monitoring with actor_type SYSTEM, and automatic feature-status updates), not only by the update-status endpoint and manual review posts. `FEATURE_OVERRIDE_PENDING`, `FEATURE_OVERRIDE_APPLIED` and `FEATURE_OVERRIDE_CANCELLED` record an override of one check's status (`PATCH /v3/session/{session_id}/features/{node_id}/update-status/`): requested to wait for the flow to finish, applied, or cancelled. They carry the check's status in `previous_value` / `new_value` (`{"status": ...}`), leave `previous_status` and `new_status` `null`, and name the override in `metadata` (`feature_override_id`, `node_id`, `feature_model`, `forced`, `state`, `cancel_reason`). The entry for the request carries the reviewer's `comment`; an override applied when the flow finishes is recorded with `actor_type` `SYSTEM`.""" actor_display: str | None = None """Human-readable author label, resolved in order: actor's full name → Console account identifier → local part of `actor_email` → `"API Client"` (for `API_KEY` actors) → `"System"` (for `SYSTEM` actors) → `"Unknown"`. Entries created via `POST …/reviews/` always display `"Unknown"` because that endpoint records no actor attribution.""" diff --git a/src/didit/types/components/sandbox_simulate_response.py b/src/didit/types/components/sandbox_simulate_response.py new file mode 100644 index 0000000..ea96c31 --- /dev/null +++ b/src/didit/types/components/sandbox_simulate_response.py @@ -0,0 +1,41 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SandboxSimulateResponse"] + + +class SandboxSimulateResponse(BaseModel): + session_id: str + + previous_status: ( + Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + ) + """The session's status before the simulation.""" + + status: Literal["Approved", "Declined", "In Review", "Expired", "Abandoned"] | str + """The status the simulation set.""" + + scenario: str | None + """The scenario slug you passed, or `null`.""" + + risk: str | None + """The risk case applied (`risk`, or the one taken from the scenario), or `null` when none.""" + + send_webhooks: bool + """The `send_webhooks` value the simulation applied.""" diff --git a/src/didit/types/sessions/retrieve_decision_response.py b/src/didit/types/components/session_decision.py similarity index 73% rename from src/didit/types/sessions/retrieve_decision_response.py rename to src/didit/types/components/session_decision.py index 86de39e..8b5cb77 100644 --- a/src/didit/types/sessions/retrieve_decision_response.py +++ b/src/didit/types/components/session_decision.py @@ -5,669 +5,138 @@ from typing import Literal from didit._models import BaseModel +from didit.types.components.aml_screening import AMLScreening from didit.types.components.document_ai_detected_code import DocumentAIDetectedCode +from didit.types.components.investigator_block import InvestigatorBlock from didit.types.components.kyb_registry_address import KYBRegistryAddress from didit.types.components.kyb_registry_industry import KYBRegistryIndustry from didit.types.components.kyb_registry_ownership import KYBRegistryOwnership from didit.types.components.kyb_registry_shareholding import KYBRegistryShareholding +from didit.types.components.signature_result import SignatureResult __all__ = [ - "SessionsRetrieveDecisionResponse", - "SessionsRetrieveDecisionResponseAmlScreenings", - "SessionsRetrieveDecisionResponseAmlScreeningsHits", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaDetails", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaMatches", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsLinkedEntities", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsPepMatches", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsProperties", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskView", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCategories", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCountries", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCrimes", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsSanctionMatches", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsScoreBreakdown", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatches", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalData", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataData", - "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataDataCaseDetails", - "SessionsRetrieveDecisionResponseAmlScreeningsScreenedData", - "SessionsRetrieveDecisionResponseAmlScreeningsWarnings", - "SessionsRetrieveDecisionResponseContactDetails", - "SessionsRetrieveDecisionResponseDatabaseValidations", - "SessionsRetrieveDecisionResponseDatabaseValidationsErrors", - "SessionsRetrieveDecisionResponseDatabaseValidationsScreenedData", - "SessionsRetrieveDecisionResponseDatabaseValidationsValidations", - "SessionsRetrieveDecisionResponseDatabaseValidationsValidationsValidation", - "SessionsRetrieveDecisionResponseDatabaseValidationsWarnings", - "SessionsRetrieveDecisionResponseDocumentAiDocuments", - "SessionsRetrieveDecisionResponseDocumentAiDocumentsItems", - "SessionsRetrieveDecisionResponseDocumentAiDocumentsItemsFields", - "SessionsRetrieveDecisionResponseDocumentVerifications", - "SessionsRetrieveDecisionResponseDocumentVerificationsGroupsValue", - "SessionsRetrieveDecisionResponseDocumentVerificationsItems", - "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata", - "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulation", - "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions", - "SessionsRetrieveDecisionResponseEmailVerifications", - "SessionsRetrieveDecisionResponseEmailVerificationsBreaches", - "SessionsRetrieveDecisionResponseEmailVerificationsLifecycle", - "SessionsRetrieveDecisionResponseEmailVerificationsLifecycleDetails", - "SessionsRetrieveDecisionResponseEmailVerificationsMatches", - "SessionsRetrieveDecisionResponseEmailVerificationsWarnings", - "SessionsRetrieveDecisionResponseExpectedDetails", - "SessionsRetrieveDecisionResponseFaceMatches", - "SessionsRetrieveDecisionResponseFaceMatchesWarnings", - "SessionsRetrieveDecisionResponseIdVerifications", - "SessionsRetrieveDecisionResponseIdVerificationsBackImageQualityScore", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentLiveness", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPortraitReplace", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPrintedCopy", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessScreenReplay", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatches", - "SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatchesUserDetails", - "SessionsRetrieveDecisionResponseIdVerificationsExtraFields", - "SessionsRetrieveDecisionResponseIdVerificationsFallbackFrom", - "SessionsRetrieveDecisionResponseIdVerificationsFrontImageQualityScore", - "SessionsRetrieveDecisionResponseIdVerificationsIdLookup", - "SessionsRetrieveDecisionResponseIdVerificationsIdLookupComparison", - "SessionsRetrieveDecisionResponseIdVerificationsMatches", - "SessionsRetrieveDecisionResponseIdVerificationsMatchesUserDetails", - "SessionsRetrieveDecisionResponseIdVerificationsMrz", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddress", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressDocumentLocation", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResults", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsAddressComponents", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometry", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryLocation", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewport", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportNortheast", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPoints", - "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPointsLocation", - "SessionsRetrieveDecisionResponseIdVerificationsWalletVerification", - "SessionsRetrieveDecisionResponseIdVerificationsWarnings", - "SessionsRetrieveDecisionResponseIpAnalyses", - "SessionsRetrieveDecisionResponseIpAnalysesIdDocument", - "SessionsRetrieveDecisionResponseIpAnalysesIdDocumentLocation", - "SessionsRetrieveDecisionResponseIpAnalysesIp", - "SessionsRetrieveDecisionResponseIpAnalysesIpLocation", - "SessionsRetrieveDecisionResponseIpAnalysesMatches", - "SessionsRetrieveDecisionResponseIpAnalysesMatchesDeviceInfo", - "SessionsRetrieveDecisionResponseIpAnalysesMatchesLocationInfo", - "SessionsRetrieveDecisionResponseIpAnalysesPoaDocument", - "SessionsRetrieveDecisionResponseIpAnalysesPoaDocumentLocation", - "SessionsRetrieveDecisionResponseIpAnalysesWarnings", - "SessionsRetrieveDecisionResponseKeyPeopleChecks", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistry", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersKycAmlCategories", - "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures", - "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmitted", - "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties", - "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesRoles", - "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures", - "SessionsRetrieveDecisionResponseKeyPeopleChecksUboKycSummary", - "SessionsRetrieveDecisionResponseLivenessChecks", - "SessionsRetrieveDecisionResponseLivenessChecksMatches", - "SessionsRetrieveDecisionResponseLivenessChecksMatchesUserDetails", - "SessionsRetrieveDecisionResponseLivenessChecksWarnings", - "SessionsRetrieveDecisionResponseNfcVerifications", - "SessionsRetrieveDecisionResponseNfcVerificationsAuthenticity", - "SessionsRetrieveDecisionResponseNfcVerificationsCertificateSummary", - "SessionsRetrieveDecisionResponseNfcVerificationsChipData", - "SessionsRetrieveDecisionResponseNfcVerificationsWarnings", - "SessionsRetrieveDecisionResponsePhoneVerifications", - "SessionsRetrieveDecisionResponsePhoneVerificationsCarrier", - "SessionsRetrieveDecisionResponsePhoneVerificationsLifecycle", - "SessionsRetrieveDecisionResponsePhoneVerificationsLifecycleDetails", - "SessionsRetrieveDecisionResponsePhoneVerificationsMatches", - "SessionsRetrieveDecisionResponsePhoneVerificationsWarnings", - "SessionsRetrieveDecisionResponsePoaVerifications", - "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadata", - "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulation", - "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions", - "SessionsRetrieveDecisionResponsePoaVerificationsExtraFields", - "SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddress", - "SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddressDocumentLocation", - "SessionsRetrieveDecisionResponsePoaVerificationsWarnings", - "SessionsRetrieveDecisionResponseQuestionnaireResponses", - "SessionsRetrieveDecisionResponseQuestionnaireResponsesSections", - "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItems", - "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsAnswer", - "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsChoices", - "SessionsRetrieveDecisionResponseRegistryChecks", - "SessionsRetrieveDecisionResponseRegistryChecksCompany", - "SessionsRetrieveDecisionResponseRegistryChecksCompanyBeneficialOwners", - "SessionsRetrieveDecisionResponseRegistryChecksCompanyOfficers", - "SessionsRetrieveDecisionResponseResubmissions", - "SessionsRetrieveDecisionResponseResubmissionsNodes", - "SessionsRetrieveDecisionResponseResubmissionsRequestedBy", - "SessionsRetrieveDecisionResponseReviews", + "SessionDecision", + "SessionDecisionContactDetails", + "SessionDecisionDatabaseValidations", + "SessionDecisionDatabaseValidationsErrors", + "SessionDecisionDatabaseValidationsScreenedData", + "SessionDecisionDatabaseValidationsValidations", + "SessionDecisionDatabaseValidationsValidationsValidation", + "SessionDecisionDatabaseValidationsWarnings", + "SessionDecisionDocumentAiDocuments", + "SessionDecisionDocumentAiDocumentsItems", + "SessionDecisionDocumentAiDocumentsItemsFields", + "SessionDecisionDocumentVerifications", + "SessionDecisionDocumentVerificationsGroupsValue", + "SessionDecisionDocumentVerificationsItems", + "SessionDecisionDocumentVerificationsItemsDocumentMetadata", + "SessionDecisionDocumentVerificationsItemsDocumentMetadataOverlayManipulation", + "SessionDecisionDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions", + "SessionDecisionEmailVerifications", + "SessionDecisionEmailVerificationsBreaches", + "SessionDecisionEmailVerificationsLifecycle", + "SessionDecisionEmailVerificationsLifecycleDetails", + "SessionDecisionEmailVerificationsMatches", + "SessionDecisionEmailVerificationsWarnings", + "SessionDecisionExpectedDetails", + "SessionDecisionFaceMatches", + "SessionDecisionFaceMatchesWarnings", + "SessionDecisionIdVerifications", + "SessionDecisionIdVerificationsBackImageQualityScore", + "SessionDecisionIdVerificationsDocumentLiveness", + "SessionDecisionIdVerificationsDocumentLivenessPortraitReplace", + "SessionDecisionIdVerificationsDocumentLivenessPrintedCopy", + "SessionDecisionIdVerificationsDocumentLivenessScreenReplay", + "SessionDecisionIdVerificationsDocumentSelfieFaceMatches", + "SessionDecisionIdVerificationsDocumentSelfieFaceMatchesUserDetails", + "SessionDecisionIdVerificationsExtraFields", + "SessionDecisionIdVerificationsFallbackFrom", + "SessionDecisionIdVerificationsFrontImageQualityScore", + "SessionDecisionIdVerificationsIdLookup", + "SessionDecisionIdVerificationsIdLookupComparison", + "SessionDecisionIdVerificationsMatches", + "SessionDecisionIdVerificationsMatchesUserDetails", + "SessionDecisionIdVerificationsMrz", + "SessionDecisionIdVerificationsParsedAddress", + "SessionDecisionIdVerificationsParsedAddressDocumentLocation", + "SessionDecisionIdVerificationsParsedAddressRawResults", + "SessionDecisionIdVerificationsParsedAddressRawResultsAddressComponents", + "SessionDecisionIdVerificationsParsedAddressRawResultsGeometry", + "SessionDecisionIdVerificationsParsedAddressRawResultsGeometryLocation", + "SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewport", + "SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewportNortheast", + "SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest", + "SessionDecisionIdVerificationsParsedAddressRawResultsNavigationPoints", + "SessionDecisionIdVerificationsParsedAddressRawResultsNavigationPointsLocation", + "SessionDecisionIdVerificationsWalletVerification", + "SessionDecisionIdVerificationsWarnings", + "SessionDecisionIpAnalyses", + "SessionDecisionIpAnalysesIdDocument", + "SessionDecisionIpAnalysesIdDocumentLocation", + "SessionDecisionIpAnalysesIp", + "SessionDecisionIpAnalysesIpLocation", + "SessionDecisionIpAnalysesMatches", + "SessionDecisionIpAnalysesMatchesDeviceInfo", + "SessionDecisionIpAnalysesMatchesLocationInfo", + "SessionDecisionIpAnalysesPoaDocument", + "SessionDecisionIpAnalysesPoaDocumentLocation", + "SessionDecisionIpAnalysesWarnings", + "SessionDecisionKeyPeopleChecks", + "SessionDecisionKeyPeopleChecksRegistry", + "SessionDecisionKeyPeopleChecksRegistryBeneficialOwners", + "SessionDecisionKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories", + "SessionDecisionKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures", + "SessionDecisionKeyPeopleChecksRegistryOfficers", + "SessionDecisionKeyPeopleChecksRegistryOfficersKycAmlCategories", + "SessionDecisionKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures", + "SessionDecisionKeyPeopleChecksSubmitted", + "SessionDecisionKeyPeopleChecksSubmittedParties", + "SessionDecisionKeyPeopleChecksSubmittedPartiesRoles", + "SessionDecisionKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures", + "SessionDecisionKeyPeopleChecksUboKycSummary", + "SessionDecisionLivenessChecks", + "SessionDecisionLivenessChecksMatches", + "SessionDecisionLivenessChecksMatchesUserDetails", + "SessionDecisionLivenessChecksWarnings", + "SessionDecisionNfcVerifications", + "SessionDecisionNfcVerificationsAuthenticity", + "SessionDecisionNfcVerificationsCertificateSummary", + "SessionDecisionNfcVerificationsChipData", + "SessionDecisionNfcVerificationsJpki", + "SessionDecisionNfcVerificationsJpkiAttributes", + "SessionDecisionNfcVerificationsJpkiCertificates", + "SessionDecisionNfcVerificationsWarnings", + "SessionDecisionPhoneVerifications", + "SessionDecisionPhoneVerificationsCarrier", + "SessionDecisionPhoneVerificationsLifecycle", + "SessionDecisionPhoneVerificationsLifecycleDetails", + "SessionDecisionPhoneVerificationsMatches", + "SessionDecisionPhoneVerificationsWarnings", + "SessionDecisionPoaVerifications", + "SessionDecisionPoaVerificationsDocumentMetadata", + "SessionDecisionPoaVerificationsDocumentMetadataOverlayManipulation", + "SessionDecisionPoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions", + "SessionDecisionPoaVerificationsExtraFields", + "SessionDecisionPoaVerificationsPoaParsedAddress", + "SessionDecisionPoaVerificationsPoaParsedAddressDocumentLocation", + "SessionDecisionPoaVerificationsWarnings", + "SessionDecisionQuestionnaireResponses", + "SessionDecisionQuestionnaireResponsesSections", + "SessionDecisionQuestionnaireResponsesSectionsItems", + "SessionDecisionQuestionnaireResponsesSectionsItemsAnswer", + "SessionDecisionQuestionnaireResponsesSectionsItemsChoices", + "SessionDecisionRegistryChecks", + "SessionDecisionRegistryChecksCompany", + "SessionDecisionRegistryChecksCompanyBeneficialOwners", + "SessionDecisionRegistryChecksCompanyOfficers", + "SessionDecisionResubmissions", + "SessionDecisionResubmissionsNodes", + "SessionDecisionResubmissionsRequestedBy", + "SessionDecisionReviews", ] -class SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaDetails(BaseModel): - sentiment: str | None = None - - entity_type: str | None = None - - sentiment_score: int | None = None - - adverse_keywords: dict[str, int] | None = None - - -class SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaMatches(BaseModel): - country: str | None = None - - summary: str | None = None - - headline: str | None = None - - sentiment: str | None = None - - thumbnail: str | None = None - - source_url: str | None = None - - author_name: str | None = None - - other_sources: list[str] | None = None - - adverse_keywords: list[str] | None = None - - sentiment_score: int | None = None - - publication_date: _dt.datetime | None = None - - -class SessionsRetrieveDecisionResponseAmlScreeningsHitsLinkedEntities(BaseModel): - name: list[str] | None = None - - active: list[str] | None = None - - status: list[str] | None = None - - details: list[str] | None = None - - relation: list[str] | None = None - - -class SessionsRetrieveDecisionResponseAmlScreeningsHitsPepMatches(BaseModel): - aliases: list[str] | None = None - - education: list[str] | None = None - - list_name: str | None = None - - publisher: str | None = None - - source_url: str | None = None - - description: str | None = None - - matched_name: str | None = None - - pep_position: str | None = None - - date_of_birth: str | None = None - - other_sources: list[str] | None = None - - place_of_birth: str | None = None - - -class SessionsRetrieveDecisionResponseAmlScreeningsHitsProperties(BaseModel): - name: list[str] | None = None - - alias: list[str] | None = None - - notes: list[str] | None = None - - title: str | None = None - - gender: list[str] | None = None - - height: str | None = None - - topics: str | None = None - - weight: str | None = None - - address: str | None = None - - country: str | None = None - - website: str | None = None - - eyeColor: str | None = None - - keywords: str | None = None - - lastName: list[str] | None = None - - position: str | None = None - - religion: str | None = None - - birthDate: list[str] | None = None - - education: list[str] | None = None - - ethnicity: str | None = None - - firstName: list[str] | None = None - - hairColor: str | None = None - - weakAlias: str | None = None - - birthPlace: list[str] | None = None - - modifiedAt: str | None = None - - wikidataId: str | None = None - - citizenship: str | None = None - - nationality: str | None = None - - -class SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCategories(BaseModel): - score: int | None = None - - weightage: int | None = None - - risk_level: str | None = None - - risk_scores: dict[str, object] | None = None - - -class SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCountries(BaseModel): - score: int | None = None - - weightage: int | None = None - - risk_level: str | None = None - - risk_scores: dict[str, object] | None = None - - -class SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCrimes(BaseModel): - score: int | None = None - - weightage: int | None = None - - risk_level: str | None = None - - risk_scores: dict[str, object] | None = None - - -class SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskView(BaseModel): - crimes: SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCrimes | None = None - - countries: SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCountries | None = None - - categories: SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCategories | None = None - - custom_list: dict[str, object] | None = None - - -class SessionsRetrieveDecisionResponseAmlScreeningsHitsSanctionMatches(BaseModel): - list_name: list[str] | None = None - """Name(s) of the sanction list(s)""" - - matched_name: str | None = None - """Name matched in the sanction list""" - - description: str | None = None - """Description of the sanction entry""" - - reason: str | None = None - """Free-text reason why the entity was sanctioned. Joined from the sanction list's reason entries. Examples: 'Executive Order 13224 (Terrorism)', 'Support for actions undermining Ukraine's sovereignty', 'Listed due to national security concerns.'""" - - source_url: str | None = None - """URL to the source of the sanction""" - - other_sources: list[str] | None = None - """Additional source URLs""" - - details: list[str] | None = None - """Additional details about the sanction""" - - legal_basis: list[str] | None = None - """Legal basis for the sanction""" - - listed_on: list[str] | None = None - """Date(s) when the entity was listed""" - - sanction_list: list[str] | None = None - """Sanction list identifier(s)""" - - sanction_program: list[str] | None = None - """Sanction program(s) the entity is listed under""" - - sanctioning_authority: list[str] | None = None - """Authority/authorities that imposed the sanction""" - - updated_on: list[str] | None = None - """Date(s) when the sanction entry was last updated""" - - additional_data: dict[str, object] | None = None - """Additional data from the sanction source not captured in the main fields""" - - -class SessionsRetrieveDecisionResponseAmlScreeningsHitsScoreBreakdown(BaseModel): - """Detailed breakdown of how the match_score was calculated.""" - - name_score: int | None = None - """Raw name similarity percentage (0-100).""" - - name_weight: int | None = None - """Original weight for name from config (0-100).""" - - name_weight_normalized: float | None = None - """Normalized weight for name after re-weighting for missing data (0-100).""" - - name_contribution: float | None = None - """Points contributed by name to the total score.""" - - dob_score: int | None = None - """DOB match score (-100 to 100).""" - - dob_weight: int | None = None - """Original weight for DOB from config (0-100).""" - - dob_weight_normalized: float | None = None - """Normalized weight for DOB after re-weighting for missing data (0-100).""" - - dob_contribution: float | None = None - """Points contributed by DOB to the total score.""" - - country_score: int | None = None - """Country match score (-100 to 100).""" - - country_weight: int | None = None - """Original weight for country from config (0-100).""" - - country_weight_normalized: float | None = None - """Normalized weight for country after re-weighting for missing data (0-100).""" - - country_contribution: float | None = None - """Points contributed by country to the total score.""" - - document_number_match_type: Literal["MATCH", "NEUTRAL", "HARD_MISMATCH"] | str | None = None - """Result of document number 'Golden Key' evaluation.""" - - document_number_effect: str | None = None - """Human-readable description of the document number effect on the score.""" - - total_score: int | None = None - """Final weighted match score (0-100).""" - - -class SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataDataCaseDetails( - BaseModel -): - case_type: ( - Literal[ - "Accusation", - "Arrest", - "Bribery", - "Criminal", - "Fraud", - "Illegal Fundraising", - "Non-conviction Terror", - "Proclaimed Offender", - "Regulatory Enforcement", - "Terrorism", - "Terrorism-related", - "Terrorism-related listing", - ] - | str - | None - ) = None - """Type of the warning case""" - - -class SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataData(BaseModel): - case_details: ( - list[ - SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataDataCaseDetails - ] - | None - ) = None - """Details of warning cases associated with this match""" - - -class SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalData(BaseModel): - """Additional data from the warning source. May contain a `data` object with `case_details` array.""" - - data: ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataData | None - ) = None - - -class SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatches(BaseModel): - list_name: str | None = None - """Name of the warning list publisher""" - - matched_name: str | None = None - """Name matched in the warning list""" - - description: str | None = None - """Description of the warning entry""" - - publisher: str | None = None - """Publisher or source of the warning""" - - source_url: str | None = None - """URL to the source of the warning""" - - other_sources: list[str] | None = None - """Additional source URLs""" - - countries: list[str] | None = None - """Countries associated with the warning""" - - created_at: str | None = None - """Date the warning entry was created""" - - updated_at: str | None = None - """Date the warning entry was last updated""" - - additional_data: ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalData | None - ) = None - """Additional data from the warning source. May contain a `data` object with `case_details` array.""" - - -class SessionsRetrieveDecisionResponseAmlScreeningsHits(BaseModel): - id: str | None = None - """Unique identifier of the match""" - - url: str | None = None - """Source URL for the match""" - - match: bool | None = None - """Whether this is a confirmed match""" - - score: float | None = None - """Deprecated. Deprecated weighted match score in the 0-1 range (e.g. `0.73`). Use `match_score` (0-100) instead.""" - - target: bool | None = None - """Whether the matched entity is the direct target of the listing (as opposed to a related party). `null` when the source does not specify it.""" - - caption: str | None = None - """Caption or title of the match""" - - datasets: ( - list[ - Literal[ - "PEP", - "PEP Level 1", - "PEP Level 2", - "PEP Level 3", - "PEP Level 4", - "Sanctions", - "Adverse Media", - "Warnings and Regulatory Enforcement", - "Fitness and Probity", - "SIP", - "SIE", - "Insolvency", - ] - | str - ] - | None - ) = None - """List of dataset categories where the match was found. Indicates whether the entity appeared in PEP lists (with level), sanctions databases, adverse media, warnings/regulatory enforcement, or other compliance datasets.""" - - features: dict[str, object] | None = None - """Additional features of the match""" - - rca_name: str | None = None - """RCA name if applicable""" - - last_seen: _dt.datetime | None = None - """Date when match was last seen""" - - risk_view: SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskView | None = None - - first_seen: _dt.datetime | None = None - """Date when match was first seen""" - - properties: SessionsRetrieveDecisionResponseAmlScreeningsHitsProperties | None = None - - match_score: float | None = None - """Weighted identity confidence score (0-100) calculated from name similarity, date of birth, and country matching. Used to determine if hit is a False Positive or Possible Match.""" - - risk_score: float | None = None - """Entity risk score (0-100). Used to determine final AML status (Approved/In Review/Declined) based on the highest risk_score among non-False-Positive hits.""" - - review_status: ( - Literal["Unreviewed", "Confirmed Match", "False Positive", "Inconclusive"] | str | None - ) = None - """Review status for this hit. Can be 'Unreviewed', 'Confirmed Match', 'False Positive', or 'Inconclusive'.""" - - score_breakdown: SessionsRetrieveDecisionResponseAmlScreeningsHitsScoreBreakdown | None = None - """Detailed breakdown of how the match_score was calculated.""" - - pep_matches: list[SessionsRetrieveDecisionResponseAmlScreeningsHitsPepMatches] | None = None - - linked_entities: ( - list[SessionsRetrieveDecisionResponseAmlScreeningsHitsLinkedEntities] | None - ) = None - - warning_matches: ( - list[SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatches] | None - ) = None - """List of warning and regulatory enforcement matches. Includes entries from categories: Warning, SIP, Warnings and Regulatory Enforcement, Fitness and Probity, SIE, and Insolvency.""" - - sanction_matches: ( - list[SessionsRetrieveDecisionResponseAmlScreeningsHitsSanctionMatches] | None - ) = None - """List of sanction list matches found for this entity.""" - - adverse_media_details: ( - SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaDetails | None - ) = None - - adverse_media_matches: ( - list[SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaMatches] | None - ) = None - - additional_information: dict[str, object] | None = None - - -class SessionsRetrieveDecisionResponseAmlScreeningsScreenedData(BaseModel): - full_name: str | None = None - - nationality: str | None = None - - date_of_birth: _dt.date | None = None - - document_number: str | None = None - - -class SessionsRetrieveDecisionResponseAmlScreeningsWarnings(BaseModel): - feature: ( - Literal[ - "ID_VERIFICATION", - "NFC", - "LIVENESS", - "FACEMATCH", - "PROOF_OF_ADDRESS", - "PHONE", - "EMAIL", - "AML", - "LOCATION", - "DATABASE_VALIDATION", - "QUESTIONNAIRE", - "DOCUMENT_AI", - "KYB_REGISTRY", - "KYB_DOCUMENTS", - "KYB_KEY_PEOPLE", - ] - | str - | None - ) = None - """The feature that generated this warning. The value identifies the feature block the warning belongs to. Within an AML screening this is typically `AML`, but the enum above is authoritative across all feature warnings on the decision payload.""" - - risk: str | None = None - - additional_data: dict[str, object] | None = None - - log_type: str | None = None - - short_description: str | None = None - - long_description: str | None = None - - node_id: str | None = None - """The workflow graph node ID that generated this warning""" - - -class SessionsRetrieveDecisionResponseAmlScreenings(BaseModel): - node_id: str | None = None - """Unique identifier for this AML step in the workflow graph.""" - - status: ( - Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str | None - ) = None - """Feature-level status of this AML screening. Independent of the top-level session `status` - a session can reach its overall decision while an individual feature stays `In Review`.""" - - total_hits: int | None = None - """Total number of matches found""" - - entity_type: str | None = None - """Type of entity screened: person or company""" - - hits: list[SessionsRetrieveDecisionResponseAmlScreeningsHits] | None = None - - score: float | None = None - """Overall AML screening score - the highest risk score across all hits (float, 0-100). `null` until computed.""" - - screened_data: SessionsRetrieveDecisionResponseAmlScreeningsScreenedData | None = None - - is_ongoing_monitoring_enabled: bool | None = None - """Whether AML ongoing monitoring is enabled for this screening. When `true`, the entity is re-screened periodically and any new hits update the report.""" - - next_ongoing_monitoring_bill_date: _dt.date | None = None - """Date (YYYY-MM-DD) when this ongoing-monitoring subscription will next be billed. Computed as the date of the last ongoing-monitoring billing plus 365 days when `is_ongoing_monitoring_enabled = true`; otherwise `null`. This is a billing date, not a screening date - use `last_ongoing_screening_at` for that.""" - - last_ongoing_screening_at: _dt.datetime | None = None - """When ongoing monitoring last completed a re-screening of this record, whether or not the result changed. `null` when the record has never been re-screened; it is never back-filled from the enablement or billing date.""" - - last_ongoing_screening_datasets_updated_at: _dt.datetime | None = None - """The most recent AML dataset update date the provider reported at the time of that screening. `null` when the provider reported no dataset dates; never estimated.""" - - warnings: list[SessionsRetrieveDecisionResponseAmlScreeningsWarnings] | None = None - - -class SessionsRetrieveDecisionResponseContactDetails(BaseModel): +class SessionDecisionContactDetails(BaseModel): """Contact details supplied when the session was created (used for notification emails, prefilled phone/email verification, and communication language). `null` when neither an email nor a phone number was provided.""" email: str | None = None @@ -683,14 +152,14 @@ class SessionsRetrieveDecisionResponseContactDetails(BaseModel): """Phone number of the user in E.164 format if available (e.g., +14155552671). `null` when only an email was supplied at session creation. Note: This field returns the original phone number provided during session creation. If the number was invalid, it may not be in E.164 format.""" -class SessionsRetrieveDecisionResponseDatabaseValidationsErrors(BaseModel): +class SessionDecisionDatabaseValidationsErrors(BaseModel): """The upstream service's error object spread as-is - its keys vary by service (commonly a message/code and any provider-specific details) - plus `service_id` identifying the service that errored.""" service_id: str | None = None """Identifier of the registry service that errored.""" -class SessionsRetrieveDecisionResponseDatabaseValidationsScreenedData(BaseModel): +class SessionDecisionDatabaseValidationsScreenedData(BaseModel): """The input data used for validation. Fields vary by country - see the Database Validation docs (`/core-technology/database-validation/overview`) for country-specific formats.""" last_name: str | None = None @@ -715,7 +184,7 @@ class SessionsRetrieveDecisionResponseDatabaseValidationsScreenedData(BaseModel) """Required for ESP""" -class SessionsRetrieveDecisionResponseDatabaseValidationsValidationsValidation(BaseModel): +class SessionDecisionDatabaseValidationsValidationsValidation(BaseModel): """The results of the field validations.""" full_name: str | None = None @@ -728,16 +197,14 @@ class SessionsRetrieveDecisionResponseDatabaseValidationsValidationsValidation(B """Match result for identification number.""" -class SessionsRetrieveDecisionResponseDatabaseValidationsValidations(BaseModel): +class SessionDecisionDatabaseValidationsValidations(BaseModel): service_id: str | None = None """Identifier of the registry service that produced this validation result.""" service_name: str | None = None """Human-readable name of the registry service.""" - validation: SessionsRetrieveDecisionResponseDatabaseValidationsValidationsValidation | None = ( - None - ) + validation: SessionDecisionDatabaseValidationsValidationsValidation | None = None """The results of the field validations.""" outcome_code: str | None = None @@ -750,7 +217,7 @@ class SessionsRetrieveDecisionResponseDatabaseValidationsValidations(BaseModel): """The data returned from the government database. Common fields: identification_number, first_name, last_name, full_name, date_of_birth, gender. Additional fields by country: BRA always includes lgpd_minor, minor_under_18, and minor_under_16 on successful CPF matches; it also includes the upstream HTTP status when CPF data is withheld under LGPD (HTTP 422/451). ECU adds street, formatted_address, nationality, education, marital_status, spouse, mother_name, father_name, profession. PER adds paternal_name, maternal_name, verification_number, verification_letter. ESP adds document_type, expiration_date. VEN/COL add document_type.""" -class SessionsRetrieveDecisionResponseDatabaseValidationsWarnings(BaseModel): +class SessionDecisionDatabaseValidationsWarnings(BaseModel): feature: ( Literal[ "ID_VERIFICATION", @@ -788,7 +255,7 @@ class SessionsRetrieveDecisionResponseDatabaseValidationsWarnings(BaseModel): """The workflow graph node ID that generated this warning""" -class SessionsRetrieveDecisionResponseDatabaseValidations(BaseModel): +class SessionDecisionDatabaseValidations(BaseModel): node_id: str | None = None """Unique identifier for this database validation step in the workflow graph.""" @@ -798,13 +265,13 @@ class SessionsRetrieveDecisionResponseDatabaseValidations(BaseModel): validation_type: Literal["one_by_one", "two_by_two", "not_enabled"] | str | None = None """How the result was corroborated: `one_by_one` (single-service outcome), `two_by_two` (two or more distinct services independently full-matched the identification number), or `not_enabled` (persisted when no service produced a usable match). The value is never `null`. Dedicated biometric 2x2 validation modes are currently available for `ECU` and `PER`.""" - screened_data: SessionsRetrieveDecisionResponseDatabaseValidationsScreenedData | None = None + screened_data: SessionDecisionDatabaseValidationsScreenedData | None = None """The input data used for validation. Fields vary by country - see the Database Validation docs (`/core-technology/database-validation/overview`) for country-specific formats.""" - validations: list[SessionsRetrieveDecisionResponseDatabaseValidationsValidations] | None = None + validations: list[SessionDecisionDatabaseValidationsValidations] | None = None """One entry per registry service that returned a validation payload. Each entry identifies the service (`service_id`, `service_name`) and carries the per-field `validation` comparison, the service `outcome_code` / `outcome_detail` when reported, and the normalised registry record in `source_data`.""" - errors: list[SessionsRetrieveDecisionResponseDatabaseValidationsErrors] | None = None + errors: list[SessionDecisionDatabaseValidationsErrors] | None = None """Errors returned by registry services while validating, each tagged with the `service_id` that produced it. **This key is omitted entirely when there are no errors.**""" match_type: Literal["no_match", "partial_match", "full_match"] | str | None = None @@ -815,11 +282,11 @@ class SessionsRetrieveDecisionResponseDatabaseValidations(BaseModel): ) = None """Feature-level status of this database validation. Independent of the top-level session `status` - a session can reach its overall decision while an individual feature stays `In Review`.""" - warnings: list[SessionsRetrieveDecisionResponseDatabaseValidationsWarnings] | None = None + warnings: list[SessionDecisionDatabaseValidationsWarnings] | None = None """Warnings related to the database validation.""" -class SessionsRetrieveDecisionResponseDocumentAiDocumentsItemsFields(BaseModel): +class SessionDecisionDocumentAiDocumentsItemsFields(BaseModel): key: str | None = None """Stable field key used as the key in `extracted_data`.""" @@ -836,7 +303,7 @@ class SessionsRetrieveDecisionResponseDocumentAiDocumentsItemsFields(BaseModel): """Whether this field is required.""" -class SessionsRetrieveDecisionResponseDocumentAiDocumentsItems(BaseModel): +class SessionDecisionDocumentAiDocumentsItems(BaseModel): uuid: str | None = None node_id: str | None = None @@ -863,7 +330,7 @@ class SessionsRetrieveDecisionResponseDocumentAiDocumentsItems(BaseModel): detected_codes: list[DocumentAIDetectedCode] | None = None """QR codes and barcodes detected across every processed page. Empty when none are found. A located but undecodable QR code has `parsed_payload: null`.""" - fields: list[SessionsRetrieveDecisionResponseDocumentAiDocumentsItemsFields] | None = None + fields: list[SessionDecisionDocumentAiDocumentsItemsFields] | None = None """The configured extraction fields for this document.""" document_metadata: dict[str, object] | None = None @@ -875,7 +342,7 @@ class SessionsRetrieveDecisionResponseDocumentAiDocumentsItems(BaseModel): created_at: _dt.datetime | None = None -class SessionsRetrieveDecisionResponseDocumentAiDocuments(BaseModel): +class SessionDecisionDocumentAiDocuments(BaseModel): """Document AI results grouped by workflow node.""" status: Literal["Approved", "Declined", "In Review", "Not Finished"] | str | None = None @@ -884,13 +351,13 @@ class SessionsRetrieveDecisionResponseDocumentAiDocuments(BaseModel): node_id: str | None = None """Unique identifier for this Document AI step in the workflow graph.""" - items: list[SessionsRetrieveDecisionResponseDocumentAiDocumentsItems] | None = None + items: list[SessionDecisionDocumentAiDocumentsItems] | None = None warnings: list[dict[str, object]] | None = None """Warning logs raised for this Document AI node.""" -class SessionsRetrieveDecisionResponseDocumentVerificationsGroupsValue(BaseModel): +class SessionDecisionDocumentVerificationsGroupsValue(BaseModel): total: int | None = None approved: int | None = None @@ -907,7 +374,7 @@ class SessionsRetrieveDecisionResponseDocumentVerificationsGroupsValue(BaseModel """Requested placeholders with no file uploaded yet.""" -class SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions( +class SessionDecisionDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions( BaseModel ): page: int | None = None @@ -925,9 +392,7 @@ class SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata page_height: float | None = None -class SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulation( - BaseModel -): +class SessionDecisionDocumentVerificationsItemsDocumentMetadataOverlayManipulation(BaseModel): """PDF forensic analysis for suspected overlay-text manipulation. Null when no overlay manipulation evidence was found or the document was not a PDF.""" detected: bool | None = None @@ -941,14 +406,14 @@ class SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata manipulated_regions: ( list[ - SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions + SessionDecisionDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions ] | None ) = None """Suspected manipulated areas in PDF page coordinates.""" -class SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata(BaseModel): +class SessionDecisionDocumentVerificationsItemsDocumentMetadata(BaseModel): """Metadata extracted from the uploaded corporate document. When metadata extraction succeeded, all 16 keys are always present (individual values are `null` when not applicable to the file type); the whole object is `null` when nothing could be extracted. For PDFs, `overlay_manipulation` carries forensic overlay-text analysis.""" file_size: int | None = None @@ -997,13 +462,12 @@ class SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata """Whether creation and modification dates differ - a possible (but not conclusive) editing signal.""" overlay_manipulation: ( - SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulation - | None + SessionDecisionDocumentVerificationsItemsDocumentMetadataOverlayManipulation | None ) = None """PDF forensic analysis for suspected overlay-text manipulation. Null when no overlay manipulation evidence was found or the document was not a PDF.""" -class SessionsRetrieveDecisionResponseDocumentVerificationsItems(BaseModel): +class SessionDecisionDocumentVerificationsItems(BaseModel): uuid: str | None = None document_type: ( @@ -1054,9 +518,7 @@ class SessionsRetrieveDecisionResponseDocumentVerificationsItems(BaseModel): cross_check_result: dict[str, object] | None = None """Cross-reference of the OCR-extracted document fields against the registry company profile. An object with `critical`, `non_critical` and `people` sections, each mapping a field name (e.g. `company_name`, `registration_number`, `incorporation_date`, `registered_address`) to a comparison record like `{"document": "...", "registry": "...", "match": true, "score": 0.97}`. `null` when the document had no OCR data or there was no registry company to compare against.""" - document_metadata: ( - SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata | None - ) = None + document_metadata: SessionDecisionDocumentVerificationsItemsDocumentMetadata | None = None """Metadata extracted from the uploaded corporate document. When metadata extraction succeeded, all 16 keys are always present (individual values are `null` when not applicable to the file type); the whole object is `null` when nothing could be extracted. For PDFs, `overlay_manipulation` carries forensic overlay-text analysis.""" description: str | None = None @@ -1074,18 +536,16 @@ class SessionsRetrieveDecisionResponseDocumentVerificationsItems(BaseModel): """Console uploads start `false` and flip `true` once the admin confirms them; end-user uploads are always `true`.""" -class SessionsRetrieveDecisionResponseDocumentVerifications(BaseModel): +class SessionDecisionDocumentVerifications(BaseModel): """Documents grouped by workflow node. Present only when session_kind = business.""" status: str | None = None node_id: str | None = None - items: list[SessionsRetrieveDecisionResponseDocumentVerificationsItems] | None = None + items: list[SessionDecisionDocumentVerificationsItems] | None = None - groups: dict[str, SessionsRetrieveDecisionResponseDocumentVerificationsGroupsValue] | None = ( - None - ) + groups: dict[str, SessionDecisionDocumentVerificationsGroupsValue] | None = None """Per-group progress counters keyed by group name (e.g. legal_presence, ownership_structure).""" required_groups: list[str] | None = None @@ -1093,7 +553,7 @@ class SessionsRetrieveDecisionResponseDocumentVerifications(BaseModel): warnings: list[dict[str, object]] | None = None -class SessionsRetrieveDecisionResponseEmailVerificationsBreaches(BaseModel): +class SessionDecisionEmailVerificationsBreaches(BaseModel): name: str | None = None """Name of the breach""" @@ -1119,7 +579,7 @@ class SessionsRetrieveDecisionResponseEmailVerificationsBreaches(BaseModel): """Number of email addresses affected in the breach""" -class SessionsRetrieveDecisionResponseEmailVerificationsLifecycleDetails(BaseModel): +class SessionDecisionEmailVerificationsLifecycleDetails(BaseModel): """Event-specific payload. Send events carry `status`, `reason`; code-check events carry `code_tried`, `status`; final status events carry `reason` - or `null` when there is no reason to report (e.g. `EMAIL_VERIFICATION_APPROVED`).""" status: str | None = None @@ -1129,18 +589,18 @@ class SessionsRetrieveDecisionResponseEmailVerificationsLifecycleDetails(BaseMod code_tried: str | None = None -class SessionsRetrieveDecisionResponseEmailVerificationsLifecycle(BaseModel): +class SessionDecisionEmailVerificationsLifecycle(BaseModel): type: str | None = None timestamp: _dt.datetime | None = None - details: SessionsRetrieveDecisionResponseEmailVerificationsLifecycleDetails | None = None + details: SessionDecisionEmailVerificationsLifecycleDetails | None = None """Event-specific payload. Send events carry `status`, `reason`; code-check events carry `code_tried`, `status`; final status events carry `reason` - or `null` when there is no reason to report (e.g. `EMAIL_VERIFICATION_APPROVED`).""" fee: float | None = None -class SessionsRetrieveDecisionResponseEmailVerificationsMatches(BaseModel): +class SessionDecisionEmailVerificationsMatches(BaseModel): session_id: str | None = None session_number: int | None = None @@ -1161,7 +621,7 @@ class SessionsRetrieveDecisionResponseEmailVerificationsMatches(BaseModel): """Where this match came from. `session` means another verification session in your application matched on this value. `list_entry` means the value was hit against an org-managed list (allowlist/blocklist) - see the management API lists overview at `/management-api/lists/overview` for how to manage those lists.""" -class SessionsRetrieveDecisionResponseEmailVerificationsWarnings(BaseModel): +class SessionDecisionEmailVerificationsWarnings(BaseModel): feature: ( Literal[ "ID_VERIFICATION", @@ -1199,7 +659,7 @@ class SessionsRetrieveDecisionResponseEmailVerificationsWarnings(BaseModel): """The workflow graph node ID that generated this warning""" -class SessionsRetrieveDecisionResponseEmailVerifications(BaseModel): +class SessionDecisionEmailVerifications(BaseModel): node_id: str | None = None """Unique identifier for this email step in the workflow graph.""" @@ -1214,7 +674,7 @@ class SessionsRetrieveDecisionResponseEmailVerifications(BaseModel): is_breached: bool | None = None """Indicates if the email was found in known data breaches""" - breaches: list[SessionsRetrieveDecisionResponseEmailVerificationsBreaches] | None = None + breaches: list[SessionDecisionEmailVerificationsBreaches] | None = None """List of known breaches this email was found in""" is_disposable: bool | None = None @@ -1229,17 +689,17 @@ class SessionsRetrieveDecisionResponseEmailVerifications(BaseModel): verified_at: _dt.datetime | None = None """Timestamp when the email was verified""" - warnings: list[SessionsRetrieveDecisionResponseEmailVerificationsWarnings] | None = None + warnings: list[SessionDecisionEmailVerificationsWarnings] | None = None """List of warnings related to email verification""" - lifecycle: list[SessionsRetrieveDecisionResponseEmailVerificationsLifecycle] | None = None + lifecycle: list[SessionDecisionEmailVerificationsLifecycle] | None = None """Chronological list of events in the email verification lifecycle.""" - matches: list[SessionsRetrieveDecisionResponseEmailVerificationsMatches] | None = None + matches: list[SessionDecisionEmailVerificationsMatches] | None = None """Matches found for this email address. Each match has a `source`: `session` (another verification session in your application matched on the same email, returned with `session_id`/`session_number`/`vendor_data`/`verification_date`) or `list_entry` (the email is on an org-managed allow/block list - see `/management-api/lists/overview`). Cross-session matches use different `vendor_data` and are capped at 5.""" -class SessionsRetrieveDecisionResponseExpectedDetails(BaseModel): +class SessionDecisionExpectedDetails(BaseModel): """Expected details supplied at session creation - used for cross-validation against extracted data (KYC) or to pre-fill the company registry search (KYB). Echoes back exactly the keys that were provided; `null` when none were supplied. KYC sessions carry the person fields; Business (KYB) sessions carry `company_name`, `registry_country` and `registration_number`.""" first_name: str | None = None @@ -1290,7 +750,7 @@ class SessionsRetrieveDecisionResponseExpectedDetails(BaseModel): """Expected company registration number. Business (KYB) sessions only.""" -class SessionsRetrieveDecisionResponseFaceMatchesWarnings(BaseModel): +class SessionDecisionFaceMatchesWarnings(BaseModel): feature: ( Literal[ "ID_VERIFICATION", @@ -1328,7 +788,7 @@ class SessionsRetrieveDecisionResponseFaceMatchesWarnings(BaseModel): """The workflow graph node ID that generated this warning""" -class SessionsRetrieveDecisionResponseFaceMatches(BaseModel): +class SessionDecisionFaceMatches(BaseModel): """One face-match report - the full report.""" node_id: str | None = None @@ -1351,11 +811,11 @@ class SessionsRetrieveDecisionResponseFaceMatches(BaseModel): target_image: str | None = None """URL of the target image used for comparison""" - warnings: list[SessionsRetrieveDecisionResponseFaceMatchesWarnings] | None = None + warnings: list[SessionDecisionFaceMatchesWarnings] | None = None """Face match verification warnings""" -class SessionsRetrieveDecisionResponseIdVerificationsBackImageQualityScore(BaseModel): +class SessionDecisionIdVerificationsBackImageQualityScore(BaseModel): """Quality metrics for the back document image. Same structure as front_image_quality_score.""" focus_score: float | None = None @@ -1377,7 +837,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsBackImageQualityScore(BaseM """Weighted composite of sub-scores (focus 45%, brightness 30%, resolution 25%).""" -class SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPortraitReplace(BaseModel): +class SessionDecisionIdVerificationsDocumentLivenessPortraitReplace(BaseModel): score: float | None = None """Likelihood that the document is genuine for this fraud type, 0-100 (higher = more likely genuine). The lowest of the two sides - the value the decision used. `null` when neither side produced a usable score.""" @@ -1397,7 +857,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPortraitRep """The fraud-type bucket saved at evaluation time. Null when there is no usable score, the check was not applied, or historical evaluation evidence is unavailable. Later workflow edits do not change it.""" -class SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPrintedCopy(BaseModel): +class SessionDecisionIdVerificationsDocumentLivenessPrintedCopy(BaseModel): score: float | None = None """Likelihood that the document is genuine for this fraud type, 0-100 (higher = more likely genuine). The lowest of the two sides - the value the decision used. `null` when neither side produced a usable score.""" @@ -1417,7 +877,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPrintedCopy """The fraud-type bucket saved at evaluation time. Null when there is no usable score, the check was not applied, or historical evaluation evidence is unavailable. Later workflow edits do not change it.""" -class SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessScreenReplay(BaseModel): +class SessionDecisionIdVerificationsDocumentLivenessScreenReplay(BaseModel): score: float | None = None """Likelihood that the document is genuine for this fraud type, 0-100 (higher = more likely genuine). The lowest of the two sides - the value the decision used. `null` when neither side produced a usable score.""" @@ -1437,25 +897,17 @@ class SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessScreenRepla """The fraud-type bucket saved at evaluation time. Null when there is no usable score, the check was not applied, or historical evaluation evidence is unavailable. Later workflow edits do not change it.""" -class SessionsRetrieveDecisionResponseIdVerificationsDocumentLiveness(BaseModel): +class SessionDecisionIdVerificationsDocumentLiveness(BaseModel): """Document-liveness scores per fraud type, reported whether or not a warning fired, so an approved document can still be audited. `null` when the check did not run.""" - portrait_replace: ( - SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPortraitReplace | None - ) = None + portrait_replace: SessionDecisionIdVerificationsDocumentLivenessPortraitReplace | None = None - printed_copy: ( - SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPrintedCopy | None - ) = None + printed_copy: SessionDecisionIdVerificationsDocumentLivenessPrintedCopy | None = None - screen_replay: ( - SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessScreenReplay | None - ) = None + screen_replay: SessionDecisionIdVerificationsDocumentLivenessScreenReplay | None = None -class SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatchesUserDetails( - BaseModel -): +class SessionDecisionIdVerificationsDocumentSelfieFaceMatchesUserDetails(BaseModel): """Identity data from the matching session or imported User profile. Always `null` for `retained_template` and `list_entry` matches.""" full_name: str | None = None @@ -1465,7 +917,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatchesUs document_number: str | None = None -class SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatches(BaseModel): +class SessionDecisionIdVerificationsDocumentSelfieFaceMatches(BaseModel): session_id: str | None = None """Session ID of the matching face. `null` when `source` is `imported`, `list_entry`, or `retained_template`.""" @@ -1486,9 +938,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatches(B verification_date: _dt.datetime | None = None """When the matching face was captured, or when a retained template was created. `null` for list entries.""" - user_details: ( - SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatchesUserDetails | None - ) = None + user_details: SessionDecisionIdVerificationsDocumentSelfieFaceMatchesUserDetails | None = None """Identity data from the matching session or imported User profile. Always `null` for `retained_template` and `list_entry` matches.""" match_image_url: str | None = None @@ -1510,7 +960,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatches(B """Origin of the matching face: `session`, `imported` (User face upload), `list_entry` (allow/block list), or `retained_template` (an image-free biometric template stored on a User, listed under `/v3/biometric-templates/`).""" -class SessionsRetrieveDecisionResponseIdVerificationsExtraFields(BaseModel): +class SessionDecisionIdVerificationsExtraFields(BaseModel): """Additional document fields. Includes document-specific attributes (e.g. `dl_categories`, `blood_group`, `sponsor`, `employer`, `dl_class_code_b_from`, `dl_class_code_b_to`). Driver license class validity dates are returned as `dl_class_code__from` and `dl_class_code__to`, and per-class restriction/information codes (e.g. column 12 of UK licences) as `dl_class_code__notes`. When the document carries names, address, or place of birth in multiple scripts, this object also includes alternate-script versions of those fields. The workflow setting `preferred_characters` (`latin` or `non_latin`) controls which script populates the top-level fields (`first_name`, `last_name`, `full_name`, `address`, `place_of_birth`); the opposite script lands here as `first_name_non_latin` / `last_name_non_latin` / `full_name_non_latin` / `middle_name_non_latin` / `address_non_latin` / `place_of_birth_non_latin` (when `preferred_characters` is `latin`) or with a `_latin` suffix (when `preferred_characters` is `non_latin`). Keys appear only when the OCR actually extracts a real alternate-script value - a field whose alternate value is in the same script as the preferred set (for example, a Cyrillic-labelled field whose OCR value came back in Latin homoglyphs) is skipped. When `preferred_characters` is `latin` but the document only carries non-Latin text, Didit transliterates the extracted values and exposes `first_name_latin` / `last_name_latin` / etc. as a fallback.""" dl_categories: list[str] | None = None @@ -1554,7 +1004,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsExtraFields(BaseModel): """Place of birth in Latin script. Same rules as `first_name_latin`.""" -class SessionsRetrieveDecisionResponseIdVerificationsFallbackFrom(BaseModel): +class SessionDecisionIdVerificationsFallbackFrom(BaseModel): """Why a non-document method ended the ID step. Set on the declined report when a lookup or a wallet sign-in failed and the country's fallback switch was `decline`. `null` otherwise.""" method: Literal["id_lookup", "wallet"] | str | None = None @@ -1567,7 +1017,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsFallbackFrom(BaseModel): """The configured action that was taken, e.g. `decline`.""" -class SessionsRetrieveDecisionResponseIdVerificationsFrontImageQualityScore(BaseModel): +class SessionDecisionIdVerificationsFrontImageQualityScore(BaseModel): """Quality metrics for the front document image. Each sub-score ranges from 0 to 100.""" focus_score: float | None = None @@ -1589,7 +1039,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsFrontImageQualityScore(Base """Weighted composite of sub-scores (focus 45%, brightness 30%, resolution 25%).""" -class SessionsRetrieveDecisionResponseIdVerificationsIdLookupComparison(BaseModel): +class SessionDecisionIdVerificationsIdLookupComparison(BaseModel): field: ( Literal["identification_number", "full_name", "date_of_birth", "address", "provider"] | str ) @@ -1608,7 +1058,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsIdLookupComparison(BaseMode """Per-field comparison result. `error` appears only on a `provider` row and means that source did not answer; treat it as evidence about the query, never as a field mismatch.""" -class SessionsRetrieveDecisionResponseIdVerificationsIdLookup(BaseModel): +class SessionDecisionIdVerificationsIdLookup(BaseModel): """Registry evidence. Present only when `verification_method` is `id_lookup`; `null` otherwise.""" source: str | None = None @@ -1630,9 +1080,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsIdLookup(BaseModel): ) = None """What the register answered. Only `match`, `partial_match` and `no_match` count against the attempt budget and bill.""" - comparison: list[SessionsRetrieveDecisionResponseIdVerificationsIdLookupComparison] | None = ( - None - ) + comparison: list[SessionDecisionIdVerificationsIdLookupComparison] | None = None """One row per comparable field the source answered on, plus one row per source error. Comparison rows use the normalised field names below rather than the country's request field keys, because several request fields (first and last name, or any of the identifier aliases) collapse into a single comparison. A source that failed appends a row with `field: "provider"` and `result: "error"`, so read `result` before treating a row as a field verdict.""" registry_portrait: str | None = None @@ -1642,7 +1090,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsIdLookup(BaseModel): """Selfie against the registry portrait, 0-100. Only where the register returns a portrait.""" -class SessionsRetrieveDecisionResponseIdVerificationsMatchesUserDetails(BaseModel): +class SessionDecisionIdVerificationsMatchesUserDetails(BaseModel): name: str | None = None """Full name from the matching document""" @@ -1653,7 +1101,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsMatchesUserDetails(BaseMode """Document number of the matching document""" -class SessionsRetrieveDecisionResponseIdVerificationsMatches(BaseModel): +class SessionDecisionIdVerificationsMatches(BaseModel): session_id: str | None = None """Session ID of the matching document""" @@ -1666,7 +1114,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsMatches(BaseModel): verification_date: _dt.datetime | None = None """Date when the matching session was verified""" - user_details: SessionsRetrieveDecisionResponseIdVerificationsMatchesUserDetails | None = None + user_details: SessionDecisionIdVerificationsMatchesUserDetails | None = None status: str | None = None """Status of the matching session""" @@ -1681,7 +1129,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsMatches(BaseModel): """URL of the front document image from the matching document""" -class SessionsRetrieveDecisionResponseIdVerificationsMrz(BaseModel): +class SessionDecisionIdVerificationsMrz(BaseModel): """Parsed Machine-Readable Zone (MRZ) fields extracted from the document. `null` if no MRZ was detected.""" surname: str | None = None @@ -1727,15 +1175,13 @@ class SessionsRetrieveDecisionResponseIdVerificationsMrz(BaseModel): mrz_key: str | None = None -class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressDocumentLocation(BaseModel): +class SessionDecisionIdVerificationsParsedAddressDocumentLocation(BaseModel): latitude: float | None = None longitude: float | None = None -class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsAddressComponents( - BaseModel -): +class SessionDecisionIdVerificationsParsedAddressRawResultsAddressComponents(BaseModel): types: list[str] | None = None long_name: str | None = None @@ -1743,100 +1189,73 @@ class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsAddr short_name: str | None = None -class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryLocation( - BaseModel -): +class SessionDecisionIdVerificationsParsedAddressRawResultsGeometryLocation(BaseModel): lat: float | None = None lng: float | None = None -class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportNortheast( - BaseModel -): +class SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewportNortheast(BaseModel): lat: float | None = None lng: float | None = None -class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest( - BaseModel -): +class SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest(BaseModel): lat: float | None = None lng: float | None = None -class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewport( - BaseModel -): +class SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewport(BaseModel): northeast: ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportNortheast - | None + SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewportNortheast | None ) = None southwest: ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest - | None + SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest | None ) = None -class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometry(BaseModel): - location: ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryLocation - | None - ) = None +class SessionDecisionIdVerificationsParsedAddressRawResultsGeometry(BaseModel): + location: SessionDecisionIdVerificationsParsedAddressRawResultsGeometryLocation | None = None - viewport: ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewport - | None - ) = None + viewport: SessionDecisionIdVerificationsParsedAddressRawResultsGeometryViewport | None = None location_type: str | None = None -class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPointsLocation( - BaseModel -): +class SessionDecisionIdVerificationsParsedAddressRawResultsNavigationPointsLocation(BaseModel): latitude: float | None = None longitude: float | None = None -class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPoints( - BaseModel -): +class SessionDecisionIdVerificationsParsedAddressRawResultsNavigationPoints(BaseModel): location: ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPointsLocation - | None + SessionDecisionIdVerificationsParsedAddressRawResultsNavigationPointsLocation | None ) = None -class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResults(BaseModel): +class SessionDecisionIdVerificationsParsedAddressRawResults(BaseModel): types: list[str] | None = None - geometry: ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometry | None - ) = None + geometry: SessionDecisionIdVerificationsParsedAddressRawResultsGeometry | None = None place_id: str | None = None formatted_address: str | None = None navigation_points: ( - list[SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPoints] - | None + list[SessionDecisionIdVerificationsParsedAddressRawResultsNavigationPoints] | None ) = None address_components: ( - list[ - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsAddressComponents - ] - | None + list[SessionDecisionIdVerificationsParsedAddressRawResultsAddressComponents] | None ) = None -class SessionsRetrieveDecisionResponseIdVerificationsParsedAddress(BaseModel): +class SessionDecisionIdVerificationsParsedAddress(BaseModel): """Structured address details""" city: str | None = None @@ -1860,20 +1279,16 @@ class SessionsRetrieveDecisionResponseIdVerificationsParsedAddress(BaseModel): postal_code: str | None = None - raw_results: SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResults | None = ( - None - ) + raw_results: SessionDecisionIdVerificationsParsedAddressRawResults | None = None address_type: str | None = None - document_location: ( - SessionsRetrieveDecisionResponseIdVerificationsParsedAddressDocumentLocation | None - ) = None + document_location: SessionDecisionIdVerificationsParsedAddressDocumentLocation | None = None formatted_address: str | None = None -class SessionsRetrieveDecisionResponseIdVerificationsWalletVerification(BaseModel): +class SessionDecisionIdVerificationsWalletVerification(BaseModel): """Credential evidence. Present only when `verification_method` is `wallet`; `null` otherwise.""" provider: str | None = None @@ -1910,7 +1325,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsWalletVerification(BaseMode """Selfie against the wallet portrait, 0-100, where the wallet shares one.""" -class SessionsRetrieveDecisionResponseIdVerificationsWarnings(BaseModel): +class SessionDecisionIdVerificationsWarnings(BaseModel): feature: ( Literal[ "ID_VERIFICATION", @@ -1950,7 +1365,7 @@ class SessionsRetrieveDecisionResponseIdVerificationsWarnings(BaseModel): """The workflow graph node ID that generated this warning (for graph-based workflows)""" -class SessionsRetrieveDecisionResponseIdVerifications(BaseModel): +class SessionDecisionIdVerifications(BaseModel): """One ID verification report - the full report for a single ID Verification step. `verification_method` says which of the three methods produced it (document capture, non-doc lookup or wallet sign-in) and the method-specific evidence block is populated accordingly.""" node_id: str | None = None @@ -1965,15 +1380,13 @@ class SessionsRetrieveDecisionResponseIdVerifications(BaseModel): wallet_provider: str | None = None """Catalog wallet id when `verification_method` is `wallet` (`mitid`, `bankid_se`, `itsme`, ...). `null` otherwise.""" - fallback_from: SessionsRetrieveDecisionResponseIdVerificationsFallbackFrom | None = None + fallback_from: SessionDecisionIdVerificationsFallbackFrom | None = None """Why a non-document method ended the ID step. Set on the declined report when a lookup or a wallet sign-in failed and the country's fallback switch was `decline`. `null` otherwise.""" - id_lookup: SessionsRetrieveDecisionResponseIdVerificationsIdLookup | None = None + id_lookup: SessionDecisionIdVerificationsIdLookup | None = None """Registry evidence. Present only when `verification_method` is `id_lookup`; `null` otherwise.""" - wallet_verification: ( - SessionsRetrieveDecisionResponseIdVerificationsWalletVerification | None - ) = None + wallet_verification: SessionDecisionIdVerificationsWalletVerification | None = None """Credential evidence. Present only when `verification_method` is `wallet`; `null` otherwise.""" status: Literal["Not Finished", "Approved", "Declined", "In Review", "Expired"] | str | None = ( @@ -2065,13 +1478,13 @@ class SessionsRetrieveDecisionResponseIdVerifications(BaseModel): nationality: str | None = None """ISO nationality code""" - extra_fields: SessionsRetrieveDecisionResponseIdVerificationsExtraFields | None = None + extra_fields: SessionDecisionIdVerificationsExtraFields | None = None """Additional document fields. Includes document-specific attributes (e.g. `dl_categories`, `blood_group`, `sponsor`, `employer`, `dl_class_code_b_from`, `dl_class_code_b_to`). Driver license class validity dates are returned as `dl_class_code__from` and `dl_class_code__to`, and per-class restriction/information codes (e.g. column 12 of UK licences) as `dl_class_code__notes`. When the document carries names, address, or place of birth in multiple scripts, this object also includes alternate-script versions of those fields. The workflow setting `preferred_characters` (`latin` or `non_latin`) controls which script populates the top-level fields (`first_name`, `last_name`, `full_name`, `address`, `place_of_birth`); the opposite script lands here as `first_name_non_latin` / `last_name_non_latin` / `full_name_non_latin` / `middle_name_non_latin` / `address_non_latin` / `place_of_birth_non_latin` (when `preferred_characters` is `latin`) or with a `_latin` suffix (when `preferred_characters` is `non_latin`). Keys appear only when the OCR actually extracts a real alternate-script value - a field whose alternate value is in the same script as the preferred set (for example, a Cyrillic-labelled field whose OCR value came back in Latin homoglyphs) is skipped. When `preferred_characters` is `latin` but the document only carries non-Latin text, Didit transliterates the extracted values and exposes `first_name_latin` / `last_name_latin` / etc. as a fallback.""" - mrz: SessionsRetrieveDecisionResponseIdVerificationsMrz | None = None + mrz: SessionDecisionIdVerificationsMrz | None = None """Parsed Machine-Readable Zone (MRZ) fields extracted from the document. `null` if no MRZ was detected.""" - parsed_address: SessionsRetrieveDecisionResponseIdVerificationsParsedAddress | None = None + parsed_address: SessionDecisionIdVerificationsParsedAddress | None = None """Structured address details""" extra_files: list[str] | None = None @@ -2084,31 +1497,27 @@ class SessionsRetrieveDecisionResponseIdVerifications(BaseModel): """Face match score between the portrait extracted from the back document image and the front camera image""" document_selfie_face_matches: ( - list[SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatches] | None + list[SessionDecisionIdVerificationsDocumentSelfieFaceMatches] | None ) = None """Previous sessions or face blocklist entries whose face matched the selfie the front camera took during document capture (Silent Capture Face Search). Same entry format as `liveness_checks[].matches`. `null` for sessions created before this field existed or when the search did not run; an empty array when it ran and found nothing. `match_image_url` is a presigned URL that expires.""" - front_image_quality_score: ( - SessionsRetrieveDecisionResponseIdVerificationsFrontImageQualityScore | None - ) = None + front_image_quality_score: SessionDecisionIdVerificationsFrontImageQualityScore | None = None """Quality metrics for the front document image. Each sub-score ranges from 0 to 100.""" - back_image_quality_score: ( - SessionsRetrieveDecisionResponseIdVerificationsBackImageQualityScore | None - ) = None + back_image_quality_score: SessionDecisionIdVerificationsBackImageQualityScore | None = None """Quality metrics for the back document image. Same structure as front_image_quality_score.""" - warnings: list[SessionsRetrieveDecisionResponseIdVerificationsWarnings] | None = None + warnings: list[SessionDecisionIdVerificationsWarnings] | None = None """Document verification warnings""" - matches: list[SessionsRetrieveDecisionResponseIdVerificationsMatches] | None = None + matches: list[SessionDecisionIdVerificationsMatches] | None = None """Matching documents found during duplicate/blocklist detection (up to 5 matches)""" - document_liveness: SessionsRetrieveDecisionResponseIdVerificationsDocumentLiveness | None = None + document_liveness: SessionDecisionIdVerificationsDocumentLiveness | None = None """Document-liveness scores per fraud type, reported whether or not a warning fired, so an approved document can still be audited. `null` when the check did not run.""" -class SessionsRetrieveDecisionResponseIpAnalysesIdDocumentLocation(BaseModel): +class SessionDecisionIpAnalysesIdDocumentLocation(BaseModel): """Coordinates, or `null` when unavailable.""" latitude: float | None = None @@ -2116,10 +1525,10 @@ class SessionsRetrieveDecisionResponseIpAnalysesIdDocumentLocation(BaseModel): longitude: float | None = None -class SessionsRetrieveDecisionResponseIpAnalysesIdDocument(BaseModel): +class SessionDecisionIpAnalysesIdDocument(BaseModel): """Location extracted from the ID document address plus distances (km) to the IP geolocation and the POA document address. `location` is `null` when no document address could be geocoded.""" - location: SessionsRetrieveDecisionResponseIpAnalysesIdDocumentLocation | None = None + location: SessionDecisionIpAnalysesIdDocumentLocation | None = None """Coordinates, or `null` when unavailable.""" distance_from_ip: float | None = None @@ -2127,7 +1536,7 @@ class SessionsRetrieveDecisionResponseIpAnalysesIdDocument(BaseModel): distance_from_poa_document: float | None = None -class SessionsRetrieveDecisionResponseIpAnalysesIpLocation(BaseModel): +class SessionDecisionIpAnalysesIpLocation(BaseModel): """Coordinates, or `null` when unavailable.""" latitude: float | None = None @@ -2135,10 +1544,10 @@ class SessionsRetrieveDecisionResponseIpAnalysesIpLocation(BaseModel): longitude: float | None = None -class SessionsRetrieveDecisionResponseIpAnalysesIp(BaseModel): +class SessionDecisionIpAnalysesIp(BaseModel): """IP geolocation of this entry plus great-circle distances (km) to the ID document address and the POA document address. Distances are `null` when either side has no resolvable location.""" - location: SessionsRetrieveDecisionResponseIpAnalysesIpLocation | None = None + location: SessionDecisionIpAnalysesIpLocation | None = None """Coordinates, or `null` when unavailable.""" distance_from_id_document: float | None = None @@ -2146,7 +1555,7 @@ class SessionsRetrieveDecisionResponseIpAnalysesIp(BaseModel): distance_from_poa_document: float | None = None -class SessionsRetrieveDecisionResponseIpAnalysesMatchesDeviceInfo(BaseModel): +class SessionDecisionIpAnalysesMatchesDeviceInfo(BaseModel): """Device details observed on the matching session.""" device_brand: str | None = None @@ -2162,7 +1571,7 @@ class SessionsRetrieveDecisionResponseIpAnalysesMatchesDeviceInfo(BaseModel): device_fingerprint: str | None = None -class SessionsRetrieveDecisionResponseIpAnalysesMatchesLocationInfo(BaseModel): +class SessionDecisionIpAnalysesMatchesLocationInfo(BaseModel): """Network and geolocation details observed on the matching session.""" ip_address: str | None = None @@ -2180,7 +1589,7 @@ class SessionsRetrieveDecisionResponseIpAnalysesMatchesLocationInfo(BaseModel): is_data_center: bool | None = None -class SessionsRetrieveDecisionResponseIpAnalysesMatches(BaseModel): +class SessionDecisionIpAnalysesMatches(BaseModel): session_id: str | None = None """Session ID of the matching session.""" @@ -2216,10 +1625,10 @@ class SessionsRetrieveDecisionResponseIpAnalysesMatches(BaseModel): source: Literal["session"] | str | None = None """Always `session` for device/IP matches - every match is another verification session in your application.""" - device_info: SessionsRetrieveDecisionResponseIpAnalysesMatchesDeviceInfo | None = None + device_info: SessionDecisionIpAnalysesMatchesDeviceInfo | None = None """Device details observed on the matching session.""" - location_info: SessionsRetrieveDecisionResponseIpAnalysesMatchesLocationInfo | None = None + location_info: SessionDecisionIpAnalysesMatchesLocationInfo | None = None """Network and geolocation details observed on the matching session.""" confidence: float | None = None @@ -2238,7 +1647,7 @@ class SessionsRetrieveDecisionResponseIpAnalysesMatches(BaseModel): """Why the recovery match passed the false-positive hard gate: `hardware_root_match` (a hardware-rooted device identifier matched exactly) or, on configurations that admit probe-only evidence, `exact_composite_plus__families` / `very_high_similarity_plus__families`. Present only when `match_source` is `recovered_high`.""" -class SessionsRetrieveDecisionResponseIpAnalysesPoaDocumentLocation(BaseModel): +class SessionDecisionIpAnalysesPoaDocumentLocation(BaseModel): """Coordinates, or `null` when unavailable.""" latitude: float | None = None @@ -2246,10 +1655,10 @@ class SessionsRetrieveDecisionResponseIpAnalysesPoaDocumentLocation(BaseModel): longitude: float | None = None -class SessionsRetrieveDecisionResponseIpAnalysesPoaDocument(BaseModel): +class SessionDecisionIpAnalysesPoaDocument(BaseModel): """Location extracted from the Proof of Address document plus distances (km) to the IP geolocation and the ID document address. `location` is `null` when the session has no POA step or the address could not be geocoded.""" - location: SessionsRetrieveDecisionResponseIpAnalysesPoaDocumentLocation | None = None + location: SessionDecisionIpAnalysesPoaDocumentLocation | None = None """Coordinates, or `null` when unavailable.""" distance_from_ip: float | None = None @@ -2257,7 +1666,7 @@ class SessionsRetrieveDecisionResponseIpAnalysesPoaDocument(BaseModel): distance_from_id_document: float | None = None -class SessionsRetrieveDecisionResponseIpAnalysesWarnings(BaseModel): +class SessionDecisionIpAnalysesWarnings(BaseModel): feature: ( Literal[ "ID_VERIFICATION", @@ -2295,7 +1704,7 @@ class SessionsRetrieveDecisionResponseIpAnalysesWarnings(BaseModel): """The workflow graph node ID that generated this warning""" -class SessionsRetrieveDecisionResponseIpAnalyses(BaseModel): +class SessionDecisionIpAnalyses(BaseModel): node_id: str | None = None """Unique identifier for this Device & IP Analysis step in the workflow graph.""" @@ -2361,24 +1770,22 @@ class SessionsRetrieveDecisionResponseIpAnalyses(BaseModel): time_zone_offset: str | None = None """Time zone offset""" - ip: SessionsRetrieveDecisionResponseIpAnalysesIp | None = None + ip: SessionDecisionIpAnalysesIp | None = None """IP geolocation of this entry plus great-circle distances (km) to the ID document address and the POA document address. Distances are `null` when either side has no resolvable location.""" - id_document: SessionsRetrieveDecisionResponseIpAnalysesIdDocument | None = None + id_document: SessionDecisionIpAnalysesIdDocument | None = None """Location extracted from the ID document address plus distances (km) to the IP geolocation and the POA document address. `location` is `null` when no document address could be geocoded.""" - poa_document: SessionsRetrieveDecisionResponseIpAnalysesPoaDocument | None = None + poa_document: SessionDecisionIpAnalysesPoaDocument | None = None """Location extracted from the Proof of Address document plus distances (km) to the IP geolocation and the ID document address. `location` is `null` when the session has no POA step or the address could not be geocoded.""" - warnings: list[SessionsRetrieveDecisionResponseIpAnalysesWarnings] | None = None + warnings: list[SessionDecisionIpAnalysesWarnings] | None = None - matches: list[SessionsRetrieveDecisionResponseIpAnalysesMatches] | None = None + matches: list[SessionDecisionIpAnalysesMatches] | None = None """Cross-session matches found for this entry's IP address or device identity. Every match is another verification session in your application (`source` is always `session`) that shares the same value, always belonging to a different vendor user / `vendor_data` than the current session, capped at 5 per collector (`device_fingerprint` sums two independently capped collectors, so up to 10 entries are possible). Every item carries `match_source`, `confidence`, `match_mode`, `device_info` and `location_info`; the keys `recovery_similarity`, `tls_ja4_corroborated` and `recovery_gate_reason` are present only when `match_source` is `recovered_high`.""" -class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories( - BaseModel -): +class SessionDecisionKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories(BaseModel): """Per-category summary derived from the person's AML hits. `null` when there are no hits.""" sanctions: Literal["match", "clear"] | str | None = None @@ -2390,9 +1797,7 @@ class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersKyc warnings: Literal["match", "clear"] | str | None = None -class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures( - BaseModel -): +class SessionDecisionKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures(BaseModel): feature: str | None = None """Feature display name (e.g. `ID_VERIFICATION`, `LIVENESS`, `AML`).""" @@ -2402,7 +1807,7 @@ class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersVer """Feature-level status inside the child session.""" -class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners(BaseModel): +class SessionDecisionKeyPeopleChecksRegistryBeneficialOwners(BaseModel): uuid: str | None = None name: str | None = None @@ -2479,9 +1884,7 @@ class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners(Ba """Human-readable label of the child session's workflow. `null` when unset.""" verification_workflow_features: ( - list[ - SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures - ] + list[SessionDecisionKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures] | None ) = None """Per-feature progress of the child verification session, e.g. `[{"feature": "ID_VERIFICATION", "status": "Approved"}]`. `null` when the child session has no resolvable feature list.""" @@ -2502,8 +1905,7 @@ class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners(Ba """When the child KYC session reached `Approved`. `null` while the child session has any other status.""" kyc_aml_categories: ( - SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories - | None + SessionDecisionKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories | None ) = None """Per-category summary derived from the person's AML hits. `null` when there are no hits.""" @@ -2524,7 +1926,7 @@ class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners(Ba """Appointment date when reported.""" -class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersKycAmlCategories(BaseModel): +class SessionDecisionKeyPeopleChecksRegistryOfficersKycAmlCategories(BaseModel): """Per-category summary derived from the person's AML hits. `null` when there are no hits.""" sanctions: Literal["match", "clear"] | str | None = None @@ -2536,9 +1938,7 @@ class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersKycAmlCateg warnings: Literal["match", "clear"] | str | None = None -class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures( - BaseModel -): +class SessionDecisionKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures(BaseModel): feature: str | None = None """Feature display name (e.g. `ID_VERIFICATION`, `LIVENESS`, `AML`).""" @@ -2548,7 +1948,7 @@ class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersVerificatio """Feature-level status inside the child session.""" -class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers(BaseModel): +class SessionDecisionKeyPeopleChecksRegistryOfficers(BaseModel): uuid: str | None = None name: str | None = None @@ -2634,10 +2034,7 @@ class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers(BaseModel) """Human-readable label of the child session's workflow. `null` when unset.""" verification_workflow_features: ( - list[ - SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures - ] - | None + list[SessionDecisionKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures] | None ) = None """Per-feature progress of the child verification session, e.g. `[{"feature": "ID_VERIFICATION", "status": "Approved"}]`. `null` when the child session has no resolvable feature list.""" @@ -2656,9 +2053,7 @@ class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers(BaseModel) kyc_verified_at: _dt.datetime | None = None """When the child KYC session reached `Approved`. `null` while the child session has any other status.""" - kyc_aml_categories: ( - SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersKycAmlCategories | None - ) = None + kyc_aml_categories: SessionDecisionKeyPeopleChecksRegistryOfficersKycAmlCategories | None = None """Per-category summary derived from the person's AML hits. `null` when there are no hits.""" kyc_notification_sent_at: str | None = None @@ -2680,19 +2075,17 @@ class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers(BaseModel) """Registry-reported birth month. Null when only the year is known.""" -class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistry(BaseModel): +class SessionDecisionKeyPeopleChecksRegistry(BaseModel): """Parties discovered via the registry check.""" - officers: list[SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers] | None = None + officers: list[SessionDecisionKeyPeopleChecksRegistryOfficers] | None = None """Officers discovered via the registry check, each enriched with the status of any child KYC session and AML screening spawned for them.""" - beneficial_owners: ( - list[SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners] | None - ) = None + beneficial_owners: list[SessionDecisionKeyPeopleChecksRegistryBeneficialOwners] | None = None """Beneficial owners discovered via the registry check, each enriched with the status of any child KYC session and AML screening spawned for them.""" -class SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesRoles(BaseModel): +class SessionDecisionKeyPeopleChecksSubmittedPartiesRoles(BaseModel): role: ( Literal[ "director", @@ -2722,9 +2115,7 @@ class SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesRoles(BaseM voting_percent: float | None = None -class SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures( - BaseModel -): +class SessionDecisionKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures(BaseModel): feature: str | None = None """Feature display name (e.g. `ID_VERIFICATION`, `LIVENESS`, `AML`).""" @@ -2734,7 +2125,7 @@ class SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesVerificatio """Feature-level status inside the child session.""" -class SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties(BaseModel): +class SessionDecisionKeyPeopleChecksSubmittedParties(BaseModel): """Party submitted by the business admin via the Key People flow (source=USER).""" uuid: str | None = None @@ -2777,7 +2168,7 @@ class SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties(BaseModel) requires_verification: bool | None = None """True when a child KYC / KYB sub-session is required for this party based on role settings and ownership thresholds.""" - roles: list[SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesRoles] | None = None + roles: list[SessionDecisionKeyPeopleChecksSubmittedPartiesRoles] | None = None """All roles held by this party, each with its own ownership and voting percentages. There is no scalar `role`/`ownership_percent` on the party itself.""" kyc_session_id: str | None = None @@ -2837,10 +2228,7 @@ class SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties(BaseModel) """Human-readable label of the child session's workflow. `null` when unset.""" verification_workflow_features: ( - list[ - SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures - ] - | None + list[SessionDecisionKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures] | None ) = None """Per-feature progress of the child verification session, e.g. `[{"feature": "ID_VERIFICATION", "status": "Approved"}]`. `null` when the child session has no resolvable feature list.""" @@ -2848,13 +2236,13 @@ class SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties(BaseModel) """Stable internal identifier of the verified individual behind the child session. `null` when not resolved.""" -class SessionsRetrieveDecisionResponseKeyPeopleChecksSubmitted(BaseModel): +class SessionDecisionKeyPeopleChecksSubmitted(BaseModel): """Parties submitted by the business admin during the Key People flow (BusinessParty rows where source=USER).""" - parties: list[SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties] | None = None + parties: list[SessionDecisionKeyPeopleChecksSubmittedParties] | None = None -class SessionsRetrieveDecisionResponseKeyPeopleChecksUboKycSummary(BaseModel): +class SessionDecisionKeyPeopleChecksUboKycSummary(BaseModel): """Aggregate UBO KYC progress, or null when no UBOs have linked KYC sessions.""" total: int | None = None @@ -2867,26 +2255,26 @@ class SessionsRetrieveDecisionResponseKeyPeopleChecksUboKycSummary(BaseModel): pending: int | None = None -class SessionsRetrieveDecisionResponseKeyPeopleChecks(BaseModel): +class SessionDecisionKeyPeopleChecks(BaseModel): """Aggregate check over the company's officers + UBOs. Uses the `registry` / `submitted` shape. Present only when session_kind = business.""" status: str | None = None node_id: str | None = None - registry: SessionsRetrieveDecisionResponseKeyPeopleChecksRegistry | None = None + registry: SessionDecisionKeyPeopleChecksRegistry | None = None """Parties discovered via the registry check.""" - submitted: SessionsRetrieveDecisionResponseKeyPeopleChecksSubmitted | None = None + submitted: SessionDecisionKeyPeopleChecksSubmitted | None = None """Parties submitted by the business admin during the Key People flow (BusinessParty rows where source=USER).""" - ubo_kyc_summary: SessionsRetrieveDecisionResponseKeyPeopleChecksUboKycSummary | None = None + ubo_kyc_summary: SessionDecisionKeyPeopleChecksUboKycSummary | None = None """Aggregate UBO KYC progress, or null when no UBOs have linked KYC sessions.""" warnings: list[dict[str, object]] | None = None -class SessionsRetrieveDecisionResponseLivenessChecksMatchesUserDetails(BaseModel): +class SessionDecisionLivenessChecksMatchesUserDetails(BaseModel): """Identity data from the matching session or imported User profile. Always `null` for `retained_template` and `list_entry` matches.""" full_name: str | None = None @@ -2896,7 +2284,7 @@ class SessionsRetrieveDecisionResponseLivenessChecksMatchesUserDetails(BaseModel document_number: str | None = None -class SessionsRetrieveDecisionResponseLivenessChecksMatches(BaseModel): +class SessionDecisionLivenessChecksMatches(BaseModel): session_id: str | None = None """Session ID of the matching face. `null` when `source` is `imported`, `list_entry`, or `retained_template`.""" @@ -2917,7 +2305,7 @@ class SessionsRetrieveDecisionResponseLivenessChecksMatches(BaseModel): verification_date: _dt.datetime | None = None """When the matching face was captured, or when a retained template was created. `null` for list entries.""" - user_details: SessionsRetrieveDecisionResponseLivenessChecksMatchesUserDetails | None = None + user_details: SessionDecisionLivenessChecksMatchesUserDetails | None = None """Identity data from the matching session or imported User profile. Always `null` for `retained_template` and `list_entry` matches.""" match_image_url: str | None = None @@ -2939,7 +2327,7 @@ class SessionsRetrieveDecisionResponseLivenessChecksMatches(BaseModel): """Origin of the matching face: `session`, `imported` (User face upload), `list_entry` (allow/block list), or `retained_template` (an image-free biometric template stored on a User, listed under `/v3/biometric-templates/`).""" -class SessionsRetrieveDecisionResponseLivenessChecksWarnings(BaseModel): +class SessionDecisionLivenessChecksWarnings(BaseModel): feature: ( Literal[ "ID_VERIFICATION", @@ -2977,7 +2365,7 @@ class SessionsRetrieveDecisionResponseLivenessChecksWarnings(BaseModel): """The workflow graph node ID that generated this warning""" -class SessionsRetrieveDecisionResponseLivenessChecks(BaseModel): +class SessionDecisionLivenessChecks(BaseModel): """One liveness report - the full report for a single liveness pass.""" node_id: str | None = None @@ -3003,10 +2391,10 @@ class SessionsRetrieveDecisionResponseLivenessChecks(BaseModel): age_estimation: float | None = None """Estimated age of the person""" - matches: list[SessionsRetrieveDecisionResponseLivenessChecksMatches] | None = None + matches: list[SessionDecisionLivenessChecksMatches] | None = None """Similar faces found by the duplicate-face search (top matches, blocklisted/allowlisted entries first). Each entry's `source` tells you where the matching face came from: `session` (another verification session), `imported` (a face uploaded against a vendor user), `list_entry` (an org-managed allow/block list face), or `retained_template` (an image-free biometric template stored on a User, listed under `/v3/biometric-templates/`). `session_id`, `session_number`, `status` and `api_service` are `null` for non-session sources. `imported` matches still populate `vendor_data`, `verification_date` and - when the vendor user has a name on file - `user_details` (with `full_name` only) from the vendor-user record; `retained_template` matches populate `vendor_user_id`, `vendor_data`, `biometric_template_id` and `verification_date`, while their image and identity fields are `null`; `list_entry` matches return all of these as `null`.""" - warnings: list[SessionsRetrieveDecisionResponseLivenessChecksWarnings] | None = None + warnings: list[SessionDecisionLivenessChecksWarnings] | None = None face_quality: float | None = None """Face image quality score (0-100%). Only available for passive liveness.""" @@ -3015,13 +2403,17 @@ class SessionsRetrieveDecisionResponseLivenessChecks(BaseModel): """Face luminance/brightness score (0-100%). Only available for passive liveness.""" -class SessionsRetrieveDecisionResponseNfcVerificationsAuthenticity(BaseModel): +class SessionDecisionNfcVerificationsAuthenticity(BaseModel): + """ICAO passive authentication: SOD signature (`sod_integrity`) and Data Group hashes (`dg_integrity`). `null` for a JPKI read, which reports its proof under `jpki`.""" + sod_integrity: bool | None = None dg_integrity: bool | None = None -class SessionsRetrieveDecisionResponseNfcVerificationsCertificateSummary(BaseModel): +class SessionDecisionNfcVerificationsCertificateSummary(BaseModel): + """Summary of the ICAO Document Signer Certificate. `null` for a JPKI read (`chip_protocol: "jpki"`) and for skipped steps.""" + issuer: str | None = None subject: str | None = None @@ -3033,7 +2425,9 @@ class SessionsRetrieveDecisionResponseNfcVerificationsCertificateSummary(BaseMod not_valid_before: _dt.datetime | None = None -class SessionsRetrieveDecisionResponseNfcVerificationsChipData(BaseModel): +class SessionDecisionNfcVerificationsChipData(BaseModel): + """Personal data parsed from the ICAO chip's Data Groups. `null` for a JPKI read (`chip_protocol: "jpki"`) and for skipped steps.""" + document_type: str | None = None issuing_country: str | None = None @@ -3057,7 +2451,101 @@ class SessionsRetrieveDecisionResponseNfcVerificationsChipData(BaseModel): place_of_birth: str | None = None -class SessionsRetrieveDecisionResponseNfcVerificationsWarnings(BaseModel): +class SessionDecisionNfcVerificationsJpkiAttributes(BaseModel): + """The holder's data from the signature certificate, once its chain is valid. `null` when the signature certificate was not requested or did not chain to the JPKI certification authority.""" + + full_name: str | None = None + """Name as registered, usually in Japanese script.""" + + date_of_birth: _dt.date | None = None + """Date of birth, `YYYY-MM-DD`. `null` when the registered date is incomplete.""" + + gender: Literal["M", "F"] | str | None = None + """`M`, `F`, or `null` when not registered.""" + + address: str | None = None + """Registered address.""" + + name_substitute_characters: str | None = None + """Position string marking the characters of `full_name` that were replaced by a substitute because the original character is outside the standard character set. `null` when none were replaced.""" + + address_substitute_characters: str | None = None + """Position string marking substitute characters in `address`, as for `name_substitute_characters`.""" + + +class SessionDecisionNfcVerificationsJpkiCertificates(BaseModel): + """What one certificate read from the card proved. Never the certificate itself or its serial number.""" + + type: Literal["user_authentication", "signature"] | str | None = None + """Which card certificate this entry describes.""" + + chain_valid: bool | None = None + """`true` when the certificate chains to the JPKI certification authority of its type, is inside its validity period and is a My Number Card certificate of that type.""" + + signature_valid: bool | None = None + """`true` when the card's signature over the one-time challenge verifies against this certificate.""" + + ca_generation: str | None = None + """Generation of the JPKI certification authority that issued the certificate, for example `2023`. `null` when no trusted issuer was found.""" + + not_valid_before: _dt.datetime | None = None + """Start of the certificate's validity period. `null` when the certificate could not be read.""" + + not_valid_after: _dt.datetime | None = None + """End of the certificate's validity period. `null` when the certificate could not be read.""" + + failure: ( + Literal[ + "unreadable_certificate", + "untrusted_issuer", + "expired", + "not_yet_valid", + "wrong_policy", + "key_usage", + ] + | str + | None + ) = None + """Why `chain_valid` is `false`: `unreadable_certificate`, `untrusted_issuer` (not issued by the JPKI certification authority of its type), `expired`, `not_yet_valid`, `wrong_policy` (not a My Number Card certificate of that type) or `key_usage` (the key may not sign). `null` when the chain is valid.""" + + +class SessionDecisionNfcVerificationsJpki(BaseModel): + """What a Japan My Number Card read through JPKI proved. Present when `chip_protocol` is `jpki` and the card was read. `null` for ICAO chips, for skipped steps, and when the NFC step closed without any card read (that case raises `JPKI_SIGNATURE_INVALID`). Never carries the certificates, their serial numbers, the signatures or the 12-digit My Number.""" + + certificate_types: list[Literal["user_authentication", "signature"] | str] | None = None + """The card certificates read. `user_authentication` (4-digit PIN) is always read; `signature` (signature PIN) is read when the NFC node's `jpki_request_attributes` setting is on.""" + + chain_valid: bool | None = None + """`true` when every certificate read chains to the JPKI certification authority, is inside its validity period and is a My Number Card certificate of its type.""" + + signature_valid: bool | None = None + """`true` when every signature the card made over the one-time challenge verifies. The card only signs after its PIN is entered, so this proves possession of the card and knowledge of the PIN.""" + + revocation_status: Literal["good", "revoked", "unchecked"] | str | None = None + """`good` when the revocation check confirmed every certificate is valid, `revoked` when any certificate is revoked (for example a card reported lost), `unchecked` when no revocation answer was available. `unchecked` is never treated as `good`: the NFC node's `jpki_revocation_unchecked_action` decides the outcome (default `REVIEW`).""" + + ca_generation: str | None = None + """Generation of the JPKI certification authority that issued the certificates, for example `2023`. `null` when no certificate chains to one.""" + + certificate_expires_at: _dt.datetime | None = None + """The earliest expiry among the certificates read.""" + + certificates: list[SessionDecisionNfcVerificationsJpkiCertificates] | None = None + """Per-certificate results, one entry per certificate read.""" + + attributes: SessionDecisionNfcVerificationsJpkiAttributes | None = None + """The holder's data from the signature certificate, once its chain is valid. `null` when the signature certificate was not requested or did not chain to the JPKI certification authority.""" + + attributes_match_ocr: bool | None = None + """`true` when the attributes match the data read from the photo of the same card, `false` when any attribute differs (`JPKI_AND_OCR_DATA_NOT_SAME`), `null` when nothing could be compared, including whenever `attributes` is `null`.""" + + mismatched_attributes: ( + list[Literal["full_name", "date_of_birth", "gender", "address"] | str] | None + ) = None + """Names of the attributes that differ from the photo data, never their values. Empty when everything compared matches.""" + + +class SessionDecisionNfcVerificationsWarnings(BaseModel): feature: ( Literal[ "ID_VERIFICATION", @@ -3095,8 +2583,8 @@ class SessionsRetrieveDecisionResponseNfcVerificationsWarnings(BaseModel): """The workflow graph node ID that generated this warning""" -class SessionsRetrieveDecisionResponseNfcVerifications(BaseModel): - """One NFC report - the full report for a single ePassport chip read.""" +class SessionDecisionNfcVerifications(BaseModel): + """One NFC report: the full report for a single chip read (an ICAO 9303 ePassport or eID chip, or a Japan My Number Card read through JPKI).""" node_id: str | None = None """Workflow graph node id of the NFC step that produced this report. Use it to correlate the report with the workflow definition.""" @@ -3117,28 +2605,38 @@ class SessionsRetrieveDecisionResponseNfcVerifications(BaseModel): "DEVICE_WITHOUT_NFC", "INTEGRATION_WITHOUT_NFC_ACCESS", "MRZ_KEY_UNAVAILABLE", + "JPKI_UNAVAILABLE", ] | str | None ) = None """Why this NFC step was skipped. Only set when `is_nfc_skipped` is `true`. Same taxonomy as the session-level `nfc_skip_reason`, which additionally covers the cases where NFC never produced a report at all.""" + chip_protocol: Literal["icao", "jpki"] | str | None = None + """How the NFC step reads the chip. `icao`: ICAO 9303 ePassport or eID chip, opened with the access key from the MRZ. `jpki`: Japan My Number Card, where the card signs a one-time challenge after the holder enters the card PIN in a native SDK; the result is under `jpki`. Also set on skipped steps, so you can tell a skipped My Number Card read apart.""" + + jpki: SessionDecisionNfcVerificationsJpki | None = None + """What a Japan My Number Card read through JPKI proved. Present when `chip_protocol` is `jpki` and the card was read. `null` for ICAO chips, for skipped steps, and when the NFC step closed without any card read (that case raises `JPKI_SIGNATURE_INVALID`). Never carries the certificates, their serial numbers, the signatures or the 12-digit My Number.""" + portrait_image: str | None = None + """Presigned URL to the portrait read from the chip (ICAO DG2). `null` for a JPKI read (`chip_protocol: "jpki"`) and for skipped steps.""" signature_image: str | None = None + """Presigned URL to the holder's signature image read from the chip (ICAO DG7), when the chip stores one. `null` for a JPKI read (`chip_protocol: "jpki"`) and for skipped steps.""" - chip_data: SessionsRetrieveDecisionResponseNfcVerificationsChipData | None = None + chip_data: SessionDecisionNfcVerificationsChipData | None = None + """Personal data parsed from the ICAO chip's Data Groups. `null` for a JPKI read (`chip_protocol: "jpki"`) and for skipped steps.""" - authenticity: SessionsRetrieveDecisionResponseNfcVerificationsAuthenticity | None = None + authenticity: SessionDecisionNfcVerificationsAuthenticity | None = None + """ICAO passive authentication: SOD signature (`sod_integrity`) and Data Group hashes (`dg_integrity`). `null` for a JPKI read, which reports its proof under `jpki`.""" - certificate_summary: ( - SessionsRetrieveDecisionResponseNfcVerificationsCertificateSummary | None - ) = None + certificate_summary: SessionDecisionNfcVerificationsCertificateSummary | None = None + """Summary of the ICAO Document Signer Certificate. `null` for a JPKI read (`chip_protocol: "jpki"`) and for skipped steps.""" - warnings: list[SessionsRetrieveDecisionResponseNfcVerificationsWarnings] | None = None + warnings: list[SessionDecisionNfcVerificationsWarnings] | None = None -class SessionsRetrieveDecisionResponsePhoneVerificationsCarrier(BaseModel): +class SessionDecisionPhoneVerificationsCarrier(BaseModel): name: str | None = None """Name of the phone carrier""" @@ -3146,7 +2644,7 @@ class SessionsRetrieveDecisionResponsePhoneVerificationsCarrier(BaseModel): """Type of phone line (mobile, landline, etc)""" -class SessionsRetrieveDecisionResponsePhoneVerificationsLifecycleDetails(BaseModel): +class SessionDecisionPhoneVerificationsLifecycleDetails(BaseModel): """Event-specific payload. Send events carry `status`, `reason`, `channel`, `actual_channel`; delivery events carry `channel`, `status`; code-check events carry `code_tried`, `status`; final status events carry `reason` - or `null` when there is no reason to report (e.g. `PHONE_VERIFICATION_APPROVED`).""" status: str | None = None @@ -3160,18 +2658,18 @@ class SessionsRetrieveDecisionResponsePhoneVerificationsLifecycleDetails(BaseMod code_tried: str | None = None -class SessionsRetrieveDecisionResponsePhoneVerificationsLifecycle(BaseModel): +class SessionDecisionPhoneVerificationsLifecycle(BaseModel): type: str | None = None timestamp: _dt.datetime | None = None - details: SessionsRetrieveDecisionResponsePhoneVerificationsLifecycleDetails | None = None + details: SessionDecisionPhoneVerificationsLifecycleDetails | None = None """Event-specific payload. Send events carry `status`, `reason`, `channel`, `actual_channel`; delivery events carry `channel`, `status`; code-check events carry `code_tried`, `status`; final status events carry `reason` - or `null` when there is no reason to report (e.g. `PHONE_VERIFICATION_APPROVED`).""" fee: float | None = None -class SessionsRetrieveDecisionResponsePhoneVerificationsMatches(BaseModel): +class SessionDecisionPhoneVerificationsMatches(BaseModel): session_id: str | None = None session_number: int | None = None @@ -3192,7 +2690,7 @@ class SessionsRetrieveDecisionResponsePhoneVerificationsMatches(BaseModel): """Where this match came from. `session` means another verification session in your application matched on this value. `list_entry` means the value was hit against an org-managed list (allowlist/blocklist) - see the management API lists overview at `/management-api/lists/overview` for how to manage those lists.""" -class SessionsRetrieveDecisionResponsePhoneVerificationsWarnings(BaseModel): +class SessionDecisionPhoneVerificationsWarnings(BaseModel): feature: ( Literal[ "ID_VERIFICATION", @@ -3230,7 +2728,7 @@ class SessionsRetrieveDecisionResponsePhoneVerificationsWarnings(BaseModel): """The workflow graph node ID that generated this warning""" -class SessionsRetrieveDecisionResponsePhoneVerifications(BaseModel): +class SessionDecisionPhoneVerifications(BaseModel): node_id: str | None = None """Unique identifier for this phone step in the workflow graph.""" @@ -3254,7 +2752,7 @@ class SessionsRetrieveDecisionResponsePhoneVerifications(BaseModel): country_name: str | None = None """Full country name""" - carrier: SessionsRetrieveDecisionResponsePhoneVerificationsCarrier | None = None + carrier: SessionDecisionPhoneVerificationsCarrier | None = None is_disposable: bool | None = None """Whether the phone number is from a disposable service""" @@ -3271,17 +2769,17 @@ class SessionsRetrieveDecisionResponsePhoneVerifications(BaseModel): verified_at: _dt.datetime | None = None """Timestamp when the phone was verified""" - lifecycle: list[SessionsRetrieveDecisionResponsePhoneVerificationsLifecycle] | None = None + lifecycle: list[SessionDecisionPhoneVerificationsLifecycle] | None = None """Chronological list of events in the phone verification lifecycle.""" - warnings: list[SessionsRetrieveDecisionResponsePhoneVerificationsWarnings] | None = None + warnings: list[SessionDecisionPhoneVerificationsWarnings] | None = None """List of warnings related to phone verification""" - matches: list[SessionsRetrieveDecisionResponsePhoneVerificationsMatches] | None = None + matches: list[SessionDecisionPhoneVerificationsMatches] | None = None """Matches found for this phone number. Each match has a `source`: `session` (another verification session in your application matched on the same number, returned with `session_id`/`session_number`/`vendor_data`/`verification_date`) or `list_entry` (the number is on an org-managed allow/block list - see `/management-api/lists/overview`). Cross-session matches use different `vendor_data` and are capped at 5.""" -class SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions( +class SessionDecisionPoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions( BaseModel ): page: int | None = None @@ -3299,9 +2797,7 @@ class SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayMan page_height: float | None = None -class SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulation( - BaseModel -): +class SessionDecisionPoaVerificationsDocumentMetadataOverlayManipulation(BaseModel): """PDF forensic analysis for suspected overlay-text manipulation. Null when no overlay manipulation evidence was found or the document was not a PDF.""" detected: bool | None = None @@ -3314,15 +2810,13 @@ class SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayMan """Forensic signals that fired, such as duplicate_font_subset or glyph_fragmentation.""" manipulated_regions: ( - list[ - SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions - ] + list[SessionDecisionPoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions] | None ) = None """Suspected manipulated areas in PDF page coordinates.""" -class SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadata(BaseModel): +class SessionDecisionPoaVerificationsDocumentMetadata(BaseModel): """File-forensics metadata extracted from the uploaded POA document. Always contains the keys below (values are `null` when not applicable, e.g. EXIF dates for PDFs). `null` when no metadata could be extracted.""" file_size: int | None = None @@ -3338,7 +2832,7 @@ class SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadata(BaseModel """Date when the document was modified""" overlay_manipulation: ( - SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulation | None + SessionDecisionPoaVerificationsDocumentMetadataOverlayManipulation | None ) = None """PDF forensic analysis for suspected overlay-text manipulation. Null when no overlay manipulation evidence was found or the document was not a PDF.""" @@ -3376,7 +2870,7 @@ class SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadata(BaseModel """Whether the creation and modification dates differ - a common manipulation signal.""" -class SessionsRetrieveDecisionResponsePoaVerificationsExtraFields(BaseModel): +class SessionDecisionPoaVerificationsExtraFields(BaseModel): """Additional banking and contact details extracted from the document.""" bank_account_number: str | None = None @@ -3398,7 +2892,7 @@ class SessionsRetrieveDecisionResponsePoaVerificationsExtraFields(BaseModel): additional_names: list[str] | None = None -class SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddressDocumentLocation(BaseModel): +class SessionDecisionPoaVerificationsPoaParsedAddressDocumentLocation(BaseModel): latitude: float | None = None """Latitude coordinate""" @@ -3406,7 +2900,7 @@ class SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddressDocumentLo """Longitude coordinate""" -class SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddress(BaseModel): +class SessionDecisionPoaVerificationsPoaParsedAddress(BaseModel): address_type: str | None = None """Type of address (Street, Avenue, etc)""" @@ -3428,12 +2922,10 @@ class SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddress(BaseModel postal_code: str | None = None """Postal code""" - document_location: ( - SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddressDocumentLocation | None - ) = None + document_location: SessionDecisionPoaVerificationsPoaParsedAddressDocumentLocation | None = None -class SessionsRetrieveDecisionResponsePoaVerificationsWarnings(BaseModel): +class SessionDecisionPoaVerificationsWarnings(BaseModel): feature: ( Literal[ "ID_VERIFICATION", @@ -3471,7 +2963,7 @@ class SessionsRetrieveDecisionResponsePoaVerificationsWarnings(BaseModel): """The workflow graph node ID that generated this warning""" -class SessionsRetrieveDecisionResponsePoaVerifications(BaseModel): +class SessionDecisionPoaVerifications(BaseModel): node_id: str | None = None """Unique identifier for this POA step in the workflow graph.""" @@ -3495,9 +2987,7 @@ class SessionsRetrieveDecisionResponsePoaVerifications(BaseModel): document_language: str | None = None """Language code of the document""" - document_metadata: SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadata | None = ( - None - ) + document_metadata: SessionDecisionPoaVerificationsDocumentMetadata | None = None """File-forensics metadata extracted from the uploaded POA document. Always contains the keys below (values are `null` when not applicable, e.g. EXIF dates for PDFs). `null` when no metadata could be extracted.""" issuer: str | None = None @@ -3515,9 +3005,7 @@ class SessionsRetrieveDecisionResponsePoaVerifications(BaseModel): poa_formatted_address: str | None = None """Formatted address from the proof of address document""" - poa_parsed_address: SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddress | None = ( - None - ) + poa_parsed_address: SessionDecisionPoaVerificationsPoaParsedAddress | None = None name_on_document: str | None = None """Account-holder / addressee name extracted from the POA document, used for name matching. `null` when no name could be extracted.""" @@ -3537,19 +3025,19 @@ class SessionsRetrieveDecisionResponsePoaVerifications(BaseModel): expected_details_parsed_address: dict[str, object] | None = None """Expected parsed address details for verification""" - extra_fields: SessionsRetrieveDecisionResponsePoaVerificationsExtraFields | None = None + extra_fields: SessionDecisionPoaVerificationsExtraFields | None = None """Additional banking and contact details extracted from the document.""" extra_files: list[str] | None = None """Additional proof of address files""" - warnings: list[SessionsRetrieveDecisionResponsePoaVerificationsWarnings] | None = None + warnings: list[SessionDecisionPoaVerificationsWarnings] | None = None detected_codes: list[DocumentAIDetectedCode] | None = None """QR codes and barcodes detected across every processed page of the document. Empty when none are found. A located but undecodable QR code has `parsed_payload: null`.""" -class SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsAnswer(BaseModel): +class SessionDecisionQuestionnaireResponsesSectionsItemsAnswer(BaseModel): """Answer provided for the item""" value: str | None = None @@ -3559,7 +3047,7 @@ class SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsAnswer( files: list[str] | None = None -class SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsChoices(BaseModel): +class SessionDecisionQuestionnaireResponsesSectionsItemsChoices(BaseModel): label: str value: str @@ -3568,7 +3056,7 @@ class SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsChoices """Whether this choice requires additional text input""" -class SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItems(BaseModel): +class SessionDecisionQuestionnaireResponsesSectionsItems(BaseModel): uuid: str """Unique identifier for the item""" @@ -3590,15 +3078,13 @@ class SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItems(BaseMo placeholder: str | None = None """Placeholder text""" - choices: ( - list[SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsChoices] | None - ) = None + choices: list[SessionDecisionQuestionnaireResponsesSectionsItemsChoices] | None = None """Choices for dropdown or single choice elements""" max_files: int | None = None """Maximum number of files allowed""" - answer: SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsAnswer | None = None + answer: SessionDecisionQuestionnaireResponsesSectionsItemsAnswer | None = None """Answer provided for the item""" required_if: dict[str, object] | None = None @@ -3608,17 +3094,17 @@ class SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItems(BaseMo """Configuration for repeatable element groups, when applicable.""" -class SessionsRetrieveDecisionResponseQuestionnaireResponsesSections(BaseModel): +class SessionDecisionQuestionnaireResponsesSections(BaseModel): title: str | None = None """Section title""" description: str | None = None """Section description""" - items: list[SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItems] | None = None + items: list[SessionDecisionQuestionnaireResponsesSectionsItems] | None = None -class SessionsRetrieveDecisionResponseQuestionnaireResponses(BaseModel): +class SessionDecisionQuestionnaireResponses(BaseModel): node_id: str | None = None """Unique identifier for this questionnaire step in the workflow graph.""" @@ -3652,13 +3138,13 @@ class SessionsRetrieveDecisionResponseQuestionnaireResponses(BaseModel): published_at: _dt.datetime | None = None """Timestamp at which this questionnaire version was published.""" - sections: list[SessionsRetrieveDecisionResponseQuestionnaireResponsesSections] | None = None + sections: list[SessionDecisionQuestionnaireResponsesSections] | None = None status: str | None = None """Status of the questionnaire""" -class SessionsRetrieveDecisionResponseRegistryChecksCompanyBeneficialOwners(BaseModel): +class SessionDecisionRegistryChecksCompanyBeneficialOwners(BaseModel): uuid: str | None = None name: str | None = None @@ -3711,7 +3197,7 @@ class SessionsRetrieveDecisionResponseRegistryChecksCompanyBeneficialOwners(Base """What an owner holds, in the same shape for every country and registry source. Null when the registry states no holding. A band (for example a UK PSC's 25-50%) is not a stake: `percentage` is null and the band stays in `ownership_min_shares` / `ownership_max_shares`.""" -class SessionsRetrieveDecisionResponseRegistryChecksCompanyOfficers(BaseModel): +class SessionDecisionRegistryChecksCompanyOfficers(BaseModel): uuid: str | None = None name: str | None = None @@ -3753,7 +3239,7 @@ class SessionsRetrieveDecisionResponseRegistryChecksCompanyOfficers(BaseModel): """Registry-reported birth month. Null when only the year is known.""" -class SessionsRetrieveDecisionResponseRegistryChecksCompany(BaseModel): +class SessionDecisionRegistryChecksCompany(BaseModel): """Full registry payload for the company (registry record merged with any user-provided or console-edited data).""" uuid: str | None = None @@ -3864,11 +3350,9 @@ class SessionsRetrieveDecisionResponseRegistryChecksCompany(BaseModel): financial_summary: dict[str, object] | None = None - officers: list[SessionsRetrieveDecisionResponseRegistryChecksCompanyOfficers] | None = None + officers: list[SessionDecisionRegistryChecksCompanyOfficers] | None = None - beneficial_owners: ( - list[SessionsRetrieveDecisionResponseRegistryChecksCompanyBeneficialOwners] | None - ) = None + beneficial_owners: list[SessionDecisionRegistryChecksCompanyBeneficialOwners] | None = None addresses: list[KYBRegistryAddress] | None = None """Every address the registry states, one canonical shape. Null when it states none.""" @@ -3904,7 +3388,7 @@ class SessionsRetrieveDecisionResponseRegistryChecksCompany(BaseModel): """`not_requested`, `pending`, `delivered` or `unavailable`. Also in `ownership.status`.""" -class SessionsRetrieveDecisionResponseRegistryChecks(BaseModel): +class SessionDecisionRegistryChecks(BaseModel): """One company registry check - the full company-registry payload enriched with ownership structure and warnings. Present only when `session_kind = business`.""" status: str | None = None @@ -3914,7 +3398,7 @@ class SessionsRetrieveDecisionResponseRegistryChecks(BaseModel): data_resolved: bool | None = None - company: SessionsRetrieveDecisionResponseRegistryChecksCompany | None = None + company: SessionDecisionRegistryChecksCompany | None = None """Full registry payload for the company (registry record merged with any user-provided or console-edited data).""" ownership_structure: dict[str, object] | None = None @@ -3923,13 +3407,13 @@ class SessionsRetrieveDecisionResponseRegistryChecks(BaseModel): warnings: list[dict[str, object]] | None = None -class SessionsRetrieveDecisionResponseResubmissionsNodes(BaseModel): +class SessionDecisionResubmissionsNodes(BaseModel): node_id: str | None = None feature: str | None = None -class SessionsRetrieveDecisionResponseResubmissionsRequestedBy(BaseModel): +class SessionDecisionResubmissionsRequestedBy(BaseModel): """Who requested the resubmission.""" name: str | None = None @@ -3939,7 +3423,7 @@ class SessionsRetrieveDecisionResponseResubmissionsRequestedBy(BaseModel): type: str | None = None -class SessionsRetrieveDecisionResponseResubmissions(BaseModel): +class SessionDecisionResubmissions(BaseModel): number: int | None = None """1 for the first resubmission of the session, 2 for the second, and so on.""" @@ -3949,20 +3433,20 @@ class SessionsRetrieveDecisionResponseResubmissions(BaseModel): previous_status: str | None = None """The session status just before the request.""" - nodes: list[SessionsRetrieveDecisionResponseResubmissionsNodes] | None = None + nodes: list[SessionDecisionResubmissionsNodes] | None = None """The workflow steps sent back to the user.""" reasons: dict[str, str] | None = None """The reason given for each step, keyed by `node_id`.""" - requested_by: SessionsRetrieveDecisionResponseResubmissionsRequestedBy | None = None + requested_by: SessionDecisionResubmissionsRequestedBy | None = None """Who requested the resubmission.""" results: dict[str, object] | None = None """What the resubmitted steps returned before the request, under the same keys and in the same shape as the current results of this response (`id_verifications`, `nfc_verifications`, `liveness_checks`, `face_matches`, `poa_verifications`, `phone_verifications`, `email_verifications`, `aml_screenings`, `ip_analyses`, `database_validations`, `questionnaire_responses`, `document_ai_documents`, `geolocation_checks`; for business sessions also `document_verifications`). Only the keys of the steps that were sent back appear.""" -class SessionsRetrieveDecisionResponseReviews(BaseModel): +class SessionDecisionReviews(BaseModel): user: str | None = None """Who performed the review. Usually the console reviewer's identifier (their email), but the value is `"API Client"` for status updates made with an API key and `"Unknown"` when no actor could be resolved - so do not parse this field as an email address.""" @@ -3973,13 +3457,16 @@ class SessionsRetrieveDecisionResponseReviews(BaseModel): created_at: _dt.datetime | None = None -class SessionsRetrieveDecisionResponse(BaseModel): +class SessionDecision(BaseModel): session_id: str | None = None - """The unique identifier of the session for which to retrieve verification results.""" + """The unique identifier of the session.""" session_kind: Literal["user", "business"] | str | None = None """Discriminator indicating whether this is a User Verification (KYC) or Business Verification (KYB) session. When `user`, the KYC feature arrays (`id_verifications`, `nfc_verifications`, `liveness_checks`, `face_matches`, `poa_verifications`, `document_ai_documents`, `database_validations`, ...) apply and the business arrays are absent. When `business`, the payload instead carries `registry_checks`, `document_verifications`, `key_people_checks` plus the shared arrays (`aml_screenings`, `phone_verifications`, `email_verifications`, `ip_analyses`, `questionnaire_responses`). **Omitted when `include=events` is passed** - the events shape only exists for user sessions, so the discriminator is dropped from that variant.""" + shared_from_session: str | None = None + """For a session cloned with `POST /v3/session/import-shared/`: the `session_id` of the original session in the source application. `null` for every other session. Omitted when `include=events` is passed.""" + session_number: int | None = None """The number of the session.""" @@ -4016,16 +3503,16 @@ class SessionsRetrieveDecisionResponse(BaseModel): metadata: dict[str, object] | None = None """The metadata of the session.""" - expected_details: SessionsRetrieveDecisionResponseExpectedDetails | None = None + expected_details: SessionDecisionExpectedDetails | None = None """Expected details supplied at session creation - used for cross-validation against extracted data (KYC) or to pre-fill the company registry search (KYB). Echoes back exactly the keys that were provided; `null` when none were supplied. KYC sessions carry the person fields; Business (KYB) sessions carry `company_name`, `registry_country` and `registration_number`.""" - contact_details: SessionsRetrieveDecisionResponseContactDetails | None = None + contact_details: SessionDecisionContactDetails | None = None """Contact details supplied when the session was created (used for notification emails, prefilled phone/email verification, and communication language). `null` when neither an email nor a phone number was provided.""" callback: str | None = None """The callback URL of the session.""" - id_verifications: list[SessionsRetrieveDecisionResponseIdVerifications] | None = None + id_verifications: list[SessionDecisionIdVerifications] | None = None """**Array of ID verification (OCR) reports** produced by ID-document steps in the workflow graph. Always a JSON array - read `id_verifications[0]` and iterate if your workflow can run more than one ID check (for example a primary check plus a step-up). `null` until at least one ID step has produced data. Each item is the full report, including `node_id`, feature-level `status`, extracted document fields, image URLs, quality scores, MRZ, address parsing, cross-session matches, and `warnings[]`.""" nfc_skip_reason: ( @@ -4036,45 +3523,47 @@ class SessionsRetrieveDecisionResponse(BaseModel): "DEVICE_WITHOUT_NFC", "INTEGRATION_WITHOUT_NFC_ACCESS", "MRZ_KEY_UNAVAILABLE", + "JPKI_UNAVAILABLE", ] | str | None ) = None - """Why NFC chip verification did not run, populated whenever the workflow includes an NFC step. `null` while NFC is pending or when the chip was actually read. Values: `USER_SKIPPED` - the user chose to skip on an NFC-capable device (allowed by `allow_nfc_skip`); `DOCUMENT_WITHOUT_CHIP` - the document has no readable NFC chip (not an ICAO 9303 electronic document); `CHIP_CERTIFICATE_UNAVAILABLE` - the issuing country's signing certificate (CSCA) covering the document's issue period is unavailable, so the chip signature cannot be verified; `DEVICE_WITHOUT_NFC` - the device reported no NFC reader; `INTEGRATION_WITHOUT_NFC_ACCESS` - the verification ran without a native SDK (e.g. in a web browser), which has no access to the phone's NFC reader; `MRZ_KEY_UNAVAILABLE` - the chip access key could not be derived from the document's MRZ.""" + """Why NFC chip verification did not run, populated whenever the workflow includes an NFC step. `null` while NFC is pending or when the chip was actually read. Values: `USER_SKIPPED` - the user chose to skip on an NFC-capable device (allowed by `allow_nfc_skip`); `DOCUMENT_WITHOUT_CHIP` - the document has no readable NFC chip (not an ICAO 9303 electronic document); `CHIP_CERTIFICATE_UNAVAILABLE` - the issuing country's signing certificate (CSCA) covering the document's issue period is unavailable, so the chip signature cannot be verified; `DEVICE_WITHOUT_NFC` - the device reported no NFC reader; `INTEGRATION_WITHOUT_NFC_ACCESS` - the verification ran without a native SDK (e.g. in a web browser), which has no access to the phone's NFC reader; `MRZ_KEY_UNAVAILABLE` - the chip access key could not be derived from the document's MRZ; `JPKI_UNAVAILABLE` - the document is a Japan My Number Card, whose chip is read through JPKI, and JPKI was not available for this verification: it is not enabled, or the app's native SDK cannot read My Number Cards.""" - nfc_verifications: list[SessionsRetrieveDecisionResponseNfcVerifications] | None = None - """**Array of NFC / ePassport reports** produced by NFC steps in the workflow graph. **This is always a JSON array, never a singular `nfc` object** - read `nfc_verifications[0]` and iterate if your workflow can run NFC more than once. `null` until at least one NFC step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `chip_data`, `authenticity` (SOD + DG integrity), `certificate_summary`, and `warnings[]`. When the NFC step was bypassed before it could ever be offered (chipless document, missing certificate, no NFC capability reported, unreadable MRZ key), this stays `null` and `nfc_skip_reason` explains why; a skip at the step itself (user tapped skip, or the reader turned out unavailable at scan time) still produces a report with `is_nfc_skipped: true`.""" + nfc_verifications: list[SessionDecisionNfcVerifications] | None = None + """**Array of NFC / ePassport reports** produced by NFC steps in the workflow graph. **This is always a JSON array, never a singular `nfc` object** - read `nfc_verifications[0]` and iterate if your workflow can run NFC more than once. `null` until at least one NFC step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `chip_protocol`, `chip_data`, `authenticity` (SOD + DG integrity), `certificate_summary`, and `warnings[]`. A Japan My Number Card is read through JPKI instead of ICAO 9303: its item has `chip_protocol: "jpki"`, carries its evidence under `jpki`, and leaves the ICAO fields `null`. When the NFC step was bypassed before it could ever be offered (chipless document, missing certificate, no NFC capability reported, unreadable MRZ key, JPKI unavailable for a My Number Card), this stays `null` and `nfc_skip_reason` explains why; a skip at the step itself (user tapped skip, or the reader turned out unavailable at scan time) still produces a report with `is_nfc_skipped: true`.""" - liveness_checks: list[SessionsRetrieveDecisionResponseLivenessChecks] | None = None + liveness_checks: list[SessionDecisionLivenessChecks] | None = None """**Array of liveness reports** produced by liveness steps in the workflow graph. Always a JSON array - never a singular `liveness` object. `null` until at least one liveness step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `method`, `score`, `reference_image`, `video_url`, optional `age_estimation`, cross-session `matches[]`, and `warnings[]`.""" - face_matches: list[SessionsRetrieveDecisionResponseFaceMatches] | None = None + face_matches: list[SessionDecisionFaceMatches] | None = None """**Array of face-match reports** produced by face-match steps in the workflow graph. Always a JSON array - never a singular `face_match` object. `null` until at least one face-match step has produced data. Each item is the full report, including `node_id`, feature-level `status`, similarity `score`, `source_image`, `target_image`, and `warnings[]`.""" - phone_verifications: list[SessionsRetrieveDecisionResponsePhoneVerifications] | None = None + phone_verifications: list[SessionDecisionPhoneVerifications] | None = None """**Array of phone verification reports** - always a JSON array, never a singular `phone` object. `null` until at least one phone step has produced data. Each item is the full report, including `node_id`, feature-level `status`, the phone number and country metadata, `lifecycle[]`, `matches[]`, and `warnings[]`.""" - email_verifications: list[SessionsRetrieveDecisionResponseEmailVerifications] | None = None + email_verifications: list[SessionDecisionEmailVerifications] | None = None """**Array of email verification reports** - always a JSON array, never a singular `email` object. `null` until at least one email step has produced data. Each item is the full report, including `node_id`, feature-level `status`, the email address, breach data, `lifecycle[]`, `matches[]`, and `warnings[]`.""" - poa_verifications: list[SessionsRetrieveDecisionResponsePoaVerifications] | None = None + poa_verifications: list[SessionDecisionPoaVerifications] | None = None """**Array of Proof of Address (POA) reports** - always a JSON array, never a singular `poa` object. `null` until at least one POA step has produced data. Each item is the full report, including `node_id`, feature-level `status`, the document file URL, issuer, dates, parsed address, and `warnings[]`.""" - document_ai_documents: list[SessionsRetrieveDecisionResponseDocumentAiDocuments] | None = None + document_ai_documents: list[SessionDecisionDocumentAiDocuments] | None = None """**Document AI reports grouped by workflow node** - always a JSON array, never a singular object. `null` until at least one Document AI step has produced data. Present only when `session_kind = user`. Each item is a node group with an aggregate `status`, the `node_id`, an `items[]` array of the per-document results (extracted data keyed by the configured field key), and `warnings`.""" - questionnaire_responses: list[SessionsRetrieveDecisionResponseQuestionnaireResponses] | None = ( - None - ) + questionnaire_responses: list[SessionDecisionQuestionnaireResponses] | None = None """**Array of questionnaire response reports** - always a JSON array, never a singular `questionnaire` object. `null` until at least one questionnaire step has produced data. Each item is the full report, including `node_id`, the questionnaire metadata, sections with form elements, and the user's answers.""" - aml_screenings: list[SessionsRetrieveDecisionResponseAmlScreenings] | None = None + signatures: list[SignatureResult] | None = None + """Electronic Signature results, one item per signer and signature step, for User and Business Verification. Present only when the workflow has an Electronic Signature step; an empty array until a signer reaches it. See the [Electronic Signature report](/core-technology/electronic-signature/report-electronic-signature).""" + + aml_screenings: list[AMLScreening] | None = None """**Array of AML screening reports** - always a JSON array, never a singular `aml` object. `null` until at least one AML step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `total_hits`, `entity_type`, `hits[]`, `score`, `screened_data`, `is_ongoing_monitoring_enabled`, and `warnings[]`.""" - ip_analyses: list[SessionsRetrieveDecisionResponseIpAnalyses] | None = None + ip_analyses: list[SessionDecisionIpAnalyses] | None = None """**Array of Device & IP Analysis reports** - always a JSON array, never a singular `ip_analysis` object. `null` until at least one IP-analysis step has produced data. Each item is the full report, including `node_id`, feature-level `status`, IP and geolocation data, device fingerprint, and `warnings[]`. Entries with duplicate `(node_id, ip_address, device_fingerprint)` are deduplicated server-side.""" - database_validations: list[SessionsRetrieveDecisionResponseDatabaseValidations] | None = None + database_validations: list[SessionDecisionDatabaseValidations] | None = None """**Array of Database Validation reports** - always a JSON array, never a singular `database_validation` object. `null` until at least one DB validation step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `screened_data`, the per-service `validations[]` (each with its registry `source_data`), and `warnings[]`. The `screened_data` and `source_data` fields vary by country: **ARG (Argentina)**: screened_data: {document_number, selfie, gender, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, date_of_birth, date_of_death, tax_id, tax_id_type, face_match_score, sit_1_since, banks, last_position, highest_position, rejected_checks}. Uses RENAPER biometric face-match validation. Gender is auto-inferred from face analysis or given name when not present in the document (e.g., driver licenses) @@ -4114,7 +3603,7 @@ class SessionsRetrieveDecisionResponse(BaseModel): **VEN (Venezuela)**: screened_data: {document_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, gender, date_of_birth, document_type} """ - reviews: list[SessionsRetrieveDecisionResponseReviews] | None = None + reviews: list[SessionDecisionReviews] | None = None created_at: _dt.datetime | None = None """Timestamp at which the session was created (ISO 8601, UTC).""" @@ -4122,19 +3611,20 @@ class SessionsRetrieveDecisionResponse(BaseModel): expires_at: _dt.datetime | None = None """Timestamp at which this session is scheduled to expire. Set on User Verification (KYC) sessions when an expiration was configured at creation time (or once `Approved` status triggers KYC expiry computation). `null` for sessions without a configured expiration, and for kinds that do not use expiry (e.g. Business Verification (KYB) sessions that never received an `expires_at`).""" - registry_checks: list[SessionsRetrieveDecisionResponseRegistryChecks] | None = None + registry_checks: list[SessionDecisionRegistryChecks] | None = None """Company registry checks. Present only when `session_kind = business`. Each item wraps the full company-registry payload.""" - key_people_checks: list[SessionsRetrieveDecisionResponseKeyPeopleChecks] | None = None + key_people_checks: list[SessionDecisionKeyPeopleChecks] | None = None """Aggregate key-people checks. Present only when `session_kind = business`. Each item uses the two-bucket shape (`registry`, `submitted`, `ubo_kyc_summary`).""" - document_verifications: list[SessionsRetrieveDecisionResponseDocumentVerifications] | None = ( - None - ) + document_verifications: list[SessionDecisionDocumentVerifications] | None = None """Document verification checks grouped by workflow node. Present only when `session_kind = business`.""" environment: Literal["live", "sandbox"] | str | None = None """Whether the session was created by a live application (`live`) or a sandbox application (`sandbox`). Sandbox sessions are free, fully simulated, and never bill credits.""" - resubmissions: list[SessionsRetrieveDecisionResponseResubmissions] | None = None + resubmissions: list[SessionDecisionResubmissions] | None = None """Returned only with `include=resubmissions`, for user and business sessions. One entry per resubmission request, oldest first. Each entry carries the results the resubmitted steps returned before the request, so earlier submissions stay readable. Deleting the session deletes every submission.""" + + investigator: InvestigatorBlock | None = None + """The item's latest Investigator review, or null when it never had one. Carried by the V3 session and business session decision (and so by `status.updated` webhooks).""" diff --git a/src/didit/types/sessions/retrieve_deletion_job_response.py b/src/didit/types/components/session_deletion_job.py similarity index 89% rename from src/didit/types/sessions/retrieve_deletion_job_response.py rename to src/didit/types/components/session_deletion_job.py index 47c5f3c..24de8d7 100644 --- a/src/didit/types/sessions/retrieve_deletion_job_response.py +++ b/src/didit/types/components/session_deletion_job.py @@ -6,10 +6,10 @@ from didit._models import BaseModel -__all__ = ["SessionsRetrieveDeletionJobResponse", "SessionsRetrieveDeletionJobResponseOutcomes"] +__all__ = ["SessionDeletionJob", "SessionDeletionJobOutcomes"] -class SessionsRetrieveDeletionJobResponseOutcomes(BaseModel): +class SessionDeletionJobOutcomes(BaseModel): """How many sessions ended in each outcome. Every key is always present.""" deleted: int | None = None @@ -31,7 +31,7 @@ class SessionsRetrieveDeletionJobResponseOutcomes(BaseModel): """Selected sessions that were already deleted, for example by another request, when the job reached them.""" -class SessionsRetrieveDeletionJobResponse(BaseModel): +class SessionDeletionJob(BaseModel): """A bulk session deletion running in the background: progress and counts per outcome, never which sessions it deleted.""" job_id: str | None = None @@ -46,7 +46,7 @@ class SessionsRetrieveDeletionJobResponse(BaseModel): processed: int | None = None """Sessions the job has finished with so far.""" - outcomes: SessionsRetrieveDeletionJobResponseOutcomes | None = None + outcomes: SessionDeletionJobOutcomes | None = None """How many sessions ended in each outcome. Every key is always present.""" deleted_child_verifications: int | None = None diff --git a/src/didit/types/components/session_event.py b/src/didit/types/components/session_event.py new file mode 100644 index 0000000..0877ed6 --- /dev/null +++ b/src/didit/types/components/session_event.py @@ -0,0 +1,93 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SessionEvent"] + + +class SessionEvent(BaseModel): + """One entry of a verification session's event timeline.""" + + uuid: str + """Event identifier.""" + + event_type: ( + Literal[ + "SESSION_CREATED", + "SESSION_COMPLETED", + "SESSION_STATUS_CHANGED", + "LINK_OPENED", + "FORM_SUBMITTED", + "BUTTON_CLICKED", + "FILE_UPLOADED", + "ATTEMPT_FAILED", + "FEATURE_STEP_CHANGED", + "STEP_RENDERED", + "NOTE_ADDED", + "MANUAL_REVIEW_STARTED", + "WORKFLOW_BRANCH_EVALUATED", + "WORKFLOW_STATUS_NODE_REACHED", + "LOG", + ] + | str + ) + """What happened. Reported by the device during the hosted flow or SDK: `LINK_OPENED`, `STEP_RENDERED`, `BUTTON_CLICKED`, `FORM_SUBMITTED`, `FILE_UPLOADED`, `ATTEMPT_FAILED` and `LOG` (debugging). Recorded by the server: `SESSION_CREATED`, `SESSION_STATUS_CHANGED` (`details.old_status`, `details.current_status`), `FEATURE_STEP_CHANGED` (`details.old_feature_step`, `details.current_feature_step`), `WORKFLOW_BRANCH_EVALUATED` and `WORKFLOW_STATUS_NODE_REACHED` (the workflow routing trace). `SESSION_COMPLETED`, `NOTE_ADDED` and `MANUAL_REVIEW_STARTED` can appear on older sessions.""" + + created_at: _dt.datetime + """When Didit stored the event. Events are stored asynchronously, a few seconds after `sent_at`.""" + + component: str | None + """UI component that reported the event (device events only).""" + + feature_step: ( + Literal[ + "OCR", + "OCR_BACK", + "NFC", + "AML", + "FACE", + "LIVENESS", + "FACE_MATCH", + "IP_ANALYSIS", + "AGE_ESTIMATION", + "PROOF_OF_ADDRESS", + "PHONE_VERIFICATION", + "EMAIL_VERIFICATION", + "FACE_SEARCH", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + "DOCUMENT_AI", + "GEOLOCATION", + ] + | str + | None + ) + """Verification feature the session was on when the event happened.""" + + step: str | None + """Screen within the feature, for example `DOCUMENT_CAPTURE_FRONT` or `FACE_PASSIVE_LIVENESS`.""" + + details: dict[str, object] | None + """Event-specific data. The keys depend on `event_type`.""" + + ip_address: str | None + """IP address the device reported the event from. `null` for server-recorded events.""" + + user_agent: str | None + """User agent of the device. `null` for server-recorded events.""" + + device_info: dict[str, object] | None + """Device details the SDK or browser reported.""" + + file_url: str | None + """Short-lived presigned link to a file attached to the event (for example a captured image). `null` when there is none, and always `null` for a scoped API key without `media: read`.""" + + sent_at: int + """When the event happened, as a Unix timestamp in milliseconds. Results are ordered by it.""" diff --git a/src/didit/types/components/session_sign_document.py b/src/didit/types/components/session_sign_document.py new file mode 100644 index 0000000..c5ab1af --- /dev/null +++ b/src/didit/types/components/session_sign_document.py @@ -0,0 +1,24 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import Required, TypedDict + +from didit.types.components.signature_field import SignatureField + +__all__ = ["SessionSignDocument"] + + +class SessionSignDocument(TypedDict, total=False): + title: str + """Title shown to the signer. Defaults to `file_name`.""" + + file_name: Required[str] + """File name, ending in `.pdf` or `.docx`.""" + + content_base64: Required[str] + """The file, base64-encoded. A PDF can have up to 20 MB and 100 pages, without a password, scripts or attached files. A Word file (`.docx`) can have up to 20 MB and is converted to PDF while the request runs, in at most 25 seconds; the PDF then has the same limits. Up to 5 Word files per session. A Word file with macros, tracked changes, links to external content or embedded objects is refused with `signature_document_not_supported`, and one that can't be converted returns `signature_document_conversion_failed`, each with a `reason`.""" + + fields: Sequence[SignatureField] + """Optional fields to place on this document, up to 200. Pages and positions refer to the PDF, or to the converted PDF for a Word file. Without fields, the signer reads the document and agrees to it.""" diff --git a/src/didit/types/components/session_webhook_delivery.py b/src/didit/types/components/session_webhook_delivery.py new file mode 100644 index 0000000..818418e --- /dev/null +++ b/src/didit/types/components/session_webhook_delivery.py @@ -0,0 +1,40 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.webhook_event import WebhookEvent + +__all__ = ["SessionWebhookDelivery"] + + +class SessionWebhookDelivery(BaseModel): + """One webhook delivery attempt recorded for a verification session, with the exact request headers and body Didit sent.""" + + uuid: str + """Delivery ID. Use it as `{webhook_uuid}` to get or resend this delivery.""" + + session_id: str + """The session the delivery belongs to (the session in the path).""" + + request_url: str | None + """URL the delivery was sent to.""" + + response_status: int | None + """HTTP status your endpoint returned. `null` while the delivery is queued and not yet attempted. When no HTTP answer was received Didit records a status of its own: `504` when your endpoint did not answer within 5 seconds, `503` for a connection failure, `400` when the URL was refused as unsafe (for example a private or internal address), and `404` for any other request error. `400` or above counts as a failed delivery.""" + + status: str | None + """Session status when the event was generated, for example `In Progress`, `Approved` or `Declined`.""" + + webhook_type: WebhookEvent + """Supported webhook event name. Use these exact strings in `subscribed_events`; unsupported values are rejected.""" + + request_header: str + """JSON text (a string) of the request headers Didit sent: `Content-Type`, `X-Signature`, `X-Signature-V2`, `X-Signature-Simple`, `X-Timestamp` and `User-Agent`. Parse it with a JSON parser. It holds signatures, never the signing secret.""" + + request_body: str + """JSON text (a string) of the exact payload Didit sent, keys sorted, including `event_id` and `timestamp`. Parse it with a JSON parser; see [Payload shape](https://docs.didit.me/integration/webhooks#payload-shape). For a scoped key without `media: read`, media URLs inside it are `null`; without `sessions: write`, session tokens and hosted verification links are `null`. A body changed that way no longer matches its signatures. Didit does not record the response body your endpoint returned.""" + + created_at: _dt.datetime + """When the delivery was created.""" diff --git a/src/didit/types/components/signature_field.py b/src/didit/types/components/signature_field.py new file mode 100644 index 0000000..8f8367f --- /dev/null +++ b/src/didit/types/components/signature_field.py @@ -0,0 +1,71 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +from didit.types.components.signature_field_anchor import SignatureFieldAnchor +from didit.types.components.signature_field_validation import SignatureFieldValidation + +__all__ = ["SignatureField"] + + +class SignatureField(TypedDict, total=False): + """A field to place on a session document: which signer fills it, how, and where. Give either a position (`page`, `x`, `y`, `width` and `height`) or an `anchor`. Positions are fractions of the page as it's displayed, from its top-left corner, rounded to 4 decimals, and the field must fit inside the page. A document takes up to 200 fields and one signer up to 500, and every role with fields on a document needs a `signature` or `initials` field there. See [Fields on the document](/core-technology/electronic-signature/overview#fields-on-the-document).""" + + key: Required[str] + """Names the field in the results and in `sign.field_values`. Unique within the document.""" + + type: Required[ + Literal[ + "signature", "initials", "full_name", "date_signed", "text", "checkbox", "email", "data" + ] + | str + ] + """What the field holds. The signer applies their adopted `signature` and `initials`, types `text`, ticks a `checkbox`, and types an `email` when the session has no verified email. Didit fills `full_name` (the verified name), `date_signed` (the date of signing, in the signer's time zone), `data` (from `prefill`) and a verified `email`, and shows them read-only. There is no date picker type: use `text`.""" + + role: Literal["verified_person", "legal_representative", "key_people"] | str + """Who fills the field, with the `signature_signer` values. A step fills and stamps only the fields of its own signer. Defaults to the step's signer.""" + + page: int + """Page number, from 1 to the document's page count. For a Word document, a page of the converted PDF.""" + + x: float + """Left edge, as a fraction of the page width.""" + + y: float + """Top edge, as a fraction of the page height.""" + + width: float + """Width, as a fraction of the page width. A signature is at least `0.12` wide.""" + + height: float + """Height, as a fraction of the page height. A signature is at least `0.035` high.""" + + anchor: SignatureFieldAnchor + """Places a field by a piece of text in the document instead of a position. Didit doesn't remove the text from the PDF, so write it in the same color as the page background. When the text isn't found, the call returns `400` with `field_anchor_not_found`. See [Place fields by anchor text](/sessions-api/create-session#place-fields-by-anchor-text).""" + + required: bool + """`signature` and `initials` are required unless you set `false`. `full_name`, `date_signed` and `data` are always required. The signer can't finish a document until its required fields have a value, and a required checkbox must be ticked.""" + + label: str + """Shown to the signer and in the results. Defaults from the type.""" + + placeholder: str | None + """`text` and `email` only.""" + + validation: SignatureFieldValidation | None + """Rules for a `text` field's value. The console preview, the signing flow and the API apply the same rules.""" + + prefill: str | None + """Where the value comes from: `identity.full_name`, `identity.first_name`, `identity.last_name`, `identity.date_of_birth` (`DD/MM/YYYY`), `identity.nationality`, `identity.document_number`, `identity.document_type`, `identity.issuing_country`, `identity.address`, `contact.email`, `contact.phone`, `company.name`, `company.registration_number`, `company.address`, `company.country` (KYB), `session.vendor_data`, `metadata.` (a key of the session's `metadata`) or `api.` (a key of `sign.field_values`). Required for `data`. `full_name` always uses `identity.full_name` and `email` uses `contact.email`. The values of `data`, `full_name` and verified `email` fields are fixed when the signer starts signing.""" + + editable_when_prefilled: bool + """`text` and `email` only: the signer can change a pre-filled value.""" + + checked_by_default: bool + """`checkbox` only: the box starts ticked.""" + + date_format: Literal["DD/MM/YYYY", "MM/DD/YYYY", "YYYY-MM-DD", "D MMMM YYYY"] | str + """`date_signed` only.""" diff --git a/src/didit/types/components/signature_field_anchor.py b/src/didit/types/components/signature_field_anchor.py new file mode 100644 index 0000000..a3d401a --- /dev/null +++ b/src/didit/types/components/signature_field_anchor.py @@ -0,0 +1,30 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +__all__ = ["SignatureFieldAnchor"] + + +class SignatureFieldAnchor(TypedDict, total=False): + """Places a field by a piece of text in the document instead of a position. Didit doesn't remove the text from the PDF, so write it in the same color as the page background. When the text isn't found, the call returns `400` with `field_anchor_not_found`. See [Place fields by anchor text](/sessions-api/create-session#place-fields-by-anchor-text).""" + + text: Required[str] + """Text searched in the document. For a Word file, in the converted PDF.""" + + occurrence: Literal["first", "all"] | str + """`all` places one field per match; the keys after the first get `_2`, `_3` and so on.""" + + offset_x: float + """Added to the left edge of the text, as a fraction of the page width.""" + + offset_y: float + """Added to the top edge of the text, as a fraction of the page height.""" + + width: float + """Width of the field, as a fraction of the page width.""" + + height: float + """Height of the field, as a fraction of the page height.""" diff --git a/src/didit/types/components/signature_field_result.py b/src/didit/types/components/signature_field_result.py new file mode 100644 index 0000000..b521aa1 --- /dev/null +++ b/src/didit/types/components/signature_field_result.py @@ -0,0 +1,68 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SignatureFieldResult"] + + +class SignatureFieldResult(BaseModel): + """A field of a signed document, with its value on this signer's copy. See [Fields](/core-technology/electronic-signature/report-electronic-signature#fields).""" + + key: str | None = None + """The field's key, as used in `sign.field_values`.""" + + type: ( + Literal[ + "signature", "initials", "full_name", "date_signed", "text", "checkbox", "email", "data" + ] + | str + | None + ) = None + + role: Literal["verified_person", "legal_representative", "key_people"] | str | None = None + + label: str | None = None + + page: int | None = None + """Page number, from 1. For a Word document, a page of the converted PDF.""" + + x: float | None = None + """Left edge, as a fraction of the page width.""" + + y: float | None = None + """Top edge, as a fraction of the page height.""" + + width: float | None = None + """Width, as a fraction of the page width.""" + + height: float | None = None + """Height, as a fraction of the page height.""" + + required: bool | None = None + + value: object = None + """A string for `text`, `email`, `full_name`, `date_signed` and `data`, a boolean for a `checkbox`, and `true` for an applied signature or initials. `null` when empty.""" + + display_value: str | None = None + """The value as stamped on the page: dates in the field's `date_format`, `Yes` or `No` for a checkbox, `Signed` for a signature or initials.""" + + value_source: ( + Literal[ + "signer", "identity", "contact", "company", "session", "metadata", "api", "signing_time" + ] + | str + | None + ) = None + """Where the value came from: `signer` (typed, ticked or applied by the signer), `identity` (the verified ID), `contact` (the verified email or phone), `company` (the verified company), `session` (`vendor_data`), `metadata`, `api` (`sign.field_values`) or `signing_time` (the date signed).""" + + filled_at: _dt.datetime | None = None + + image_url: str | None = None + """Signature and initials only: the path of the applied image. See [Download a signature image](/sessions-api/electronic-signature/download-field-image).""" + + value_unavailable_reason: Literal["source_empty"] | str | None = None + """`source_empty` when a pre-filled field had no value in the session and `signature_field_data_missing_action` let the signer sign anyway.""" diff --git a/src/didit/types/components/signature_field_validation.py b/src/didit/types/components/signature_field_validation.py new file mode 100644 index 0000000..4c41c3b --- /dev/null +++ b/src/didit/types/components/signature_field_validation.py @@ -0,0 +1,26 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypedDict + +__all__ = ["SignatureFieldValidation"] + + +class SignatureFieldValidation(TypedDict, total=False): + """Rules for a `text` field's value. The console preview, the signing flow and the API apply the same rules.""" + + kind: Literal["any", "letters", "number", "email", "pattern"] | str + """`letters`: letters in any script, spaces, hyphens and apostrophes. `number`: digits with an optional decimal separator. `pattern`: the value must match `pattern`.""" + + min_length: int + """From `0` up to `max_length`.""" + + max_length: int + + pattern: str | None + """Required with `kind: "pattern"`. A regular expression the whole value must match. Lookarounds and backreferences aren't supported.""" + + message: str | None + """Shown to the signer instead of the default error.""" diff --git a/src/didit/types/components/signature_result.py b/src/didit/types/components/signature_result.py new file mode 100644 index 0000000..eb27dbc --- /dev/null +++ b/src/didit/types/components/signature_result.py @@ -0,0 +1,343 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from pydantic import Field + +from didit._models import BaseModel +from didit.types.components.signature_field_result import SignatureFieldResult + +__all__ = [ + "SignatureResult", + "SignatureResultAdoptedSignature", + "SignatureResultCertificate", + "SignatureResultConfirmation", + "SignatureResultDocuments", + "SignatureResultDocumentsConversion", + "SignatureResultIdentityUsed", + "SignatureResultSigner", + "SignatureResultTimestamp", + "SignatureResultValidation", + "SignatureResultValidationDocuments", + "SignatureResultValidationDocumentsCertificate", + "SignatureResultValidationDocumentsTimestamp", + "SignatureResultValidationDocumentsTrustedList", + "SignatureResultWarnings", +] + + +class SignatureResultAdoptedSignature(BaseModel): + """The signature and initials the signer adopted for signature and initials fields. `null` until the signer adopts one.""" + + method: Literal["typed", "drawn"] | str | None = None + """`typed`: the verified name in a handwriting style. `drawn`: drawn with a finger or a mouse.""" + + font: str | None = None + """The handwriting style of a typed signature. `null` when drawn.""" + + adopted_at: _dt.datetime | None = None + + +class SignatureResultCertificate(BaseModel): + """The certificate the documents were signed with. `null` until signed.""" + + issuer: str | None = None + + issued_to: str | None = None + + serial: str | None = None + """Serial number, hexadecimal.""" + + valid_from: _dt.datetime | None = None + + valid_until: _dt.datetime | None = None + + +class SignatureResultConfirmation(BaseModel): + channel: Literal["email", "sms"] | str | None = None + + destination: str | None = None + """The masked contact the code went to.""" + + confirmed_at: _dt.datetime | None = None + + attempts: int | None = None + """Codes entered, the right one included.""" + + +class SignatureResultDocumentsConversion(BaseModel): + """`null` for a PDF. For a Word document, the original file and the conversion.""" + + from_: Literal["docx"] | str | None = Field(default=None, alias="from") + + file_name: str | None = None + """Your original Word file name.""" + + sha256: str | None = None + """SHA-256 of your original Word file.""" + + size_bytes: int | None = None + """Size of your original Word file.""" + + converter: str | None = None + """Version of the converter that produced the PDF.""" + + +class SignatureResultDocuments(BaseModel): + id: str | None = None + """The document on this signer's copy. Use it to download the signed file.""" + + uuid: str | None = None + """Same as `id`.""" + + title: str | None = None + + file_name: str | None = None + + pages: int | None = None + """Pages of the document signed. For Word, of the converted PDF.""" + + size_bytes: int | None = None + + sha256_original: str | None = None + """SHA-256 of the document as the signer was shown it.""" + + sha256_signed: str | None = None + """SHA-256 of the signed PDF, once signed.""" + + status: Literal["signed", "declined", "not_signed", "not_reached"] | str | None = None + """`not_signed`: opened but not signed. `not_reached`: never opened.""" + + opened_at: _dt.datetime | None = None + + read_at: _dt.datetime | None = None + """When the signer reached the end of a document without fields, when the step requires it.""" + + read_to_end: bool | None = None + + consented_at: _dt.datetime | None = None + """When the signer agreed to a document without fields, or finished the fields of a document with fields.""" + + consent: bool | None = None + + signed_file: str | None = None + """Path of the signed PDF, once signed. See [Download a signed document](/sessions-api/electronic-signature/download-signed-document).""" + + mode: Literal["fields", "read_and_agree"] | str | None = None + """`fields` for a document with fields, `read_and_agree` for a document without.""" + + source_format: Literal["pdf", "docx"] | str | None = None + """`pdf`, or `docx` for a Word document Didit converted to PDF.""" + + conversion: SignatureResultDocumentsConversion | None = None + """`null` for a PDF. For a Word document, the original file and the conversion.""" + + fields: list[SignatureFieldResult] | None = None + """Fields on the document, with their values. Empty for a document without fields.""" + + +class SignatureResultIdentityUsed(BaseModel): + session_id: str | None = None + + kyc_id: str | None = None + + verified: bool | None = None + + expected_name: str | None = None + """The name the signer had to match.""" + + +class SignatureResultSigner(BaseModel): + name: str | None = None + """Verified name.""" + + capacity: Literal["self", "company"] | str | None = None + + company: str | None = None + + role: str | None = None + """The key person's designation, as declared.""" + + roles: list[str] | None = None + """The key person's structured roles, as declared for the company.""" + + identity_session_id: str | None = None + """The KYC session that verified the signer.""" + + +class SignatureResultTimestamp(BaseModel): + """The trusted timestamp in the signature. `null` until signed.""" + + time: _dt.datetime | None = None + + qualified: bool | None = None + """Always `false` for AES.""" + + valid: bool | None = None + + +class SignatureResultValidationDocumentsCertificate(BaseModel): + """The certificate the documents were signed with. `null` until signed.""" + + issuer: str | None = None + + issued_to: str | None = None + + serial: str | None = None + """Serial number, hexadecimal.""" + + valid_from: _dt.datetime | None = None + + valid_until: _dt.datetime | None = None + + +class SignatureResultValidationDocumentsTimestamp(BaseModel): + """The trusted timestamp in the signature. `null` until signed.""" + + time: _dt.datetime | None = None + + qualified: bool | None = None + """Always `false` for AES.""" + + valid: bool | None = None + + +class SignatureResultValidationDocumentsTrustedList(BaseModel): + """EU Trusted List check. For AES: `{"status": "not_applicable", "reason": "aes_not_qualified"}`.""" + + status: str | None = None + + reason: str | None = None + + +class SignatureResultValidationDocuments(BaseModel): + id: str | None = None + + checked_at: _dt.datetime | None = None + + valid: bool | None = None + """`true` when the signature and the timestamp are both valid and trusted.""" + + integrity: bool | None = None + """`true` when the document is unchanged since signing.""" + + trusted: bool | None = None + """`true` when the signing certificate chains to a trusted root.""" + + revoked: bool | None = None + + trusted_list: SignatureResultValidationDocumentsTrustedList | None = None + """EU Trusted List check. For AES: `{"status": "not_applicable", "reason": "aes_not_qualified"}`.""" + + certificate: SignatureResultValidationDocumentsCertificate | None = None + """The certificate the documents were signed with. `null` until signed.""" + + timestamp: SignatureResultValidationDocumentsTimestamp | None = None + """The trusted timestamp in the signature. `null` until signed.""" + + +class SignatureResultValidation(BaseModel): + """The last cryptographic check of the signed documents. `null` until the documents are signed. If Didit can't confirm the outcome of signing, it's `{"valid": null, "reason": "signature_provider_outcome_unknown", "checked_at": ...}` and `status` stays `signing`: treat it as not signed.""" + + valid: bool | None = None + """`true` when every document is valid. `null` when the outcome of signing couldn't be confirmed.""" + + reason: str | None = None + """Set when `valid` is `null`: `signature_provider_outcome_unknown`.""" + + checked_at: _dt.datetime | None = None + + documents: list[SignatureResultValidationDocuments] | None = None + + +class SignatureResultWarnings(BaseModel): + code: ( + Literal[ + "SIGNATURE_DECLINED", + "SIGNATURE_OTP_FAILED", + "SIGNATURE_NAME_MISMATCH", + "SIGNATURE_IDENTITY_NOT_VERIFIED", + "SIGNATURE_DOCUMENT_CHANGED", + "SIGNATURE_FIELD_DATA_MISSING", + ] + | str + | None + ) = None + + node_id: str | None = None + + +class SignatureResult(BaseModel): + """One signer's result for one Electronic Signature step. See the [Electronic Signature report](/core-technology/electronic-signature/report-electronic-signature).""" + + id: str | None = None + """Pass it as `signature_id` to download this signer's evidence or validate their signature.""" + + node_id: str | None = None + """The workflow step that produced it.""" + + status: Literal["intro", "review", "code", "signing", "signed", "declined"] | str | None = None + """Where the signer is in the step. Only `signed` means the documents are signed.""" + + verdict: Literal["Not Finished", "Approved", "In Review", "Declined"] | str | None = None + """The step's outcome. A warning sets it through its rule; custom status rules apply after.""" + + level: Literal["aes", "qes"] | str | None = None + """`aes`. `qes` (Qualified Electronic Signature, QES) is coming soon.""" + + format: Literal["pades"] | str | None = None + + profile: str | None = None + """`PAdES-B-LT` once signed.""" + + signer: SignatureResultSigner | None = None + + identity_used: SignatureResultIdentityUsed | None = None + + certificate: SignatureResultCertificate | None = None + """The certificate the documents were signed with. `null` until signed.""" + + confirmation: SignatureResultConfirmation | None = None + + timestamp: SignatureResultTimestamp | None = None + """The trusted timestamp in the signature. `null` until signed.""" + + documents: list[SignatureResultDocuments] | None = None + + adopted_signature: SignatureResultAdoptedSignature | None = None + """The signature and initials the signer adopted for signature and initials fields. `null` until the signer adopts one.""" + + electronic_signature_consent_at: _dt.datetime | None = None + """When the signer agreed, on the review screen, to sign the documents electronically. Set when any document has fields.""" + + evidence_file: str | None = None + """Path of the evidence PDF, once signed. See [Download the signature evidence](/sessions-api/electronic-signature/download-evidence).""" + + warnings: list[SignatureResultWarnings] | None = None + """Warnings raised for this signer. See [Electronic Signature warnings](/core-technology/electronic-signature/warnings-electronic-signature).""" + + validation: SignatureResultValidation | None = None + """The last cryptographic check of the signed documents. `null` until the documents are signed. If Didit can't confirm the outcome of signing, it's `{"valid": null, "reason": "signature_provider_outcome_unknown", "checked_at": ...}` and `status` stays `signing`: treat it as not signed.""" + + declined_at: _dt.datetime | None = None + + declined_action: Literal["DECLINE", "REVIEW", "NO_ACTION"] | str | None = None + + signed_at: _dt.datetime | None = None + + expires_at: _dt.datetime | None = None + """When the signing link stops working.""" + + email_sent_at: _dt.datetime | None = None + + email_unavailable_reason: str | None = None + + ip_address: str | None = None + """The signer's IP address when they asked for their code.""" + + place: str | None = None + """Not collected: always `null`.""" + + place_unavailable_reason: Literal["not_collected"] | str | None = None diff --git a/src/didit/types/components/simple_workflow_feature.py b/src/didit/types/components/simple_workflow_feature.py index a86e3e4..13ee806 100644 --- a/src/didit/types/components/simple_workflow_feature.py +++ b/src/didit/types/components/simple_workflow_feature.py @@ -27,13 +27,14 @@ class SimpleWorkflowFeature(TypedDict, total=False): "KYB_REGISTRY", "KYB_DOCUMENTS", "KYB_KEY_PEOPLE", + "SIGNATURE", ] | str ] """Feature to run. Use the uppercase values shown here.""" config: Mapping[str, object] - """Feature-specific settings. For `QUESTIONNAIRE`, include `questionnaire_uuid` with the `questionnaire_id` returned by the questionnaire create endpoint. For `OCR`, `methods` selects which ID Verification methods each country may use (document capture, non-doc lookup against a government or other authoritative source, and digital identity wallets); omit it and every country is document capture only.""" + """Feature-specific settings. For `QUESTIONNAIRE`, include `questionnaire_uuid` with the `questionnaire_id` returned by the questionnaire create endpoint. For `OCR`, `methods` selects which ID Verification methods each country may use (document capture, non-doc lookup against a government or other authoritative source, and digital identity wallets); omit it and every country is document capture only. For `SIGNATURE`, see [Electronic Signature settings](/management-api/workflows/feature-configs#electronic-signature): `signature_level` accepts `aes` only for now, and workflow documents and their fields are managed in the console, so through the API send documents and fields per session with `signature_documents_source: "session"`.""" label: str """Optional internal label for this workflow node.""" diff --git a/src/didit/types/components/transaction_detail.py b/src/didit/types/components/transaction_detail.py index 8205a93..2b04ecf 100644 --- a/src/didit/types/components/transaction_detail.py +++ b/src/didit/types/components/transaction_detail.py @@ -5,11 +5,16 @@ from typing import Literal from didit._models import BaseModel +from didit.types.components.transaction_travel_rule import TransactionTravelRule +from didit.types.components.transaction_verification_session_action import ( + TransactionVerificationSessionAction, +) +from didit.types.components.transaction_wallet_ownership_action import ( + TransactionWalletOwnershipAction, +) __all__ = [ "TransactionDetail", - "TransactionDetailActionRequiredVerificationSession", - "TransactionDetailActionRequiredWalletOwnership", "TransactionDetailActivities", "TransactionDetailAlerts", "TransactionDetailDocuments", @@ -23,39 +28,9 @@ "TransactionDetailRemediation", "TransactionDetailRuleRuns", "TransactionDetailTags", - "TransactionDetailTravelRule", ] -class TransactionDetailActionRequiredVerificationSession(BaseModel): - type: Literal["verification_session"] | str - - url: str - """Hosted verification URL to open for the end user.""" - - session_id: str | None = None - """Verification session id.""" - - session_token: str | None = None - """Session token, used by the mobile SDKs to launch the flow natively.""" - - status: str | None = None - """Verification session status.""" - - -class TransactionDetailActionRequiredWalletOwnership(BaseModel): - type: Literal["wallet_ownership"] | str - - url: str - """Hosted wallet-ownership widget URL. Open it in a real browser surface - on mobile use SFSafariViewController or a Chrome Custom Tab, never an embedded webview.""" - - widget_session_id: str | None = None - """Wallet-ownership widget session id.""" - - expires_at: _dt.datetime | None = None - """When the widget session expires.""" - - class TransactionDetailActivities(BaseModel): uuid: str | None = None @@ -354,7 +329,7 @@ class TransactionDetailRuleRuns(BaseModel): """Summary of the actions the rule executed, joined with `; ` (e.g. `score +30; change_status:IN_REVIEW; add_tag:high_risk_country`). Parts: `score +N`, `change_status:`, `add_tag:`, `add_note`, `add_to_list`, `open_case`. `null` when the rule executed no action.""" metadata: dict[str, object] | None = None - """Evaluation detail: `scope_match`, `conditions[]` and `aggregations[]`. Each condition entry carries `field`, `operator`, `expected_value`, the `current_value` read from the transaction, `matched`, `group_index` and `group_logic`. Each aggregation entry (velocity / count / sum) carries `metric`, `field`, `operator`, `expected_value`, the `computed_value` the aggregate evaluated to, `window`, `filters`, `matched` and `sample_references`; deferred async aggregations have `computed_value: null` and `deferred: true`.""" + """Evaluation detail: `scope_match`, `conditions[]` and `aggregations[]`. Each condition entry carries `field`, `operator`, `expected_value`, the `current_value` read from the transaction, `matched`, `group_index` and `group_logic`. Each aggregation entry (velocity / count / sum) carries `metric`, `field`, `operator`, `expected_value`, the `computed_value` the aggregate evaluated to, `window`, `filters`, `matched` and `sample_references`; deferred async aggregations have `computed_value: null` and `deferred: true`. A nested condition group is one `conditions[]` entry with `operator` `any_of` or `all_of`, `field` null, and its members' results under its own `conditions`.""" created_at: _dt.datetime | None = None @@ -372,26 +347,6 @@ class TransactionDetailTags(BaseModel): """Whether the tag is a Didit preset or a custom tag.""" -class TransactionDetailTravelRule(BaseModel): - """Travel Rule transfer or check. Populated when `travel_rule_details` was submitted, and also when Travel Rule is enabled and the transaction was created with `transaction_category: travelRule` (Didit initiates the exchange itself, even without `travel_rule_details`). `null` when no transfer or check exists.""" - - uuid: str | None = None - - status: str | None = None - - protocol: str | None = None - - required: bool | None = None - - obligations_count: int | None = None - - originator_data: dict[str, object] | None = None - - beneficiary_data: dict[str, object] | None = None - - metadata: dict[str, object] | None = None - - class TransactionDetail(BaseModel): """Full monitoring record for one transaction, as returned by `GET /v3/transactions/{transaction_id}/` and `POST /v3/transactions/`.""" @@ -498,8 +453,8 @@ class TransactionDetail(BaseModel): provider_results: list[TransactionDetailProviderResults] | None = None """Raw provider payloads (AML screening, blockchain analytics, etc.).""" - travel_rule: TransactionDetailTravelRule | None = None - """Travel Rule transfer or check. Populated when `travel_rule_details` was submitted, and also when Travel Rule is enabled and the transaction was created with `transaction_category: travelRule` (Didit initiates the exchange itself, even without `travel_rule_details`). `null` when no transfer or check exists.""" + travel_rule: TransactionTravelRule | None = None + """Travel Rule transfer or check. Populated when `travel_rule_details` was submitted, and also when Travel Rule is enabled and the transaction was created with `transaction_category: travelRule` (Didit initiates the exchange itself, even without `travel_rule_details`). `null` when no transfer or check exists. On an inbound transfer, the counterparty's `originator_data` and `beneficiary_data` stay masked (`data_masked: true`) until wallet ownership is confirmed.""" network_snapshot: TransactionDetailNetworkSnapshot | None = None """Graph snapshot of related transactions/parties used by the rules engine. Either the `network_snapshot` submitted with the transaction or the graph Didit builds from a successful AML wallet screening (which replaces a submitted one). `null` when neither exists.""" @@ -508,9 +463,7 @@ class TransactionDetail(BaseModel): """Remediation session offered to the user when re-verification is required. Superseded by `action_required`, which is the canonical block; kept for backward compatibility.""" action_required: ( - TransactionDetailActionRequiredVerificationSession - | TransactionDetailActionRequiredWalletOwnership - | None + TransactionVerificationSessionAction | TransactionWalletOwnershipAction | None ) = None """Pending end-user action required to complete this transaction, or null when there is none. The Didit SDKs auto-launch the action by default and refresh the transaction when it finishes. Two variants, discriminated by `type`: `verification_session` (a hosted verification session created by a rule action) and `wallet_ownership` (a wallet-ownership widget session auto-minted for a Travel Rule transfer that needs proof of wallet control, gated by the `auto_wallet_verification` Travel Rule setting). When both could apply, `wallet_ownership` takes precedence because it blocks the Travel Rule exchange.""" diff --git a/src/didit/types/components/transaction_note.py b/src/didit/types/components/transaction_note.py new file mode 100644 index 0000000..d215498 --- /dev/null +++ b/src/didit/types/components/transaction_note.py @@ -0,0 +1,61 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["TransactionNote", "TransactionNoteMetadata"] + + +class TransactionNoteMetadata(BaseModel): + """Tells the note types apart. Notes written through the API or the Console carry `mentioned_emails`. A status change made in the Console carries `type: status_change` with `previous_status`, `new_status`, `reason_code` and `reason_label`. Notes Didit adds carry `source`: `rule_action` (with `rule_uuid`) for a rule's `add_note` action, or `remediation_session` for a problem creating the user-action verification session.""" + + mentioned_emails: list[str] | None = None + """Mentioned organization members, lower-cased and de-duplicated. Empty when nobody was mentioned.""" + + type: Literal["status_change"] | str | None = None + """Present on notes recording a status change made in the Console.""" + + previous_status: Literal["APPROVED", "IN_REVIEW", "DECLINED", "AWAITING_USER"] | str | None = ( + None + ) + """Status before the change.""" + + new_status: Literal["APPROVED", "IN_REVIEW", "DECLINED", "AWAITING_USER"] | str | None = None + """Status after the change.""" + + reason_code: str | None = None + """Reason code given with the change.""" + + reason_label: str | None = None + """Reason label given with the change.""" + + source: Literal["rule_action", "remediation_session"] | str | None = None + """Present on notes Didit adds automatically.""" + + rule_uuid: str | None = None + """Rule whose `add_note` action wrote the note (with `source: rule_action`).""" + + +class TransactionNote(BaseModel): + """One note on a monitored transaction's review timeline.""" + + uuid: str + """Note identifier.""" + + note: str + """Plain-text note body.""" + + actor_email: str | None + """Email of the Console user who wrote the note or changed the status. `null` for notes created with an API key and for notes Didit adds automatically.""" + + actor_name: str | None + """Display name of the Console user (their email when they have no name). `Rule Engine` on notes Didit adds automatically. `null` for notes created with an API key.""" + + metadata: TransactionNoteMetadata + """Tells the note types apart. Notes written through the API or the Console carry `mentioned_emails`. A status change made in the Console carries `type: status_change` with `previous_status`, `new_status`, `reason_code` and `reason_label`. Notes Didit adds carry `source`: `rule_action` (with `rule_uuid`) for a rule's `add_note` action, or `remediation_session` for a problem creating the user-action verification session.""" + + created_at: _dt.datetime + """When the note was stored.""" diff --git a/src/didit/types/components/transaction_rule_action.py b/src/didit/types/components/transaction_rule_action.py index cf290a7..bb9d2d1 100644 --- a/src/didit/types/components/transaction_rule_action.py +++ b/src/didit/types/components/transaction_rule_action.py @@ -1,185 +1,52 @@ # Code generated by codegen from openapi/*.json. DO NOT EDIT. from __future__ import annotations -from typing import Literal +from typing_extensions import TypeAlias -from typing_extensions import Required, TypeAlias, TypedDict - -from didit._models import BaseModel - -__all__ = [ - "TransactionRuleAction", - "TransactionRuleActionParam", - "TransactionRuleActionVariant1", - "TransactionRuleActionVariant1Param", - "TransactionRuleActionVariant2", - "TransactionRuleActionVariant2Param", - "TransactionRuleActionVariant3", - "TransactionRuleActionVariant3Param", - "TransactionRuleActionVariant4", - "TransactionRuleActionVariant4Param", - "TransactionRuleActionVariant5", - "TransactionRuleActionVariant5Param", - "TransactionRuleActionVariant6", - "TransactionRuleActionVariant6Param", -] - - -class TransactionRuleActionVariant1(BaseModel): - """Add points to the transaction's cumulative risk score; the total decides the status via the application's review/decline thresholds.""" - - type: Literal["add_score"] | str - - value: int - """Points added to the transaction's risk score. May be negative.""" - - -class TransactionRuleActionVariant2(BaseModel): - """Directly set the transaction status. DECLINED always wins across matched rules.""" - - type: Literal["change_status"] | str - - value: Literal["APPROVED", "IN_REVIEW", "DECLINED", "AWAITING_USER"] | str - """Status to set immediately, regardless of score. Alias key: status.""" - - workflow_id: str | None = None - """REQUIRED when value is AWAITING_USER: the verification workflow the subject is asked to complete. Alias key: remediation_workflow_id. Didit creates a hosted remediation session for the subject when the rule fires.""" - - -class TransactionRuleActionVariant3(BaseModel): - """Attach an application tag to the matched transaction.""" - - type: Literal["add_tags"] | str - - tag_uuid: str | None = None - """Existing application tag to attach.""" - - tag_name: str | None = None - """Tag name to attach; created on first use (stored uppercased). Provide tag_uuid or tag_name.""" - - tag_color: str | None = None - """Hex color used when tag_name creates a new tag, e.g. #007AFE.""" - - -class TransactionRuleActionVariant4(BaseModel): - """Attach an automated analyst note to the matched transaction.""" - - type: Literal["add_note"] | str - - note: str - """Note text attached to the transaction (author: Rule Engine). Alias key: value.""" - - -class TransactionRuleActionVariant5(BaseModel): - """Add the subject's vendor_data to a blocklist, allowlist or custom list.""" - - type: Literal["add_to_list"] | str - - list_id: str - """List to add the subject's vendor_data to.""" - - -class TransactionRuleActionVariant6(BaseModel): - """Open (or add to) an investigation case. Only the first open_case action on a rule is applied.""" - - type: Literal["open_case"] | str - - blueprint: str | None = None - """Case blueprint UUID; defaults to the application's default blueprint.""" - - grouping: Literal["by_applicant", "by_rule_and_applicant"] | str | None = None - """Whether repeat matches join one case per applicant or one per rule+applicant.""" +from didit.types.components.transaction_rule_action_add_note import ( + TransactionRuleActionAddNote, + TransactionRuleActionAddNoteParam, +) +from didit.types.components.transaction_rule_action_add_score import ( + TransactionRuleActionAddScore, + TransactionRuleActionAddScoreParam, +) +from didit.types.components.transaction_rule_action_add_tags import ( + TransactionRuleActionAddTags, + TransactionRuleActionAddTagsParam, +) +from didit.types.components.transaction_rule_action_add_to_list import ( + TransactionRuleActionAddToList, + TransactionRuleActionAddToListParam, +) +from didit.types.components.transaction_rule_action_change_status import ( + TransactionRuleActionChangeStatus, + TransactionRuleActionChangeStatusParam, +) +from didit.types.components.transaction_rule_action_open_case import ( + TransactionRuleActionOpenCase, + TransactionRuleActionOpenCaseParam, +) - attach_matched_transaction: bool | None = None - """Attach the matched transaction to the case.""" +__all__ = ["TransactionRuleAction", "TransactionRuleActionParam"] TransactionRuleAction: TypeAlias = ( - TransactionRuleActionVariant1 - | TransactionRuleActionVariant2 - | TransactionRuleActionVariant3 - | TransactionRuleActionVariant4 - | TransactionRuleActionVariant5 - | TransactionRuleActionVariant6 + TransactionRuleActionAddScore + | TransactionRuleActionChangeStatus + | TransactionRuleActionAddTags + | TransactionRuleActionAddNote + | TransactionRuleActionAddToList + | TransactionRuleActionOpenCase ) """One rule action. A rule can carry several; all are applied together when it matches. Rules in TEST mode record what they would have done without applying any action.""" - -class TransactionRuleActionVariant1Param(TypedDict, total=False): - """Add points to the transaction's cumulative risk score; the total decides the status via the application's review/decline thresholds.""" - - type: Required[Literal["add_score"] | str] - - value: Required[int] - """Points added to the transaction's risk score. May be negative.""" - - -class TransactionRuleActionVariant2Param(TypedDict, total=False): - """Directly set the transaction status. DECLINED always wins across matched rules.""" - - type: Required[Literal["change_status"] | str] - - value: Required[Literal["APPROVED", "IN_REVIEW", "DECLINED", "AWAITING_USER"] | str] - """Status to set immediately, regardless of score. Alias key: status.""" - - workflow_id: str - """REQUIRED when value is AWAITING_USER: the verification workflow the subject is asked to complete. Alias key: remediation_workflow_id. Didit creates a hosted remediation session for the subject when the rule fires.""" - - -class TransactionRuleActionVariant3Param(TypedDict, total=False): - """Attach an application tag to the matched transaction.""" - - type: Required[Literal["add_tags"] | str] - - tag_uuid: str - """Existing application tag to attach.""" - - tag_name: str - """Tag name to attach; created on first use (stored uppercased). Provide tag_uuid or tag_name.""" - - tag_color: str - """Hex color used when tag_name creates a new tag, e.g. #007AFE.""" - - -class TransactionRuleActionVariant4Param(TypedDict, total=False): - """Attach an automated analyst note to the matched transaction.""" - - type: Required[Literal["add_note"] | str] - - note: Required[str] - """Note text attached to the transaction (author: Rule Engine). Alias key: value.""" - - -class TransactionRuleActionVariant5Param(TypedDict, total=False): - """Add the subject's vendor_data to a blocklist, allowlist or custom list.""" - - type: Required[Literal["add_to_list"] | str] - - list_id: Required[str] - """List to add the subject's vendor_data to.""" - - -class TransactionRuleActionVariant6Param(TypedDict, total=False): - """Open (or add to) an investigation case. Only the first open_case action on a rule is applied.""" - - type: Required[Literal["open_case"] | str] - - blueprint: str - """Case blueprint UUID; defaults to the application's default blueprint.""" - - grouping: Literal["by_applicant", "by_rule_and_applicant"] | str - """Whether repeat matches join one case per applicant or one per rule+applicant.""" - - attach_matched_transaction: bool - """Attach the matched transaction to the case.""" - - TransactionRuleActionParam: TypeAlias = ( - TransactionRuleActionVariant1Param - | TransactionRuleActionVariant2Param - | TransactionRuleActionVariant3Param - | TransactionRuleActionVariant4Param - | TransactionRuleActionVariant5Param - | TransactionRuleActionVariant6Param + TransactionRuleActionAddScoreParam + | TransactionRuleActionChangeStatusParam + | TransactionRuleActionAddTagsParam + | TransactionRuleActionAddNoteParam + | TransactionRuleActionAddToListParam + | TransactionRuleActionOpenCaseParam ) """One rule action. A rule can carry several; all are applied together when it matches. Rules in TEST mode record what they would have done without applying any action.""" diff --git a/src/didit/types/components/transaction_rule_action_add_note.py b/src/didit/types/components/transaction_rule_action_add_note.py new file mode 100644 index 0000000..e38fb25 --- /dev/null +++ b/src/didit/types/components/transaction_rule_action_add_note.py @@ -0,0 +1,28 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +from didit._models import BaseModel + +__all__ = ["TransactionRuleActionAddNote", "TransactionRuleActionAddNoteParam"] + + +class TransactionRuleActionAddNote(BaseModel): + """Attach an automated analyst note to the matched transaction.""" + + type: Literal["add_note"] | str + + note: str + """Note text attached to the transaction (author: Rule Engine). Alias key: value.""" + + +class TransactionRuleActionAddNoteParam(TypedDict, total=False): + """Attach an automated analyst note to the matched transaction.""" + + type: Required[Literal["add_note"] | str] + + note: Required[str] + """Note text attached to the transaction (author: Rule Engine). Alias key: value.""" diff --git a/src/didit/types/components/transaction_rule_action_add_score.py b/src/didit/types/components/transaction_rule_action_add_score.py new file mode 100644 index 0000000..0c3a7e0 --- /dev/null +++ b/src/didit/types/components/transaction_rule_action_add_score.py @@ -0,0 +1,28 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +from didit._models import BaseModel + +__all__ = ["TransactionRuleActionAddScore", "TransactionRuleActionAddScoreParam"] + + +class TransactionRuleActionAddScore(BaseModel): + """Add points to the transaction's cumulative risk score; the total decides the status via the application's review/decline thresholds.""" + + type: Literal["add_score"] | str + + value: int + """Points added to the transaction's risk score. May be negative.""" + + +class TransactionRuleActionAddScoreParam(TypedDict, total=False): + """Add points to the transaction's cumulative risk score; the total decides the status via the application's review/decline thresholds.""" + + type: Required[Literal["add_score"] | str] + + value: Required[int] + """Points added to the transaction's risk score. May be negative.""" diff --git a/src/didit/types/components/transaction_rule_action_add_tags.py b/src/didit/types/components/transaction_rule_action_add_tags.py new file mode 100644 index 0000000..a8f633f --- /dev/null +++ b/src/didit/types/components/transaction_rule_action_add_tags.py @@ -0,0 +1,40 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +from didit._models import BaseModel + +__all__ = ["TransactionRuleActionAddTags", "TransactionRuleActionAddTagsParam"] + + +class TransactionRuleActionAddTags(BaseModel): + """Attach an application tag to the matched transaction.""" + + type: Literal["add_tags"] | str + + tag_uuid: str | None = None + """Existing application tag to attach.""" + + tag_name: str | None = None + """Tag name to attach; created on first use (stored uppercased). Provide tag_uuid or tag_name.""" + + tag_color: str | None = None + """Hex color used when tag_name creates a new tag, e.g. #007AFE.""" + + +class TransactionRuleActionAddTagsParam(TypedDict, total=False): + """Attach an application tag to the matched transaction.""" + + type: Required[Literal["add_tags"] | str] + + tag_uuid: str + """Existing application tag to attach.""" + + tag_name: str + """Tag name to attach; created on first use (stored uppercased). Provide tag_uuid or tag_name.""" + + tag_color: str + """Hex color used when tag_name creates a new tag, e.g. #007AFE.""" diff --git a/src/didit/types/components/transaction_rule_action_add_to_list.py b/src/didit/types/components/transaction_rule_action_add_to_list.py new file mode 100644 index 0000000..58c3e50 --- /dev/null +++ b/src/didit/types/components/transaction_rule_action_add_to_list.py @@ -0,0 +1,28 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +from didit._models import BaseModel + +__all__ = ["TransactionRuleActionAddToList", "TransactionRuleActionAddToListParam"] + + +class TransactionRuleActionAddToList(BaseModel): + """Add the subject's vendor_data to a blocklist, allowlist or custom list.""" + + type: Literal["add_to_list"] | str + + list_id: str + """List to add the subject's vendor_data to.""" + + +class TransactionRuleActionAddToListParam(TypedDict, total=False): + """Add the subject's vendor_data to a blocklist, allowlist or custom list.""" + + type: Required[Literal["add_to_list"] | str] + + list_id: Required[str] + """List to add the subject's vendor_data to.""" diff --git a/src/didit/types/components/transaction_rule_action_change_status.py b/src/didit/types/components/transaction_rule_action_change_status.py new file mode 100644 index 0000000..e0ab07f --- /dev/null +++ b/src/didit/types/components/transaction_rule_action_change_status.py @@ -0,0 +1,34 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +from didit._models import BaseModel + +__all__ = ["TransactionRuleActionChangeStatus", "TransactionRuleActionChangeStatusParam"] + + +class TransactionRuleActionChangeStatus(BaseModel): + """Directly set the transaction status. DECLINED always wins across matched rules.""" + + type: Literal["change_status"] | str + + value: Literal["APPROVED", "IN_REVIEW", "DECLINED", "AWAITING_USER"] | str + """Status to set immediately, regardless of score. Alias key: status.""" + + workflow_id: str | None = None + """REQUIRED when value is AWAITING_USER: the verification workflow the subject is asked to complete. Alias key: remediation_workflow_id. Didit creates a hosted remediation session for the subject when the rule fires.""" + + +class TransactionRuleActionChangeStatusParam(TypedDict, total=False): + """Directly set the transaction status. DECLINED always wins across matched rules.""" + + type: Required[Literal["change_status"] | str] + + value: Required[Literal["APPROVED", "IN_REVIEW", "DECLINED", "AWAITING_USER"] | str] + """Status to set immediately, regardless of score. Alias key: status.""" + + workflow_id: str + """REQUIRED when value is AWAITING_USER: the verification workflow the subject is asked to complete. Alias key: remediation_workflow_id. Didit creates a hosted remediation session for the subject when the rule fires.""" diff --git a/src/didit/types/components/transaction_rule_action_open_case.py b/src/didit/types/components/transaction_rule_action_open_case.py new file mode 100644 index 0000000..f21146e --- /dev/null +++ b/src/didit/types/components/transaction_rule_action_open_case.py @@ -0,0 +1,40 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +from didit._models import BaseModel + +__all__ = ["TransactionRuleActionOpenCase", "TransactionRuleActionOpenCaseParam"] + + +class TransactionRuleActionOpenCase(BaseModel): + """Open (or add to) an investigation case. Only the first open_case action on a rule is applied.""" + + type: Literal["open_case"] | str + + blueprint: str | None = None + """Case blueprint UUID; defaults to the application's default blueprint.""" + + grouping: Literal["by_applicant", "by_rule_and_applicant"] | str | None = None + """Whether repeat matches join one case per applicant or one per rule+applicant.""" + + attach_matched_transaction: bool | None = None + """Attach the matched transaction to the case.""" + + +class TransactionRuleActionOpenCaseParam(TypedDict, total=False): + """Open (or add to) an investigation case. Only the first open_case action on a rule is applied.""" + + type: Required[Literal["open_case"] | str] + + blueprint: str + """Case blueprint UUID; defaults to the application's default blueprint.""" + + grouping: Literal["by_applicant", "by_rule_and_applicant"] | str + """Whether repeat matches join one case per applicant or one per rule+applicant.""" + + attach_matched_transaction: bool + """Attach the matched transaction to the case.""" diff --git a/src/didit/types/components/transaction_rule_condition.py b/src/didit/types/components/transaction_rule_condition.py index fdfa56d..708489b 100644 --- a/src/didit/types/components/transaction_rule_condition.py +++ b/src/didit/types/components/transaction_rule_condition.py @@ -14,7 +14,7 @@ class TransactionRuleCondition(BaseModel): """A condition reads only the transaction being evaluated - it never filters the aggregation window.""" field: str - """Transaction field path to evaluate. Core fields: amount, currency, direction, action_type, score, severity, transaction_type, payment_details, preferred_currency_amount (falls back to amount_in_default_currency), default_currency_code, subject_country, counterparty_country, subject_vendor_data, counterparty_vendor_data, subject_device_fingerprint, counterparty_device_fingerprint, subject_browser_family, subject_browser_version, subject_platform, subject_accept_language, subject_session_age_ms, subject_ip_country, counterparty_ip_country, subject_ip_address, counterparty_ip_address, subject_payment_method_type, subject_payment_method_country, subject_payment_method_fingerprint, subject_payment_method_account_id (and the counterparty_payment_method_* equivalents), travel_rule_status, travel_rule_required, travel_rule_obligations_count, subject_days_since_previous_transaction, tags, and custom_values. for any key submitted in custom_properties on transaction create. Country-code fields accept ISO-2 or ISO-3; both sides are normalized to ISO-3 before comparison.""" + """Transaction field path to evaluate. Core fields: amount, currency, direction, action_type, score, severity, transaction_type, payment_details, preferred_currency_amount (falls back to amount_in_default_currency), default_currency_code, subject_country, counterparty_country, subject_vendor_data, counterparty_vendor_data, subject_device_fingerprint, counterparty_device_fingerprint, subject_browser_family, subject_browser_version, subject_platform, subject_accept_language, subject_session_age_ms, subject_ip_country, counterparty_ip_country, subject_ip_address, counterparty_ip_address, subject_payment_method_type, subject_payment_method_country, subject_payment_method_fingerprint, subject_payment_method_account_id (and the counterparty_payment_method_* equivalents), travel_rule_status, travel_rule_required, travel_rule_obligations_count, counterparty_dd_status (your decision on the Travel Rule counterparty: APPROVED, RESTRICTED or BLOCKED, an entity following its brand's), counterparty_risk_level (LOW, MEDIUM or HIGH; HIGH while your own counterparty screening is flagged), counterparty_jurisdiction (the counterparty VASP's country), counterparty_responsiveness (RESPONSIVE, UNRESPONSIVE or UNKNOWN), subject_days_since_previous_transaction, tags, and custom_values. for any key submitted in custom_properties on transaction create. Country-code fields accept ISO-2 or ISO-3; both sides are normalized to ISO-3 before comparison.""" operator: ( Literal[ @@ -65,7 +65,7 @@ class TransactionRuleConditionParam(TypedDict, total=False): """A condition reads only the transaction being evaluated - it never filters the aggregation window.""" field: Required[str] - """Transaction field path to evaluate. Core fields: amount, currency, direction, action_type, score, severity, transaction_type, payment_details, preferred_currency_amount (falls back to amount_in_default_currency), default_currency_code, subject_country, counterparty_country, subject_vendor_data, counterparty_vendor_data, subject_device_fingerprint, counterparty_device_fingerprint, subject_browser_family, subject_browser_version, subject_platform, subject_accept_language, subject_session_age_ms, subject_ip_country, counterparty_ip_country, subject_ip_address, counterparty_ip_address, subject_payment_method_type, subject_payment_method_country, subject_payment_method_fingerprint, subject_payment_method_account_id (and the counterparty_payment_method_* equivalents), travel_rule_status, travel_rule_required, travel_rule_obligations_count, subject_days_since_previous_transaction, tags, and custom_values. for any key submitted in custom_properties on transaction create. Country-code fields accept ISO-2 or ISO-3; both sides are normalized to ISO-3 before comparison.""" + """Transaction field path to evaluate. Core fields: amount, currency, direction, action_type, score, severity, transaction_type, payment_details, preferred_currency_amount (falls back to amount_in_default_currency), default_currency_code, subject_country, counterparty_country, subject_vendor_data, counterparty_vendor_data, subject_device_fingerprint, counterparty_device_fingerprint, subject_browser_family, subject_browser_version, subject_platform, subject_accept_language, subject_session_age_ms, subject_ip_country, counterparty_ip_country, subject_ip_address, counterparty_ip_address, subject_payment_method_type, subject_payment_method_country, subject_payment_method_fingerprint, subject_payment_method_account_id (and the counterparty_payment_method_* equivalents), travel_rule_status, travel_rule_required, travel_rule_obligations_count, counterparty_dd_status (your decision on the Travel Rule counterparty: APPROVED, RESTRICTED or BLOCKED, an entity following its brand's), counterparty_risk_level (LOW, MEDIUM or HIGH; HIGH while your own counterparty screening is flagged), counterparty_jurisdiction (the counterparty VASP's country), counterparty_responsiveness (RESPONSIVE, UNRESPONSIVE or UNKNOWN), subject_days_since_previous_transaction, tags, and custom_values. for any key submitted in custom_properties on transaction create. Country-code fields accept ISO-2 or ISO-3; both sides are normalized to ISO-3 before comparison.""" operator: Required[ Literal[ diff --git a/src/didit/types/components/transaction_summary.py b/src/didit/types/components/transaction_summary.py new file mode 100644 index 0000000..1688fdd --- /dev/null +++ b/src/didit/types/components/transaction_summary.py @@ -0,0 +1,132 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["TransactionSummary", "TransactionSummaryTags"] + + +class TransactionSummaryTags(BaseModel): + uuid: str | None = None + + name: str | None = None + + color: str | None = None + """Hex color, for example `#F59E0B`.""" + + description: str | None = None + + source: Literal["didit", "custom"] | str | None = None + """Didit preset or custom tag.""" + + +class TransactionSummary(BaseModel): + """Summary of a monitored transaction, the shape `GET /v3/transactions/` lists. Call `GET /v3/transactions/{transaction_id}/` with `uuid` for parties, alerts and rule runs.""" + + uuid: str | None = None + """Didit transaction UUID. Use it as `{transaction_id}` on the transaction endpoints.""" + + transaction_number: int | None = None + """Application-scoped sequential number shown in the Console.""" + + txn_id: str | None = None + """Your own identifier from the create call. Unique per application.""" + + transaction_type: str | None = None + """Category as stored: `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet`, `gamblingLimitChange`, `gamblingBonusChange` or `auditTrailEvent`.""" + + action_type: str | None = None + """Sub-type within the category, for example `withdrawal`.""" + + status: Literal["APPROVED", "IN_REVIEW", "DECLINED", "AWAITING_USER"] | str | None = None + """Current monitoring verdict.""" + + source: Literal["LIVE", "IMPORTED"] | str | None = None + """`LIVE` for transactions submitted for monitoring, `IMPORTED` for historical imports.""" + + direction: Literal["INBOUND", "OUTBOUND"] | str | None = None + """Direction relative to the subject.""" + + amount: str | None = None + """Amount as a decimal string with trailing zeros removed (up to 18 decimal places).""" + + currency: str | None = None + """Currency code or crypto asset symbol of `amount`.""" + + currency_type: str | None = None + """`fiat` or `crypto` when known.""" + + preferred_currency_amount: str | None = None + """`amount` converted to the application's preferred currency, when a rate was available.""" + + preferred_currency_code: str | None = None + """Currency of `preferred_currency_amount`.""" + + payment_details: str | None = None + """Free-text payment reference.""" + + txn_date: _dt.datetime | None = None + """When the transaction occurred.""" + + created_at: _dt.datetime | None = None + """When Didit recorded it.""" + + score: int | None = None + """Risk score. Higher is riskier.""" + + severity: Literal["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"] | str | None = None + """Risk severity, `null` until something sets it.""" + + decision_reason_code: str | None = None + """Machine-readable reason for the current `status`.""" + + decision_reason_label: str | None = None + """Human-readable label for `decision_reason_code`.""" + + tags: list[TransactionSummaryTags] | None = None + """Tags on the transaction.""" + + subject_name: str | None = None + """Full name of the `APPLICANT` party.""" + + subject_vendor_data: str | None = None + """Your `vendor_data` for the applicant.""" + + subject_type: Literal["user", "business"] | str | None = None + """Whether the applicant is linked to a Didit user or business; `null` when it is linked to neither.""" + + subject_uuid: str | None = None + """UUID of the Didit user or business the applicant is linked to.""" + + subject_country: str | None = None + """Country code of the applicant.""" + + counterparty_name: str | None = None + """Full name of the `COUNTERPARTY` party, if any.""" + + counterparty_vendor_data: str | None = None + """Your `vendor_data` for the counterparty.""" + + counterparty_country: str | None = None + """Country code of the counterparty.""" + + assigned_to_name: str | None = None + """Name of the Console reviewer assigned to the transaction.""" + + travel_rule_status: str | None = None + """Status of the transaction's Travel Rule transfer, or `null` when it has none.""" + + application_id: str | None = None + """Application that owns the transaction.""" + + application_name: str | None = None + """Name of that application.""" + + organization_id: str | None = None + """Organization that owns the application.""" + + organization_name: str | None = None + """Name of that organization.""" diff --git a/src/didit/types/components/transaction_travel_rule.py b/src/didit/types/components/transaction_travel_rule.py new file mode 100644 index 0000000..68b1d16 --- /dev/null +++ b/src/didit/types/components/transaction_travel_rule.py @@ -0,0 +1,113 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.travel_rule_chain_details import TravelRuleChainDetails +from didit.types.components.travel_rule_data_match import TravelRuleDataMatch +from didit.types.components.travel_rule_data_unmasked import TravelRuleDataUnmasked +from didit.types.components.travel_rule_delivery import TravelRuleDelivery +from didit.types.components.travel_rule_discovery_step import TravelRuleDiscoveryStep +from didit.types.components.travel_rule_next_action import TravelRuleNextAction + +__all__ = [ + "TransactionTravelRule", + "TransactionTravelRuleCounterpartyVasp", + "TransactionTravelRuleCounterpartyVaspDdScoreBreakdownValue", +] + + +class TransactionTravelRuleCounterpartyVaspDdScoreBreakdownValue(BaseModel): + points: float | None = None + """Points awarded for this component.""" + + max: int | None = None + """Maximum points this component can award.""" + + reason: str | None = None + """Machine-readable code of the component's verdict.""" + + detail: str | None = None + """Human-readable reason for the awarded points.""" + + +class TransactionTravelRuleCounterpartyVasp(BaseModel): + """The counterparty's directory entry, or `null` when the transfer links none.""" + + name: str | None = None + + lei: str | None = None + + dd_score: int | None = None + """Due-diligence score from 0 to 100, where higher is better. Same scale as in the VASP directory.""" + + dd_score_breakdown: ( + dict[str, TransactionTravelRuleCounterpartyVaspDdScoreBreakdownValue] | None + ) = None + """Per-component contributions to dd_score, one object per component as in the VASP directory (jurisdiction, verification, protocol, lei, history, essentials, responsiveness), each {points, max, reason, detail}. {} until the counterparty has been assessed.""" + + dd_assessed_at: _dt.datetime | None = None + """When the due-diligence assessment last ran. Null until the counterparty has been assessed.""" + + +class TransactionTravelRule(BaseModel): + """A transaction's Travel Rule transfer, as `GET /v3/transactions/{transaction_id}/`, the `201` of `POST /v3/transactions/` and each row of `GET /v3/transactions/` return it. `direction`, `rail`, `counterparty_vasp`, `delivery`, `discovery`, `reason` and `next_actions` have the same shape as on `PATCH /v3/transactions/{transaction_id}/travel-rule/`, so one read of a transaction says why its transfer is stuck and what moves it on.""" + + uuid: str | None = None + + status: str | None = None + + protocol: str | None = None + + required: bool | None = None + + obligations_count: int | None = None + + originator_data: dict[str, object] | None = None + """Originator party. For an inbound transfer the counterparty's values are masked until ownership is confirmed.""" + + beneficiary_data: dict[str, object] | None = None + """Beneficiary party. For an inbound transfer the counterparty's values are masked until ownership is confirmed.""" + + name_match: dict[str, object] | None = None + """The name comparison that decided the transfer (score and strictness). Null until one ran. The compared names are masked together with the parties.""" + + data_match: TravelRuleDataMatch | None = None + """Field-by-field comparison of the transfer's party data with what the other side knows about the wallet holder. It names fields and results, never values, so it stays readable while the counterparty's data is masked. Null until a comparison ran.""" + + mismatched_fields: list[str] | None = None + """Shortcut for `data_match.mismatched_fields`. Empty when nothing differs.""" + + data_unmasked: TravelRuleDataUnmasked | None = None + """Audit record of the moment the counterparty's data was unmasked. Null while the data is still masked, and for outbound transfers.""" + + data_masked: bool | None = None + """True while the counterparty's personal data is masked in this response.""" + + metadata: dict[str, object] | None = None + + direction: Literal["INBOUND", "OUTBOUND"] | str | None = None + """Which side of the exchange this transfer is.""" + + rail: str | None = None + """The rail the transfer went over: `INTERNAL`, `TRP`, `GTR`, `TRUST`, `VERIFYVASP`, `SYGNA` or `EMAIL` (`TEST_VASP` on a sandbox application, when the Didit Test VASP answered), or `null` until a rail is chosen.""" + + counterparty_vasp: TransactionTravelRuleCounterpartyVasp | None = None + """The counterparty's directory entry, or `null` when the transfer links none.""" + + delivery: TravelRuleDelivery | None = None + """The newest message the transfer owes another party. `null` when it has owed none, for example below `threshold_amount` or for a declared self-hosted wallet.""" + + discovery: list[TravelRuleDiscoveryStep] | None = None + """How Didit looked for the counterparty, one entry per step that ran. Empty when no discovery ran, for example on an inbound transfer.""" + + reason: str | None = None + """Why a `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE` transfer is stuck; `""` for any other status. A failed delivery decides it first, on `COUNTERPARTY_VASP_NOT_REACHABLE` only: `email_undeliverable` (the mail provider refused the compliance email and no address is left to try: when it was sent, on a reminder, or as a permanent bounce reported later) or `delivery_failed` (the rail that was delivering failed for good: a network rail refused the message or ran out of retries, a `TRP` identity host did not answer, or the counterparty's `TRP` host did not accept the inquiry). Otherwise the `discovery` trace decides it: `awaiting_end_user` (no step named a counterparty Didit can reach), `directory_match_no_route` (a step named a directory VASP that has no route), `picked_member_without_proven_wallet` (the end user picked a Didit customer that has not proven control of the wallet), or `no_directory_match` when the trace gives no other reason. A resend moves the transfer on, so the reason clears. See [Why a transfer is stuck](/transaction-monitoring/travel-rule#why-a-transfer-is-stuck).""" + + next_actions: list[TravelRuleNextAction] | None = None + """What moves the transfer on; `[]` for any status but `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE`. `email_undeliverable` offers `update_compliance_email`, then `resend`. `delivery_failed` offers `resend`. A discovery reason offers `select_counterparty_vasp`, `declare_self_hosted`, `add_vasp_hint` and `resend`.""" + + chain_details: TravelRuleChainDetails | None = None + """How the transfer's chain names its two wallets, when that is more than one address paying another. Empty (`{}`) for an account-chain transfer (EVM, Solana, Tron), where the two addresses are the wallets. Matching, routing and the address book read the wallets from here first.""" diff --git a/src/didit/types/components/transaction_verification_session_action.py b/src/didit/types/components/transaction_verification_session_action.py new file mode 100644 index 0000000..c10762c --- /dev/null +++ b/src/didit/types/components/transaction_verification_session_action.py @@ -0,0 +1,24 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["TransactionVerificationSessionAction"] + + +class TransactionVerificationSessionAction(BaseModel): + type: Literal["verification_session"] | str + + url: str + """Hosted verification URL to open for the end user.""" + + session_id: str | None = None + """Verification session id.""" + + session_token: str | None = None + """Session token, used by the mobile SDKs to launch the flow natively.""" + + status: str | None = None + """Verification session status.""" diff --git a/src/didit/types/components/transaction_wallet_ownership_action.py b/src/didit/types/components/transaction_wallet_ownership_action.py new file mode 100644 index 0000000..820b23a --- /dev/null +++ b/src/didit/types/components/transaction_wallet_ownership_action.py @@ -0,0 +1,22 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["TransactionWalletOwnershipAction"] + + +class TransactionWalletOwnershipAction(BaseModel): + type: Literal["wallet_ownership"] | str + + url: str + """Hosted wallet-ownership widget URL. Open it in a real browser surface - on mobile use SFSafariViewController or a Chrome Custom Tab, never an embedded webview.""" + + widget_session_id: str | None = None + """Wallet-ownership widget session id.""" + + expires_at: _dt.datetime | None = None + """When the widget session expires.""" diff --git a/src/didit/types/components/travel_rule_chain_details.py b/src/didit/types/components/travel_rule_chain_details.py new file mode 100644 index 0000000..5271aaf --- /dev/null +++ b/src/didit/types/components/travel_rule_chain_details.py @@ -0,0 +1,70 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.travel_rule_chain_side import TravelRuleChainSide +from didit.types.components.travel_rule_utxo_entry import TravelRuleUtxoEntry + +__all__ = ["TravelRuleChainDetails", "TravelRuleChainDetailsUtxo"] + + +class TravelRuleChainDetailsUtxo(BaseModel): + """On a UTXO transfer: the inputs and outputs as stored (amounts as decimal strings) and which outputs Didit read as what.""" + + inputs: list[TravelRuleUtxoEntry] | None = None + + outputs: list[TravelRuleUtxoEntry] | None = None + + change_output_indexes: list[int] | None = None + """Outputs that pay an input address back.""" + + beneficiary_output_index: int | None = None + """The output that pays the beneficiary; null when none could be told apart.""" + + +class TravelRuleChainDetails(BaseModel): + """How the transfer's chain names its two wallets, when that is more than one address paying another. Empty (`{}`) for an account-chain transfer (EVM, Solana, Tron), where the two addresses are the wallets. Matching, routing and the address book read the wallets from here first.""" + + chain: str | None = None + """Canonical chain, e.g. `BTC`, `XRP`, or `LIGHTNING` for a Lightning transfer.""" + + shape: Literal["utxo", "memo", "lightning"] | str | None = None + + originator: TravelRuleChainSide | None = None + """One side's wallet as Didit resolved it from the chain data.""" + + beneficiary: TravelRuleChainSide | None = None + """One side's wallet as Didit resolved it from the chain data.""" + + utxo: TravelRuleChainDetailsUtxo | None = None + """On a UTXO transfer: the inputs and outputs as stored (amounts as decimal strings) and which outputs Didit read as what.""" + + lightning: dict[str, object] | None = None + """On a Lightning transfer: what the payment request says. Always `kind` (`bolt11`, `bolt12_offer`, `bolt12_invoice`, `bolt12_invoice_request`, `lnurl`, `lightning_address`, `node_id` or `unknown`); a BOLT11 invoice adds `network`, `payee_node_id`, `payment_hash`, `amount_msat`, `created_at` and `expires_at`; a BOLT12 string `blinded_paths` and `payee_node_id`; an LNURL or a Lightning Address its `domain`. Nothing is resolved over the network: the request is decoded only.""" + + warnings: ( + list[ + Literal[ + "declared_originator_not_an_input", + "declared_beneficiary_not_an_output", + "beneficiary_output_ambiguous", + "beneficiary_output_amount_differs", + ] + | str + ] + | None + ) = None + """UTXO inconsistencies worth a look: the declared originator is not an input, the declared beneficiary is not an output, no single beneficiary output could be told apart, or the beneficiary output pays a different amount than the transaction states.""" + + not_applicable_reason: ( + Literal[ + "LIGHTNING_NO_PAYEE_IDENTIFIER", + "LIGHTNING_BOLT12_BLINDED_PAYEE", + "LIGHTNING_LNURL_NAMES_NO_PAYEE", + ] + | str + | None + ) = None + """On an outbound Lightning transfer whose payment request names no payee: why the transfer is `NOT_APPLICABLE`. `LIGHTNING_NO_PAYEE_IDENTIFIER` (the request names no payee node, or could not be decoded), `LIGHTNING_BOLT12_BLINDED_PAYEE` (the offer hides its payee behind blinded paths), `LIGHTNING_LNURL_NAMES_NO_PAYEE` (an LNURL names a service, not a payee). The same code opens the transfer's last `timeline` entry.""" diff --git a/src/didit/types/components/travel_rule_chain_side.py b/src/didit/types/components/travel_rule_chain_side.py new file mode 100644 index 0000000..d112fdb --- /dev/null +++ b/src/didit/types/components/travel_rule_chain_side.py @@ -0,0 +1,33 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["TravelRuleChainSide"] + + +class TravelRuleChainSide(BaseModel): + """One side's wallet as Didit resolved it from the chain data.""" + + address: str | None = None + """The wallet. On a memo network the classic address (an XRP X-address is split into its classic address and its tag); on Lightning the payee node id or Lightning Address.""" + + memo: str | None = None + """The destination tag or memo that is part of the wallet; `""` for none.""" + + source: ( + Literal[ + "declared", + "x_address", + "largest_input", + "only_non_change_output", + "amount_match", + "invoice_payee", + "updated", + ] + | str + | None + ) = None + """Where the wallet came from: `declared` (your request), `x_address` (split from an XRP X-address), `largest_input` (the UTXO input address that contributed the most), `only_non_change_output` (the only UTXO output that is not change), `amount_match` (the one UTXO output paying the transfer amount), `invoice_payee` (the payee a Lightning payment request names), `updated` (set later with `action: update_originator_wallet`).""" diff --git a/src/didit/types/components/travel_rule_counterparty_contact.py b/src/didit/types/components/travel_rule_counterparty_contact.py new file mode 100644 index 0000000..11b1d91 --- /dev/null +++ b/src/didit/types/components/travel_rule_counterparty_contact.py @@ -0,0 +1,51 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["TravelRuleCounterpartyContact"] + + +class TravelRuleCounterpartyContact(BaseModel): + """A compliance address the EMAIL rail writes to for one directory entry.""" + + uuid: str | None = None + + counterparty: str | None = None + """The directory entry's `id`, as GET /v3/travel-rule/vasps/ shows it.""" + + counterparty_name: str | None = None + + email: str | None = None + + name: str | None = None + """Optional label, for example the team name.""" + + source: Literal["staff", "application", "learned"] | str | None = None + """staff: curated by Didit. learned: an address at one of the VASP's own domains that answered a request through its response link. application: added by your application.""" + + scope: Literal["platform", "application"] | str | None = None + """platform: curated by Didit or learned from a verified answer, used for every Didit customer, read-only for you. application: private to your application.""" + + created_by: str | None = None + """Empty for platform contacts and for contacts an API key added.""" + + verified_at: _dt.datetime | None = None + """When this address last answered a request through its response link.""" + + is_invalid: bool | None = None + """true once the address bounced or complained; the EMAIL rail skips it.""" + + invalid_reason: ( + Literal["", "hard_bounce", "soft_bounce", "spam_complaint", "suppressed"] | str | None + ) = None + + invalid_detail: str | None = None + """The receiving mail server's reason, when it gave one.""" + + invalid_at: _dt.datetime | None = None + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/components/travel_rule_counterparty_decision.py b/src/didit/types/components/travel_rule_counterparty_decision.py new file mode 100644 index 0000000..9674f02 --- /dev/null +++ b/src/didit/types/components/travel_rule_counterparty_decision.py @@ -0,0 +1,35 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["TravelRuleCounterpartyDecision", "TravelRuleCounterpartyDecisionInheritedFrom"] + + +class TravelRuleCounterpartyDecisionInheritedFrom(BaseModel): + """The brand whose decision this entity follows because you made none on the entity itself. Null when the decision is the counterparty's own.""" + + id: str | None = None + + name: str | None = None + + +class TravelRuleCounterpartyDecision(BaseModel): + """Your application's decision on a counterparty VASP. Private to your application: it never changes the shared directory entry and another customer never sees it. It acts only through your transaction rules (the counterparty_dd_status rule field and the travel-rule-counterparty-blocked and travel-rule-counterparty-restricted presets); it never changes how a transfer is routed.""" + + decision: Literal["APPROVED", "RESTRICTED", "BLOCKED"] | str | None = None + """APPROVED: your team cleared the counterparty. RESTRICTED: transfers to it may go ahead after review. BLOCKED: your team refused it.""" + + reason: str | None = None + """Free-text reason you gave, empty when none.""" + + decided_by: str | None = None + """The console user who made or last changed the decision. Empty when an API key made it.""" + + updated_at: _dt.datetime | None = None + + inherited_from: TravelRuleCounterpartyDecisionInheritedFrom | None = None + """The brand whose decision this entity follows because you made none on the entity itself. Null when the decision is the counterparty's own.""" diff --git a/src/didit/types/components/travel_rule_counterparty_decision_response.py b/src/didit/types/components/travel_rule_counterparty_decision_response.py new file mode 100644 index 0000000..7fa3647 --- /dev/null +++ b/src/didit/types/components/travel_rule_counterparty_decision_response.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.travel_rule_counterparty_decision import TravelRuleCounterpartyDecision + +__all__ = ["TravelRuleCounterpartyDecisionResponse"] + + +class TravelRuleCounterpartyDecisionResponse(BaseModel): + decision: TravelRuleCounterpartyDecision | None = None + """The decision that applies to the counterparty for your application: its own, else (for an entity of a brand) the brand's. Null when there is none.""" diff --git a/src/didit/types/components/travel_rule_counterparty_questionnaire.py b/src/didit/types/components/travel_rule_counterparty_questionnaire.py new file mode 100644 index 0000000..b61dfe3 --- /dev/null +++ b/src/didit/types/components/travel_rule_counterparty_questionnaire.py @@ -0,0 +1,39 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["TravelRuleCounterpartyQuestionnaire"] + + +class TravelRuleCounterpartyQuestionnaire(BaseModel): + """One due-diligence questionnaire your application sent the counterparty: Didit's standard questionnaire, emailed to a compliance address on record with a single-use answer link. Private to your application.""" + + id: str | None = None + + status: Literal["SENT", "ANSWERED", "EXPIRED"] | str | None = None + """SENT: waiting for answers. ANSWERED: the counterparty answered every question. EXPIRED: the answer link closed unanswered (its deadline passed, or a newer questionnaire replaced it), or answers passed valid_until.""" + + form_version: str | None = None + """Version of Didit's standard questionnaire the answers belong to. Currently 1.""" + + sent_to: str | None = None + + sent_at: _dt.datetime | None = None + + expires_at: _dt.datetime | None = None + """The answer-by deadline, 30 days after sending. The link stops working after it.""" + + answered_at: _dt.datetime | None = None + + valid_until: _dt.datetime | None = None + """How long the answers stay current: 365 days after answering. Null until answered.""" + + current: bool | None = None + """true while the questionnaire is ANSWERED and valid_until has not passed.""" + + answers: dict[str, str] | None = None + """The answers keyed by question id: legal_name, licensing, jurisdictions, aml_program, compliance_officer, sanctions_screening, transaction_monitoring, travel_rule, custody, ownership, record_keeping, data_protection. Each answer is up to 2,000 characters. Empty until answered.""" diff --git a/src/didit/types/components/travel_rule_counterparty_questionnaire_list.py b/src/didit/types/components/travel_rule_counterparty_questionnaire_list.py new file mode 100644 index 0000000..750c3fd --- /dev/null +++ b/src/didit/types/components/travel_rule_counterparty_questionnaire_list.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.travel_rule_counterparty_questionnaire import ( + TravelRuleCounterpartyQuestionnaire, +) + +__all__ = ["TravelRuleCounterpartyQuestionnaireList"] + + +class TravelRuleCounterpartyQuestionnaireList(BaseModel): + questionnaires: list[TravelRuleCounterpartyQuestionnaire] | None = None + """Newest first.""" diff --git a/src/didit/types/components/travel_rule_counterparty_screening.py b/src/didit/types/components/travel_rule_counterparty_screening.py new file mode 100644 index 0000000..6ff7f85 --- /dev/null +++ b/src/didit/types/components/travel_rule_counterparty_screening.py @@ -0,0 +1,32 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["TravelRuleCounterpartyScreening"] + + +class TravelRuleCounterpartyScreening(BaseModel): + """One sanctions and adverse-media screening of the counterparty entity, run for and billed to your application. Private to your application: another customer never sees it.""" + + id: str | None = None + + status: Literal["Approved", "In Review", "Declined"] | str | None = None + """Approved: no possible match needs attention. In Review or Declined: a possible sanctions or adverse-media match.""" + + total_hits: int | None = None + """How many possible matches the screening returned.""" + + score: float | None = None + """The highest risk score among the possible matches. Null when nothing matched.""" + + screened_at: _dt.datetime | None = None + + fresh: bool | None = None + """Whether the screening is younger than fresh_days (30 days). A fresh screening is reused instead of billed again.""" + + flagged: bool | None = None + """true while the screening is fresh and its status is In Review or Declined. While your latest screening is flagged and counterparty screening stays on, counterparty_risk_level is HIGH in your rules.""" diff --git a/src/didit/types/components/travel_rule_counterparty_screening_result.py b/src/didit/types/components/travel_rule_counterparty_screening_result.py new file mode 100644 index 0000000..37035f1 --- /dev/null +++ b/src/didit/types/components/travel_rule_counterparty_screening_result.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.travel_rule_counterparty_screening import ( + TravelRuleCounterpartyScreening, +) + +__all__ = ["TravelRuleCounterpartyScreeningResult"] + + +class TravelRuleCounterpartyScreeningResult(BaseModel): + outcome: Literal["screened", "reused"] | str | None = None + """screened: a new screening ran and was billed (201). reused: a fresh screening was returned and nothing was billed (200).""" + + screening: TravelRuleCounterpartyScreening | None = None + """One sanctions and adverse-media screening of the counterparty entity, run for and billed to your application. Private to your application: another customer never sees it.""" diff --git a/src/didit/types/components/travel_rule_counterparty_screening_status.py b/src/didit/types/components/travel_rule_counterparty_screening_status.py new file mode 100644 index 0000000..7952b5c --- /dev/null +++ b/src/didit/types/components/travel_rule_counterparty_screening_status.py @@ -0,0 +1,23 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.travel_rule_counterparty_screening import ( + TravelRuleCounterpartyScreening, +) + +__all__ = ["TravelRuleCounterpartyScreeningStatus"] + + +class TravelRuleCounterpartyScreeningStatus(BaseModel): + enabled: bool | None = None + """Your counterparty_screening_enabled Travel Rule setting. Off by default; nothing is screened or billed while it is off.""" + + price: str | None = None + """Decimal string, in USD: what one screening costs your organization, the Company AML Screening price.""" + + fresh_days: int | None = None + """How many days a screening stays fresh and is reused.""" + + screening: TravelRuleCounterpartyScreening | None = None + """Your latest screening of the counterparty. Null when you never screened it.""" diff --git a/src/didit/types/components/travel_rule_data_match.py b/src/didit/types/components/travel_rule_data_match.py new file mode 100644 index 0000000..75bea0e --- /dev/null +++ b/src/didit/types/components/travel_rule_data_match.py @@ -0,0 +1,30 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["TravelRuleDataMatch", "TravelRuleDataMatchFieldsValue"] + + +class TravelRuleDataMatchFieldsValue(BaseModel): + result: Literal["matched", "mismatched", "not_compared"] | str | None = None + """`not_compared` when the rule is `NONE` or either side has no value for the field.""" + + strictness: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str | None = None + + +class TravelRuleDataMatch(BaseModel): + """Field-by-field comparison of the transfer's party data with what the other side knows about the wallet holder. It names fields and results, never values, so it stays readable while the counterparty's data is masked. Null until a comparison ran.""" + + matched: bool | None = None + """True when no compared field differs.""" + + mismatched_fields: list[Literal["name", "date_of_birth", "address", "account"] | str] | None = ( + None + ) + """The fields that differ.""" + + fields: dict[str, TravelRuleDataMatchFieldsValue] | None = None + """One entry per field compared for the party's entity type (`name`, `date_of_birth`, `address`, `account`; a company has no `date_of_birth`).""" diff --git a/src/didit/types/components/travel_rule_data_sharing_fields.py b/src/didit/types/components/travel_rule_data_sharing_fields.py new file mode 100644 index 0000000..0737c42 --- /dev/null +++ b/src/didit/types/components/travel_rule_data_sharing_fields.py @@ -0,0 +1,63 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import TypedDict + +from didit._models import BaseModel + +__all__ = ["TravelRuleDataSharingFields", "TravelRuleDataSharingFieldsParam"] + + +class TravelRuleDataSharingFields(BaseModel): + """The fields Didit sends to the counterparty VASP about your own users, per entity type. The `name` and the `wallet_address` are always sent, whatever the list says. An entity type you never chose shares every field. An empty list is a choice too: it shares the name and the wallet only.""" + + individual: ( + list[ + Literal[ + "name", + "wallet_address", + "address", + "national_id", + "customer_identification", + "date_of_birth", + "place_of_birth", + ] + | str + ] + | None + ) = None + """Fields shared about a natural person.""" + + company: ( + list[ + Literal["name", "wallet_address", "address", "national_id", "customer_identification"] + | str + ] + | None + ) = None + """Fields shared about a legal person.""" + + +class TravelRuleDataSharingFieldsParam(TypedDict, total=False): + """The fields Didit sends to the counterparty VASP about your own users, per entity type. The `name` and the `wallet_address` are always sent, whatever the list says. An entity type you never chose shares every field. An empty list is a choice too: it shares the name and the wallet only.""" + + individual: Sequence[ + Literal[ + "name", + "wallet_address", + "address", + "national_id", + "customer_identification", + "date_of_birth", + "place_of_birth", + ] + | str + ] + """Fields shared about a natural person.""" + + company: Sequence[ + Literal["name", "wallet_address", "address", "national_id", "customer_identification"] | str + ] + """Fields shared about a legal person.""" diff --git a/src/didit/types/components/travel_rule_data_unmasked.py b/src/didit/types/components/travel_rule_data_unmasked.py new file mode 100644 index 0000000..64c28e7 --- /dev/null +++ b/src/didit/types/components/travel_rule_data_unmasked.py @@ -0,0 +1,33 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["TravelRuleDataUnmasked"] + + +class TravelRuleDataUnmasked(BaseModel): + """Audit record of the moment the counterparty's data was unmasked. Null while the data is still masked, and for outbound transfers.""" + + at: _dt.datetime | None = None + + reason: ( + Literal[ + "ownership_confirmed", + "wallet_proof", + "address_book_match", + "own_verified_wallet", + "inquiry_approved", + "known_payment_method", + "travel_address", + ] + | str + | None + ) = None + """What confirmed ownership: ownership_confirmed (your confirmation through the console or the API), wallet_proof (a wallet proof in the widget, or an INTERNAL inbound transfer, set when it is created), address_book_match (a verified address-book wallet), own_verified_wallet (your user's own verified wallet), inquiry_approved (your approval of an inbound TRP inquiry or a GTR or Sygna message), known_payment_method (a wallet your user already paid with) or travel_address (the per-wallet travel address named the wallet).""" + + actor: str | None = None + """Who confirmed it: the console user's email or the API key's label. Blank for an automatic decision.""" diff --git a/src/didit/types/components/travel_rule_delivery.py b/src/didit/types/components/travel_rule_delivery.py new file mode 100644 index 0000000..0f974e6 --- /dev/null +++ b/src/didit/types/components/travel_rule_delivery.py @@ -0,0 +1,36 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["TravelRuleDelivery"] + + +class TravelRuleDelivery(BaseModel): + """Where the latest message the transfer owes the other side stands. Didit contacts the counterparty after the request that created or changed the transfer has returned, so this object says whether that message got through: queued, being sent, waiting to be retried after an error, delivered, or failed. A failure the network may still recover from (no answer in time, a failed connection, HTTP `408`, `429` or `5xx`) is tried again after 30 seconds, 2 minutes, 10 minutes, 30 minutes and 1 hour. When a first message (`kind: SEND`) fails for good, because the network refused it or the sixth attempt failed too, the transfer moves to `COUNTERPARTY_VASP_NOT_REACHABLE` with `last_error` in its `timeline`; any other message that fails for good is noted in the `timeline`. The transfer's `status` stays the source of truth for the exchange itself.""" + + kind: Literal["SEND", "ANSWER", "TXID", "CANCEL", "POLL"] | str + """What the message carries. `SEND`: the transfer's first message to the counterparty (outbound; the rail is chosen when it is sent). `ANSWER`: your accept or decline, owed to the network that asked (inbound). `TXID`: the on-chain transaction hash, after you finish the transfer. `CANCEL`: the cancellation, after you cancel it. `POLL`: a status query to a network that offers one, repeated every 5 minutes while the transfer waits on the counterparty.""" + + status: Literal["QUEUED", "SENDING", "RETRYING", "DELIVERED", "FAILED"] | str + """`QUEUED`: waiting to be sent. `SENDING`: an attempt is running. `RETRYING`: an attempt failed and the next one runs at `next_attempt_at`. `DELIVERED`: the other side took the message. `FAILED`: the other side refused it, or the last attempt failed.""" + + rail: Literal["INTERNAL", "TRP", "GTR", "TRUST", "VERIFYVASP", "SYGNA", "EMAIL", ""] | str + """The rail that carries the message. Empty until Didit has chosen one for a first message.""" + + attempts: int + """Attempts made so far for this message. A message that is not a status poll is attempted at most 6 times.""" + + last_error: str + """The kind of failure the last attempt hit, `""` when none did: for example `email undeliverable`, `trp identity endpoint unreachable` or `trp transfer inquiry delivery failed`. It never carries the counterparty's own answer, which stays on the transfer's `timeline`. + When GTR answers with an error, its own status code and message are included. Response bodies are never included. + """ + + next_attempt_at: _dt.datetime | None + """When the next attempt runs while the message is `QUEUED` or `RETRYING`.""" + + delivered_at: _dt.datetime | None + """When the other side took the message.""" diff --git a/src/didit/types/components/travel_rule_discovery_step.py b/src/didit/types/components/travel_rule_discovery_step.py new file mode 100644 index 0000000..34b2703 --- /dev/null +++ b/src/didit/types/components/travel_rule_discovery_step.py @@ -0,0 +1,40 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["TravelRuleDiscoveryStep"] + + +class TravelRuleDiscoveryStep(BaseModel): + """One step of the counterparty discovery Didit ran for an outbound transfer, in the order it ran them. The first step that names a VASP with a route ends the chain.""" + + step: ( + Literal[ + "internal_address_book", + "wallet_attribution", + "network_lookup", + "customer_hint", + "end_user", + ] + | str + | None + ) = None + """`internal_address_book`: another Didit customer that proved control of the destination wallet. `wallet_attribution`: the wallet's blockchain analytics attribution. `network_lookup`: a network that can say which VASP holds the address. `customer_hint`: the VASP name, LEI or jurisdiction you sent in `beneficiary_data`. `end_user`: the counterparty the end user (or you) picked, or the request to pick one.""" + + outcome: Literal["match", "miss", "skipped"] | str | None = None + """`match`: the step named a counterparty. `miss`: it ran and found none. `skipped`: it could not run, and `reason` says why.""" + + reason: str | None = None + """Machine-readable reason, `""` on a match: `no_proven_internal_wallet`, `attribution_disabled`, `already_attributed`, `no_attributed_entity`, `directory_match_no_route`, `gtr_address_lookup_runs_on_dispatch`, `no_membership`, `network_switched_off`, `no_network_address_lookup`, `no_directory_match`, `awaiting_end_user` or `picked_member_without_proven_wallet`.""" + + vasp: str | None = None + """The directory VASP the step named, when it named one.""" + + rail: str | None = None + """The rail that reaches that counterparty, when the step found one.""" + + at: _dt.datetime | None = None diff --git a/src/didit/types/components/travel_rule_inbound_response.py b/src/didit/types/components/travel_rule_inbound_response.py index 63bc397..34d0c93 100644 --- a/src/didit/types/components/travel_rule_inbound_response.py +++ b/src/didit/types/components/travel_rule_inbound_response.py @@ -12,10 +12,10 @@ class TravelRuleInboundResponse(BaseModel): """Didit transaction UUID for the inbound deposit.""" txn_id: str | None = None - """Didit-generated txn_id for the sunrise transaction.""" + """txn_id of the inbound transaction: Didit-generated when Didit created the transaction, or your own when an unclaimed deposit was reused.""" created: bool | None = None - """true when a new transaction was minted (HTTP 201); false when an existing unclaimed deposit was reused (HTTP 200).""" + """true when this call registered the transfer (HTTP 201); false when a transfer for the same txid and wallet_address already existed (HTTP 200).""" travel_rule: TravelRuleTransferDetail | None = None - """Full transfer object returned by the ownership-confirmation and finish/cancel endpoints. originator_data/beneficiary_data are omitted while PII is masked (see description).""" + """Full transfer object returned by the ownership-confirmation and finish/cancel endpoints. While an inbound transfer's wallet ownership is unconfirmed, the counterparty's `originator_data` and `beneficiary_data` come back masked (the first letter of each word is kept; the wallet, chain and VASP routing stay readable) and `data_masked` is true.""" diff --git a/src/didit/types/components/travel_rule_matching_rules.py b/src/didit/types/components/travel_rule_matching_rules.py new file mode 100644 index 0000000..787b976 --- /dev/null +++ b/src/didit/types/components/travel_rule_matching_rules.py @@ -0,0 +1,95 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypedDict + +from didit._models import BaseModel + +__all__ = [ + "TravelRuleMatchingRules", + "TravelRuleMatchingRulesCompany", + "TravelRuleMatchingRulesCompanyParam", + "TravelRuleMatchingRulesIndividual", + "TravelRuleMatchingRulesIndividualParam", + "TravelRuleMatchingRulesParam", +] + + +class TravelRuleMatchingRulesCompany(BaseModel): + """Rules for a legal person.""" + + name: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str | None = None + """Default: follows `name_matching_strictness`.""" + + address: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str | None = None + """Default `NONE`.""" + + account: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str | None = None + """The wallet the exchange was routed by. Default `STRICT`.""" + + +class TravelRuleMatchingRulesIndividual(BaseModel): + """Rules for a natural person.""" + + name: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str | None = None + """Default: follows `name_matching_strictness`.""" + + date_of_birth: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str | None = None + """Default `NONE`.""" + + address: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str | None = None + """Default `NONE`.""" + + account: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str | None = None + """The wallet the exchange was routed by. Default `STRICT`.""" + + +class TravelRuleMatchingRules(BaseModel): + """How strictly each field of the counterparty's data is compared with what you hold, per entity type. A field you leave out keeps its default. `NONE` skips the comparison, `STRICT` needs exactly the same words, `CONTAINS` accepts one value whose words all appear in the other, `DEFAULT` is the standard subset match, and `FUZZY` accepts close spellings (and, for dates of birth, a swapped day and month).""" + + individual: TravelRuleMatchingRulesIndividual | None = None + """Rules for a natural person.""" + + company: TravelRuleMatchingRulesCompany | None = None + """Rules for a legal person.""" + + +class TravelRuleMatchingRulesCompanyParam(TypedDict, total=False): + """Rules for a legal person.""" + + name: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str + """Default: follows `name_matching_strictness`.""" + + address: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str + """Default `NONE`.""" + + account: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str + """The wallet the exchange was routed by. Default `STRICT`.""" + + +class TravelRuleMatchingRulesIndividualParam(TypedDict, total=False): + """Rules for a natural person.""" + + name: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str + """Default: follows `name_matching_strictness`.""" + + date_of_birth: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str + """Default `NONE`.""" + + address: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str + """Default `NONE`.""" + + account: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str + """The wallet the exchange was routed by. Default `STRICT`.""" + + +class TravelRuleMatchingRulesParam(TypedDict, total=False): + """How strictly each field of the counterparty's data is compared with what you hold, per entity type. A field you leave out keeps its default. `NONE` skips the comparison, `STRICT` needs exactly the same words, `CONTAINS` accepts one value whose words all appear in the other, `DEFAULT` is the standard subset match, and `FUZZY` accepts close spellings (and, for dates of birth, a swapped day and month).""" + + individual: TravelRuleMatchingRulesIndividualParam + """Rules for a natural person.""" + + company: TravelRuleMatchingRulesCompanyParam + """Rules for a legal person.""" diff --git a/src/didit/types/components/travel_rule_message.py b/src/didit/types/components/travel_rule_message.py new file mode 100644 index 0000000..788cd0c --- /dev/null +++ b/src/didit/types/components/travel_rule_message.py @@ -0,0 +1,37 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.travel_rule_message_attachment import TravelRuleMessageAttachment + +__all__ = ["TravelRuleMessage"] + + +class TravelRuleMessage(BaseModel): + id: str | None = None + + kind: Literal["MESSAGE", "DOCUMENT_REQUEST"] | str | None = None + + direction: Literal["INBOUND", "OUTBOUND"] | str | None = None + """`OUTBOUND` is what the reader's own side sent. In the pickup portal the direction is from the counterparty's side.""" + + author: str | None = None + """Who wrote it on its own side: a console user's email or an API key label, or the other VASP's name.""" + + body: str | None = None + + requested_documents: list[str] | None = None + """What a DOCUMENT_REQUEST asks for, in the requester's words.""" + + reply_to: str | None = None + + fulfilled_at: _dt.datetime | None = None + """Set on a DOCUMENT_REQUEST once a reply to it carries a document.""" + + attachment: TravelRuleMessageAttachment | None = None + """The document attached to a message, stored like any other transaction document.""" + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/components/travel_rule_message_attachment.py b/src/didit/types/components/travel_rule_message_attachment.py new file mode 100644 index 0000000..1a35a18 --- /dev/null +++ b/src/didit/types/components/travel_rule_message_attachment.py @@ -0,0 +1,36 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["TravelRuleMessageAttachment", "TravelRuleMessageAttachmentFiles"] + + +class TravelRuleMessageAttachmentFiles(BaseModel): + uuid: str | None = None + + file_url: str | None = None + """Time-limited download URL.""" + + original_filename: str | None = None + + file_size: int | None = None + + +class TravelRuleMessageAttachment(BaseModel): + """The document attached to a message, stored like any other transaction document.""" + + uuid: str | None = None + + label: str | None = None + + note: str | None = None + + uploaded_by: str | None = None + """Who attached it. In the pickup portal, a file you sent shows your VASP name, never the person or API key behind it.""" + + created_at: _dt.datetime | None = None + + files: list[TravelRuleMessageAttachmentFiles] | None = None diff --git a/src/didit/types/components/travel_rule_name_match.py b/src/didit/types/components/travel_rule_name_match.py new file mode 100644 index 0000000..ffff675 --- /dev/null +++ b/src/didit/types/components/travel_rule_name_match.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["TravelRuleNameMatch"] + + +class TravelRuleNameMatch(BaseModel): + """The name comparison that decided the transfer, or null until one ran. While the parties are masked, and on the other customer's copy of an INTERNAL exchange, `transfer_name`, `compared_name`, `matched_names` and `mismatched_names` are hidden. `score` and `matched` can disagree: `matched` follows the strictness, `score` is the similarity of the two full names.""" + + score: int | None = None + """Similarity of the two full names, 0 to 100. 0 when either name is blank.""" + + matched: bool | None = None + """Whether the names satisfy `strictness`. The transfer's outcome follows `data_match`, which compares every field, so a transfer can be COUNTERPARTY_MISMATCHED_DATA while this is true.""" + + method: ( + Literal["none", "exact_token", "token_subset", "token_containment", "sequence_ratio"] + | str + | None + ) = None + """How `matched` was decided: none (`NONE`), exact_token (`STRICT`), token_subset (`DEFAULT`), token_containment (`CONTAINS`) or sequence_ratio (`FUZZY`).""" + + strictness: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str | None = None + + compared_with: Literal["wallet_holder", "counterparty_vasp"] | str | None = None + """wallet_holder: the holder recorded for the wallet. counterparty_vasp: the name the counterparty answered on the email response page, or the Didit Test VASP's answer in sandbox.""" + + transfer_name: str | None = None + """The name on the transfer.""" + + compared_name: str | None = None + """The name it was compared with.""" + + matched_names: list[str] | None = None + """Words of `transfer_name` found in `compared_name`.""" + + mismatched_names: list[str] | None = None + """Words of `transfer_name` missing from `compared_name`, then words of `compared_name` missing from `transfer_name`.""" diff --git a/src/didit/types/components/travel_rule_next_action.py b/src/didit/types/components/travel_rule_next_action.py new file mode 100644 index 0000000..db9705a --- /dev/null +++ b/src/didit/types/components/travel_rule_next_action.py @@ -0,0 +1,27 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["TravelRuleNextAction"] + + +class TravelRuleNextAction(BaseModel): + """One thing that moves a `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE` transfer on. See [Why a transfer is stuck](/transaction-monitoring/travel-rule#why-a-transfer-is-stuck).""" + + action: ( + Literal[ + "select_counterparty_vasp", + "declare_self_hosted", + "add_vasp_hint", + "update_compliance_email", + "resend", + ] + | str + ) + """`select_counterparty_vasp`: the end user picks the destination VASP in the wallet-ownership widget. `declare_self_hosted`: the end user declares the wallet self-hosted in the widget. `add_vasp_hint`: tell Didit who the counterparty is, with a VASP name, LEI or travel address (for example a `travel_address` on the destination wallet's address-book entry), then resend. `update_compliance_email` and `resend` are `action` values of `PATCH /v3/transactions/{transaction_id}/travel-rule/`: give the counterparty's compliance email again, and re-run routing.""" + + url: str | None = None + """The wallet-ownership widget link, on `select_counterparty_vasp` and `declare_self_hosted` while the transfer has an open widget session.""" diff --git a/src/didit/types/components/travel_rule_settings_detail.py b/src/didit/types/components/travel_rule_settings_detail.py index bef37c2..f0b5df7 100644 --- a/src/didit/types/components/travel_rule_settings_detail.py +++ b/src/didit/types/components/travel_rule_settings_detail.py @@ -4,9 +4,12 @@ from typing import Literal from didit._models import BaseModel +from didit.types.components.travel_rule_data_sharing_fields import TravelRuleDataSharingFields +from didit.types.components.travel_rule_matching_rules import TravelRuleMatchingRules __all__ = [ "TravelRuleSettingsDetail", + "TravelRuleSettingsDetailDiditNetwork", "TravelRuleSettingsDetailNetworks", "TravelRuleSettingsDetailNetworksGtr", "TravelRuleSettingsDetailNetworksSygna", @@ -15,6 +18,33 @@ ] +class TravelRuleSettingsDetailDiditNetwork(BaseModel): + """Whether the Didit network lists your VASP profile, so other Didit customers find it in the directory and reach it on the INTERNAL rail, and each condition it still misses.""" + + listed: bool | None = None + + missing: ( + list[ + Literal[ + "travel_rule_off", + "sandbox_application", + "not_discoverable", + "legal_name_missing", + "compliance_email_missing", + "compliance_email_unconfirmed", + "excluded_by_staff", + "internal_organization", + ] + | str + ] + | None + ) = None + """Empty when listed. compliance_email_unconfirmed: send the confirmation with POST /v3/travel-rule/settings/compliance-email/confirmation/ and open the emailed link. excluded_by_staff: Didit marked the application as a test, demo or internal one.""" + + compliance_email_confirmed: bool | None = None + """Whether the current compliance_email was confirmed through the emailed link.""" + + class TravelRuleSettingsDetailNetworksGtr(BaseModel): rail: Literal["GTR"] | str | None = None @@ -84,19 +114,33 @@ class TravelRuleSettingsDetail(BaseModel): """Whether the managed Travel Rule negotiation flow is active for outbound `travelRule` transactions on this application.""" jurisdiction: str | None = None - """Your VASP's operating jurisdiction code. Default `"EU"`.""" + """Your VASP's operating jurisdiction code. Default `"EU"`. Didit maps it to the regime it stamps on your Travel Rule transactions as `travel_rule_regime` (for example `EU` or an EU member code becomes `eu`), which transaction rules can key on. Other Didit customers see it as your jurisdiction in the VASP directory once the Didit network lists your profile.""" - name_matching_strictness: Literal["NONE", "STRICT", "DEFAULT", "FUZZY"] | str | None = None - """How strictly beneficiary names must match before a transfer can reach COMPLETED.""" + name_matching_strictness: ( + Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str | None + ) = None + """How strictly beneficiary names must match before a transfer can reach COMPLETED. `CONTAINS` accepts one name whose words all appear in the other.""" confirmation_timeout_hours: int | None = None """Hours an AWAITING_COUNTERPARTY transfer may remain unconfirmed before the periodic sweeper expires it. Default 48.""" timeout_outcome: Literal["HOLD", "REJECT", "PROCEED"] | str | None = None - """What happens to the underlying transaction when a transfer expires. Default HOLD.""" + """What happens to the underlying transaction when a transfer expires. Default HOLD. sunrise_policy and non_responsive_policy take precedence when they apply and are set.""" threshold_amount: str | None = None - """Decimal string. Minimum transfer amount that triggers a managed exchange; transfers below it resolve straight to NOT_APPLICABLE. Defaults to "0.00" (EU TFR applies no minimum amount).""" + """Decimal string, in your application's default currency. Minimum transfer amount that triggers a managed exchange; transfers below it resolve straight to NOT_APPLICABLE. Didit compares the transaction amount converted to your default currency, or the `amount_in_default_currency` you send; when no converted amount is available, the threshold does not apply. Defaults to "0.00" (EU TFR applies no minimum amount).""" + + ownership_validation_mode: Literal["FULL", "SIMPLIFIED", "AUTOMATED"] | str | None = None + """How much proof a wallet's ownership takes. `FULL` always needs a proof (signed message, test deposit or reviewed screenshot), never the end user's word. `SIMPLIFIED` accepts a self-declaration below `simplified_validation_threshold` and a proof above it. `AUTOMATED` lets the address book and earlier proofs answer on their own, self-declarations included while `allow_self_declaration` is true. Default `AUTOMATED`.""" + + simplified_validation_threshold: str | None = None + """Decimal string, default "0.00". In `SIMPLIFIED` mode a transfer below this amount (in the threshold currency) accepts a self-declaration. A transfer with no amount in that currency counts as above it.""" + + data_sharing_fields: TravelRuleDataSharingFields | None = None + """The fields Didit sends to the counterparty VASP about your own users, per entity type. The `name` and the `wallet_address` are always sent, whatever the list says. An entity type you never chose shares every field. An empty list is a choice too: it shares the name and the wallet only.""" + + matching_rules: TravelRuleMatchingRules | None = None + """How strictly each field of the counterparty's data is compared with what you hold, per entity type. A field you leave out keeps its default. `NONE` skips the comparison, `STRICT` needs exactly the same words, `CONTAINS` accepts one value whose words all appear in the other, `DEFAULT` is the standard subset match, and `FUZZY` accepts close spellings (and, for dates of birth, a swapped day and month).""" legal_name: str | None = None """Your VASP's legal name. Required (non-blank) to set is_enabled to true.""" @@ -105,16 +149,16 @@ class TravelRuleSettingsDetail(BaseModel): """Your Legal Entity Identifier, surfaced in the VASP directory.""" compliance_email: str | None = None - """Compliance contact email for your VASP profile.""" + """Compliance contact email for your VASP profile. The Didit network lists your profile only once this address is confirmed through an emailed link (POST /v3/travel-rule/settings/compliance-email/confirmation/); changing it needs a new confirmation.""" is_discoverable: bool | None = None - """Whether your VASP profile appears in GET /v3/travel-rule/vasps/ search results and can be reached on the INTERNAL rail. Default true.""" + """Whether your VASP profile can be listed in the Didit network: found in GET /v3/travel-rule/vasps/ by other Didit customers and reached on the INTERNAL rail. Listing also needs a live application, Travel Rule on, a legal name and a confirmed compliance email; didit_network says which are missing. Default true.""" inbound_auto_accept: bool | None = None - """When true, inbound transfers whose beneficiary wallet is a verified address-book entry with a matching name are accepted automatically, without a manual review step. Default false.""" + """When true, an inbound TRP inquiry whose beneficiary wallet is a verified address-book entry with a matching name is approved automatically. When false, that inquiry waits in ON_HOLD for you to approve or deny it with the ownership endpoint. INTERNAL-rail and after-deposit transfers are not affected. Default true.""" allow_self_declaration: bool | None = None - """When true, the wallet-ownership widget offers self-declaration as a fallback proof method. Default false.""" + """When true, the wallet-ownership widget offers self-declaration as a fallback proof method. Default true.""" allow_screenshot_proof: bool | None = None """When true, the wallet-ownership widget offers screenshot upload as an ownership evidence method. Default true.""" @@ -125,11 +169,26 @@ class TravelRuleSettingsDetail(BaseModel): vasp_attribution_enabled: bool | None = None """When true, an outbound Travel Rule transfer that no rail can route is attributed through blockchain analytics: the destination wallet is resolved to its owning VASP, the entity is recorded on the transfer and in the VASP directory, and the rails are re-run once with the attributed name. Default true.""" + counterparty_screening_enabled: bool | None = None + """Opt-in sanctions and adverse-media screening of counterparty VASPs, billed per screening at the Company AML Screening price. When on, Didit screens the counterparty an outbound exchange reaches (reusing a screening younger than 30 days) and you can screen one with POST /v3/travel-rule/vasps/{vasp_id}/screening/. Default false: nothing is screened or billed.""" + + sunrise_policy: Literal["", "HOLD", "REJECT", "PROCEED"] | str | None = None + """What an expired transfer does to its transaction when the counterparty is a directory entry in a jurisdiction on sunrise_jurisdictions, which does not enforce the Travel Rule yet: HOLD moves it to review, REJECT declines it, PROCEED leaves its status untouched. Empty (the default) follows timeout_outcome. Applies before non_responsive_policy.""" + + sunrise_jurisdictions: list[str] | None = None + """Read-only. The countries (ISO 3166 alpha-2) Didit treats as in a Travel Rule sunrise period, where sunrise_policy applies. Currently CO, KH and VN. Any other jurisdiction, and an empty or unknown one, follows timeout_outcome.""" + + non_responsive_policy: Literal["", "HOLD", "REJECT", "PROCEED"] | str | None = None + """What an expired transfer does to its transaction when the counterparty is a directory entry whose responsiveness is UNRESPONSIVE: HOLD, REJECT or PROCEED, as for timeout_outcome. Empty (the default) follows timeout_outcome. sunrise_policy applies first when both fit.""" + travel_address: str | None = None - """Read-only. Your VASP's own travel address - the encoded inbound TRP inquiry endpoint other VASPs use to reach you. Populated once your VASP profile is complete.""" + """Read-only. Your VASP's own travel address - the encoded inbound TRP inquiry endpoint other VASPs use to reach you. Empty while `is_enabled` is false.""" disabled_networks: list[Literal["GTR", "TRUST", "VERIFYVASP", "SYGNA"] | str] | None = None """Networks opted out of platform membership. Does not disable your connected own memberships or provision unavailable networks.""" networks: TravelRuleSettingsDetailNetworks | None = None """Effective membership configuration for each network. No network, including GTR, is included by default. This is not a live health or delivery check.""" + + didit_network: TravelRuleSettingsDetailDiditNetwork | None = None + """Whether the Didit network lists your VASP profile, so other Didit customers find it in the directory and reach it on the INTERNAL rail, and each condition it still misses.""" diff --git a/src/didit/types/components/travel_rule_test_vasp_inbound_response.py b/src/didit/types/components/travel_rule_test_vasp_inbound_response.py new file mode 100644 index 0000000..6e5529c --- /dev/null +++ b/src/didit/types/components/travel_rule_test_vasp_inbound_response.py @@ -0,0 +1,30 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.travel_rule_transfer_detail import TravelRuleTransferDetail + +__all__ = ["TravelRuleTestVaspInboundResponse"] + + +class TravelRuleTestVaspInboundResponse(BaseModel): + transaction_id: str | None = None + """Didit transaction UUID of the inbound transfer.""" + + txn_id: str | None = None + """Didit-generated txn_id.""" + + scenario: str | None = None + """The scenario that ran.""" + + travel_rule_status: str | None = None + """The status your application's own decision resolved to.""" + + detail: str | None = None + """Why the transfer resolved to that status.""" + + test_vasp: bool | None = None + """Always true: the sender was the Didit Test VASP.""" + + travel_rule: TravelRuleTransferDetail | None = None + """Full transfer object returned by the ownership-confirmation and finish/cancel endpoints. While an inbound transfer's wallet ownership is unconfirmed, the counterparty's `originator_data` and `beneficiary_data` come back masked (the first letter of each word is kept; the wallet, chain and VASP routing stay readable) and `data_masked` is true.""" diff --git a/src/didit/types/components/travel_rule_transfer_detail.py b/src/didit/types/components/travel_rule_transfer_detail.py index 890fce0..b49be84 100644 --- a/src/didit/types/components/travel_rule_transfer_detail.py +++ b/src/didit/types/components/travel_rule_transfer_detail.py @@ -5,20 +5,51 @@ from typing import Literal from didit._models import BaseModel +from didit.types.components.travel_rule_chain_details import TravelRuleChainDetails +from didit.types.components.travel_rule_data_match import TravelRuleDataMatch +from didit.types.components.travel_rule_data_unmasked import TravelRuleDataUnmasked +from didit.types.components.travel_rule_delivery import TravelRuleDelivery +from didit.types.components.travel_rule_discovery_step import TravelRuleDiscoveryStep +from didit.types.components.travel_rule_name_match import TravelRuleNameMatch +from didit.types.components.travel_rule_next_action import TravelRuleNextAction __all__ = [ "TravelRuleTransferDetail", "TravelRuleTransferDetailCounterpartyVasp", + "TravelRuleTransferDetailCounterpartyVaspDdScoreBreakdownValue", "TravelRuleTransferDetailTimeline", ] +class TravelRuleTransferDetailCounterpartyVaspDdScoreBreakdownValue(BaseModel): + points: float | None = None + """Points awarded for this component.""" + + max: int | None = None + """Maximum points this component can award.""" + + reason: str | None = None + """Machine-readable code of the component's verdict.""" + + detail: str | None = None + """Human-readable reason for the awarded points.""" + + class TravelRuleTransferDetailCounterpartyVasp(BaseModel): name: str | None = None lei: str | None = None dd_score: int | None = None + """Due-diligence score from 0 to 100, where higher is better. Same scale as in the VASP directory.""" + + dd_score_breakdown: ( + dict[str, TravelRuleTransferDetailCounterpartyVaspDdScoreBreakdownValue] | None + ) = None + """Per-component contributions to dd_score, one object per component as in the VASP directory (jurisdiction, verification, protocol, lei, history, essentials, responsiveness), each {points, max, reason, detail}. {} until the counterparty has been assessed.""" + + dd_assessed_at: _dt.datetime | None = None + """When the due-diligence assessment last ran. Null until the counterparty has been assessed.""" class TravelRuleTransferDetailTimeline(BaseModel): @@ -28,9 +59,15 @@ class TravelRuleTransferDetailTimeline(BaseModel): detail: str | None = None + name_match: TravelRuleNameMatch | None = None + """The name comparison this entry decided, next to `detail`. Absent on entries that compared no name.""" + + reason: str | None = None + """Present only on the entry of a transition a failed delivery caused: `email_undeliverable` or `delivery_failed`, the value the transfer's `reason` reports.""" + class TravelRuleTransferDetail(BaseModel): - """Full transfer object returned by the ownership-confirmation and finish/cancel endpoints. originator_data/beneficiary_data are omitted while PII is masked (see description).""" + """Full transfer object returned by the ownership-confirmation and finish/cancel endpoints. While an inbound transfer's wallet ownership is unconfirmed, the counterparty's `originator_data` and `beneficiary_data` come back masked (the first letter of each word is kept; the wallet, chain and VASP routing stay readable) and `data_masked` is true.""" uuid: str | None = None @@ -56,7 +93,12 @@ class TravelRuleTransferDetail(BaseModel): direction: Literal["INBOUND", "OUTBOUND"] | str | None = None - rail: Literal["INTERNAL", "TRP", "GTR", "EMAIL"] | str | None = None + rail: ( + Literal["INTERNAL", "TRP", "GTR", "TRUST", "VERIFYVASP", "SYGNA", "EMAIL", "TEST_VASP"] + | str + | None + ) = None + """The rail the transfer was routed over, or null when no rail was selected (for example below the threshold, a self-hosted counterparty wallet, an after-deposit registration, or no reachable counterparty). INTERNAL means both sides are Didit applications; EMAIL means the counterparty was reached by an emailed pickup link. TEST_VASP means the Didit Test VASP answered a sandbox application's transfer. Over GTR, VERIFYVASP and SYGNA, Didit reads the counterparty's answer from the network's response or by polling it every 5 minutes. TRUST only sends, so a TRUST transfer stays AWAITING_COUNTERPARTY until it expires. A network rail is used only when you have connected your own membership for that network. It is chosen after the transaction create returns, so it is null in that response.""" protocol: str | None = None @@ -73,7 +115,39 @@ class TravelRuleTransferDetail(BaseModel): counterparty_vasp: TravelRuleTransferDetailCounterpartyVasp | None = None originator_data: dict[str, object] | None = None + """Originator party. For an inbound transfer the counterparty's values are masked until ownership is confirmed.""" beneficiary_data: dict[str, object] | None = None + """Beneficiary party. For an inbound transfer the counterparty's values are masked until ownership is confirmed.""" + + name_match: TravelRuleNameMatch | None = None + """The name comparison that decided the transfer, or null until one ran. While the parties are masked, and on the other customer's copy of an INTERNAL exchange, `transfer_name`, `compared_name`, `matched_names` and `mismatched_names` are hidden. `score` and `matched` can disagree: `matched` follows the strictness, `score` is the similarity of the two full names.""" + + data_match: TravelRuleDataMatch | None = None + """Field-by-field comparison of the transfer's party data with what the other side knows about the wallet holder. It names fields and results, never values, so it stays readable while the counterparty's data is masked. Null until a comparison ran.""" + + mismatched_fields: list[str] | None = None + """Shortcut for `data_match.mismatched_fields`. Empty when nothing differs.""" + + data_unmasked: TravelRuleDataUnmasked | None = None + """Audit record of the moment the counterparty's data was unmasked. Null while the data is still masked, and for outbound transfers.""" + + data_masked: bool | None = None + """True while the counterparty's personal data is masked in this response.""" created_at: _dt.datetime | None = None + + delivery: TravelRuleDelivery | None = None + """Where the latest message this transfer owes the other side stands. Null when the transfer never owed one (for example below `threshold_amount`, a declared self-hosted wallet, or a sunrise registration).""" + + discovery: list[TravelRuleDiscoveryStep] | None = None + """How Didit looked for the counterparty, one entry per step that ran. Empty when no discovery ran, for example on an inbound transfer.""" + + reason: str | None = None + """Why a `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE` transfer is stuck; `""` for any other status. A failed delivery decides it first, on `COUNTERPARTY_VASP_NOT_REACHABLE` only: `email_undeliverable` (the mail provider refused the compliance email and no address is left to try: when it was sent, on a reminder, or as a permanent bounce reported later) or `delivery_failed` (the rail that was delivering failed for good: a network rail refused the message or ran out of retries, a `TRP` identity host did not answer, or the counterparty's `TRP` host did not accept the inquiry). Otherwise the `discovery` trace decides it: `awaiting_end_user` (no step named a counterparty Didit can reach), `directory_match_no_route` (a step named a directory VASP that has no route), `picked_member_without_proven_wallet` (the end user picked a Didit customer that has not proven control of the wallet), or `no_directory_match` when the trace gives no other reason. A resend moves the transfer on, so the reason clears. See [Why a transfer is stuck](/transaction-monitoring/travel-rule#why-a-transfer-is-stuck).""" + + next_actions: list[TravelRuleNextAction] | None = None + """What moves the transfer on; `[]` for any status but `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE`. `email_undeliverable` offers `update_compliance_email`, then `resend`. `delivery_failed` offers `resend`. A discovery reason offers `select_counterparty_vasp`, `declare_self_hosted`, `add_vasp_hint` and `resend`.""" + + chain_details: TravelRuleChainDetails | None = None + """How the transfer's chain names its two wallets, when that is more than one address paying another. Empty (`{}`) for an account-chain transfer (EVM, Solana, Tron), where the two addresses are the wallets. Matching, routing and the address book read the wallets from here first.""" diff --git a/src/didit/types/components/travel_rule_transfer_row.py b/src/didit/types/components/travel_rule_transfer_row.py new file mode 100644 index 0000000..2df82f7 --- /dev/null +++ b/src/didit/types/components/travel_rule_transfer_row.py @@ -0,0 +1,92 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["TravelRuleTransferRow"] + + +class TravelRuleTransferRow(BaseModel): + """One Travel Rule transfer as the transfers list shows it: VASP names, wallets and memos only, never the originator or beneficiary person data.""" + + id: str | None = None + """The transfer's id.""" + + transaction_id: str | None = None + """The Didit transaction the transfer belongs to.""" + + txn_id: str | None = None + """Your own id for the transaction. Transfers Didit books for you, such as a Didit Test VASP inbound transfer, carry a generated one.""" + + txn_date: _dt.datetime | None = None + + kind: Literal["withdrawal", "deposit", "message"] | str | None = None + """withdrawal: outbound. message: an inbound transfer a rail created from a counterparty's message (a TRP inquiry, the INTERNAL inbound leg, or a GTR, VerifyVASP or Sygna Bridge message). deposit: any other inbound transfer.""" + + direction: Literal["INBOUND", "OUTBOUND"] | str | None = None + + status: ( + Literal[ + "AWAITING_COUNTERPARTY", + "ON_HOLD", + "COMPLETED", + "FINISHED", + "CANCELLED", + "EXPIRED", + "COUNTERPARTY_MISMATCHED_DATA", + "COUNTERPARTY_VASP_NOT_FOUND", + "COUNTERPARTY_VASP_NOT_REACHABLE", + "UNCONFIRMED_OWNERSHIP", + "COUNTERPARTY_VASP_GENERAL_DECLINE", + "NOT_ENOUGH_COUNTERPARTY_DATA", + "NOT_APPLICABLE", + ] + | str + | None + ) = None + + reason: str | None = None + """Why a `COUNTERPARTY_VASP_NOT_FOUND` or `COUNTERPARTY_VASP_NOT_REACHABLE` transfer is stuck, the same value as the transfer's own `reason`, so the list tells a bounced compliance email from a counterparty with no route without opening each transfer. `""` for any other status. See [Why a transfer is stuck](/transaction-monitoring/travel-rule#why-a-transfer-is-stuck).""" + + rail: ( + Literal["INTERNAL", "TRP", "GTR", "TRUST", "VERIFYVASP", "SYGNA", "EMAIL", "TEST_VASP"] + | str + | None + ) = None + + asset: str | None = None + """The transaction's currency.""" + + amount: str | None = None + """The amount as a plain decimal, for example "1.25".""" + + originator_vasp: str | None = None + """Outbound: your VASP's legal name. Inbound: the counterparty's name, from its directory entry, the Didit network member, the TRP sender, the Didit Test VASP, or the name the transfer declared. Empty when none is known.""" + + beneficiary_vasp: str | None = None + """Outbound: the counterparty's name, worked out the same way. Inbound: your VASP's legal name.""" + + originator_wallet: str | None = None + + beneficiary_wallet: str | None = None + + originator_memo: str | None = None + """Memo or destination tag, empty on chains without one.""" + + beneficiary_memo: str | None = None + + originator_wallet_type: Literal["vasp", "self_hosted", "own"] | str | None = None + """vasp, self_hosted (declared unhosted) or own (a wallet your address book records as yours). Your own side is always vasp.""" + + beneficiary_wallet_type: Literal["vasp", "self_hosted", "own"] | str | None = None + + matched_transaction_id: str | None = None + """The other side of a deposit you matched to an announced transfer by hand.""" + + deadline_at: _dt.datetime | None = None + """When the transfer expires if it is still waiting on its counterparty. Set on every transfer you create with `POST /v3/transactions/` that needs an exchange, outbound or inbound. Null below the threshold, on a Lightning payment the Travel Rule does not apply to, and on inbound transfers Didit creates when a counterparty reaches you (an inbound TRP inquiry, a network message or the INTERNAL inbound leg).""" + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/components/travel_rule_utxo.py b/src/didit/types/components/travel_rule_utxo.py new file mode 100644 index 0000000..1bf1612 --- /dev/null +++ b/src/didit/types/components/travel_rule_utxo.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import Required, TypedDict + +from didit.types.components.travel_rule_utxo_entry import TravelRuleUtxoEntryParam + +__all__ = ["TravelRuleUtxo"] + + +class TravelRuleUtxo(TypedDict, total=False): + """The inputs and outputs of a UTXO transfer (BTC, LTC, BCH, DOGE). Only accepted on those chains: on any other chain it is a `400`. From them Didit infers the originator wallet (the declared one, else the input address that contributed the most) and the beneficiary output (the declared one, else the only output that does not pay an input address back as change, else the one output paying the transfer amount). The result is on the transfer's `chain_details`.""" + + inputs: Sequence[TravelRuleUtxoEntryParam] + """Optional. Without inputs no originator is inferred and no change output is recognised.""" + + outputs: Required[Sequence[TravelRuleUtxoEntryParam]] diff --git a/src/didit/types/components/travel_rule_utxo_entry.py b/src/didit/types/components/travel_rule_utxo_entry.py new file mode 100644 index 0000000..ed0f317 --- /dev/null +++ b/src/didit/types/components/travel_rule_utxo_entry.py @@ -0,0 +1,38 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from didit._models import BaseModel + +__all__ = ["TravelRuleUtxoEntry", "TravelRuleUtxoEntryParam"] + + +class TravelRuleUtxoEntry(BaseModel): + """One input or output of a UTXO transaction, as the chain recorded it.""" + + address: str + + amount: str | None = None + """Decimal string in the coin's own unit (BTC, not satoshis). Optional.""" + + txid: str | None = None + """Optional. The transaction an input spends.""" + + vout: int | None = None + """Optional. The output index an input spends.""" + + +class TravelRuleUtxoEntryParam(TypedDict, total=False): + """One input or output of a UTXO transaction, as the chain recorded it.""" + + address: Required[str] + + amount: str | None + """Decimal string in the coin's own unit (BTC, not satoshis). Optional.""" + + txid: str + """Optional. The transaction an input spends.""" + + vout: int | None + """Optional. The output index an input spends.""" diff --git a/src/didit/types/components/travel_rule_workspace.py b/src/didit/types/components/travel_rule_workspace.py new file mode 100644 index 0000000..b8268b8 --- /dev/null +++ b/src/didit/types/components/travel_rule_workspace.py @@ -0,0 +1,65 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.travel_rule_settings_detail import TravelRuleSettingsDetail +from didit.types.components.travel_rule_workspace_txt_record import TravelRuleWorkspaceTxtRecord + +__all__ = ["TravelRuleWorkspace", "TravelRuleWorkspaceClaim", "TravelRuleWorkspaceDomain"] + + +class TravelRuleWorkspaceClaim(BaseModel): + status: Literal["unclaimed", "pending_review", "claimed", "rejected"] | str | None = None + + proved_by_domain: bool | None = None + + claimed_at: _dt.datetime | None = None + + reviewed_at: _dt.datetime | None = None + + review_note: str | None = None + + vasp_id: str | None = None + + vasp_name: str | None = None + + +class TravelRuleWorkspaceDomain(BaseModel): + domain: str | None = None + + status: Literal["unverified", "code_sent", "verified"] | str | None = None + + verified_at: _dt.datetime | None = None + + txt: TravelRuleWorkspaceTxtRecord | None = None + """The TXT record to publish for the DNS proof. Empty until a proof has been started.""" + + +class TravelRuleWorkspace(BaseModel): + """The free Travel Rule workspace of an application that joined free.""" + + joined_free: bool | None = None + + work_email: str | None = None + + joined_at: _dt.datetime | None = None + + claim: TravelRuleWorkspaceClaim | None = None + + prefill: dict[str, object] | None = None + """Profile values taken from the directory entry: `vasp_id`, `legal_name`, `lei`, `jurisdiction`, `compliance_email`, `domains`. Nothing is written until the claim is made.""" + + domain: TravelRuleWorkspaceDomain | None = None + + billing_enabled: bool | None = None + """Receiving Travel Rule data is free. Sending is billed only after billing is enabled.""" + + billing_enabled_at: _dt.datetime | None = None + + didit_network: dict[str, object] | None = None + """Whether the Didit network lists this VASP and, when not, each condition it still misses.""" + + settings: TravelRuleSettingsDetail | None = None diff --git a/src/didit/types/components/travel_rule_workspace_domain_code_response.py b/src/didit/types/components/travel_rule_workspace_domain_code_response.py new file mode 100644 index 0000000..61a5dbd --- /dev/null +++ b/src/didit/types/components/travel_rule_workspace_domain_code_response.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.travel_rule_workspace_txt_record import TravelRuleWorkspaceTxtRecord + +__all__ = ["TravelRuleWorkspaceDomainCodeResponse"] + + +class TravelRuleWorkspaceDomainCodeResponse(BaseModel): + domain: str | None = None + + status: Literal["unverified", "code_sent", "verified"] | str | None = None + + txt: TravelRuleWorkspaceTxtRecord | None = None + """The TXT record to publish for the DNS proof. Empty until a proof has been started.""" diff --git a/src/didit/types/components/travel_rule_workspace_txt_record.py b/src/didit/types/components/travel_rule_workspace_txt_record.py new file mode 100644 index 0000000..39dd840 --- /dev/null +++ b/src/didit/types/components/travel_rule_workspace_txt_record.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["TravelRuleWorkspaceTxtRecord"] + + +class TravelRuleWorkspaceTxtRecord(BaseModel): + """The TXT record to publish for the DNS proof. Empty until a proof has been started.""" + + host: str | None = None + + value: str | None = None diff --git a/src/didit/types/components/user_session_list_item.py b/src/didit/types/components/user_session_list_item.py new file mode 100644 index 0000000..3b2bb26 --- /dev/null +++ b/src/didit/types/components/user_session_list_item.py @@ -0,0 +1,263 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "UserSessionListItem", + "UserSessionListItemAdditionalDetails", + "UserSessionListItemAdditionalDetailsAml", + "UserSessionListItemAdditionalDetailsAmlScreenedData", + "UserSessionListItemAdditionalDetailsDatabaseValidation", + "UserSessionListItemAdditionalDetailsEmailVerification", + "UserSessionListItemAdditionalDetailsFaceMatch", + "UserSessionListItemAdditionalDetailsLiveness", + "UserSessionListItemAdditionalDetailsPhoneVerification", + "UserSessionListItemAdditionalDetailsPoa", + "UserSessionListItemEmailAddress", + "UserSessionListItemFeatures", + "UserSessionListItemPhoneNumber", +] + + +class UserSessionListItemAdditionalDetailsAmlScreenedData(BaseModel): + """Subset of the data that was screened. Only non-empty keys are included.""" + + nationality: str | None = None + + document_number: str | None = None + + date_of_birth: str | None = None + + +class UserSessionListItemAdditionalDetailsAml(BaseModel): + """Present when AML screening ran.""" + + total_hits: int | None = None + """Number of AML screening hits.""" + + entity_type: str | None = None + """Screened entity type.""" + + is_adverse_media_fetched: bool | None = None + + screened_data: UserSessionListItemAdditionalDetailsAmlScreenedData | None = None + """Subset of the data that was screened. Only non-empty keys are included.""" + + +class UserSessionListItemAdditionalDetailsDatabaseValidation(BaseModel): + """Present when database validation ran.""" + + validation_type: str | None = None + + match_type: str | None = None + + identification_number: str | None = None + """Tax, personal, or document number that was validated (first available).""" + + +class UserSessionListItemAdditionalDetailsEmailVerification(BaseModel): + """Present when email verification ran.""" + + is_breached: bool | None = None + + is_disposable: bool | None = None + + is_undeliverable: bool | None = None + + is_blocklisted: bool | None = None + + +class UserSessionListItemAdditionalDetailsFaceMatch(BaseModel): + """Present when face match ran.""" + + score: float | None = None + """Face-match similarity score, rounded to 2 decimals.""" + + source_image: str | None = None + """Expiring (presigned) URL of the source face image.""" + + target_image: str | None = None + """Expiring (presigned) URL of the target face image.""" + + +class UserSessionListItemAdditionalDetailsLiveness(BaseModel): + """Present when a liveness check ran.""" + + method: str | None = None + + score: float | None = None + """Liveness confidence score, rounded to 2 decimals.""" + + +class UserSessionListItemAdditionalDetailsPhoneVerification(BaseModel): + """Present when phone verification ran.""" + + country_code: str | None = None + """ISO 3166-1 alpha-2 country of the phone number.""" + + is_temporary: bool | None = None + + is_ported: bool | None = None + + is_blocked: bool | None = None + + +class UserSessionListItemAdditionalDetailsPoa(BaseModel): + """Present when proof of address ran.""" + + document_type: str | None = None + + issuer: str | None = None + + address: str | None = None + + name_on_document: str | None = None + + +class UserSessionListItemAdditionalDetails(BaseModel): + """Compact per-feature extras. Only features that produced data appear as keys; `null` when no feature produced extras. Fields excluded by the workflow's response-attributes configuration are omitted.""" + + aml: UserSessionListItemAdditionalDetailsAml | None = None + """Present when AML screening ran.""" + + database_validation: UserSessionListItemAdditionalDetailsDatabaseValidation | None = None + """Present when database validation ran.""" + + poa: UserSessionListItemAdditionalDetailsPoa | None = None + """Present when proof of address ran.""" + + liveness: UserSessionListItemAdditionalDetailsLiveness | None = None + """Present when a liveness check ran.""" + + face_match: UserSessionListItemAdditionalDetailsFaceMatch | None = None + """Present when face match ran.""" + + phone_verification: UserSessionListItemAdditionalDetailsPhoneVerification | None = None + """Present when phone verification ran.""" + + email_verification: UserSessionListItemAdditionalDetailsEmailVerification | None = None + """Present when email verification ran.""" + + +class UserSessionListItemEmailAddress(BaseModel): + """Email associated with the session: the verified email when email verification ran, otherwise the contact email provided at session creation. `null` when neither exists.""" + + email: str + + is_verified: bool + """`true` only when the email-verification feature approved this address.""" + + +class UserSessionListItemFeatures(BaseModel): + feature: ( + Literal[ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACE_MATCH", + "POA", + "QUESTIONNAIRE", + "EMAIL_VERIFICATION", + "PHONE", + "AML", + "IP_ANALYSIS", + "AGE_ESTIMATION", + "DATABASE_VALIDATION", + ] + | str + ) + """Verification feature identifier.""" + + status: Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str + """Status of this feature. `Not Finished` until the step completes.""" + + +class UserSessionListItemPhoneNumber(BaseModel): + """Phone associated with the session: the verified phone when phone verification ran, otherwise the contact phone provided at session creation. `null` when neither exists.""" + + number: str + """Phone number in E.164 format.""" + + is_verified: bool + """`true` only when the phone-verification feature approved this number.""" + + +class UserSessionListItem(BaseModel): + """Summary row for a User Verification (KYC) session. Identity fields (`full_name`, `country`, `document_type`, `portrait_image`, contact fields) are populated progressively as the user completes verification steps, and are `null` until the relevant step finishes. Fields restricted by the workflow's response-attributes configuration are also returned as `null`.""" + + session_id: str + """Unique identifier of the verification session. Use it with `GET /v3/session/{sessionId}/decision/` to fetch the full decision payload.""" + + session_kind: Literal["user"] | str + """Row discriminator. Always `"user"` for KYC rows; Business Verification (KYB) rows have `"business"` and a different shape.""" + + session_number: int + """Human-friendly sequential number of the session within your application. Also matched by the `search` parameter.""" + + session_url: str | None + """Hosted verification URL the end user opens to complete the flow. `null` when the session has no hosted URL.""" + + portrait_image: str | None + """Expiring (presigned) URL of the best available portrait: the NFC chip photo when available, otherwise the ID document portrait, otherwise the liveness reference image. `null` until one of those exists. Download promptly; the link expires.""" + + document_type: ( + Literal[ + "Passport", + "Identity Card", + "Driver's License", + "Residence Permit", + "Health Insurance Card", + "Tax Card", + ] + | str + | None + ) + """Human-readable type of the verified ID document. `null` before document verification completes, and also `null` after completion when the verified code has no display mapping (`SSC`, `WP`, `VISA`, `PSC`, `BC`, `OTHER`). Note: the `document_type` query parameter filters by short codes (`P`, `ID`, `DL`, `RP`, ...), not by these display values.""" + + full_name: str | None + """Best available full name for the session, resolved in order: ID document OCR, AML screened data, database-validation screened data, proof-of-address document, then the expected details provided at session creation. `null` when none is available yet.""" + + country: str | None + """ISO 3166-1 alpha-3 country associated with the session, resolved in order: ID document issuing state, database-validation issuing state, proof-of-address issuing state, expected details, then the verified phone's country. May differ from nationality.""" + + status: ( + Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + ) + """Overall session status. See the [Verification Statuses](/integration/verification-statuses) page for the lifecycle.""" + + vendor_data: str | None + """Your own identifier for the end user, echoed back exactly as sent at session creation.""" + + didit_internal_id: str | None + """Didit's internal identifier for the verified user profile behind this session. Stable across multiple sessions of the same user; filterable via the `didit_internal_id` query parameter.""" + + created_at: _dt.datetime + """UTC timestamp when the session was created.""" + + features: list[UserSessionListItemFeatures] + """Per-feature status for every verification step configured in the session's workflow, in workflow order.""" + + phone_number: UserSessionListItemPhoneNumber | None + """Phone associated with the session: the verified phone when phone verification ran, otherwise the contact phone provided at session creation. `null` when neither exists.""" + + email_address: UserSessionListItemEmailAddress | None + """Email associated with the session: the verified email when email verification ran, otherwise the contact email provided at session creation. `null` when neither exists.""" + + additional_details: UserSessionListItemAdditionalDetails | None + """Compact per-feature extras. Only features that produced data appear as keys; `null` when no feature produced extras. Fields excluded by the workflow's response-attributes configuration are omitted.""" diff --git a/src/didit/types/components/vasp_directory_entry.py b/src/didit/types/components/vasp_directory_entry.py index ad2f82e..4bff6ed 100644 --- a/src/didit/types/components/vasp_directory_entry.py +++ b/src/didit/types/components/vasp_directory_entry.py @@ -5,28 +5,266 @@ from typing import Literal from didit._models import BaseModel +from didit.types.components.travel_rule_counterparty_decision import TravelRuleCounterpartyDecision +from didit.types.components.travel_rule_counterparty_questionnaire import ( + TravelRuleCounterpartyQuestionnaire, +) +from didit.types.components.travel_rule_counterparty_screening import ( + TravelRuleCounterpartyScreening, +) -__all__ = ["VaspDirectoryEntry"] +__all__ = [ + "VaspDirectoryEntry", + "VaspDirectoryEntryAliases", + "VaspDirectoryEntryContacts", + "VaspDirectoryEntryDdScoreBreakdownValue", + "VaspDirectoryEntryEntities", + "VaspDirectoryEntryEssentialChecks", + "VaspDirectoryEntryEssentialChecksChecks", + "VaspDirectoryEntryNetworks", + "VaspDirectoryEntryResponsiveness", + "VaspDirectoryEntryRoutes", + "VaspDirectoryEntryUmbrella", +] + + +class VaspDirectoryEntryAliases(BaseModel): + name: str | None = None + + kind: ( + Literal[ + "trading_name", "other_legal_name", "network_name", "attribution", "merged", "staff" + ] + | str + | None + ) = None + """trading_name, other_legal_name (from the global LEI register), network_name (the name on a Travel Rule network), attribution (the entity name blockchain analytics gives the VASP's wallets), merged (the name of a duplicate entry merged into this one) or staff.""" + + +class VaspDirectoryEntryContacts(BaseModel): + """A compliance contact the EMAIL rail can write to.""" + + id: str | None = None + + email: str | None = None + + name: str | None = None + + source: Literal["staff", "application", "learned"] | str | None = None + """staff: curated by Didit; application: added by your application; learned: an address that answered a Travel Rule request through its response link.""" + + scope: Literal["platform", "application"] | str | None = None + """platform contacts are used for every Didit customer's transfers; application contacts are private to your application, which sees only its own.""" + + verified_at: _dt.datetime | None = None + """When someone at this address answered a request. Null when it has not yet.""" + + +class VaspDirectoryEntryDdScoreBreakdownValue(BaseModel): + points: float | None = None + """Points the component contributes, rounded to one decimal.""" + + max: int | None = None + """The component's weight: the points it contributes at best.""" + + reason: str | None = None + """Machine-readable code of the component's verdict.""" + + detail: str | None = None + """One-line English explanation.""" + + +class VaspDirectoryEntryEntities(BaseModel): + """A regulated entity of a brand: the legal entity that receives a transfer.""" + + id: str | None = None + + name: str | None = None + + lei: str | None = None + + jurisdiction: str | None = None + + reachable: bool | None = None + """Whether your application has an available route to it.""" + + +class VaspDirectoryEntryEssentialChecksChecks(BaseModel): + id: Literal["registration", "licence", "domain", "lei"] | str | None = None + + status: Literal["pass", "fail", "unknown"] | str | None = None + """unknown when no source on record can decide the check.""" + + reason: str | None = None + """Machine-readable code of the verdict.""" + + detail: str | None = None + """One-line English explanation.""" + + +class VaspDirectoryEntryEssentialChecks(BaseModel): + """The essential-checks report of a directory entry, built only from the platform's verified sources: registration (an official register lists the VASP: the EU's interim MiCA register, the public TRISA directory or the global LEI register), licence (a MiCA authorisation is on record and current), domain (the entry's domain matches its TRP endpoint or the website its register gives) and lei (its LEI record is active, issued and consistent with the entry's names and country). The essentials component of dd_score is the share of decided checks that pass. Empty until the weekly directory refresh first checks the entry, and always empty for a Didit network member.""" + + level: Literal["essential"] | str | None = None + + checked_at: _dt.datetime | None = None + + checks: list[VaspDirectoryEntryEssentialChecksChecks] | None = None + + passed: int | None = None + + failed: int | None = None + + unknown: int | None = None + + total: int | None = None + + +class VaspDirectoryEntryNetworks(BaseModel): + """A current listing of this VASP in a Travel Rule network's member directory. A TRISA listing says who the VASP is on TRISA; Didit has no TRISA rail, so it is never a route.""" + + network: Literal["GTR", "TRUST", "VERIFYVASP", "SYGNA", "TRISA"] | str | None = None + + code: str | None = None + """The VASP's identifier on that network.""" + + listed_name: str | None = None + """The name the network lists the VASP under.""" + + country: str | None = None + """The country the network lists, when it gives one.""" + + verified_at: _dt.datetime | None = None + """When Didit staff verified the listing. Null for listings read from a public directory.""" + + last_seen_at: _dt.datetime | None = None + """When the source last showed the listing. A listing the source has not shown for three weeks is removed.""" + + source: Literal["trisa_directory", "staff"] | str | None = None + """Where the listing comes from: the public TRISA directory, or Didit staff.""" + + +class VaspDirectoryEntryResponsiveness(BaseModel): + """Whether the VASP answers Travel Rule requests: the share of the requests Didit customers started towards it in the last 90 days that it answered. Only requests sent over a network or protocol rail, or by email to a shared address (the entry's compliance address or a platform contact), count; a request sent to an address a customer typed, or to a customer's private contact, never does. An expired request is unanswered; a completed exchange, a decline or a data mismatch is an answer; a cancelled request or an unreachable rail is neither. Always UNKNOWN with zero counts for a Didit network member.""" + + status: Literal["RESPONSIVE", "UNRESPONSIVE", "UNKNOWN"] | str | None = None + """UNKNOWN with fewer than 5 decided requests in the window; UNRESPONSIVE when less than 10 percent of them were answered; RESPONSIVE otherwise.""" + + answered: int | None = None + """Decided requests in the window that the VASP answered.""" + + total: int | None = None + """Decided requests in the window: answered plus expired.""" + + computed_at: _dt.datetime | None = None + + +class VaspDirectoryEntryRoutes(BaseModel): + """One way your application can deliver Travel Rule data to this VASP, in the order dispatch tries them. A network route needs a membership on that network: your own (connected in the Marketplace) or Didit's once Didit holds one. Without it the route is listed as unavailable with the reason, never as a way to reach the VASP.""" + + rail: ( + Literal["INTERNAL", "TRP", "GTR", "TRUST", "VERIFYVASP", "SYGNA", "EMAIL"] | str | None + ) = None + + status: Literal["available", "unavailable"] | str | None = None + + reason: ( + Literal["", "no_membership", "network_switched_off", "sandbox_application"] | str | None + ) = None + """Why an unavailable route cannot be used: no_membership (neither you nor Didit holds a membership on that network), network_switched_off (you turned that network off in your settings) or sandbox_application (a sandbox application sends only to the Didit Test VASP, so no route is available to it). Empty when the route is available.""" + + via: ( + Literal["", "own_membership", "didit_membership", "directory", "contact", "didit_network"] + | str + | None + ) = None + """How an available route is reached: own_membership or didit_membership for a network rail, directory for a TRP endpoint the directory verified, contact for the EMAIL rail, didit_network for a Didit network member. Empty when the route is unavailable.""" + + +class VaspDirectoryEntryUmbrella(BaseModel): + """The brand this entry is a regulated entity of. Null when it is not part of one.""" + + id: str | None = None + + name: str | None = None class VaspDirectoryEntry(BaseModel): + """A VASP of the directory or a member of the Didit network, with the routes your application has to it. A customer is a member of the Didit network only while its application is live, its Travel Rule is on, its profile is discoverable, has a legal name and a confirmed compliance email, and Didit has not marked the application as a test, demo or internal one. Each row also carries the shared due-diligence assessment (dd_score, risk_level, responsiveness, essential_checks) and your application's own decision, entity screening and questionnaires, which no other customer sees.""" + + id: str | None = None + """The entry's id, for GET /v3/travel-rule/vasps/{vasp_id}/.""" + + kind: Literal["directory", "didit"] | str | None = None + """directory for an entry of the shared directory; didit for a member of the Didit network.""" + name: str | None = None lei: str | None = None jurisdiction: str | None = None + """ISO 3166 country code, as the source gives it.""" + + domains: list[str] | None = None + """The VASP's web domains. Always empty for Didit network members.""" + + reachable: bool | None = None + """Whether your application has an available route to this VASP, or to one of its entities for a brand. Always true for a Didit network member, except on a sandbox application, where every row is false.""" + + aliases: list[VaspDirectoryEntryAliases] | None = None + """Other names the VASP is found by.""" + + networks: list[VaspDirectoryEntryNetworks] | None = None + + routes: list[VaspDirectoryEntryRoutes] | None = None + + umbrella: VaspDirectoryEntryUmbrella | None = None + """The brand this entry is a regulated entity of. Null when it is not part of one.""" + + entities: list[VaspDirectoryEntryEntities] | None = None + """For a brand, the regulated entities that receive its transfers. A transfer naming the brand goes to the reachable entity in the jurisdiction it states (beneficiary_data.counterparty_vasp_jurisdiction), else to the entity blockchain analytics attribution linked to the destination wallet, else to the only reachable entity when no jurisdiction is stated. Two entities that fit equally are never guessed between.""" + + contacts: list[VaspDirectoryEntryContacts] | None = None + """The platform's contacts for this VASP and your application's own.""" dd_status: Literal["NOT_STARTED", "IN_PROGRESS", "COMPLETED", "REJECTED"] | str | None = None + """Didit's verification status of the entry, scored by the verification component of dd_score. Always COMPLETED for a Didit network member.""" dd_score: int | None = None + """The shared due-diligence score of a directory entry, 0 to 100, where higher is safer (see dd_score_semantic): the sum of the points in dd_score_breakdown. Null for a Didit network member, which is not scored. An entry not assessed yet (dd_assessed_at null) shows 0 with an empty risk_level, so read risk_level rather than the raw score.""" - reachable_rails: list[Literal["INTERNAL", "TRP", "GTR", "EMAIL"] | str] | None = None + reachable_rails: ( + list[Literal["INTERNAL", "TRP", "GTR", "TRUST", "VERIFYVASP", "SYGNA", "EMAIL"] | str] + | None + ) = None + """Rails recorded on the entry. Read routes for what your application can use today.""" is_didit_customer: bool | None = None - """true for discoverable Didit applications (dd_status is always COMPLETED, dd_score always 0 for these); false for internal counterparty-registry rows, which carry a real due-diligence status and score.""" + """true for a member of the Didit network (dd_status is always COMPLETED, dd_score null and risk_level empty for these) and for a directory entry linked to a Didit customer.""" - dd_score_breakdown: dict[str, int] | None = None - """Per-component contributions to dd_score (licensing/registry, sanctions/PEP, jurisdiction risk, on-chain analytics risk), all derived from checks Didit runs in-house. Empty for Didit-customer rows.""" + dd_score_breakdown: dict[str, VaspDirectoryEntryDdScoreBreakdownValue] | None = None + """One object per component of dd_score. Components, their weight (max) and their reason codes: jurisdiction, 25 (jurisdiction_not_high_risk, jurisdiction_high_risk, jurisdiction_unknown); verification, 15 (dd_status_completed, dd_status_in_progress, dd_status_not_started, dd_status_rejected); protocol, 15 (native_rail, email_only, no_rail); lei, 10 (lei_valid, lei_missing); history, 10 (history, no_history); essentials, 15 (essential_checks, no_essential_checks); responsiveness, 10 (responsive, unresponsive, responsiveness_unknown). Empty for a Didit network member.""" dd_assessed_at: _dt.datetime | None = None - """When the due-diligence assessment last ran. Null for Didit-customer rows.""" + """When dd_score, risk_level and responsiveness were last computed: after an exchange with the entry completes, finishes, expires, mismatches or is declined, when Didit refreshes or curates the entry, and in the weekly directory refresh. Null for a Didit network member.""" + + dd_score_semantic: Literal["higher_is_safer"] | str | None = None + """Which way dd_score points: always higher_is_safer.""" + + risk_level: Literal["LOW", "MEDIUM", "HIGH", ""] | str | None = None + """The band dd_score falls in: LOW (70 or more), MEDIUM (40 to 69) or HIGH (below 40). Empty for a Didit network member and for an entry not assessed yet. Filter on it with risk. Your own flagged screening never changes this shared value: it makes counterparty_risk_level HIGH in your rules instead.""" + + responsiveness: VaspDirectoryEntryResponsiveness | None = None + """Whether the VASP answers Travel Rule requests: the share of the requests Didit customers started towards it in the last 90 days that it answered. Only requests sent over a network or protocol rail, or by email to a shared address (the entry's compliance address or a platform contact), count; a request sent to an address a customer typed, or to a customer's private contact, never does. An expired request is unanswered; a completed exchange, a decline or a data mismatch is an answer; a cancelled request or an unreachable rail is neither. Always UNKNOWN with zero counts for a Didit network member.""" + + essential_checks: VaspDirectoryEntryEssentialChecks | None = None + """The essential-checks report of a directory entry, built only from the platform's verified sources: registration (an official register lists the VASP: the EU's interim MiCA register, the public TRISA directory or the global LEI register), licence (a MiCA authorisation is on record and current), domain (the entry's domain matches its TRP endpoint or the website its register gives) and lei (its LEI record is active, issued and consistent with the entry's names and country). The essentials component of dd_score is the share of decided checks that pass. Empty until the weekly directory refresh first checks the entry, and always empty for a Didit network member.""" + + decision: TravelRuleCounterpartyDecision | None = None + """Your application's decision on this VASP, or for an entity with no decision of its own its brand's (inherited_from names the brand). Null when there is none. Private to your application.""" + + entity_screening: TravelRuleCounterpartyScreening | None = None + """Your application's latest sanctions and adverse-media screening of this VASP. Null when you never screened it. Private to your application.""" + + questionnaires: list[TravelRuleCounterpartyQuestionnaire] | None = None + """Your application's due-diligence questionnaires with this VASP, newest first. Private to your application.""" diff --git a/src/didit/types/components/wallet_address_entry.py b/src/didit/types/components/wallet_address_entry.py index e78c70c..ab8e87c 100644 --- a/src/didit/types/components/wallet_address_entry.py +++ b/src/didit/types/components/wallet_address_entry.py @@ -12,10 +12,10 @@ class WalletAddressEntry(BaseModel): uuid: str | None = None address: str | None = None - """The wallet address. Unique per application and chain among non-deleted entries.""" + """The wallet address; `""` for an entry imported by its hash only. Unique per application, chain and memo, comparing the address without regard to case. After you delete an entry, you can register the same address again.""" chain: str | None = None - """Free-text chain identifier, e.g. "ethereum", "bitcoin".""" + """Chain token for the address, for example `ETH` or `BTC`. When Didit routes or registers a transfer, it matches address book entries on the wallet address and, when the transfer carries a chain, on this value too, ignoring case. A transfer's chain is `beneficiary_data.chain`, filled from the transaction's crypto chain or currency symbol (for example `ETH`) when you do not send it, or the `chain` of an after-deposit registration, so use the same token everywhere. An entry with an empty chain matches any chain. A Lightning payee (a node id or a Lightning Address) registered on bitcoin is stored lowercased on `LIGHTNING`, where Lightning transfers are matched.""" holder_vendor_data: str | None = None """Your internal identifier for the holder.""" @@ -31,8 +31,14 @@ class WalletAddressEntry(BaseModel): created_at: _dt.datetime | None = None - self_declared: bool | None = None - """Whether the entry was created as a self-declared ownership proof.""" - travel_address: str | None = None - """The counterparty VASP's travel address for this wallet, if known. Lets Didit route this beneficiary's transfers over the TRP rail.""" + """The counterparty VASP's travel address for this wallet, if known. Lets Didit route this beneficiary's transfers over the TRP rail, for the assets TRP carries (BTC, ETH, and USDC on a named network).""" + + address_hash: str | None = None + """Read-only. SHA-256 of the lowercased address, in lowercase hex. An entry imported by its hash only is matched on it.""" + + memo: str | None = None + """The destination tag or memo that is part of the wallet on a memo network (XRP, XLM, EOS, ATOM, HBAR); `""` for the whole address. An entry with a memo matches only transfers naming that memo; one without matches the whole address.""" + + receiving_travel_address: str | None = None + """Your per-wallet travel address for this wallet: give it to the sending VASP so its TRP inquiry names this wallet. Empty for a wallet recorded as another VASP's, while Travel Rule is off, and before your VASP profile is saved.""" diff --git a/src/didit/types/components/wallet_address_entry_update.py b/src/didit/types/components/wallet_address_entry_update.py index 7ff137b..58ba0f9 100644 --- a/src/didit/types/components/wallet_address_entry_update.py +++ b/src/didit/types/components/wallet_address_entry_update.py @@ -7,7 +7,7 @@ class WalletAddressEntryUpdate(BaseModel): - """PATCH is a partial update; the response echoes only holder_name, holder_vendor_data, entity_type, and travel_address.""" + """PATCH is a partial update; the response echoes only holder_name, holder_vendor_data, entity_type, travel_address, receiving_travel_address, label, and owner_type.""" holder_name: str | None = None diff --git a/src/didit/types/components/wallet_address_import_report.py b/src/didit/types/components/wallet_address_import_report.py new file mode 100644 index 0000000..9537deb --- /dev/null +++ b/src/didit/types/components/wallet_address_import_report.py @@ -0,0 +1,41 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["WalletAddressImportReport", "WalletAddressImportReportRows"] + + +class WalletAddressImportReportRows(BaseModel): + row: int | None = None + """The row's number, from 1.""" + + status: Literal["created", "existing", "duplicate", "failed"] | str | None = None + """`created`: added to the book. `existing`: the wallet is already in the book and was left untouched. `duplicate`: repeats an earlier row of this import. `failed`: see `errors`.""" + + entry_id: str | None = None + """The address-book entry the row created or found.""" + + errors: dict[str, object] | None = None + """Field errors of a `failed` row; empty otherwise.""" + + duplicate_of_row: int | None = None + """On a `duplicate` row, the row it repeats.""" + + +class WalletAddressImportReport(BaseModel): + """What happened to each row of an import, in the order sent.""" + + total: int | None = None + + created: int | None = None + + existing: int | None = None + + duplicate: int | None = None + + failed: int | None = None + + rows: list[WalletAddressImportReportRows] | None = None diff --git a/src/didit/types/components/wallet_address_import_row.py b/src/didit/types/components/wallet_address_import_row.py new file mode 100644 index 0000000..3822620 --- /dev/null +++ b/src/didit/types/components/wallet_address_import_row.py @@ -0,0 +1,39 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypedDict + +__all__ = ["WalletAddressImportRow"] + + +class WalletAddressImportRow(TypedDict, total=False): + """One wallet to import: by its `address`, its `address_hash`, or both. The camelCase forms of these fields are accepted too, as are `walletAddress`, `walletAddressHash`, `destinationTag` and `externalUserId`.""" + + address: str + """The wallet address. Alias: `walletAddress`.""" + + address_hash: str + """SHA-256 of the lowercased address, as 64 hex characters or 44 base64 characters. Send it alone to register a wallet without disclosing its address; sent with `address`, it must be that address's hash. Alias: `walletAddressHash`.""" + + chain: str + + memo: str + """The destination tag or memo. XRP: a destination tag, a whole number from 0 to 4294967295 (a JSON number is accepted). XLM: a text memo of up to 28 bytes, a 64-bit id or a 32-byte hash in hex. EOS: up to 256 bytes. ATOM: up to 256 characters. HBAR: up to 100 bytes. Bitcoin, EVM, Solana, Tron, Litecoin, Bitcoin Cash, Dogecoin and Lightning wallets take no memo: one there is a `400`.""" + + holder_name: str + + holder_vendor_data: str + """Your internal identifier for the holder. Alias: `externalUserId`.""" + + entity_type: Literal["individual", "company"] | str + + label: str + + owner_type: Literal["customer", "internal", "vasp"] | str + + travel_address: str + + self_declared: bool + """Create the entry already verified through a self-declaration, when your settings allow it.""" diff --git a/src/didit/types/components/webhook_config.py b/src/didit/types/components/webhook_config.py new file mode 100644 index 0000000..2c29ea3 --- /dev/null +++ b/src/didit/types/components/webhook_config.py @@ -0,0 +1,52 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.webhook_config_destination import WebhookConfigDestination +from didit.types.components.webhook_event import WebhookEvent + +__all__ = ["WebhookConfig", "WebhookConfigAvailableEvents"] + + +class WebhookConfigAvailableEvents(BaseModel): + value: WebhookEvent + """Supported webhook event name. Use these exact strings in `subscribed_events`; unsupported values are rejected.""" + + label: str + """Display label, for example `Session status updated`.""" + + +class WebhookConfig(BaseModel): + """Application-level webhook configuration, retention policy and Digital ID wallet return hosts, with every webhook destination.""" + + webhook_url: str | None + """Application-level (legacy) webhook URL. Didit sends to it only while the application has no enabled destination, and then only `status.updated` and `data.updated`. Every create, update or delete of a destination sets it to the URL of the oldest enabled destination, or `null` when none is enabled.""" + + webhook_version: Literal["v1", "v2", "v3"] | str + """Payload version of the legacy `webhook_url`. Each destination has its own `webhook_version`.""" + + secret_shared_key: str + """Application-level signing secret (43-character URL-safe token). It signs only deliveries to the legacy `webhook_url`; each destination signs with its own `secret_shared_key`. API-key callers always receive it in full; Console users without `read:webhooks` receive an empty string.""" + + capture_method: Literal["MOBILE", "MOBILE_AND_DESKTOP"] | str + """Legacy application setting. The verification flow does not read it: capture options are configured per workflow.""" + + data_retention_months: int | None + """Automatic deletion window. User verification sessions created more than this many months ago (a month counts as 30 days) are deleted automatically, and that deletion is irreversible. `null` means sessions are never deleted automatically. See [Data retention](https://docs.didit.me/console/data-retention).""" + + face_retention_policy: Literal["delete_with_session", "retain_until_user_deleted"] | str + """What happens to a session's face embedding when the session is deleted. `delete_with_session` (default) deletes it with the session. `retain_until_user_deleted` is the opt-in [biometric template retention](https://docs.didit.me/console/data-retention#biometric-template-retention): an image-free biometric template linked to the User is managed separately until its retention period ends, the User is deleted, or you purge it.""" + + face_retention_days: int | None + """Retention period, in days, of biometric templates under `retain_until_user_deleted`. Required when that policy is set.""" + + available_events: list[WebhookConfigAvailableEvents] + """Every event type a destination can subscribe to, with a display label.""" + + allowed_return_hosts: list[str] + """Hostnames (exact match, HTTPS only) a standalone Digital ID wallet verification may send the user back to with its `return_url`. Your white-label domain is always allowed as well. See [Register your return destination](https://docs.didit.me/standalone-apis/digital-id-wallets#register-your-return-destination).""" + + endpoints: list[WebhookConfigDestination] + """Every webhook destination of the application, oldest first, each with its signing secret. Read only here: manage destinations with the `/v3/webhook/destinations/` endpoints.""" diff --git a/src/didit/types/components/webhook_config_destination.py b/src/didit/types/components/webhook_config_destination.py new file mode 100644 index 0000000..4ece578 --- /dev/null +++ b/src/didit/types/components/webhook_config_destination.py @@ -0,0 +1,39 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.webhook_event import WebhookEvent + +__all__ = ["WebhookConfigDestination"] + + +class WebhookConfigDestination(BaseModel): + """A webhook destination as listed in the webhook configuration, without delivery metrics. `GET /v3/webhook/destinations/{destination_uuid}/` adds its delivery summary.""" + + uuid: str + """Destination ID.""" + + label: str + """Human-readable name.""" + + url: str + """HTTPS endpoint Didit posts events to.""" + + enabled: bool + """`false` pauses deliveries to this destination.""" + + webhook_version: Literal["v1", "v2", "v3"] | str + """Payload version sent to this destination.""" + + secret_shared_key: str + """This destination's signing secret (43-character URL-safe token). Verify each webhook with the secret of the destination it was sent to. API-key callers always receive it in full; Console users without `read:webhooks` receive an empty string.""" + + subscribed_events: list[WebhookEvent] + """Event types delivered to this destination. Empty when the destination was created without events, in which case it receives nothing.""" + + created_at: _dt.datetime + + updated_at: _dt.datetime diff --git a/src/didit/types/components/webhook_destination_delivery.py b/src/didit/types/components/webhook_destination_delivery.py new file mode 100644 index 0000000..4de4104 --- /dev/null +++ b/src/didit/types/components/webhook_destination_delivery.py @@ -0,0 +1,37 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.webhook_event import WebhookEvent + +__all__ = ["WebhookDestinationDelivery"] + + +class WebhookDestinationDelivery(BaseModel): + """One delivery attempt to a webhook destination. The first send of an event, each automatic retry and each resend are separate deliveries.""" + + uuid: str + """Delivery ID, for the detail and resend calls.""" + + session_id: str | None + """User verification session the event is about. `null` for business session, user, business, transaction, Travel Rule and workflow events.""" + + request_url: str | None + """URL the delivery was sent to: the destination's URL when the event was generated.""" + + response_status: int | None + """HTTP status your endpoint returned. `null` while the delivery is queued and not yet attempted. When no HTTP answer was received Didit records a status of its own: `504` when your endpoint did not answer within 5 seconds, `503` for a connection failure, `400` when the URL was refused as unsafe (for example a private or internal address), and `404` for any other request error. `400` or above counts as a failed delivery.""" + + response_time_ms: int | None + """Time the attempt took, in milliseconds. `null` while the delivery is queued.""" + + status: str | None + """Status the event reported when it was generated: the session status for session events (for example `Approved` or `In Review`), the person's or company's status for user and business events, and the transaction status for transaction events. `null` for events that carry no status.""" + + webhook_type: WebhookEvent + """Supported webhook event name. Use these exact strings in `subscribed_events`; unsupported values are rejected.""" + + created_at: _dt.datetime + """When the delivery was created.""" diff --git a/src/didit/types/components/webhook_destination_delivery_detail.py b/src/didit/types/components/webhook_destination_delivery_detail.py new file mode 100644 index 0000000..d3fa93d --- /dev/null +++ b/src/didit/types/components/webhook_destination_delivery_detail.py @@ -0,0 +1,43 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.webhook_event import WebhookEvent + +__all__ = ["WebhookDestinationDeliveryDetail"] + + +class WebhookDestinationDeliveryDetail(BaseModel): + """One delivery attempt to a webhook destination, with the exact request headers and body Didit sent.""" + + uuid: str + """Delivery ID, for the detail and resend calls.""" + + session_id: str | None + """User verification session the event is about. `null` for business session, user, business, transaction, Travel Rule and workflow events.""" + + request_url: str | None + """URL the delivery was sent to: the destination's URL when the event was generated.""" + + response_status: int | None + """HTTP status your endpoint returned. `null` while the delivery is queued and not yet attempted. When no HTTP answer was received Didit records a status of its own: `504` when your endpoint did not answer within 5 seconds, `503` for a connection failure, `400` when the URL was refused as unsafe (for example a private or internal address), and `404` for any other request error. `400` or above counts as a failed delivery.""" + + response_time_ms: int | None + """Time the attempt took, in milliseconds. `null` while the delivery is queued.""" + + status: str | None + """Status the event reported when it was generated: the session status for session events (for example `Approved` or `In Review`), the person's or company's status for user and business events, and the transaction status for transaction events. `null` for events that carry no status.""" + + webhook_type: WebhookEvent + """Supported webhook event name. Use these exact strings in `subscribed_events`; unsupported values are rejected.""" + + created_at: _dt.datetime + """When the delivery was created.""" + + request_header: str + """JSON text (a string) of the request headers Didit sent: `Content-Type`, `X-Signature`, `X-Signature-V2`, `X-Signature-Simple`, `X-Timestamp` and `User-Agent`. Parse it with a JSON parser. It holds signatures, never the signing secret.""" + + request_body: str + """JSON text (a string) of the exact payload Didit sent, keys sorted, including `event_id` and `timestamp`. Parse it with a JSON parser; see [Payload shape](https://docs.didit.me/integration/webhooks#payload-shape). For a scoped key without `media: read`, media URLs inside it are `null`; without `sessions: write`, session tokens and hosted verification links are `null`. A body changed that way no longer matches its signatures. Didit does not record the response body your endpoint returned.""" diff --git a/src/didit/types/components/webhook_event.py b/src/didit/types/components/webhook_event.py index 502cd61..b4438bf 100644 --- a/src/didit/types/components/webhook_event.py +++ b/src/didit/types/components/webhook_event.py @@ -20,6 +20,27 @@ "transaction.created", "transaction.status.updated", "travel_rule.status.updated", + "workflow.compliance_changed", + "auth.signup", + "auth.login", + "auth.login_failed", + "auth.password_reset_requested", + "auth.password_reset_completed", + "auth.mfa_changed", + "auth.new_device", + "auth.account_locked", + "investigator.report.ready", + "investigator.decision.automated", + "investigator.decision.overridden", + "investigator.decision.contested", + "investigator.question.raised", + "investigator.automation.paused", + "investigator.rfi.answered", + "investigator.rfi.overdue", + "investigator.drift.detected", + "investigator.sla.breached", + "investigator.qa.failed", + "investigator.external.report.ready", ] | str ) diff --git a/src/didit/types/components/workflow_detail.py b/src/didit/types/components/workflow_detail.py new file mode 100644 index 0000000..102ab83 --- /dev/null +++ b/src/didit/types/components/workflow_detail.py @@ -0,0 +1,99 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["WorkflowDetail"] + + +class WorkflowDetail(BaseModel): + """The fields below are typed; the object also carries every other column of the workflow configuration (feature toggles, thresholds, warning actions, allowed documents, callbacks) as additional properties. Fields that do not apply to the `workflow_type` are `null`.""" + + uuid: str | None = None + """Per-version identifier (`settings_uuid`). Every version of a workflow has its own.""" + + workflow_id: str | None = None + """Stable identifier shared by every version of the workflow. Sessions created with it use the latest published version.""" + + workflow_label: str | None = None + """Display name of the workflow.""" + + workflow_type: ( + Literal[ + "kyc", + "kyb", + "adaptive_age_verification", + "biometric_authentication", + "address_verification", + "questionnaire_verification", + "email_verification", + "phone_verification", + ] + | str + | None + ) = None + """Workflow template. `null` for graph workflows created without a template.""" + + is_default: bool | None = None + """Whether this is the application's default workflow.""" + + is_archived: bool | None = None + """Archived workflows cannot start new sessions.""" + + version: int | None = None + """Version number within the workflow.""" + + status: Literal["draft", "published"] | str | None = None + """Draft versions are editable; published versions are immutable and used for new sessions.""" + + published_at: _dt.datetime | None = None + """When this version was published. `null` for drafts.""" + + is_simple_workflow: bool | None = None + """`true` when the workflow has no custom `workflow_graph`.""" + + is_editable: bool | None = None + """`true` only for `draft` versions.""" + + workflow_url: str | None = None + """Public URL that starts a session with this workflow.""" + + workflow_graph: dict[str, object] | None = None + """Graph structure of the workflow (nodes and edges). `null` for simple workflows.""" + + features: str | None = None + """Enabled features joined with `" + "`, for example `"OCR + LIVENESS"`.""" + + total_price: float | None = None + """Price per session; for graph workflows the most expensive path.""" + + min_price: float | None = None + """Price of the cheapest path through the workflow.""" + + max_price: float | None = None + """Price of the most expensive path through the workflow.""" + + special_add_on_prices: dict[str, object] | None = None + """Organization-specific add-on prices, or `null` when none are set.""" + + max_retry_attempts: int | None = None + """Retries allowed after a declined verification.""" + + retry_window_days: int | None = None + """Days during which the retries are allowed.""" + + session_expiration_time: int | None = None + """Seconds before an unfinished session expires.""" + + questionnaire: dict[str, object] | None = None + """The questionnaire attached to the workflow, or `null`.""" + + kyb_role_settings: list[dict[str, object]] | None = None + """Per-role verification settings for KYB key people.""" + + created_at: _dt.datetime | None = None + + updated_at: _dt.datetime | None = None diff --git a/src/didit/types/customization/update_params.py b/src/didit/types/customization/update_params.py index 1974825..b28452a 100644 --- a/src/didit/types/customization/update_params.py +++ b/src/didit/types/customization/update_params.py @@ -86,14 +86,20 @@ class CustomizationUpdateParams(TypedDict, total=False): delete_logo_rectangular: bool """When `true`, removes the current rectangular logo.""" + delete_logo_rectangular_dark: bool + """When `true`, removes the current dark-mode logo, and `logo_rectangular_dark` then reads `null`. If the same multipart request also uploads `image_rectangular_dark`, the upload wins.""" + delete_favicon: bool """When `true`, removes the current favicon.""" image_square: FileTypes - """Square logo (1:1 aspect ratio). Max **1 MB**. Allowed: `png`, `jpg`, `jpeg`, `webp`.""" + """Square logo (1:1 aspect ratio). PNG, JPG or WebP, at least 512 px on the long side, at most **1 MB**.""" image_rectangular: FileTypes - """Rectangular logo (2:1 aspect ratio). Max **1 MB**. Allowed: `png`, `jpg`, `jpeg`, `webp`.""" + """Rectangular logo (2:1 aspect ratio). PNG, JPG or WebP, at least 512 px on the long side, at most **1 MB**.""" + + image_rectangular_dark: FileTypes + """Rectangular logo for dark mode (2:1 aspect ratio), shown on a dark background. PNG, JPG or WebP, at least 512 px on the long side, at most **1 MB**. Returned as `logo_rectangular_dark`.""" image_favicon: FileTypes """Favicon image. Max **500 KB**.""" diff --git a/src/didit/types/investigator/__init__.py b/src/didit/types/investigator/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/investigator/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/investigator/accept_run_params.py b/src/didit/types/investigator/accept_run_params.py new file mode 100644 index 0000000..3086631 --- /dev/null +++ b/src/didit/types/investigator/accept_run_params.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["InvestigatorAcceptRunParams"] + + +class InvestigatorAcceptRunParams(TypedDict, total=False): + narrative: str + """A rewritten narrative. Sending one records the decision as edited and accepted.""" + + send_email: bool + """Email the person about the decision.""" + + email_address: str + """Required when `send_email` is true.""" + + email_language: str diff --git a/src/didit/types/investigator/batch_accept_runs_params.py b/src/didit/types/investigator/batch_accept_runs_params.py new file mode 100644 index 0000000..d2d91d5 --- /dev/null +++ b/src/didit/types/investigator/batch_accept_runs_params.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import Required, TypedDict + +__all__ = ["InvestigatorBatchAcceptRunsParams"] + + +class InvestigatorBatchAcceptRunsParams(TypedDict, total=False): + run_ids: Required[Sequence[str]] diff --git a/src/didit/types/investigator/list_activity_response.py b/src/didit/types/investigator/list_activity_response.py new file mode 100644 index 0000000..4cd21e1 --- /dev/null +++ b/src/didit/types/investigator/list_activity_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["InvestigatorListActivityResponse"] + + +class InvestigatorListActivityResponse(BaseModel): + count: int | None = None + + next: str | None = None + + previous: str | None = None + + counts: dict[str, object] | None = None + + actors: list[object] | None = None + + results: list[dict[str, object]] | None = None diff --git a/src/didit/types/investigator/list_guardrails_response.py b/src/didit/types/investigator/list_guardrails_response.py new file mode 100644 index 0000000..79e3db5 --- /dev/null +++ b/src/didit/types/investigator/list_guardrails_response.py @@ -0,0 +1,32 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["InvestigatorListGuardrailsResponse", "InvestigatorListGuardrailsResponseResults"] + + +class InvestigatorListGuardrailsResponseResults(BaseModel): + key: ( + Literal[ + "sanctions_to_person", + "no_tipping_off", + "no_protected_characteristics", + "never_files_reports", + "every_decision_logged", + ] + | str + | None + ) = None + + title: str | None = None + + description: str | None = None + + locked: bool | None = None + + +class InvestigatorListGuardrailsResponse(BaseModel): + results: list[InvestigatorListGuardrailsResponseResults] | None = None diff --git a/src/didit/types/investigator/list_queue_response.py b/src/didit/types/investigator/list_queue_response.py new file mode 100644 index 0000000..0e4c8b7 --- /dev/null +++ b/src/didit/types/investigator/list_queue_response.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.investigator_queue_row import InvestigatorQueueRow + +__all__ = ["InvestigatorListQueueResponse"] + + +class InvestigatorListQueueResponse(BaseModel): + count: int | None = None + + next: str | None = None + + previous: str | None = None + + counts: dict[str, object] | None = None + + results: list[InvestigatorQueueRow] | None = None diff --git a/src/didit/types/investigator/override_run_params.py b/src/didit/types/investigator/override_run_params.py new file mode 100644 index 0000000..a8dc37c --- /dev/null +++ b/src/didit/types/investigator/override_run_params.py @@ -0,0 +1,29 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +__all__ = ["InvestigatorOverrideRunParams"] + + +class InvestigatorOverrideRunParams(TypedDict, total=False): + action: Required[Literal["approve", "decline", "escalate", "request"] | str] + + category: Required[ + Literal[ + "missing_context", "needs_information", "policy_differs", "evidence_misread", "other" + ] + | str + ] + + reason: Required[str] + + send_email: bool + """Email the person about the decision.""" + + email_address: str + """Required when `send_email` is true.""" + + email_language: str diff --git a/src/didit/types/investigator/retrieve_dashboard_response.py b/src/didit/types/investigator/retrieve_dashboard_response.py new file mode 100644 index 0000000..103dd94 --- /dev/null +++ b/src/didit/types/investigator/retrieve_dashboard_response.py @@ -0,0 +1,22 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["InvestigatorRetrieveDashboardResponse"] + + +class InvestigatorRetrieveDashboardResponse(BaseModel): + stage: str | None = None + + window_days: int | None = None + + reviews: int | None = None + + decided: int | None = None + + automated: int | None = None + + automation_rate: float | None = None + + hours_saved: float | None = None diff --git a/src/didit/types/investigator/retrieve_performance_response.py b/src/didit/types/investigator/retrieve_performance_response.py new file mode 100644 index 0000000..998f32b --- /dev/null +++ b/src/didit/types/investigator/retrieve_performance_response.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["InvestigatorRetrievePerformanceResponse"] + + +class InvestigatorRetrievePerformanceResponse(BaseModel): + period: int | None = None + + start: _dt.date | None = None + + end: _dt.date | None = None + + investigated: int | None = None diff --git a/src/didit/types/investigator/retrieve_summary_response.py b/src/didit/types/investigator/retrieve_summary_response.py new file mode 100644 index 0000000..fcf0ca1 --- /dev/null +++ b/src/didit/types/investigator/retrieve_summary_response.py @@ -0,0 +1,26 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["InvestigatorRetrieveSummaryResponse"] + + +class InvestigatorRetrieveSummaryResponse(BaseModel): + stage: str | None = None + + last_report_at: _dt.datetime | None = None + + prepared_today: int | None = None + + ready_to_accept: int | None = None + + open: int | None = None + + decided_today: int | None = None + + automation_rate_month: float | None = None + + preparing: int | None = None diff --git a/src/didit/types/investigator/retrieve_transaction_response.py b/src/didit/types/investigator/retrieve_transaction_response.py new file mode 100644 index 0000000..ac20ab0 --- /dev/null +++ b/src/didit/types/investigator/retrieve_transaction_response.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.investigator_block import InvestigatorBlock + +__all__ = ["InvestigatorRetrieveTransactionResponse"] + + +class InvestigatorRetrieveTransactionResponse(BaseModel): + transaction_id: str | None = None + + investigator: InvestigatorBlock | None = None + """The item's latest Investigator review, or null when it never had one. Carried by the V3 session and business session decision (and so by `status.updated` webhooks).""" diff --git a/src/didit/types/investigator/update_settings_params.py b/src/didit/types/investigator/update_settings_params.py new file mode 100644 index 0000000..e277d94 --- /dev/null +++ b/src/didit/types/investigator/update_settings_params.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypedDict + +__all__ = ["InvestigatorUpdateSettingsParams"] + + +class InvestigatorUpdateSettingsParams(TypedDict, total=False): + stage: Literal["none", "shadow", "live"] | str + + monthly_spend_limit: str | None + """Monthly limit in your billing currency, 0.01 to 1,000,000. Null turns it off.""" diff --git a/src/didit/types/kyb/search_params.py b/src/didit/types/kyb/search_params.py index 925b9c1..eba1b64 100644 --- a/src/didit/types/kyb/search_params.py +++ b/src/didit/types/kyb/search_params.py @@ -18,8 +18,11 @@ class KybSearchParams(TypedDict, total=False): registration_number: str """Registry registration number to search for. Either `name` or `registration_number` is required.""" - search_type: Literal["contains", "start_with", "fuzzy"] | str - """Name-matching strategy applied by the registry search.""" + tier: Literal["basic", "shareholders", "ubo"] | str + """Registry tier whose provider chain this search runs on. `basic` (default) searches the country's Lite chain; `shareholders` and `ubo` search that tier's ownership chain, which can be a different source, so an empty Lite search does not prove the registry lacks the company. A country that does not offer the named tier is searched at the highest tier it offers up to that one. Retrieve the candidate from the endpoint that sells the tier you searched: `POST /v3/kyb/select/` (Lite), `POST /v3/kyb/shareholders/` or `POST /v3/kyb/ubo/`.""" + + search_type: Literal["contains"] | str + """Name-matching strategy applied by the registry search. `contains` is the only accepted value: no registry source behind the search offers a prefix or fuzzy mode, so `start_with` and `fuzzy` are rejected with `400` (`not a valid choice`).""" vendor_data: str """Your identifier for this lookup; echoed back and included in the webhook callback.""" diff --git a/src/didit/types/kyb/search_response.py b/src/didit/types/kyb/search_response.py index 403769f..c1fcf79 100644 --- a/src/didit/types/kyb/search_response.py +++ b/src/didit/types/kyb/search_response.py @@ -2,68 +2,18 @@ from __future__ import annotations import datetime as _dt -from typing import Literal from didit._models import BaseModel +from didit.types.components.kyb_registry_search_results import KYBRegistrySearchResults -__all__ = [ - "KybSearchResponse", - "KybSearchResponseKybRegistry", - "KybSearchResponseKybRegistryCompanies", - "KybSearchResponseKybRegistryPagination", -] - - -class KybSearchResponseKybRegistryCompanies(BaseModel): - kyb_response_id: str | None = None - """Handle for this candidate - pass it to `POST /v3/kyb/select/` to retrieve the full profile. Ephemeral and scoped to this search: do not store it long-term; re-search to get a fresh one.""" - - name: str | None = None - """Registered company name.""" - - registration_number: str | None = None - """Registry registration number.""" - - status: str | None = None - """Normalized registry status of the company (e.g. `active`, `dissolved`, `inactive`, `struck off`).""" - - type: str | None = None - """Legal form as reported by the registry.""" - - risk_level: str | None = None - """Registry-reported risk level, when available. Usually `null` at search time.""" - - fetch_status: str | None = None - """Always `pending` for search candidates - the full company profile is only fetched (and billed) when you select the candidate.""" - - -class KybSearchResponseKybRegistryPagination(BaseModel): - total: int | None = None - """Number of candidates returned.""" - - page: int | None = None - - per_page: int | None = None - - -class KybSearchResponseKybRegistry(BaseModel): - companies: list[KybSearchResponseKybRegistryCompanies] | None = None - """Deduplicated candidate list. Empty while `search_resolved=false` (webhook mode) or when nothing matched.""" - - pagination: KybSearchResponseKybRegistryPagination | None = None - - search_status: Literal["pending", "resolved"] | str | None = None - """Registry search lifecycle status.""" - - search_resolved: bool | None = None - """`true` once the candidate list is final.""" +__all__ = ["KybSearchResponse"] class KybSearchResponse(BaseModel): request_id: str | None = None """With `webhook_url`: the persisted search-request id, echoed as `request_id` in the `kyb.registry_search.resolved` callback. Without `webhook_url`: a transient correlation UUID (the search is not stored).""" - kyb_registry: KybSearchResponseKybRegistry | None = None + kyb_registry: KYBRegistrySearchResults | None = None vendor_data: str | None = None """Echo of the `vendor_data` you sent.""" diff --git a/src/didit/types/lists/__init__.py b/src/didit/types/lists/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/lists/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/lists/create_params.py b/src/didit/types/lists/create_params.py new file mode 100644 index 0000000..3016e89 --- /dev/null +++ b/src/didit/types/lists/create_params.py @@ -0,0 +1,38 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +__all__ = ["ListsCreateParams"] + + +class ListsCreateParams(TypedDict, total=False): + name: Required[str] + """List name. Must differ from the name of every other list of the application, system lists included.""" + + description: str + """Optional free-text description.""" + + list_type: Required[Literal["allowlist", "custom"] | str] + """`allowlist` or `custom`. `blocklist` is refused with `400`.""" + + entry_type: Required[ + Literal[ + "face", + "document", + "phone", + "email", + "ip_address", + "device_fingerprint", + "wallet_address", + "bank_account", + "country", + "user", + "business", + "key", + ] + | str + ] + """What the list holds: `face`, `document` (document number), `phone`, `email`, `ip_address` (IP address or CIDR range), `device_fingerprint`, `wallet_address`, `bank_account`, `country` (ISO 3166-1 alpha-3), `user` or `business` (your `vendor_data`), or `key` (any string). It decides how entry values are validated and matched. Cannot be changed later.""" diff --git a/src/didit/types/lists/entries/__init__.py b/src/didit/types/lists/entries/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/lists/entries/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/lists/entries/create_params.py b/src/didit/types/lists/entries/create_params.py new file mode 100644 index 0000000..5c0cab0 --- /dev/null +++ b/src/didit/types/lists/entries/create_params.py @@ -0,0 +1,48 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypedDict + +__all__ = ["ListsEntriesCreateParams", "ListsEntriesCreateParamsMetadata"] + + +class ListsEntriesCreateParamsMetadata(TypedDict, total=False): + """Free-form JSON stored with the entry and returned as stored. Use `reference_type` and `reference_id` so the console links back to the source. `reference_image_url` and `front_image_url` are set by Didit only and are dropped if you send them. For an entry read from a session, Didit's keys are merged with yours.""" + + reference_type: Literal["transaction", "vendor_user", "vendor_business"] | str + """What `reference_object_uuid` points to.""" + + reference_id: str + """Identifier of the source record.""" + + +class ListsEntriesCreateParams(TypedDict, total=False): + """Send `value`, `reference_session_id`, or both. Which fields apply depends on the list's `entry_type`.""" + + value: str + """The value to list, validated against the list's `entry_type`: `email` a valid email address; `phone` a valid international number, stored in E.164; `ip_address` an IP address or CIDR range (for example `10.0.0.0/8`); `device_fingerprint` at least 8 characters among letters, digits, `-`, `_`, `:` and `.` (a blocklist refuses a fingerprint that several distinct devices have reported); `country` an ISO 3166-1 alpha-2 or alpha-3 code, stored as alpha-3; `user` and `business` the `vendor_data`; any other type a non-empty string. Stored in lower case except phone numbers and country codes, and a list holds each value once (case-insensitive). Required unless `reference_session_id` is sent; must be left empty with a document-capture `face_image_source`.""" + + reference_session_id: str + """A person or business verification session of your application to take the value from. Didit reads the session's most recent record of the list's entry type (`face`, `document`, `phone`, `email`, `ip_address` or `device_fingerprint`), links the entry to the session and to that record, and fills `display_label` and `metadata` for documents. For a `device_fingerprint` blocklist, a session that only has a device-class fingerprint, or was completed on more than one device, is refused: send the exact fingerprint as `value`. For other entry types, or when the session has no such record, send `value` too; your `value` is then stored and the entry is still linked to the session.""" + + reference_object_uuid: str + """UUID of the record the entry stands for when it does not come from a session: a transaction UUID, or the `didit_internal_id` of a user or business. Pair it with `metadata.reference_type`. Replaced by the session's record when `reference_session_id` provides one.""" + + face_image_source: ( + Literal["liveness", "front_image_camera_front", "back_image_camera_front"] | str + ) + """Face lists with `reference_session_id`: which of the session's images the face is taken from. `liveness` (default) is the liveness selfie. `front_image_camera_front` and `back_image_camera_front` are the selfies the front camera took while the user photographed the front or back of their document: accepted only on the system face blocklist, with `reference_session_id` and an empty `value`. Each selfie can be added once and must show one clear face.""" + + reference_node_id: str | None + """With a document-capture `face_image_source`: the ID verification node whose selfie to use, for workflows with more than one. Defaults to the most recent ID verification that has that image. Refused with `liveness`.""" + + display_label: str + """Human-readable label. When empty, Didit derives one from the session where it can (for example the holder's name and document number).""" + + comment: str + """Free-text note, such as the reason for listing.""" + + metadata: ListsEntriesCreateParamsMetadata + """Free-form JSON stored with the entry and returned as stored. Use `reference_type` and `reference_id` so the console links back to the source. `reference_image_url` and `front_image_url` are set by Didit only and are dropped if you send them. For an entry read from a session, Didit's keys are merged with yours.""" diff --git a/src/didit/types/lists/entries/list_response.py b/src/didit/types/lists/entries/list_response.py new file mode 100644 index 0000000..2fff02a --- /dev/null +++ b/src/didit/types/lists/entries/list_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.compliance_list_entry import ComplianceListEntry + +__all__ = ["ListsEntriesListResponse"] + + +class ListsEntriesListResponse(BaseModel): + count: int + """Total number of entries matching the filters.""" + + next: str | None + """Absolute URL of the next page, or `null` on the last page.""" + + previous: str | None + """Absolute URL of the previous page, or `null` on the first page.""" + + results: list[ComplianceListEntry] diff --git a/src/didit/types/lists/entries/upload_face_params.py b/src/didit/types/lists/entries/upload_face_params.py new file mode 100644 index 0000000..e5adfd0 --- /dev/null +++ b/src/didit/types/lists/entries/upload_face_params.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ListsEntriesUploadFaceParams"] + + +class ListsEntriesUploadFaceParams(TypedDict, total=False): + image: Required[str] + """Base64-encoded JPEG or PNG, at most 2 MB once decoded, showing a clearly visible face. Send the bare base64 string, without a `data:image/...;base64,` prefix.""" + + comment: str + """Optional free-text note, such as the reason for listing.""" diff --git a/src/didit/types/lists/list_response.py b/src/didit/types/lists/list_response.py new file mode 100644 index 0000000..2516bbf --- /dev/null +++ b/src/didit/types/lists/list_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.compliance_list import ComplianceList + +__all__ = ["ListsListResponse"] + + +class ListsListResponse(BaseModel): + count: int + """Total number of lists matching the filters.""" + + next: str | None + """Absolute URL of the next page, or `null` on the last page.""" + + previous: str | None + """Absolute URL of the previous page, or `null` on the first page.""" + + results: list[ComplianceList] diff --git a/src/didit/types/lists/update_params.py b/src/didit/types/lists/update_params.py new file mode 100644 index 0000000..270a817 --- /dev/null +++ b/src/didit/types/lists/update_params.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["ListsUpdateParams"] + + +class ListsUpdateParams(TypedDict, total=False): + name: str + """New name. Must differ from the name of every other list of the application.""" + + description: str + """New description. Send an empty string to clear it.""" diff --git a/src/didit/types/lists/update_response.py b/src/didit/types/lists/update_response.py new file mode 100644 index 0000000..85ec068 --- /dev/null +++ b/src/didit/types/lists/update_response.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["ListsUpdateResponse"] + + +class ListsUpdateResponse(BaseModel): + name: str + """The list's name after the update.""" + + description: str + """The list's description after the update.""" diff --git a/src/didit/types/monitoring_events/__init__.py b/src/didit/types/monitoring_events/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/monitoring_events/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/monitoring_events/create_batch_params.py b/src/didit/types/monitoring_events/create_batch_params.py new file mode 100644 index 0000000..f904819 --- /dev/null +++ b/src/didit/types/monitoring_events/create_batch_params.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import Required, TypedDict + +from didit.types.components.monitoring_event_create_request import MonitoringEventCreateRequest + +__all__ = ["MonitoringEventsCreateBatchParams"] + + +class MonitoringEventsCreateBatchParams(TypedDict, total=False): + events: Required[Sequence[MonitoringEventCreateRequest]] + """1 to 500 events. Each item follows `MonitoringEventCreateRequest`, including the 7-day past and 5-minute future window on `occurred_at`.""" diff --git a/src/didit/types/monitoring_events/create_batch_response.py b/src/didit/types/monitoring_events/create_batch_response.py new file mode 100644 index 0000000..2d9348c --- /dev/null +++ b/src/didit/types/monitoring_events/create_batch_response.py @@ -0,0 +1,89 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "MonitoringEventsCreateBatchResponse", + "MonitoringEventsCreateBatchResponseResults", + "MonitoringEventsCreateBatchResponseResultsRuleMatches", +] + + +class MonitoringEventsCreateBatchResponseResultsRuleMatches(BaseModel): + rule_id: str + """UUID of the rule.""" + + library_key: str | None + """Library key when the rule was installed from the rules library, otherwise null.""" + + mode: Literal["ACTIVE", "TEST"] | str + """`TEST` rules record the match but apply no actions.""" + + is_async: bool + """`true` when the match came from the asynchronous pass.""" + + +class MonitoringEventsCreateBatchResponseResults(BaseModel): + """Outcome of one item. Accepted and replayed items also carry every field of `MonitoringEventIngestResult`; rejected items carry `event_id` and `errors` instead.""" + + index: int + """Position of the item in your `events` array.""" + + outcome: Literal["accepted", "replayed", "rejected"] | str + """`accepted`: stored and evaluated. `replayed`: already stored with the same content. `rejected`: not stored; see `errors`.""" + + event_id: str | None = None + """Your `event_id`. On items rejected by validation it is read from the item's snake_case `event_id` key, so it is null when the item omitted it or sent the `eventId` alias.""" + + errors: dict[str, object] | None = None + """Rejected items only. Validation errors keyed by field, in the same shape as the `400` of `POST /v3/events/`; a reused `event_id` with different content is reported as a string under `errors.event_id`.""" + + event_uuid: str | None = None + """Accepted and replayed items only.""" + + status: Literal["ACCEPTED", "APPROVED", "IN_REVIEW", "DECLINED"] | str | None = None + """Accepted and replayed items only.""" + + score: int | None = None + """Accepted and replayed items only.""" + + rules_evaluated_count: int | None = None + """Accepted and replayed items only.""" + + rules_matched_count: int | None = None + """Accepted and replayed items only.""" + + correlation_pending: bool | None = None + """Accepted and replayed items only.""" + + retention_class: ( + Literal["low_value", "standard", "risk_relevant", "monetary_or_compliance"] | str | None + ) = None + """Accepted and replayed items only.""" + + expires_at: _dt.datetime | None = None + """Accepted and replayed items only.""" + + idempotent_replay: bool | None = None + """Accepted and replayed items only.""" + + rule_matches: list[MonitoringEventsCreateBatchResponseResultsRuleMatches] | None = None + """Accepted and replayed items only.""" + + +class MonitoringEventsCreateBatchResponse(BaseModel): + accepted_count: int + """Items stored and evaluated.""" + + replayed_count: int + """Items already stored with the same content.""" + + rejected_count: int + """Items not stored.""" + + results: list[MonitoringEventsCreateBatchResponseResults] + """One entry per item, in request order.""" diff --git a/src/didit/types/monitoring_events/create_params.py b/src/didit/types/monitoring_events/create_params.py new file mode 100644 index 0000000..ef9ef0f --- /dev/null +++ b/src/didit/types/monitoring_events/create_params.py @@ -0,0 +1,152 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Mapping + +from typing_extensions import Required, TypedDict + +__all__ = [ + "MonitoringEventsCreateParams", + "MonitoringEventsCreateParamsBusiness", + "MonitoringEventsCreateParamsDeviceContext", + "MonitoringEventsCreateParamsDeviceContextNetworkContext", + "MonitoringEventsCreateParamsSession", + "MonitoringEventsCreateParamsSubject", + "MonitoringEventsCreateParamsTransactionContext", +] + + +class MonitoringEventsCreateParamsBusiness(TypedDict, total=False): + """The business the event belongs to, when it differs from the subject. Only `vendor_data` is read.""" + + vendor_data: str + """Your id for the business. Filterable as `business_vendor_data`.""" + + +class MonitoringEventsCreateParamsDeviceContextNetworkContext(TypedDict, total=False): + ip_address: str + """End user's IP address. Public addresses are enriched once per unique address.""" + + +class MonitoringEventsCreateParamsDeviceContext(TypedDict, total=False): + """Device and network context, stored as sent and returned by `GET /v3/events/{event_uuid}/`. On this server lane Didit does not see the end user's connection, so the IP enriched and used by rules is `network_context.ip_address` as you send it.""" + + fingerprint: str + """Device fingerprint. Filterable as `device_fingerprint`.""" + + persistent_device_id: str + """Your persistent device identifier.""" + + bot_score: int + """Bot score from your own detection, as an integer.""" + + network_context: MonitoringEventsCreateParamsDeviceContextNetworkContext + + +class MonitoringEventsCreateParamsSession(TypedDict, total=False): + """Your session context for the event.""" + + session_id: str | None + """Your session identifier. Filterable and returned as `session_id`.""" + + sequence: int | None + """Position of the event within the session. Returned as `session_sequence`.""" + + page_url: str | None + """Validated; not stored on the event.""" + + referrer: str | None + """Validated; not stored on the event.""" + + +class MonitoringEventsCreateParamsSubject(TypedDict, total=False): + """The user or company the event belongs to. It resolves to the user (or company) with this `vendor_data` in your application; one is created when none exists.""" + + vendor_data: Required[str] + """Your id for the subject.""" + + entity_type: str + """`company` resolves the subject to a company; any other value (default `individual`) resolves it to an individual. The same `vendor_data` can name one of each.""" + + full_name: str | None + """Display name, used only when this event creates the user or company.""" + + email: str | None + """Validated as an email address; not stored on the event.""" + + phone: str | None + """Validated; not stored on the event.""" + + country: str | None + """Validated; not stored on the event.""" + + +class MonitoringEventsCreateParamsTransactionContext(TypedDict, total=False): + """Money context. Non-empty values here also satisfy the required payload keys of the action.""" + + amount: str | None + """Amount as a decimal string (a number is also accepted).""" + + currency: str | None + """Currency code.""" + + direction: str | None + """Direction of the money movement, for example `inbound` or `outbound`.""" + + payment_reference_id: str | None + """Your payment reference. Filterable as `payment_reference_id`.""" + + +class MonitoringEventsCreateParams(TypedDict, total=False): + """The event envelope, the same for every category. camelCase aliases are accepted: `eventId`, `eventCategory` (or `category`), `actionType`, `occurredAt`, `timeZone` (or `zoneId`), `deviceContext` (or `device`), `transactionContext`, `customValues` (or `props`); inside `subject`: `vendorData`, `entityType`, `fullName`; inside `session`: `sessionId`, `pageUrl`; inside `transaction_context`: `paymentReferenceId`.""" + + event_id: Required[str] + """Your identifier for the event, unique per application. It is the idempotency key: resending the same `event_category`, `action_type`, `occurred_at`, `subject.vendor_data` and `payload` under it is a replay, anything different is a conflict.""" + + event_category: Required[ + Literal[ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom", + ] + | str + ] + """Event category.""" + + action_type: Required[str] + """What happened within the category. Open vocabulary; the managed actions listed in [Event ingestion](https://docs.didit.me/transaction-monitoring/events) (for example `login`, `deposit_completed`, `chargeback_opened`) unlock typed validation and managed rule fields. Leading and trailing whitespace is trimmed.""" + + occurred_at: Required[str | _dt.datetime] + """When the event happened on your side, ISO 8601 (a value without an offset is read as UTC). Must be no more than 7 days in the past and no more than 5 minutes in the future.""" + + time_zone: str | None + """Time zone of the occurrence on your side, for example `Europe/Madrid`. Stored with the event; not returned by the read endpoints.""" + + subject: Required[MonitoringEventsCreateParamsSubject] + """The user or company the event belongs to. It resolves to the user (or company) with this `vendor_data` in your application; one is created when none exists.""" + + business: MonitoringEventsCreateParamsBusiness + """The business the event belongs to, when it differs from the subject. Only `vendor_data` is read.""" + + session: MonitoringEventsCreateParamsSession + """Your session context for the event.""" + + device_context: MonitoringEventsCreateParamsDeviceContext + """Device and network context, stored as sent and returned by `GET /v3/events/{event_uuid}/`. On this server lane Didit does not see the end user's connection, so the IP enriched and used by rules is `network_context.ip_address` as you send it.""" + + transaction_context: MonitoringEventsCreateParamsTransactionContext + """Money context. Non-empty values here also satisfy the required payload keys of the action.""" + + payload: Mapping[str, object] + """The category's domain fields, at most 16384 bytes once JSON-encoded. Some actions require keys (in `payload` or `transaction_context`): `gameplay.p2p_settlement` needs `game_id`, `counterparty_vendor_data`, `stake_amount`, `net_result_amount`, `role` and one of `hand_id`, `round_id`, `match_id`; `gameplay.bet_placed` and `gameplay.bet_settled` need `game_id`; `payment.deposit_initiated`, `payment.deposit_completed`, `payment.withdrawal_requested` and `payment.withdrawal_completed` need `payment_reference_id`, `amount`, `currency`, `payment_method`, `status`; `payment.withdrawal_address_changed` needs `withdrawal_address_hash`; `dispute.chargeback_opened`, `dispute.chargeback_won` and `dispute.chargeback_lost` need `payment_reference_id` and one of `dispute_id`, `chargeback_id`; `session.bot_signal_observed` needs `telemetry_schema_version` and one of `bot_score`, `actions_per_minute`, `timing_variance_ms`, `pointer_entropy`, `keystroke_entropy`, `reaction_time_ms_p50`; `responsible_gambling.self_exclusion_match` needs `registry_source`, `exclusion_status`, `identity_match_ref`. Every `security` action ending in `_changed` needs `changed_field`, plus `previous_reference_hash` or `new_reference_hash` when the field is `password`, `mfa_method`, `mfa`, `email`, `phone`, `withdrawal_address`, `payout_method` or `api_key`. Send hashes and references, never a card number, a full withdrawal address, a password hash or an MFA secret.""" + + custom_values: Mapping[str, str | float | bool | None] + """Up to 100 keys of your own, addressable from rules as `custom_values.`. Values must be scalars (string, number, boolean or null); nested objects and arrays are rejected.""" diff --git a/src/didit/types/monitoring_events/create_sdk_token_params.py b/src/didit/types/monitoring_events/create_sdk_token_params.py new file mode 100644 index 0000000..09d3c57 --- /dev/null +++ b/src/didit/types/monitoring_events/create_sdk_token_params.py @@ -0,0 +1,44 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import Required, TypedDict + +__all__ = ["MonitoringEventsCreateSdkTokenParams"] + + +class MonitoringEventsCreateSdkTokenParams(TypedDict, total=False): + """camelCase aliases are accepted: `vendorData`, `entityType`, `allowedEventCategories`, `ttlSeconds`, `maxUses`, `bindToOrigin`.""" + + vendor_data: Required[str] + """Your id for the subject every event submitted with this credential belongs to.""" + + entity_type: str + """`company` binds a company subject; any other value (default `individual`) binds an individual.""" + + allowed_event_categories: Sequence[ + Literal[ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom", + ] + | str + ] + """Categories the credential may submit. Omit it or send `[]` to allow every category.""" + + ttl_seconds: int + """Lifetime of the credential in seconds.""" + + max_uses: int | None + """Number of events the credential may submit (a batch spends one use per event). Send `null` for no cap within the lifetime.""" + + bind_to_origin: str | None + """When set, the browser `Origin` header of every device request must equal this value exactly; a request without `Origin` is refused.""" diff --git a/src/didit/types/monitoring_events/create_sdk_token_response.py b/src/didit/types/monitoring_events/create_sdk_token_response.py new file mode 100644 index 0000000..f639f37 --- /dev/null +++ b/src/didit/types/monitoring_events/create_sdk_token_response.py @@ -0,0 +1,40 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["MonitoringEventsCreateSdkTokenResponse"] + + +class MonitoringEventsCreateSdkTokenResponse(BaseModel): + monitoring_token: str + """The device credential (a 43-character URL-safe token). Send it in the `X-Monitoring-Token` header.""" + + expires_at: _dt.datetime + """When the credential stops being accepted.""" + + allowed_event_categories: list[ + Literal[ + "auth", + "session", + "gameplay", + "bonus", + "affiliate", + "payment", + "dispute", + "security", + "responsible_gambling", + "custom", + ] + | str + ] + """Categories the credential may submit; `[]` means every category.""" + + max_uses: int | None + """Event budget of the credential; null means no cap.""" + + bind_to_origin: str | None + """Origin the credential is bound to, or null.""" diff --git a/src/didit/types/monitoring_events/retrieve_enrichment_metrics_response.py b/src/didit/types/monitoring_events/retrieve_enrichment_metrics_response.py new file mode 100644 index 0000000..979c8b4 --- /dev/null +++ b/src/didit/types/monitoring_events/retrieve_enrichment_metrics_response.py @@ -0,0 +1,41 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["MonitoringEventsRetrieveEnrichmentMetricsResponse"] + + +class MonitoringEventsRetrieveEnrichmentMetricsResponse(BaseModel): + events_ingested: int + """New events stored. Idempotent replays are not counted.""" + + events_rejected: int + """Items rejected inside batch requests (server and device lanes). A rejected single-event request is not counted.""" + + enrichment_lookups: int + """Events whose public IP address was resolved, from the cache or freshly.""" + + enrichment_cache_hits: int + """Lookups answered from the cache.""" + + enrichment_provider_calls: int + """Lookups that needed a fresh resolution of the address.""" + + enrichment_skipped_low_value: int + """Low-value telemetry events whose address was not already known. They never trigger a fresh resolution of their own.""" + + unique_ips_seen: int + """Addresses that needed a fresh resolution.""" + + period_days: int + """The `days` value applied after clamping.""" + + cache_hit_rate: float | None + """`enrichment_cache_hits / enrichment_lookups`, rounded to 4 decimals. Null when there were no lookups.""" + + unique_ip_ratio: float | None + """`unique_ips_seen / events_ingested`, rounded to 4 decimals. Null when no events were ingested.""" + + provider_calls_per_1k_events: float | None + """Fresh resolutions per 1000 ingested events, rounded to 2 decimals. Null when no events were ingested.""" diff --git a/src/didit/types/networks/add_tag_params.py b/src/didit/types/networks/add_tag_params.py new file mode 100644 index 0000000..763d410 --- /dev/null +++ b/src/didit/types/networks/add_tag_params.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["NetworksAddTagParams"] + + +class NetworksAddTagParams(TypedDict, total=False): + tag_uuid: Required[str] + """UUID of one of your application's tags.""" diff --git a/src/didit/types/networks/list_response.py b/src/didit/types/networks/list_response.py index 1e5148c..8e5ce05 100644 --- a/src/didit/types/networks/list_response.py +++ b/src/didit/types/networks/list_response.py @@ -1,14 +1,17 @@ # Code generated by codegen from openapi/*.json. DO NOT EDIT. from __future__ import annotations +from typing import Literal + from didit._models import BaseModel +from didit.types.components.network_list_counts import NetworkListCounts from didit.types.components.network_list_item import NetworkListItem __all__ = ["NetworksListResponse", "NetworksListResponseKpis"] class NetworksListResponseKpis(BaseModel): - """Each key is {value, delta} over the results matching the current filters.""" + """Each key is {value, delta} over the results matching the current filters. Omitted when `summary=false`.""" active: dict[str, object] | None = None @@ -22,14 +25,26 @@ class NetworksListResponseKpis(BaseModel): class NetworksListResponse(BaseModel): - count: int | None = None + count: int - next: int | None = None + next: int | None """Offset of the next page, or null on the last page.""" - previous: int | None = None + previous: int | None - results: list[NetworkListItem] | None = None + results: list[NetworkListItem] kpis: NetworksListResponseKpis | None = None - """Each key is {value, delta} over the results matching the current filters.""" + """Each key is {value, delta} over the results matching the current filters. Omitted when `summary=false`.""" + + counts: NetworkListCounts | None = None + """Network counts for the list's views and filter options, each network counted once. Returned only when `summary` is not `false`. `scopes` count every network visible to the application. `views` count the visible networks inside the selected `scope`. `facets` count the networks in the selected `scope` and `status` view (dismissed networks are left out unless `status` includes them). The other list filters do not change these counts.""" + + sharing_mode: Literal["off", "private", "global"] | str + """The organization's cross-organization sharing mode. Only `global` compares networks across organizations; `private` and `off` keep them to the organization's own history.""" + + cross_org_available: bool + """Whether cross-organization signals apply to this organization. They need both the Identity Enterprise plan and the `global` sharing mode.""" + + cross_org_unavailable_reason: Literal["plan", "sharing_mode"] | str | None + """Why cross-organization signals do not apply, checked in this order. `plan`: the organization's Identity plan does not include the cross-organization fraud network (it needs Identity Enterprise), whatever the sharing mode. `sharing_mode`: the plan includes it, but the sharing mode is `private` or `off`. Null when `cross_org_available` is `true`.""" diff --git a/src/didit/types/networks/remove_tag_params.py b/src/didit/types/networks/remove_tag_params.py new file mode 100644 index 0000000..5cd02ae --- /dev/null +++ b/src/didit/types/networks/remove_tag_params.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["NetworksRemoveTagParams"] + + +class NetworksRemoveTagParams(TypedDict, total=False): + tag_uuid: Required[str] + """UUID of one of your application's tags.""" diff --git a/src/didit/types/sessions/add_images_params.py b/src/didit/types/sessions/add_images_params.py new file mode 100644 index 0000000..f4f0ef1 --- /dev/null +++ b/src/didit/types/sessions/add_images_params.py @@ -0,0 +1,21 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +from didit._types import FileTypes + +__all__ = ["SessionsAddImagesParams"] + + +class SessionsAddImagesParams(TypedDict, total=False): + file: Required[FileTypes] + """The image or PDF. Allowed extensions: `jpg`, `jpeg`, `png`, `webp`, `tiff`, `pdf`. At most 15 MB as uploaded; the file is then compressed and rejected with `400` if it is still larger than 2 MB (2,097,152 bytes). Empty files are rejected.""" + + model_type: Required[Literal["kyc", "poa"] | str] + """`kyc` adds the file to the ID verification result, `poa` to the Proof of Address result.""" + + file_password: str + """Password of an encrypted PDF. Without it, or with a wrong one, an encrypted PDF returns `400`.""" diff --git a/src/didit/types/sessions/add_images_response.py b/src/didit/types/sessions/add_images_response.py new file mode 100644 index 0000000..6d2d451 --- /dev/null +++ b/src/didit/types/sessions/add_images_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["SessionsAddImagesResponse"] + + +class SessionsAddImagesResponse(BaseModel): + extra_files: list[str | None] + """Short-lived presigned links to all extra files of the result named by `model_type`, oldest first. Each link is `null` for a scoped API key without `media: read`. Read fresh links from the session decision later.""" diff --git a/src/didit/types/sessions/batch_delete_params.py b/src/didit/types/sessions/batch_delete_params.py index c242502..9420aca 100644 --- a/src/didit/types/sessions/batch_delete_params.py +++ b/src/didit/types/sessions/batch_delete_params.py @@ -14,7 +14,7 @@ class SessionsBatchDeleteParams(TypedDict, total=False): """Numeric `session_number` values of the KYC sessions to delete, as digit-only strings (JSON numbers are also accepted and coerced). Required unless `delete_all` is `true`; an empty list returns `400`. Any non-digit entry rejects the whole request with `400` (unless `delete_all` is `true`, in which case validation is skipped and the entries are ignored). Numbers that do not match a live session in your application are silently skipped.""" delete_all: bool - """When `true`, every remaining KYC session in the calling application is deleted and `session_numbers` is ignored. More than 20 matching sessions run as a deletion job (`202`) that covers only the sessions created before the request. Defaults to `false`. Irreversible; there is no confirmation step.""" + """When `true`, every remaining KYC session in the calling application is deleted and `session_numbers` is ignored, unless you narrow the selection with `filter_*` keys. More than 20 matching sessions run as a deletion job (`202`) that covers only the sessions created before the request. Defaults to `false`. Irreversible; there is no confirmation step.""" retain_face_embeddings: bool | None """Override the application's `face_retention_policy` for this deletion. `true` keeps one image-free face biometric template anchored to the session's User after the session is deleted; `false` deletes the face embedding with the session; omit or `null` to follow the application policy. Ignored for KYB sessions and for sessions without a face embedding.""" @@ -30,3 +30,60 @@ class SessionsBatchDeleteParams(TypedDict, total=False): instruction_id: str """Your durable reference for this deletion instruction (for example an erasure-ticket id). Recorded on the audit trail and on any retained template. Generated by Didit when omitted.""" + + filter_status: str | Sequence[str] + """Narrows a `delete_all` to the sessions that match the `status` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_vendor_data: str + """Narrows a `delete_all` to the sessions that match the `vendor_data` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated text, so several values are not matched as alternatives. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_search: str + """Narrows a `delete_all` to the sessions that match the `search` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated text, so several values are not matched as alternatives. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_workflow_id: str | Sequence[str] + """Narrows a `delete_all` to the sessions that match the `workflow_id` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_date_from: str | _dt.date + """Narrows a `delete_all` to the sessions that match the `date_from` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_date_to: str | _dt.date + """Narrows a `delete_all` to the sessions that match the `date_to` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_country: str | Sequence[str] + """Narrows a `delete_all` to the sessions that match the `country` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_document_type: str | Sequence[str] + """Narrows a `delete_all` to the sessions that match the `document_type` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_document_name: str | Sequence[str] + """Narrows a `delete_all` to the sessions that match the `document_name` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_region: str | Sequence[str] + """Narrows a `delete_all` to the sessions that match the `region` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_didit_internal_id: str + """Narrows a `delete_all` to the sessions that match the `didit_internal_id` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_session_type: str | Sequence[str] + """Narrows a `delete_all` to the sessions that match the `session_type` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_api_service: str + """Narrows a `delete_all` to the sessions that match the `api_service` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_screened_full_name: str + """Narrows a `delete_all` to the sessions that match the `screened_full_name` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated text, so several values are not matched as alternatives. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_warning: str | Sequence[str] + """Narrows a `delete_all` to the sessions that match the `warning` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_status_initialized_as_in_review: bool + """Narrows a `delete_all` to the sessions that match the `status_initialized_as_in_review` filter of [List Sessions](/sessions-api/list-sessions), with the same values. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_is_kyc_to_be_reviewed: bool + """Narrows a `delete_all` to the sessions that match the `is_kyc_to_be_reviewed` filter of [List Sessions](/sessions-api/list-sessions), with the same values. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_euuid: str + """Narrows a `delete_all` to the sessions that match the `euuid` filter of [List Sessions](/sessions-api/list-sessions), with the same value. One value, not a list: an array is joined into one comma-separated value, so several values are refused with `400`. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" + + filter_organization_name: str | Sequence[str] + """Narrows a `delete_all` to the sessions that match the `organization_name` filter of [List Sessions](/sessions-api/list-sessions), with the same values. A comma-separated string or an array. With an API key it can only match your own organization's name, so it selects all of your sessions or none. Ignored without `delete_all`. An empty value is ignored and does not narrow the selection.""" diff --git a/src/didit/types/sessions/batch_delete_response.py b/src/didit/types/sessions/batch_delete_response.py index 54648bc..ef7c455 100644 --- a/src/didit/types/sessions/batch_delete_response.py +++ b/src/didit/types/sessions/batch_delete_response.py @@ -6,12 +6,7 @@ from didit._models import BaseModel -__all__ = [ - "SessionsBatchDeleteResponse", - "SessionsBatchDeleteResponse202", - "SessionsBatchDeleteResponse202Outcomes", - "SessionsBatchDeleteResponseResults", -] +__all__ = ["SessionsBatchDeleteResponse", "SessionsBatchDeleteResponseResults"] class SessionsBatchDeleteResponseResults(BaseModel): @@ -42,56 +37,3 @@ class SessionsBatchDeleteResponseResults(BaseModel): class SessionsBatchDeleteResponse(BaseModel): results: list[SessionsBatchDeleteResponseResults] | None = None - - -class SessionsBatchDeleteResponse202Outcomes(BaseModel): - """How many sessions ended in each outcome. Every key is always present.""" - - deleted: int | None = None - """Sessions deleted together with their face embedding.""" - - none: int | None = None - """Sessions deleted that had no face embedding.""" - - retained_with_user: int | None = None - """Sessions deleted while one image-free biometric template was retained for their User, as the request or the application policy asked.""" - - ineligible_no_vendor_user: int | None = None - """Sessions deleted, with their face embedding, because retention was requested but they had no linked User.""" - - failed_retryable: int | None = None - """Sessions reported as not deleted. A job retries a group it could not delete by itself instead of counting it here.""" - - already_deleted: int | None = None - """Selected sessions that were already deleted, for example by another request, when the job reached them.""" - - -class SessionsBatchDeleteResponse202(BaseModel): - """A bulk session deletion running in the background: progress and counts per outcome, never which sessions it deleted.""" - - job_id: str | None = None - """Poll `GET /v3/sessions/delete/{job_id}/` with this id.""" - - status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None - """`PENDING`: accepted, not started yet. `PROCESSING`: deleting; `processed` and `outcomes` are updating. `COMPLETED`: every selected session is accounted for. `FAILED`: the job stopped early; see `error`.""" - - total: int | None = None - """Sessions selected when the deletion was requested.""" - - processed: int | None = None - """Sessions the job has finished with so far.""" - - outcomes: SessionsBatchDeleteResponse202Outcomes | None = None - """How many sessions ended in each outcome. Every key is always present.""" - - deleted_child_verifications: int | None = None - """Verifications that the deleted business verifications created, deleted with them. Always `0` for a job started by `POST /v3/sessions/delete/`, which deletes KYC sessions only.""" - - error: str | None = None - """`null` unless `status` is `FAILED`; then why the deletion stopped. The sessions the job had not reached are still in place: repeat the request to delete them.""" - - created_at: _dt.datetime | None = None - """When the deletion was requested.""" - - completed_at: _dt.datetime | None = None - """When the job finished; `null` while it runs.""" diff --git a/src/didit/types/sessions/bulk_update_aml_hit_status_response.py b/src/didit/types/sessions/bulk_update_aml_hit_status_response.py new file mode 100644 index 0000000..6ffd6e5 --- /dev/null +++ b/src/didit/types/sessions/bulk_update_aml_hit_status_response.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["SessionsBulkUpdateAmlHitStatusResponse"] + + +class SessionsBulkUpdateAmlHitStatusResponse(BaseModel): + message: str + """`Updated {n} hit(s) successfully`, where `n` is the number of entries in `hit_updates`.""" + + updated_hits: list[str] + """The `hit_id` of every entry in `hit_updates`, in request order, including hits whose status did not change.""" diff --git a/src/didit/types/sessions/create_params.py b/src/didit/types/sessions/create_params.py index f2f3d97..e9b3fe6 100644 --- a/src/didit/types/sessions/create_params.py +++ b/src/didit/types/sessions/create_params.py @@ -2,14 +2,17 @@ from __future__ import annotations import datetime as _dt -from typing import Literal, Sequence +from typing import Literal, Mapping, Sequence from typing_extensions import Required, TypedDict +from didit.types.components.session_sign_document import SessionSignDocument + __all__ = [ "SessionsCreateParams", "SessionsCreateParamsContactDetails", "SessionsCreateParamsExpectedDetails", + "SessionsCreateParamsSign", ] @@ -136,6 +139,15 @@ class SessionsCreateParamsExpectedDetails(TypedDict, total=False): """Expected registration number of the business in the company registry. For example, `00445790`. Pre-fills the registration-number field of the registry search in Business Verification (KYB) workflows; when `company_name` is also provided, the registry lookup combines both identifiers. Ignored for KYC workflows.""" +class SessionsCreateParamsSign(TypedDict, total=False): + """Electronic Signature input for this session. `documents` are the documents to sign, for a workflow whose Electronic Signature step takes documents from the session (`signature_documents_source` `session` or `both`): up to 20 documents and 50 MB in total. `field_values` fills the fields whose `prefill` is `api.`, on workflow and session documents alike. Creating a session again for the same `workflow_id` and `vendor_data` returns the pending session; if that call sends `sign`, its documents, fields and values must be identical to the first ones, and anything different returns `400` with `{"sign": "signature_document_changed"}`. See [Electronic signature documents](/sessions-api/create-session#electronic-signature-documents).""" + + documents: Sequence[SessionSignDocument] + + field_values: Mapping[str, str | bool] + """Values keyed by the `` of each `api.` pre-fill source: strings of 1 to 500 characters for `text` and `data` fields, booleans for a `checkbox`. `400` with `field_value_unknown` for a key no field uses, `field_value_missing` for a required field without its value, and `field_value_invalid` for a value that fails its field's validation.""" + + class SessionsCreateParams(TypedDict, total=False): workflow_id: Required[str] """Stable identifier of the workflow that defines which verification steps the session will run. Workflows are created and managed in the [Workflows](/console/workflows) page of the Console. The `workflow_id` also implicitly selects whether the session is KYC or KYB.""" @@ -144,7 +156,7 @@ class SessionsCreateParams(TypedDict, total=False): """A unique identifier for the user being verified, such as a UUID, email, or internal user ID. This field is used for: (1) **User grouping** - sessions with the same vendor_data are linked to the same user profile in the Users tab. (2) **Cross-session duplicate detection** - when checking for duplicated faces, documents, phone numbers, emails, IP addresses, or device fingerprints, sessions with the same vendor_data are treated as the same user and excluded from matches. Without vendor_data, every session is treated as a unique user and all potential duplicates are surfaced. We strongly recommend always providing a vendor_data to reduce noise in duplicate detection.""" callback: str - """URL to redirect the user to after verification completes. Didit automatically appends `verificationSessionId` and `status` (Approved, Declined, In Review) as query parameters. Custom URL schemes (e.g. `myapp://`) are supported for mobile callbacks. If omitted, the workflow's default `callback_url` is used.""" + """URL to redirect the user to after verification completes. Didit automatically appends `verificationSessionId` and `status` (Approved, Declined, In Review) as query parameters. Custom URL schemes (e.g. `myapp://`) are supported for mobile callbacks. If omitted, the workflow's default `callback_url` is used. When the application has allowed domains (`allowed_domains` on `PATCH /v3/webhook/`), an `http` or `https` callback must be on one of those origins (same scheme, host and port), otherwise the request fails with 400 on `callback`: `callback must be on one of the application's allowed domains: https://app.example.com`. Custom URL schemes are always accepted.""" callback_method: Literal["initiator", "completer", "both"] | str """Determines which device should handle the redirect to the provided callback URL. Use `initiator` to redirect only the device that started the flow, `completer` for the device that finishes it, or `both` to allow either device to trigger the callback. If you ever notice the callback not triggering reliably, we recommend setting this value to `both`.""" @@ -224,3 +236,6 @@ class SessionsCreateParams(TypedDict, total=False): sandbox_scenario: str """Sandbox-only scenario slug. Only accepted when the API key belongs to a sandbox-mode application; rejected on live applications. When set, the create flow expands the scenario's bundled magic values onto the session's input fields (contact_details, expected_details) so the mocked providers reproduce the scenario's outcome through the real workflow pipeline. See the scenario catalog at GET /v1/sandbox/scenarios/ or [Sandbox & Test Data](/integration/sandbox-testing). You can also arm (or re-arm) a scenario after create via POST /v3/session/{sessionId}/sandbox/arm/, as long as the session is still `Not Started`. For non-doc lookup, use `lookup_partial_match`, `lookup_no_match`, or `lookup_provider_error` to test the configured attempt budget and fallback. For identity wallets, use `wallet_cancelled`, `wallet_timeout`, or `wallet_provider_error`; `approve` simulates a verified wallet. All catalog wallets can be tested in sandbox, including schemes unavailable on live applications. Simulated attempts are not charged.""" + + sign: SessionsCreateParamsSign + """Electronic Signature input for this session. `documents` are the documents to sign, for a workflow whose Electronic Signature step takes documents from the session (`signature_documents_source` `session` or `both`): up to 20 documents and 50 MB in total. `field_values` fills the fields whose `prefill` is `api.`, on workflow and session documents alike. Creating a session again for the same `workflow_id` and `vendor_data` returns the pending session; if that call sends `sign`, its documents, fields and values must be identical to the first ones, and anything different returns `400` with `{"sign": "signature_document_changed"}`. See [Electronic signature documents](/sessions-api/create-session#electronic-signature-documents).""" diff --git a/src/didit/types/sessions/create_response.py b/src/didit/types/sessions/create_response.py index e2d24dd..b958fe3 100644 --- a/src/didit/types/sessions/create_response.py +++ b/src/didit/types/sessions/create_response.py @@ -45,7 +45,7 @@ class SessionsCreateResponse(BaseModel): """Current status of the session. Newly created sessions return `Not Started`. If a non-finished session already existed for the same `vendor_data` on the workflow's latest published version, the returned status reflects that existing session.""" callback: str | None - """Final redirect URL the user is sent to after completing the flow. Didit appends `?verificationSessionId={session_id}&status={status}` to this URL. Falls back to the workflow's configured callback URL when not provided in the request; `null` when neither is set.""" + """Final redirect URL the user is sent to after completing the flow. Didit appends `?verificationSessionId={session_id}&status={status}` to this URL. Falls back to the workflow's configured callback URL when not provided in the request; `null` when neither is set. When the application has allowed domains and this URL is a web URL outside them (for example a workflow default set before the list changed), the hosted flow does not redirect the user to it.""" workflow_id: str """Stable identifier of the workflow this session runs on. Always the workflow's stable group identifier - even if you referenced a specific workflow version UUID in the request (backward-compatible lookup), the response carries the stable `workflow_id`. Use it to correlate the session with the workflow you configured in the Console.""" diff --git a/src/didit/types/sessions/list_aml_screening_history_response.py b/src/didit/types/sessions/list_aml_screening_history_response.py new file mode 100644 index 0000000..f444f0b --- /dev/null +++ b/src/didit/types/sessions/list_aml_screening_history_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.aml_screening_run import AMLScreeningRun + +__all__ = ["SessionsListAmlScreeningHistoryResponse"] + + +class SessionsListAmlScreeningHistoryResponse(BaseModel): + count: int + """Number of runs, exact up to 100 and capped at 100 above that.""" + + next: str | None + """URL of the next page, or `null` on the last page.""" + + previous: str | None + """URL of the previous page, or `null` on the first page.""" + + results: list[AMLScreeningRun] diff --git a/src/didit/types/sessions/list_events_response.py b/src/didit/types/sessions/list_events_response.py new file mode 100644 index 0000000..1742625 --- /dev/null +++ b/src/didit/types/sessions/list_events_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.session_event import SessionEvent + +__all__ = ["SessionsListEventsResponse"] + + +class SessionsListEventsResponse(BaseModel): + count: int + """Total number of events of the session.""" + + next: str | None + """URL of the next page, or `null` on the last page.""" + + previous: str | None + """URL of the previous page, or `null` on the first page.""" + + results: list[SessionEvent] diff --git a/src/didit/types/sessions/list_feature_overrides_response.py b/src/didit/types/sessions/list_feature_overrides_response.py new file mode 100644 index 0000000..eb467d5 --- /dev/null +++ b/src/didit/types/sessions/list_feature_overrides_response.py @@ -0,0 +1,22 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.feature_status_override import FeatureStatusOverride + +__all__ = ["SessionsListFeatureOverridesResponse"] + + +class SessionsListFeatureOverridesResponse(BaseModel): + session_id: str + + person_in_flow: bool + """Whether the person is still in the flow, so a new override with `defer_until_flow_finishes: true` would wait.""" + + flow_finished_at: _dt.datetime | None + """When the person last finished the flow. `null` while they are still in it (a resubmission included) and when the session closed before they finished (abandoned, expired or decided mid-flow).""" + + overrides: list[FeatureStatusOverride] + """Every override of the session, newest first.""" diff --git a/src/didit/types/sessions/list_networks_response.py b/src/didit/types/sessions/list_networks_response.py deleted file mode 100644 index fe4de94..0000000 --- a/src/didit/types/sessions/list_networks_response.py +++ /dev/null @@ -1,11 +0,0 @@ -# Code generated by codegen from openapi/*.json. DO NOT EDIT. -from __future__ import annotations - -from didit._models import BaseModel -from didit.types.components.entity_network_membership import EntityNetworkMembership - -__all__ = ["SessionsListNetworksResponse"] - - -class SessionsListNetworksResponse(BaseModel): - results: list[EntityNetworkMembership] | None = None diff --git a/src/didit/types/sessions/list_response.py b/src/didit/types/sessions/list_response.py index 59cb35b..7008405 100644 --- a/src/didit/types/sessions/list_response.py +++ b/src/didit/types/sessions/list_response.py @@ -1,348 +1,11 @@ # Code generated by codegen from openapi/*.json. DO NOT EDIT. from __future__ import annotations -import datetime as _dt -from typing import Literal - from didit._models import BaseModel +from didit.types.components.business_session_list_item import BusinessSessionListItem +from didit.types.components.user_session_list_item import UserSessionListItem -__all__ = [ - "SessionsListResponse", - "SessionsListResponseResultsKybSessionRowSessionKindBusiness", - "SessionsListResponseResultsKycSessionRowSessionKindUser", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetails", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAml", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAmlScreenedData", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsDatabaseValidation", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsEmailVerification", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsFaceMatch", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsLiveness", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPhoneVerification", - "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPoa", - "SessionsListResponseResultsKycSessionRowSessionKindUserEmailAddress", - "SessionsListResponseResultsKycSessionRowSessionKindUserFeatures", - "SessionsListResponseResultsKycSessionRowSessionKindUserPhoneNumber", -] - - -class SessionsListResponseResultsKybSessionRowSessionKindBusiness(BaseModel): - """Summary row for a Business Verification (KYB) session. Returned only when `session_kind` is `business` or `all`.""" - - session_id: str - """Unique identifier of the business verification session.""" - - session_kind: Literal["business"] | str - """Row discriminator. Always `"business"` for KYB rows.""" - - session_number: int - """Human-friendly sequential number of the business session within your application. Numbered independently from KYC sessions.""" - - status: ( - Literal[ - "Not Started", - "In Progress", - "Approved", - "Declined", - "In Review", - "Expired", - "Abandoned", - "Kyc Expired", - "Resubmitted", - "Awaiting User", - ] - | str - ) - """Overall session status. See the [Verification Statuses](/integration/verification-statuses) page.""" - - vendor_data: str | None - """Your own identifier for the business, echoed back exactly as sent at session creation.""" - - created_at: _dt.datetime - """UTC timestamp when the session was created.""" - - workflow_id: str | None - """Identifier of the workflow this session was created with.""" - - workflow_type: str | None - """Workflow type. `kyb` for business workflows.""" - - workflow_label: str | None - """Display name of the workflow.""" - - company_name: str | None - """Name of the company being verified, from the registry company linked to the session. `null` until a company is selected.""" - - registration_number: str | None - """Company registration number from the registry company linked to the session.""" - - country: str | None - """Country code of the registry company linked to the session.""" - - -class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAmlScreenedData( - BaseModel -): - """Subset of the data that was screened. Only non-empty keys are included.""" - - nationality: str | None = None - - document_number: str | None = None - - date_of_birth: str | None = None - - -class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAml(BaseModel): - """Present when AML screening ran.""" - - total_hits: int | None = None - """Number of AML screening hits.""" - - entity_type: str | None = None - """Screened entity type.""" - - is_adverse_media_fetched: bool | None = None - - screened_data: ( - SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAmlScreenedData - | None - ) = None - """Subset of the data that was screened. Only non-empty keys are included.""" - - -class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsDatabaseValidation( - BaseModel -): - """Present when database validation ran.""" - - validation_type: str | None = None - - match_type: str | None = None - - identification_number: str | None = None - """Tax, personal, or document number that was validated (first available).""" - - -class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsEmailVerification( - BaseModel -): - """Present when email verification ran.""" - - is_breached: bool | None = None - - is_disposable: bool | None = None - - is_undeliverable: bool | None = None - - is_blocklisted: bool | None = None - - -class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsFaceMatch(BaseModel): - """Present when face match ran.""" - - score: float | None = None - """Face-match similarity score, rounded to 2 decimals.""" - - source_image: str | None = None - """Expiring (presigned) URL of the source face image.""" - - target_image: str | None = None - """Expiring (presigned) URL of the target face image.""" - - -class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsLiveness(BaseModel): - """Present when a liveness check ran.""" - - method: str | None = None - - score: float | None = None - """Liveness confidence score, rounded to 2 decimals.""" - - -class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPhoneVerification( - BaseModel -): - """Present when phone verification ran.""" - - country_code: str | None = None - """ISO 3166-1 alpha-2 country of the phone number.""" - - is_temporary: bool | None = None - - is_ported: bool | None = None - - is_blocked: bool | None = None - - -class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPoa(BaseModel): - """Present when proof of address ran.""" - - document_type: str | None = None - - issuer: str | None = None - - address: str | None = None - - name_on_document: str | None = None - - -class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetails(BaseModel): - """Compact per-feature extras. Only features that produced data appear as keys; `null` when no feature produced extras. Fields excluded by the workflow's response-attributes configuration are omitted.""" - - aml: SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAml | None = None - """Present when AML screening ran.""" - - database_validation: ( - SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsDatabaseValidation - | None - ) = None - """Present when database validation ran.""" - - poa: SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPoa | None = None - """Present when proof of address ran.""" - - liveness: ( - SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsLiveness | None - ) = None - """Present when a liveness check ran.""" - - face_match: ( - SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsFaceMatch | None - ) = None - """Present when face match ran.""" - - phone_verification: ( - SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPhoneVerification - | None - ) = None - """Present when phone verification ran.""" - - email_verification: ( - SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsEmailVerification - | None - ) = None - """Present when email verification ran.""" - - -class SessionsListResponseResultsKycSessionRowSessionKindUserEmailAddress(BaseModel): - """Email associated with the session: the verified email when email verification ran, otherwise the contact email provided at session creation. `null` when neither exists.""" - - email: str - - is_verified: bool - """`true` only when the email-verification feature approved this address.""" - - -class SessionsListResponseResultsKycSessionRowSessionKindUserFeatures(BaseModel): - feature: ( - Literal[ - "ID_VERIFICATION", - "NFC", - "LIVENESS", - "FACE_MATCH", - "POA", - "QUESTIONNAIRE", - "EMAIL_VERIFICATION", - "PHONE", - "AML", - "IP_ANALYSIS", - "AGE_ESTIMATION", - "DATABASE_VALIDATION", - ] - | str - ) - """Verification feature identifier.""" - - status: Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str - """Status of this feature. `Not Finished` until the step completes.""" - - -class SessionsListResponseResultsKycSessionRowSessionKindUserPhoneNumber(BaseModel): - """Phone associated with the session: the verified phone when phone verification ran, otherwise the contact phone provided at session creation. `null` when neither exists.""" - - number: str - """Phone number in E.164 format.""" - - is_verified: bool - """`true` only when the phone-verification feature approved this number.""" - - -class SessionsListResponseResultsKycSessionRowSessionKindUser(BaseModel): - """Summary row for a User Verification (KYC) session. Identity fields (`full_name`, `country`, `document_type`, `portrait_image`, contact fields) are populated progressively as the user completes verification steps, and are `null` until the relevant step finishes. Fields restricted by the workflow's response-attributes configuration are also returned as `null`.""" - - session_id: str - """Unique identifier of the verification session. Use it with `GET /v3/session/{sessionId}/decision/` to fetch the full decision payload.""" - - session_kind: Literal["user"] | str - """Row discriminator. Always `"user"` for KYC rows; Business Verification (KYB) rows have `"business"` and a different shape.""" - - session_number: int - """Human-friendly sequential number of the session within your application. Also matched by the `search` parameter.""" - - session_url: str | None - """Hosted verification URL the end user opens to complete the flow. `null` when the session has no hosted URL.""" - - portrait_image: str | None - """Expiring (presigned) URL of the best available portrait: the NFC chip photo when available, otherwise the ID document portrait, otherwise the liveness reference image. `null` until one of those exists. Download promptly; the link expires.""" - - document_type: ( - Literal[ - "Passport", - "Identity Card", - "Driver's License", - "Residence Permit", - "Health Insurance Card", - "Tax Card", - ] - | str - | None - ) - """Human-readable type of the verified ID document. `null` before document verification completes, and also `null` after completion when the verified code has no display mapping (`SSC`, `WP`, `VISA`, `PSC`, `BC`, `OTHER`). Note: the `document_type` query parameter filters by short codes (`P`, `ID`, `DL`, `RP`, ...), not by these display values.""" - - full_name: str | None - """Best available full name for the session, resolved in order: ID document OCR, AML screened data, database-validation screened data, proof-of-address document, then the expected details provided at session creation. `null` when none is available yet.""" - - country: str | None - """ISO 3166-1 alpha-3 country associated with the session, resolved in order: ID document issuing state, database-validation issuing state, proof-of-address issuing state, expected details, then the verified phone's country. May differ from nationality.""" - - status: ( - Literal[ - "Not Started", - "In Progress", - "Approved", - "Declined", - "In Review", - "Expired", - "Abandoned", - "Kyc Expired", - "Resubmitted", - "Awaiting User", - ] - | str - ) - """Overall session status. See the [Verification Statuses](/integration/verification-statuses) page for the lifecycle.""" - - vendor_data: str | None - """Your own identifier for the end user, echoed back exactly as sent at session creation.""" - - didit_internal_id: str | None - """Didit's internal identifier for the verified user profile behind this session. Stable across multiple sessions of the same user; filterable via the `didit_internal_id` query parameter.""" - - created_at: _dt.datetime - """UTC timestamp when the session was created.""" - - features: list[SessionsListResponseResultsKycSessionRowSessionKindUserFeatures] - """Per-feature status for every verification step configured in the session's workflow, in workflow order.""" - - phone_number: SessionsListResponseResultsKycSessionRowSessionKindUserPhoneNumber | None - """Phone associated with the session: the verified phone when phone verification ran, otherwise the contact phone provided at session creation. `null` when neither exists.""" - - email_address: SessionsListResponseResultsKycSessionRowSessionKindUserEmailAddress | None - """Email associated with the session: the verified email when email verification ran, otherwise the contact email provided at session creation. `null` when neither exists.""" - - additional_details: ( - SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetails | None - ) - """Compact per-feature extras. Only features that produced data appear as keys; `null` when no feature produced extras. Fields excluded by the workflow's response-attributes configuration are omitted.""" +__all__ = ["SessionsListResponse"] class SessionsListResponse(BaseModel): @@ -355,8 +18,5 @@ class SessionsListResponse(BaseModel): previous: str | None """Absolute URL of the previous page, or `null` on the first page.""" - results: list[ - SessionsListResponseResultsKycSessionRowSessionKindUser - | SessionsListResponseResultsKybSessionRowSessionKindBusiness - ] + results: list[UserSessionListItem | BusinessSessionListItem] """Session rows, newest first. With `session_kind=all`, KYC rows come first, then KYB rows.""" diff --git a/src/didit/types/sessions/preview_decision_email_params.py b/src/didit/types/sessions/preview_decision_email_params.py new file mode 100644 index 0000000..94c428d --- /dev/null +++ b/src/didit/types/sessions/preview_decision_email_params.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +__all__ = ["SessionsPreviewDecisionEmailParams"] + + +class SessionsPreviewDecisionEmailParams(TypedDict, total=False): + new_status: Required[Literal["Approved", "Declined"] | str] + """The decision whose email you want to preview.""" + + email_language: str + """Language of the email, the same default as Update Session Status. A language that is not offered falls back to English; `language` in the response says which one is used.""" diff --git a/src/didit/types/sessions/preview_decision_email_response.py b/src/didit/types/sessions/preview_decision_email_response.py new file mode 100644 index 0000000..1d6f2ef --- /dev/null +++ b/src/didit/types/sessions/preview_decision_email_response.py @@ -0,0 +1,50 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SessionsPreviewDecisionEmailResponse"] + + +class SessionsPreviewDecisionEmailResponse(BaseModel): + recipient: str + """The address the email goes to. An empty string when no address is known for the person: then no email can be sent and `preview_token` is empty too. Pass it as `email_address` when you confirm.""" + + recipient_source: Literal["email_verification", "contact_details", ""] | str + """Where `recipient` comes from. `email_verification`: the address the person verified in an email verification step of the flow. `contact_details`: the address given in `contact_details` when the session was created. An empty string when there is no address.""" + + sender: str + """The `From` header the email goes out with.""" + + sender_name: str + """The display name part of `sender`.""" + + sender_email: str + """The address part of `sender`.""" + + sender_kind: Literal["customer", "didit"] | str + """`customer`: the email goes out from your application's verified sender domain (white label). `didit`: it goes out from Didit's own sender.""" + + subject: str + """The subject line exactly as it is sent.""" + + html: str + """The email's HTML exactly as it is sent, with its logo embedded as a data URL. Render it in a sandboxed frame, never directly in your application DOM.""" + + language: str + """The language the email is written in. English (`en`) when the `email_language` you asked for is not offered.""" + + will_be_sent: bool + """Whether confirming the change with `send_email: true` sends this email. `false` when `not_sent_reason` is set.""" + + not_sent_reason: ( + Literal["status_unchanged", "notification_emails_disabled", "no_recipient", "sandbox"] + | str + | None + ) + """Why confirming would send no email, or `null` when it will be sent. `status_unchanged`: the session already has this status, so the update changes nothing and sends nothing. `notification_emails_disabled`: the session was created without `contact_details.send_notification_emails: true`, so no decision email goes to the address given in `contact_details`. `no_recipient`: no address is known for the person. `sandbox`: a sandbox session whose recipient has not opted into sandbox email delivery, so the send is only simulated.""" + + preview_token: str + """Signed receipt for this exact email, valid for 15 minutes. Pass it as `email_preview_token` to [Update Session Status](/sessions-api/update-status). An empty string when there is no recipient.""" diff --git a/src/didit/types/sessions/preview_resubmission_params.py b/src/didit/types/sessions/preview_resubmission_params.py index 2e1b957..32dc685 100644 --- a/src/didit/types/sessions/preview_resubmission_params.py +++ b/src/didit/types/sessions/preview_resubmission_params.py @@ -48,8 +48,8 @@ class SessionsPreviewResubmissionParams(TypedDict, total=False): nodes_to_resubmit: Sequence[SessionsPreviewResubmissionParamsNodesToResubmit] """Steps to preview, using the same validation, selection and workflow ordering as a resubmission. Omit to auto-select recorded attempts needing resubmission.""" - email_address: Required[str] - """Recipient shown in the preview. No email is sent.""" + email_address: str + """Optional. Send the email to this address instead of the person's own. Without it, the recipient is the address the person verified in the flow, else the one given in `contact_details`. No email is sent by this call.""" email_language: str """Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`).""" diff --git a/src/didit/types/sessions/preview_resubmission_response.py b/src/didit/types/sessions/preview_resubmission_response.py index 2ac709f..f485878 100644 --- a/src/didit/types/sessions/preview_resubmission_response.py +++ b/src/didit/types/sessions/preview_resubmission_response.py @@ -49,13 +49,37 @@ class SessionsPreviewResubmissionResponseNodesToResubmit(BaseModel): class SessionsPreviewResubmissionResponse(BaseModel): recipient: str + """The address the email goes to. An empty string when no address is known for the person: then no email can be sent and `preview_token` is empty too. Pass it as `email_address` when you confirm.""" + + recipient_source: Literal["email_verification", "contact_details", "request", ""] | str + """Where `recipient` comes from. `request`: the `email_address` you passed. `email_verification`: the address the person verified in an email verification step of the flow. `contact_details`: the address given in `contact_details` when the session was created. An empty string when there is no address.""" sender: str + """The `From` header the email goes out with.""" + + sender_name: str + """The display name part of `sender`.""" + + sender_email: str + """The address part of `sender`.""" + + sender_kind: Literal["customer", "didit"] | str + """`customer`: the email goes out from your application's verified sender domain (white label). `didit`: it goes out from Didit's own sender.""" subject: str + """The subject line exactly as it is sent.""" html: str - """Rendered email. Inline image attachments use data URLs for preview.""" + """The email's HTML exactly as it is sent, with its logo embedded as a data URL. Render it in a sandboxed frame, never directly in your application DOM.""" + + language: str + """The language the email is written in. English (`en`) when the `email_language` you asked for is not offered.""" + + will_be_sent: bool + """Whether confirming the change with `send_email: true` sends this email. `false` when `not_sent_reason` is set.""" + + not_sent_reason: Literal["no_user_action", "no_recipient", "sandbox"] | str | None + """Why confirming would send no email, or `null` when it will be sent. `no_user_action`: every selected step runs without the person, so the resubmission finishes on its own and no email goes out. `no_recipient`: no address is known for the person and you passed none. `sandbox`: a sandbox session whose recipient has not opted into sandbox email delivery, so the send is only simulated.""" nodes_to_resubmit: list[SessionsPreviewResubmissionResponseNodesToResubmit] @@ -63,4 +87,4 @@ class SessionsPreviewResubmissionResponse(BaseModel): """Whether at least one selected check requires user interaction. This does not predict the final workflow outcome.""" preview_token: str - """Signed confirmation receipt, valid for 15 minutes. Pass it as email_preview_token when confirming the same message.""" + """Signed receipt for this exact email, valid for 15 minutes. Pass it as `email_preview_token` to [Update Session Status](/sessions-api/update-status). An empty string when there is no recipient.""" diff --git a/src/didit/types/sessions/signatures/__init__.py b/src/didit/types/sessions/signatures/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/sessions/signatures/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/sessions/signatures/validate_params.py b/src/didit/types/sessions/signatures/validate_params.py new file mode 100644 index 0000000..122e483 --- /dev/null +++ b/src/didit/types/sessions/signatures/validate_params.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["SessionsSignaturesValidateParams"] + + +class SessionsSignaturesValidateParams(TypedDict, total=False): + signature_id: str + """`id` of one signer's item in `signatures[]`.""" diff --git a/src/didit/types/sessions/signatures/validate_response.py b/src/didit/types/sessions/signatures/validate_response.py new file mode 100644 index 0000000..51c618f --- /dev/null +++ b/src/didit/types/sessions/signatures/validate_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.signature_result import SignatureResult + +__all__ = ["SessionsSignaturesValidateResponse"] + + +class SessionsSignaturesValidateResponse(BaseModel): + signatures: list[SignatureResult] | None = None diff --git a/src/didit/types/sessions/simulate_params.py b/src/didit/types/sessions/simulate_params.py new file mode 100644 index 0000000..3d10c49 --- /dev/null +++ b/src/didit/types/sessions/simulate_params.py @@ -0,0 +1,30 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypedDict + +__all__ = ["SessionsSimulateParams"] + + +class SessionsSimulateParams(TypedDict, total=False): + """Force a sandbox session's status. Every field is optional, but at least one of `new_status`, `scenario` or `risk` is required.""" + + new_status: Literal["Approved", "Declined", "In Review", "Expired", "Abandoned"] | str + """The status to set. When you leave it out, it defaults to the default status of the risk case (`risk`, or the first risk the scenario's magic values carry), else to the scenario's default status.""" + + comment: str + """Note added to the activity entry the status change records.""" + + scenario: str + """A scenario slug from the catalog (`GET /v1/sandbox/scenarios/`). It supplies the default status and, without `risk`, the warning its magic values carry. An unknown slug returns `400`.""" + + risk: str + """A risk case from the catalog. Its warning is added to the session, with a log type that follows `new_status`. An unknown value returns `400`.""" + + node_id: str + """Workflow node the risk warning is attached to. Defaults to the session's current node.""" + + send_webhooks: bool + """`true` sends exactly the webhooks a real status change sends to the sandbox application's destinations: `status.updated`, plus the `user.status.updated` / `user.data.updated` (or `business.status.updated` / `business.data.updated`) events the change causes, for destinations subscribed to them. `false` changes the status without logging or sending any webhook: the missed-webhook recovery never re-sends that decision, the console still updates live, and the next status change of the session sends webhooks as usual.""" diff --git a/src/didit/types/sessions/update_aml_hit_status_response.py b/src/didit/types/sessions/update_aml_hit_status_response.py new file mode 100644 index 0000000..fe76f11 --- /dev/null +++ b/src/didit/types/sessions/update_aml_hit_status_response.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SessionsUpdateAmlHitStatusResponse"] + + +class SessionsUpdateAmlHitStatusResponse(BaseModel): + message: str + """Always `Hit status updated successfully`.""" + + hit_id: str + """The updated hit, with surrounding whitespace removed.""" + + review_status: Literal["Unreviewed", "Confirmed Match", "False Positive", "Inconclusive"] | str + """The review status now stored on the hit.""" diff --git a/src/didit/types/sessions/update_aml_status_params.py b/src/didit/types/sessions/update_aml_status_params.py new file mode 100644 index 0000000..7fab92c --- /dev/null +++ b/src/didit/types/sessions/update_aml_status_params.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +__all__ = ["SessionsUpdateAmlStatusParams"] + + +class SessionsUpdateAmlStatusParams(TypedDict, total=False): + status: Required[ + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str + ] + """New status of the AML check.""" + + node_id: str + """Workflow graph node ID of the AML check, as an alternative to the query parameter.""" diff --git a/src/didit/types/sessions/update_aml_status_response.py b/src/didit/types/sessions/update_aml_status_response.py new file mode 100644 index 0000000..a47e7ed --- /dev/null +++ b/src/didit/types/sessions/update_aml_status_response.py @@ -0,0 +1,21 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SessionsUpdateAmlStatusResponse"] + + +class SessionsUpdateAmlStatusResponse(BaseModel): + message: str + """Always `AML status updated successfully`.""" + + status: Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str + """Status of the AML check now.""" + + previous_status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str + ) + """Status before the call. Equal to `status` when nothing changed.""" diff --git a/src/didit/types/sessions/update_document_ai_data_params.py b/src/didit/types/sessions/update_document_ai_data_params.py new file mode 100644 index 0000000..7be9439 --- /dev/null +++ b/src/didit/types/sessions/update_document_ai_data_params.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping + +from typing_extensions import TypedDict + +__all__ = ["SessionsUpdateDocumentAiDataParams"] + + +class SessionsUpdateDocumentAiDataParams(TypedDict, total=False): + extracted_data: Mapping[str, object] + """Field keys (the keys you configured for the document) and their corrected values. Must be a JSON object; merged key by key into the stored data. Leave it out rather than sending `null`, which returns `400`.""" + + status: Literal["Approved", "Declined", "In Review", "Not Finished"] | str + """New review status of the document.""" diff --git a/src/didit/types/sessions/update_document_ai_data_response.py b/src/didit/types/sessions/update_document_ai_data_response.py new file mode 100644 index 0000000..cb5927f --- /dev/null +++ b/src/didit/types/sessions/update_document_ai_data_response.py @@ -0,0 +1,28 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SessionsUpdateDocumentAiDataResponse"] + + +class SessionsUpdateDocumentAiDataResponse(BaseModel): + uuid: str + """Document identifier.""" + + document_key: str + """The configured document this upload corresponds to.""" + + original_filename: str | None + """File name the document was uploaded with.""" + + file_url: str | None + """Storage path of the uploaded file. It is not a download link: read the presigned `file_url` from the session decision to download the file.""" + + status: Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str + """Review status of the document.""" + + extracted_data: dict[str, object] | None + """All extracted fields after the merge, keyed by field key.""" diff --git a/src/didit/types/sessions/update_feature_status_params.py b/src/didit/types/sessions/update_feature_status_params.py index d1de0d0..1647936 100644 --- a/src/didit/types/sessions/update_feature_status_params.py +++ b/src/didit/types/sessions/update_feature_status_params.py @@ -14,3 +14,40 @@ class SessionsUpdateFeatureStatusParams(TypedDict, total=False): comment: str """Optional free-text note recorded on the session's review trail for this feature change.""" + + feature_type: ( + Literal[ + "ID_VERIFICATION", + "OCR", + "OCR_BACK", + "NFC", + "LIVENESS", + "FACE", + "AGE_ESTIMATION", + "AGE_VERIFICATION", + "FACE_SEARCH", + "FACE_MATCH", + "IP_ANALYSIS", + "AML", + "PHONE", + "PHONE_VERIFICATION", + "EMAIL", + "EMAIL_VERIFICATION", + "POA", + "PROOF_OF_ADDRESS", + "DATABASE_VALIDATION", + "DATA_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + "BANK_VERIFICATION", + "GEOLOCATION", + ] + | str + ) + """Which kind of step to target. Needed on non-graph workflows (`nodeId` = `default`), where every step shares no node; optional otherwise. An unknown value returns `400`.""" + + defer_until_flow_finishes: bool + """While the person is still in the flow, store the override as `pending_until_flow_finishes` and apply it when the flow finishes, instead of at once. Omitted or `false`, the override applies at once. On a session whose flow already ended it applies at once either way.""" diff --git a/src/didit/types/sessions/update_feature_status_response.py b/src/didit/types/sessions/update_feature_status_response.py index 569e020..e7c678a 100644 --- a/src/didit/types/sessions/update_feature_status_response.py +++ b/src/didit/types/sessions/update_feature_status_response.py @@ -2,16 +2,22 @@ from __future__ import annotations from didit._models import BaseModel +from didit.types.components.feature_status_override import FeatureStatusOverride __all__ = ["SessionsUpdateFeatureStatusResponse"] class SessionsUpdateFeatureStatusResponse(BaseModel): - session_id: str | None = None + session_id: str - node_id: str | None = None + node_id: str | None + """The updated step's workflow node, or `null` for a step without a node (the `default` path in a non-graph workflow).""" - feature_type: str | None = None + feature_type: str """Model name of the updated feature (e.g. `KYC`, `Face`, `FaceMatch`, `AML`, `POA`).""" - new_status: str | None = None + new_status: str + """The check's stored status: the requested one once the override applied, the unchanged one while the override waits for the flow to finish.""" + + override: FeatureStatusOverride + """One override of a check (feature) status made with `PATCH /v3/session/{sessionId}/features/{nodeId}/update-status/`: what it changes, whether it applied or still waits for the person to finish the flow, who asked and why.""" diff --git a/src/didit/types/sessions/update_status_params.py b/src/didit/types/sessions/update_status_params.py index e0ddf01..ca9950d 100644 --- a/src/didit/types/sessions/update_status_params.py +++ b/src/didit/types/sessions/update_status_params.py @@ -64,4 +64,4 @@ class SessionsUpdateStatusParams(TypedDict, total=False): """Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`).""" email_preview_token: str - """Optional signed receipt from Preview Resubmission Email. When supplied with send_email=true and new_status=Resubmitted, the recipient, language, checks, current session state, sender and message must still match the preview. A changed or expired receipt returns 409 before changing the session or sending email. Existing integrations may omit this field.""" + """Optional receipt from [Preview Decision Email](/sessions-api/preview-decision-email) (for `Approved` and `Declined`) or [Preview Resubmission Email](/sessions-api/preview-resubmission) (for `Resubmitted`): its `preview_token`. Valid only with `send_email: true`; without it the request returns `400`. The recipient, language, session status, sender and email (and, for a resubmission, the selected steps) must still match the preview, and the receipt expires after 15 minutes. Otherwise the request returns `409` before the session changes or an email is sent. Existing integrations may omit this field.""" diff --git a/src/didit/types/transactions/create_params.py b/src/didit/types/transactions/create_params.py index 83976ac..d25b23e 100644 --- a/src/didit/types/transactions/create_params.py +++ b/src/didit/types/transactions/create_params.py @@ -6,6 +6,8 @@ from typing_extensions import Required, TypedDict +from didit.types.components.travel_rule_utxo import TravelRuleUtxo + __all__ = [ "TransactionsCreateParams", "TransactionsCreateParamsCounterparty", @@ -88,7 +90,10 @@ class TransactionsCreateParamsCounterpartyPaymentMethod(TypedDict, total=False): """Type of payment method: `bank_card`, `bank_account`, `crypto_wallet`, `ewallet`, `unhosted_wallet`, `other`. Use `unhosted_wallet` to declare a self-hosted wallet for the Travel Rule.""" account_id: str - """Account identifier (e.g. IBAN or wallet address).""" + """Account identifier (e.g. IBAN or wallet address). On a crypto transfer on bitcoin or Lightning it may be a Lightning payment request (BOLT11 invoice, BOLT12 offer, LNURL) or a Lightning Address: Didit stores the payee it names as the account ("" when it names none) and what the request says on the Travel Rule transfer's `chain_details.lightning`. At most 255 characters once stored. On a crypto transfer on bitcoin or Lightning it may be a Lightning payment request (BOLT11 invoice, BOLT12 offer, LNURL) or a Lightning Address: Didit stores the payee it names as the account ("" when it names none) and what the request says on the Travel Rule transfer's `chain_details.lightning`. At most 255 characters once stored.""" + + memo: str + """The destination tag or memo that is part of the wallet on a memo network (XRP, XLM, EOS, ATOM, HBAR). On one shared exchange address each memo is its own payment method. XRP: a destination tag, a whole number from 0 to 4294967295 (a JSON number is accepted). XLM: a text memo of up to 28 bytes, a 64-bit id or a 32-byte hash in hex. EOS: up to 256 bytes. ATOM: up to 256 characters. HBAR: up to 100 bytes. Bitcoin, EVM, Solana, Tron, Litecoin, Bitcoin Cash, Dogecoin and Lightning wallets take no memo: one there is a `400`. Alias: `destinationTag` or `destination_tag`.""" issuing_country: str @@ -221,7 +226,10 @@ class TransactionsCreateParamsSubjectPaymentMethod(TypedDict, total=False): """Type of payment method: `bank_card`, `bank_account`, `crypto_wallet`, `ewallet`, `unhosted_wallet`, `other`.""" account_id: str - """Account identifier (e.g. IBAN, wallet address). For crypto screening, this should contain the wallet address for this participant. Inbound transaction-hash screening requires the service/customer deposit address on the subject. Outbound screening uses the destination wallet on the counterparty.""" + """Account identifier (e.g. IBAN, wallet address). For crypto screening, this should contain the wallet address for this participant. Inbound transaction-hash screening requires the service/customer deposit address on the subject. Outbound screening uses the destination wallet on the counterparty. On a crypto transfer on bitcoin or Lightning it may be a Lightning payment request (BOLT11 invoice, BOLT12 offer, LNURL) or a Lightning Address: Didit stores the payee it names as the account ("" when it names none) and what the request says on the Travel Rule transfer's `chain_details.lightning`. At most 255 characters once stored. On a crypto transfer on bitcoin or Lightning it may be a Lightning payment request (BOLT11 invoice, BOLT12 offer, LNURL) or a Lightning Address: Didit stores the payee it names as the account ("" when it names none) and what the request says on the Travel Rule transfer's `chain_details.lightning`. At most 255 characters once stored.""" + + memo: str + """The destination tag or memo that is part of the wallet on a memo network (XRP, XLM, EOS, ATOM, HBAR). On one shared exchange address each memo is its own payment method. XRP: a destination tag, a whole number from 0 to 4294967295 (a JSON number is accepted). XLM: a text memo of up to 28 bytes, a 64-bit id or a 32-byte hash in hex. EOS: up to 256 bytes. ATOM: up to 256 characters. HBAR: up to 100 bytes. Bitcoin, EVM, Solana, Tron, Litecoin, Bitcoin Cash, Dogecoin and Lightning wallets take no memo: one there is a `400`. Alias: `destinationTag` or `destination_tag`.""" issuing_country: str """ISO 3166-1 alpha-3 country code of the issuer.""" @@ -265,7 +273,7 @@ class TransactionsCreateParamsTransactionDetailsCryptoParams(TypedDict, total=Fa """Crypto network selection, separate from currency (the asset symbol). Recording a custom token symbol does not guarantee transaction-hash coverage for its contract.""" crypto_chain: str - """Actual network used for screening. Managed screening supports BTC, ETH, LTC, XRP, BCH, DOGE, TRX, SOL, MATIC, BNB, BASE (Base), OP (Optimism) and ARB (Arbitrum One). Always specify this for multi-network tokens. Connected-provider coverage varies; LIGHTNING requires a supported connected provider.""" + """Actual network used for screening. Managed screening supports BTC, ETH, LTC, XRP, BCH, DOGE, TRX, SOL, MATIC, BNB, BASE (Base), OP (Optimism) and ARB (Arbitrum One). Always specify this for multi-network tokens. When it is missing for USDT, USDC or ETH sent with a 0x transaction hash, Didit looks the hash up on Ethereum, BNB Chain, Polygon, Base, Arbitrum and Optimism and screens it on the network where it is found; if no network has it, the transaction is not screened and custom_values.aml_screening_status is FAILED with the reason in custom_values.aml_screening_error. When it is missing for USDT or USDC screened by wallet (no transaction hash), a Tron address is screened on Tron, a Solana address on Solana, and a 0x address on the first of those six networks where it has sent a transaction or holds the token, or on Ethereum when it is active on none of them. Connected-provider coverage varies; LIGHTNING requires a supported connected provider.""" class TransactionsCreateParamsTransactionDetails(TypedDict, total=False): @@ -303,9 +311,10 @@ class TransactionsCreateParamsTransactionDetails(TypedDict, total=False): class TransactionsCreateParamsTravelRuleDetails(TypedDict, total=False): - """Travel Rule compliance metadata.""" + """Travel Rule data. When Travel Rule is enabled and transaction_category is travel_rule, Didit runs the exchange itself: it ignores status, protocol, required and obligations_count, and uses originator_data, beneficiary_data and metadata. Otherwise Didit stores these values as a passive Travel Rule record. While Travel Rule is on, an outbound transfer at or above your threshold must carry what its regime requires (EU TFR or FATF Recommendation 16), checked on the sides you send: a missing field answers 400 under `travel_rule_details.originator_data` or `travel_rule_details.beneficiary_data`, for example "EU TFR requires the originator name." """ status: Required[str] + """Required whenever you send travel_rule_details. One of the Travel Rule transfer statuses (for example AWAITING_COUNTERPARTY, COMPLETED, NOT_APPLICABLE); lowercase and camelCase spellings and the legacy values UNKNOWN, COMPLIANT, PENDING_ACTION, PENDING_COUNTERPARTY, FAILED and EXEMPT are also accepted and mapped. Any other value returns 400.""" protocol: str @@ -314,11 +323,25 @@ class TransactionsCreateParamsTravelRuleDetails(TypedDict, total=False): obligations_count: int originator_data: Mapping[str, object] + """Originator party, as flat fields or one IVMS 101 person. For your user with an approved KYC verification, Didit replaces the fields the verification holds with the verified values (see `metadata.originator_provenance`). It must be an object: anything else is a `400`. On a memo network a `memo` key names the originator wallet's memo or tag.""" beneficiary_data: Mapping[str, object] + """Beneficiary party, as flat fields or one IVMS 101 person. An empty `wallet_address` is filled from `counterparty.payment_method.account_id`. It must be an object: anything else is a `400`. On a memo network `memo` names the beneficiary wallet's memo or tag; `wallet_address` and `memo` win over the counterparty's payment method.""" metadata: Mapping[str, object] + utxo: TravelRuleUtxo + """Optional, UTXO chains only (BTC, LTC, BCH, DOGE). The transaction's inputs and outputs, from which Didit infers the originator wallet and the beneficiary output when you do not declare them.""" + + originator: Mapping[str, object] + """An IVMS 101 originator block: `originatorPersons` (one `naturalPerson` or `legalPerson`; a legal person makes the party a company) and `accountNumber` (wallet addresses). Used for `originator_data` when that object is empty. A malformed block answers 400 keyed by its path, for example `originator.originatorPersons`.""" + + beneficiary: Mapping[str, object] + """An IVMS 101 beneficiary block: `beneficiaryPersons` (one `naturalPerson` or `legalPerson`; a legal person makes the party a company) and `accountNumber` (wallet addresses). Used for `beneficiary_data` when that object is empty. A malformed block answers 400 keyed by its path, for example `beneficiary.beneficiaryPersons`.""" + + ivms101: Mapping[str, object] + """The `originator` and `beneficiary` blocks wrapped in one IVMS 101 object (`IVMS101` is accepted too). Top-level blocks win over the wrapper's.""" + class TransactionsCreateParams(TypedDict, total=False): transaction_id: Required[str] @@ -359,7 +382,7 @@ class TransactionsCreateParams(TypedDict, total=False): """Arbitrary key-value pairs attached to the transaction. Each key can be referenced in rule conditions with the field path `custom_values.`.""" travel_rule_details: TransactionsCreateParamsTravelRuleDetails - """Travel Rule compliance metadata.""" + """Travel Rule data. When Travel Rule is enabled and transaction_category is travel_rule, Didit runs the exchange itself: it ignores status, protocol, required and obligations_count, and uses originator_data, beneficiary_data and metadata. Otherwise Didit stores these values as a passive Travel Rule record. While Travel Rule is on, an outbound transfer at or above your threshold must carry what its regime requires (EU TFR or FATF Recommendation 16), checked on the sides you send: a missing field answers 400 under `travel_rule_details.originator_data` or `travel_rule_details.beneficiary_data`, for example "EU TFR requires the originator name." """ network_snapshot: TransactionsCreateParamsNetworkSnapshot """Pre-computed network graph snapshot.""" diff --git a/src/didit/types/transactions/list_networks_response.py b/src/didit/types/transactions/list_networks_response.py deleted file mode 100644 index 667826f..0000000 --- a/src/didit/types/transactions/list_networks_response.py +++ /dev/null @@ -1,11 +0,0 @@ -# Code generated by codegen from openapi/*.json. DO NOT EDIT. -from __future__ import annotations - -from didit._models import BaseModel -from didit.types.components.entity_network_membership import EntityNetworkMembership - -__all__ = ["TransactionsListNetworksResponse"] - - -class TransactionsListNetworksResponse(BaseModel): - results: list[EntityNetworkMembership] | None = None diff --git a/src/didit/types/transactions/list_party_networks_response.py b/src/didit/types/transactions/list_party_networks_response.py deleted file mode 100644 index 735ea96..0000000 --- a/src/didit/types/transactions/list_party_networks_response.py +++ /dev/null @@ -1,11 +0,0 @@ -# Code generated by codegen from openapi/*.json. DO NOT EDIT. -from __future__ import annotations - -from didit._models import BaseModel -from didit.types.components.entity_network_membership import EntityNetworkMembership - -__all__ = ["TransactionsListPartyNetworksResponse"] - - -class TransactionsListPartyNetworksResponse(BaseModel): - results: list[EntityNetworkMembership] | None = None diff --git a/src/didit/types/transactions/list_related_response.py b/src/didit/types/transactions/list_related_response.py new file mode 100644 index 0000000..8f142de --- /dev/null +++ b/src/didit/types/transactions/list_related_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.transaction_summary import TransactionSummary + +__all__ = ["TransactionsListRelatedResponse"] + + +class TransactionsListRelatedResponse(BaseModel): + count: int + """Number of related transactions: exact up to 100, capped at 100 above that. Follow `next` instead of computing pages from it.""" + + next: str | None + """Absolute URL of the next page, or `null` on the last page.""" + + previous: str | None + """Absolute URL of the previous page, or `null` on the first page.""" + + results: list[TransactionSummary] diff --git a/src/didit/types/transactions/list_response.py b/src/didit/types/transactions/list_response.py index 57970c6..447a394 100644 --- a/src/didit/types/transactions/list_response.py +++ b/src/didit/types/transactions/list_response.py @@ -5,6 +5,7 @@ from typing import Literal from didit._models import BaseModel +from didit.types.components.transaction_travel_rule import TransactionTravelRule __all__ = [ "TransactionsListResponse", @@ -105,6 +106,12 @@ class TransactionsListResponseResults(BaseModel): assigned_to_name: str | None = None """Display name of the Console reviewer assigned to this transaction.""" + travel_rule_status: str | None = None + """Status of the transaction's Travel Rule transfer (for example `COMPLETED`), or `null` when it has none.""" + + travel_rule: TransactionTravelRule | None = None + """The transaction's Travel Rule transfer, the same block as `travel_rule` on `GET /v3/transactions/{transaction_id}/`, or `null` when the transaction has none.""" + class TransactionsListResponse(BaseModel): count: int | None = None diff --git a/src/didit/types/transactions/notes/__init__.py b/src/didit/types/transactions/notes/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/transactions/notes/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/transactions/notes/create_params.py b/src/didit/types/transactions/notes/create_params.py new file mode 100644 index 0000000..e6421ca --- /dev/null +++ b/src/didit/types/transactions/notes/create_params.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import Required, TypedDict + +__all__ = ["TransactionsNotesCreateParams"] + + +class TransactionsNotesCreateParams(TypedDict, total=False): + note: Required[str] + """Plain-text note. Leading and trailing whitespace is trimmed and a blank note is rejected. No length limit.""" + + mentioned_emails: Sequence[str] + """Emails of members of your organization to mention. Each must be a valid email of an organization member.""" diff --git a/src/didit/types/transactions/notes/list_response.py b/src/didit/types/transactions/notes/list_response.py new file mode 100644 index 0000000..31b3e33 --- /dev/null +++ b/src/didit/types/transactions/notes/list_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.transaction_note import TransactionNote + +__all__ = ["TransactionsNotesListResponse"] + + +class TransactionsNotesListResponse(BaseModel): + count: int + """Number of notes on the transaction: exact up to 100, capped at 100 above that. Follow `next` instead of computing pages from it.""" + + next: str | None + """Absolute URL of the next page, or `null` on the last page.""" + + previous: str | None + """Absolute URL of the previous page, or `null` on the first page.""" + + results: list[TransactionNote] diff --git a/src/didit/types/transactions/travel_rule/confirm_ownership_params.py b/src/didit/types/transactions/travel_rule/confirm_ownership_params.py index 348f363..033497b 100644 --- a/src/didit/types/transactions/travel_rule/confirm_ownership_params.py +++ b/src/didit/types/transactions/travel_rule/confirm_ownership_params.py @@ -8,3 +8,7 @@ class TransactionsTravelRuleConfirmOwnershipParams(TypedDict, total=False): confirmed: Required[bool] + """true to confirm ownership, false to deny it.""" + + wallet_address: str + """Inbound TRP transfers: the payment address Didit returns to the originating VASP when you approve. Defaults to the matched address book entry's address, then beneficiary_data.wallet_address. On a declared self-hosted wallet transfer that names no counterparty wallet, the wallet whose ownership you verified; without it the request is rejected with 400. Ignored for other transfers.""" diff --git a/src/didit/types/transactions/travel_rule/create_message_params.py b/src/didit/types/transactions/travel_rule/create_message_params.py new file mode 100644 index 0000000..65d27ce --- /dev/null +++ b/src/didit/types/transactions/travel_rule/create_message_params.py @@ -0,0 +1,28 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import TypedDict + +from didit._types import FileTypes + +__all__ = ["TransactionsTravelRuleCreateMessageParams"] + + +class TransactionsTravelRuleCreateMessageParams(TypedDict, total=False): + """Send as JSON, or as multipart/form-data with the attachments in `files`.""" + + kind: Literal["MESSAGE", "DOCUMENT_REQUEST"] | str + + body: str + """Required for a MESSAGE unless a file is attached.""" + + requested_documents: Sequence[str] + """Required (at least one) for a DOCUMENT_REQUEST, and only allowed there.""" + + reply_to: str | None + """A message of the same transfer. A reply that carries a document fulfils the document request it answers.""" + + files: Sequence[FileTypes] + """Multipart only. Up to 5 files of 10 MB each.""" diff --git a/src/didit/types/transactions/travel_rule/trigger_test_vasp_inbound_params.py b/src/didit/types/transactions/travel_rule/trigger_test_vasp_inbound_params.py new file mode 100644 index 0000000..642c008 --- /dev/null +++ b/src/didit/types/transactions/travel_rule/trigger_test_vasp_inbound_params.py @@ -0,0 +1,28 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +__all__ = ["TransactionsTravelRuleTriggerTestVaspInboundParams"] + + +class TransactionsTravelRuleTriggerTestVaspInboundParams(TypedDict, total=False): + beneficiary_wallet_address: Required[str] + """The destination wallet address. The Didit Test VASP sends to it, matched against your wallet address book on the chain of the asset. A wallet with no matching entry is accepted and the inbound transaction is booked ON_HOLD.""" + + scenario: Literal["accept", "name_mismatch"] | str + """What the Didit Test VASP declares the beneficiary to be. `accept` declares the holder's own name; `name_mismatch` declares a name no holder agrees with. The decision itself is always yours.""" + + chain: str + """Chain of the asset.""" + + amount: str + """Amount as a decimal string.""" + + currency: str + """Asset code.""" + + reference: str + """Optional idempotency key. A repeat with the same value returns the transfer the first call booked.""" diff --git a/src/didit/types/transactions/travel_rule/update_transfer_params.py b/src/didit/types/transactions/travel_rule/update_transfer_params.py index 8349ea5..8788131 100644 --- a/src/didit/types/transactions/travel_rule/update_transfer_params.py +++ b/src/didit/types/transactions/travel_rule/update_transfer_params.py @@ -9,10 +9,19 @@ class TransactionsTravelRuleUpdateTransferParams(TypedDict, total=False): - """Provide exactly one of: payment_txn_id (finish), action=cancel, or action=resend.""" + """Provide exactly one of: payment_txn_id (finish), action=cancel, action=resend, action=update_compliance_email with compliance_email, or action=update_originator_wallet with wallet_address.""" payment_txn_id: str """The on-chain transaction hash. Requires the transfer to be an outbound transfer in COMPLETED status.""" - action: Literal["cancel", "resend"] | str - """"cancel" cancels a non-terminal transfer. "resend" re-runs routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA.""" + action: Literal["cancel", "resend", "update_compliance_email", "update_originator_wallet"] | str + """"cancel" cancels a non-terminal transfer. "resend" re-runs routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA. "update_compliance_email" replaces the compliance email the request goes to, on an outbound transfer in one of those three statuses; send "resend" afterwards. "update_originator_wallet" records the wallet the funds actually left from (`wallet_address`, `memo`).""" + + compliance_email: str + """Required with `action: update_compliance_email`. Kept as this transfer's `beneficiary_data.counterparty_vasp_email`: it belongs to this transfer only and joins no directory entry and no contact list.""" + + wallet_address: str + """With `action: update_originator_wallet`: the wallet the funds left from. When the transfer's UTXO inputs are known it must be one of them.""" + + memo: str + """With `action: update_originator_wallet`: the wallet's memo or tag on a memo network. Optional.""" diff --git a/src/didit/types/travel_rule/counterparty_contacts/__init__.py b/src/didit/types/travel_rule/counterparty_contacts/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/travel_rule/counterparty_contacts/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/travel_rule/counterparty_contacts/create_params.py b/src/didit/types/travel_rule/counterparty_contacts/create_params.py new file mode 100644 index 0000000..1532564 --- /dev/null +++ b/src/didit/types/travel_rule/counterparty_contacts/create_params.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["TravelRuleCounterpartyContactsCreateParams"] + + +class TravelRuleCounterpartyContactsCreateParams(TypedDict, total=False): + counterparty: Required[str] + """The directory entry's `id`.""" + + email: Required[str] + + name: str + """Optional label.""" diff --git a/src/didit/types/travel_rule/counterparty_contacts/list_response.py b/src/didit/types/travel_rule/counterparty_contacts/list_response.py new file mode 100644 index 0000000..682f767 --- /dev/null +++ b/src/didit/types/travel_rule/counterparty_contacts/list_response.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.travel_rule_counterparty_contact import TravelRuleCounterpartyContact + +__all__ = ["TravelRuleCounterpartyContactsListResponse"] + + +class TravelRuleCounterpartyContactsListResponse(BaseModel): + count: int | None = None + + next: str | None = None + + previous: str | None = None + + results: list[TravelRuleCounterpartyContact] | None = None diff --git a/src/didit/types/travel_rule/create_widget_session_params.py b/src/didit/types/travel_rule/create_widget_session_params.py index 7dda729..0b909d8 100644 --- a/src/didit/types/travel_rule/create_widget_session_params.py +++ b/src/didit/types/travel_rule/create_widget_session_params.py @@ -1,17 +1,17 @@ # Code generated by codegen from openapi/*.json. DO NOT EDIT. from __future__ import annotations -from typing_extensions import Required, TypedDict +from typing_extensions import TypedDict __all__ = ["TravelRuleCreateWidgetSessionParams"] class TravelRuleCreateWidgetSessionParams(TypedDict, total=False): - wallet_address: Required[str] - """The wallet address to verify.""" + wallet_address: str + """The wallet address to verify. Optional; when empty, the address must be supplied when a proof is started.""" - chain: Required[str] - """Chain identifier for the address, e.g. "ethereum", "bitcoin", "solana".""" + chain: str + """Chain token for the address, for example `ETH`, `BTC` or `SOL`. Use the same token as in your wallet address book. Message signing and the Satoshi test are offered only for a supported chain.""" holder_name: str """Name of the wallet holder, used for the beneficiary name match.""" @@ -19,14 +19,14 @@ class TravelRuleCreateWidgetSessionParams(TypedDict, total=False): vendor_data: str """Your internal identifier for the holder, carried onto the created address-book entry.""" - transaction_id: str + transaction_id: str | None """Optional. Didit transaction UUID of a Travel Rule transfer to advance when the proof verifies.""" satoshi_deposit_address: str """Optional. A deposit address you control; supplying it enables the SATOSHI_TEST proof method.""" callback_url: str - """Optional. Where the widget returns the customer after completion.""" + """Optional. Absolute http(s) URL where the widget returns the customer after completion.""" expires_in_minutes: int """Optional. Lifetime of the minted link, in minutes. Automatically extended to cover the Satoshi test window when SATOSHI_TEST is offered.""" diff --git a/src/didit/types/travel_rule/register_inbound_params.py b/src/didit/types/travel_rule/register_inbound_params.py index f165a71..4bbf578 100644 --- a/src/didit/types/travel_rule/register_inbound_params.py +++ b/src/didit/types/travel_rule/register_inbound_params.py @@ -5,41 +5,50 @@ from typing_extensions import Required, TypedDict +from didit.types.components.travel_rule_utxo import TravelRuleUtxo + __all__ = ["TravelRuleRegisterInboundParams", "TravelRuleRegisterInboundParamsOriginatingVasp"] class TravelRuleRegisterInboundParamsOriginatingVasp(TypedDict, total=False): - """Optional description of the sending VASP.""" + """Optional description of the sending VASP. It stays on this transfer only (`metadata.originating_vasp`, and the transfer's originator VASP in GET /v3/travel-rule/transfers/); it never creates, edits or links a shared VASP directory entry.""" name: str lei: str + """Optional. A valid 20-character ISO 17442 LEI, or empty.""" travel_address: str - """Travel address that lets Didit reach the sender on the TRP rail.""" + """The sender's travel address, recorded on this transfer.""" class TravelRuleRegisterInboundParams(TypedDict, total=False): chain: Required[str] - """Chain the deposit settled on, e.g. "ethereum".""" + """Chain the deposit settled on, for example `ETH`. Stored lowercase and compared case-insensitively with your wallet address book entries, so use the same chain token there.""" txid: Required[str] """On-chain transaction hash. Deduped together with wallet_address.""" wallet_address: Required[str] - """The destination wallet you received the deposit on.""" + """The destination wallet you received the deposit on. An XRP X-address is split into its classic address and tag.""" amount: Required[str] - """Deposit amount as a decimal string.""" + """Deposit amount as a decimal string, zero or greater, with up to 18 decimal places.""" currency: Required[str] """Deposit asset, e.g. "USDC".""" originator_data: Mapping[str, object] - """IVMS-101 originator payload from the sending VASP, if known.""" + """Originator details from the sending VASP, if known, as a flat object such as `{"name": "..."}`. Didit reads name (or full_name), first_name, last_name and vendor_data from it.""" beneficiary_data: Mapping[str, object] - """IVMS-101 beneficiary payload (your customer).""" + """Beneficiary details (your customer), as a flat object such as `{"name": "..."}`. Didit matches name against the holder_name of the matched address book entry, and sets wallet_address and chain from the request.""" originating_vasp: TravelRuleRegisterInboundParamsOriginatingVasp - """Optional description of the sending VASP.""" + """Optional description of the sending VASP. It stays on this transfer only (`metadata.originating_vasp`, and the transfer's originator VASP in GET /v3/travel-rule/transfers/); it never creates, edits or links a shared VASP directory entry.""" + + memo: str + """Optional. The deposit's destination tag or memo on a memo network: on a shared deposit address it is what tells your customers apart, and the address-book entry for that memo is matched. XRP: a destination tag, a whole number from 0 to 4294967295 (a JSON number is accepted). XLM: a text memo of up to 28 bytes, a 64-bit id or a 32-byte hash in hex. EOS: up to 256 bytes. ATOM: up to 256 characters. HBAR: up to 100 bytes. Bitcoin, EVM, Solana, Tron, Litecoin, Bitcoin Cash, Dogecoin and Lightning wallets take no memo: one there is a `400`. Alias: `destinationTag` or `destination_tag`.""" + + utxo: TravelRuleUtxo + """Optional, UTXO chains only. The deposit transaction's inputs and outputs: the sender's wallet is the declared `originator_data.wallet_address`, else the largest input.""" diff --git a/src/didit/types/travel_rule/screen_vasp_params.py b/src/didit/types/travel_rule/screen_vasp_params.py new file mode 100644 index 0000000..da35698 --- /dev/null +++ b/src/didit/types/travel_rule/screen_vasp_params.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["TravelRuleScreenVaspParams"] + + +class TravelRuleScreenVaspParams(TypedDict, total=False): + refresh: bool + """true screens again (and bills again) even when a fresh screening exists.""" diff --git a/src/didit/types/travel_rule/send_compliance_email_confirmation_response.py b/src/didit/types/travel_rule/send_compliance_email_confirmation_response.py new file mode 100644 index 0000000..10daf1f --- /dev/null +++ b/src/didit/types/travel_rule/send_compliance_email_confirmation_response.py @@ -0,0 +1,10 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["TravelRuleSendComplianceEmailConfirmationResponse"] + + +class TravelRuleSendComplianceEmailConfirmationResponse(BaseModel): + sent_to: str | None = None diff --git a/src/didit/types/travel_rule/send_vasp_questionnaire_params.py b/src/didit/types/travel_rule/send_vasp_questionnaire_params.py new file mode 100644 index 0000000..71f1317 --- /dev/null +++ b/src/didit/types/travel_rule/send_vasp_questionnaire_params.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["TravelRuleSendVaspQuestionnaireParams"] + + +class TravelRuleSendVaspQuestionnaireParams(TypedDict, total=False): + sent_to: str + """Optional. A compliance address on record for the counterparty: for a directory entry its compliance address or a contact your application sees, for a Didit network member its confirmed compliance email. Default: the address a Travel Rule email to the counterparty goes to (your own contact first, then a platform contact, then the entry's compliance address).""" diff --git a/src/didit/types/travel_rule/transfers/__init__.py b/src/didit/types/travel_rule/transfers/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/travel_rule/transfers/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/travel_rule/transfers/list_response.py b/src/didit/types/travel_rule/transfers/list_response.py new file mode 100644 index 0000000..2d0b567 --- /dev/null +++ b/src/didit/types/travel_rule/transfers/list_response.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.travel_rule_transfer_row import TravelRuleTransferRow + +__all__ = ["TravelRuleTransfersListResponse"] + + +class TravelRuleTransfersListResponse(BaseModel): + count: int | None = None + + next: str | None = None + + previous: str | None = None + + results: list[TravelRuleTransferRow] | None = None diff --git a/src/didit/types/travel_rule/update_settings_params.py b/src/didit/types/travel_rule/update_settings_params.py index 8866717..222d16e 100644 --- a/src/didit/types/travel_rule/update_settings_params.py +++ b/src/didit/types/travel_rule/update_settings_params.py @@ -5,6 +5,9 @@ from typing_extensions import TypedDict +from didit.types.components.travel_rule_data_sharing_fields import TravelRuleDataSharingFieldsParam +from didit.types.components.travel_rule_matching_rules import TravelRuleMatchingRulesParam + __all__ = ["TravelRuleUpdateSettingsParams"] @@ -19,18 +22,33 @@ class TravelRuleUpdateSettingsParams(TypedDict, total=False): jurisdiction: str - name_matching_strictness: Literal["NONE", "STRICT", "DEFAULT", "FUZZY"] | str + name_matching_strictness: Literal["NONE", "STRICT", "CONTAINS", "DEFAULT", "FUZZY"] | str + """`CONTAINS` accepts one name whose words all appear in the other.""" confirmation_timeout_hours: int timeout_outcome: Literal["HOLD", "REJECT", "PROCEED"] | str compliance_email: str + """Compliance contact email for your VASP profile. The Didit network lists your profile only once this address is confirmed through an emailed link (POST /v3/travel-rule/settings/compliance-email/confirmation/); changing it needs a new confirmation.""" is_discoverable: bool + """Whether your VASP profile can be listed in the Didit network: found in GET /v3/travel-rule/vasps/ by other Didit customers and reached on the INTERNAL rail. Listing also needs a live application, Travel Rule on, a legal name and a confirmed compliance email; didit_network says which are missing. Default true.""" threshold_amount: str - """Decimal string. Minimum transfer amount that triggers a managed exchange.""" + """Decimal string, zero or greater, in your application's default currency. Minimum transfer amount that triggers a managed exchange.""" + + ownership_validation_mode: Literal["FULL", "SIMPLIFIED", "AUTOMATED"] | str + """See the response field of the same name.""" + + simplified_validation_threshold: str + """Decimal string, 0 or more.""" + + data_sharing_fields: TravelRuleDataSharingFieldsParam + """Merged per entity type: an entity type you leave out keeps its current choice. A choice that cannot meet the originator obligations of your `jurisdiction` regime (EU TFR or FATF Recommendation 16), an empty list included, is refused with `400`.""" + + matching_rules: TravelRuleMatchingRulesParam + """Merged per entity type, and per field inside it. A field that entity type does not compare (for example `date_of_birth` for `company`) is refused with `400`.""" inbound_auto_accept: bool @@ -43,5 +61,14 @@ class TravelRuleUpdateSettingsParams(TypedDict, total=False): vasp_attribution_enabled: bool + counterparty_screening_enabled: bool + """Opt-in sanctions and adverse-media screening of counterparty VASPs, billed per screening at the Company AML Screening price. When on, Didit screens the counterparty an outbound exchange reaches (reusing a screening younger than 30 days) and you can screen one with POST /v3/travel-rule/vasps/{vasp_id}/screening/. Default false: nothing is screened or billed.""" + + sunrise_policy: Literal["", "HOLD", "REJECT", "PROCEED"] | str + """HOLD, REJECT, PROCEED, or empty to follow timeout_outcome. See the response field of the same name.""" + + non_responsive_policy: Literal["", "HOLD", "REJECT", "PROCEED"] | str + """HOLD, REJECT, PROCEED, or empty to follow timeout_outcome. See the response field of the same name.""" + disabled_networks: Sequence[Literal["GTR", "TRUST", "VERIFYVASP", "SYGNA"] | str] """Networks opted out of platform membership. Does not disable your connected own memberships or provision unavailable networks.""" diff --git a/src/didit/types/travel_rule/update_vasp_decision_params.py b/src/didit/types/travel_rule/update_vasp_decision_params.py new file mode 100644 index 0000000..bae3b39 --- /dev/null +++ b/src/didit/types/travel_rule/update_vasp_decision_params.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +__all__ = ["TravelRuleUpdateVaspDecisionParams"] + + +class TravelRuleUpdateVaspDecisionParams(TypedDict, total=False): + decision: Required[Literal["APPROVED", "RESTRICTED", "BLOCKED"] | str] + + reason: str + """Optional. Replaces the previous reason; omit it to clear it.""" diff --git a/src/didit/types/travel_rule/wallet_addresses/bulk_import_params.py b/src/didit/types/travel_rule/wallet_addresses/bulk_import_params.py new file mode 100644 index 0000000..d5a2ec2 --- /dev/null +++ b/src/didit/types/travel_rule/wallet_addresses/bulk_import_params.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import Required, TypedDict + +from didit.types.components.wallet_address_import_row import WalletAddressImportRow + +__all__ = ["TravelRuleWalletAddressesBulkImportParamsVariant1"] + + +class TravelRuleWalletAddressesBulkImportParamsVariant1(TypedDict, total=False): + entries: Required[Sequence[WalletAddressImportRow]] diff --git a/src/didit/types/travel_rule/wallet_addresses/create_params.py b/src/didit/types/travel_rule/wallet_addresses/create_params.py index 9a5495e..42ce6c3 100644 --- a/src/didit/types/travel_rule/wallet_addresses/create_params.py +++ b/src/didit/types/travel_rule/wallet_addresses/create_params.py @@ -8,8 +8,10 @@ class TravelRuleWalletAddressesCreateParams(TypedDict, total=False): address: Required[str] + """The wallet address. An XRP X-address is stored as its classic address and its tag, so it matches transfers that name either form.""" chain: str + """Chain token for the address, for example `ETH` or `BTC`. When Didit routes or registers a transfer, it matches address book entries on the wallet address and, when the transfer carries a chain, on this value too, ignoring case. A transfer's chain is `beneficiary_data.chain`, filled from the transaction's crypto chain or currency symbol (for example `ETH`) when you do not send it, or the `chain` of an after-deposit registration, so use the same token everywhere. An entry with an empty chain matches any chain.""" holder_vendor_data: str @@ -18,7 +20,10 @@ class TravelRuleWalletAddressesCreateParams(TypedDict, total=False): entity_type: str self_declared: bool - """Write-only. When true, the entry is created already ownership-verified via a SELF_DECLARATION proof.""" + """Write-only. When true, the entry is created already ownership-verified via a SELF_DECLARATION proof. Returns 400 when `allow_self_declaration` is off in your Travel Rule settings.""" travel_address: str - """Optional. The counterparty VASP's travel address for this wallet, enabling TRP routing.""" + """Optional. The counterparty VASP's travel address for this wallet, enabling TRP routing for the assets TRP carries (BTC, ETH, and USDC on a named network). An invalid travel address returns 400.""" + + memo: str + """Optional. The destination tag or memo that is part of the wallet. XRP: a destination tag, a whole number from 0 to 4294967295 (a JSON number is accepted). XLM: a text memo of up to 28 bytes, a 64-bit id or a 32-byte hash in hex. EOS: up to 256 bytes. ATOM: up to 256 characters. HBAR: up to 100 bytes. Bitcoin, EVM, Solana, Tron, Litecoin, Bitcoin Cash, Dogecoin and Lightning wallets take no memo: one there is a `400`. Alias: `destinationTag` or `destination_tag`.""" diff --git a/src/didit/types/travel_rule/wallet_addresses/update_params.py b/src/didit/types/travel_rule/wallet_addresses/update_params.py index 9b24ce9..a47d0f4 100644 --- a/src/didit/types/travel_rule/wallet_addresses/update_params.py +++ b/src/didit/types/travel_rule/wallet_addresses/update_params.py @@ -7,7 +7,7 @@ class TravelRuleWalletAddressesUpdateParams(TypedDict, total=False): - """PATCH is a partial update; the response echoes only holder_name, holder_vendor_data, entity_type, and travel_address.""" + """PATCH is a partial update; the response echoes only holder_name, holder_vendor_data, entity_type, travel_address, receiving_travel_address, label, and owner_type.""" holder_name: str diff --git a/src/didit/types/travel_rule/workspace/__init__.py b/src/didit/types/travel_rule/workspace/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/travel_rule/workspace/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/travel_rule/workspace/claim_params.py b/src/didit/types/travel_rule/workspace/claim_params.py new file mode 100644 index 0000000..55ab658 --- /dev/null +++ b/src/didit/types/travel_rule/workspace/claim_params.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["TravelRuleWorkspaceClaimParams"] + + +class TravelRuleWorkspaceClaimParams(TypedDict, total=False): + vasp_id: Required[str] + """The directory entry this workspace is. Read ids from `GET /v3/travel-rule/vasps/`.""" diff --git a/src/didit/types/travel_rule/workspace/confirm_domain_params.py b/src/didit/types/travel_rule/workspace/confirm_domain_params.py new file mode 100644 index 0000000..8aceb27 --- /dev/null +++ b/src/didit/types/travel_rule/workspace/confirm_domain_params.py @@ -0,0 +1,10 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["TravelRuleWorkspaceConfirmDomainParams"] + + +class TravelRuleWorkspaceConfirmDomainParams(TypedDict, total=False): + code: Required[str] diff --git a/src/didit/types/travel_rule/workspace/enable_billing_params.py b/src/didit/types/travel_rule/workspace/enable_billing_params.py new file mode 100644 index 0000000..c83b3b6 --- /dev/null +++ b/src/didit/types/travel_rule/workspace/enable_billing_params.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["TravelRuleWorkspaceEnableBillingParams"] + + +class TravelRuleWorkspaceEnableBillingParams(TypedDict, total=False): + enabled: bool + """Only `true` is accepted: outbound billing cannot be turned back off through the API.""" diff --git a/src/didit/types/users/create_params.py b/src/didit/types/users/create_params.py index 6b58b13..6c2a998 100644 --- a/src/didit/types/users/create_params.py +++ b/src/didit/types/users/create_params.py @@ -11,7 +11,7 @@ class UsersCreateParams(TypedDict, total=False): vendor_data: Required[str] - """Your unique identifier for this user (free-form string, NOT a UUID). Must be unique among non-deleted users for the application; matched exactly as sent.""" + """Your unique identifier for this user (free-form string, NOT a UUID). Must be unique among the application's users; matched exactly as sent.""" full_name: str | None """Full legal name of the user.""" @@ -36,3 +36,27 @@ class UsersCreateParams(TypedDict, total=False): issuing_states: Sequence[str] """Pre-recorded issuing countries (ISO 3166-1 alpha-3), e.g. `["USA"]`.""" + + first_name: str + """Given name. When `full_name` is not sent, Didit joins `first_name` and `last_name` with a space into `full_name`. Not returned as a separate field.""" + + last_name: str + """Family name. Joined with `first_name` into `full_name` when `full_name` is not sent. Not returned as a separate field.""" + + email: str + """Contact email, stored in `metadata.contact.email` (it replaces an `email` you send inside `metadata.contact`). Required when `send_verification_link` is `true`: the link is sent to this address. If `metadata.contact` is not a JSON object, the call answers `400` on this field.""" + + phone_number: str + """Contact phone number, stored in `metadata.contact.phone_number`. If `metadata.contact` is not a JSON object, the call answers `400` on this field.""" + + country: str + """Country as an ISO 3166 code, alpha-2 or alpha-3 (`ES` or `ESP`). Stored uppercased in `metadata.address.country`. If `metadata.address` is not a JSON object, the call answers `400` on this field.""" + + send_verification_link: bool + """When `true`, Didit creates a verification session for the person on `workflow_id` and emails them the link at `email`. Requires `email` and `workflow_id`. A scoped API key also needs `sessions` write.""" + + workflow_id: str + """The workflow of the verification session. Required when `send_verification_link` is `true`. It must verify a person: a Business Verification (KYB) workflow is refused. Ignored otherwise.""" + + language: str + """Language of the verification email, as a [supported language](/integration/supported-languages) code. English when omitted. Ignored when `send_verification_link` is not `true`.""" diff --git a/src/didit/types/users/create_response.py b/src/didit/types/users/create_response.py new file mode 100644 index 0000000..06ff61a --- /dev/null +++ b/src/didit/types/users/create_response.py @@ -0,0 +1,192 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "UsersCreateResponse", + "UsersCreateResponseComments", + "UsersCreateResponseFeaturesList", + "UsersCreateResponseTags", + "UsersCreateResponseTagsTag", + "UsersCreateResponseVerificationLink", +] + + +class UsersCreateResponseComments(BaseModel): + uuid: str | None = None + + comment_type: ( + Literal[ + "COMMENT", + "STATUS_CHANGED", + "METADATA_UPDATED", + "TAG_ADDED", + "TAG_REMOVED", + "PROFILE_UPDATED", + ] + | str + | None + ) = None + + comment: str | None = None + + actor_email: str | None = None + + actor_name: str | None = None + + previous_status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | None = None + + new_status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | None = None + + previous_value: dict[str, object] | None = None + """Previous field values for PROFILE_UPDATED entries.""" + + new_value: dict[str, object] | None = None + """New field values for PROFILE_UPDATED entries.""" + + changed_fields: list[str] | None = None + """Names of the fields changed by a PROFILE_UPDATED entry.""" + + metadata: dict[str, object] | None = None + + mentioned_emails: list[str] | None = None + + created_at: _dt.datetime | None = None + + +class UsersCreateResponseFeaturesList(BaseModel): + feature: str | None = None + + status: str | None = None + + +class UsersCreateResponseTagsTag(BaseModel): + uuid: str | None = None + + name: str | None = None + + color: str | None = None + + description: str | None = None + + source: Literal["didit", "custom"] | str | None = None + + created_at: _dt.datetime | None = None + + updated_at: _dt.datetime | None = None + + +class UsersCreateResponseTags(BaseModel): + uuid: str | None = None + """UUID of the tag assignment (not the tag itself).""" + + tag: UsersCreateResponseTagsTag | None = None + + added_by_email: str | None = None + + added_by_name: str | None = None + + created_at: _dt.datetime | None = None + + +class UsersCreateResponseVerificationLink(BaseModel): + """The verification session Didit created and emailed to the person. Always present: `null` when `send_verification_link` was not `true`.""" + + session_id: str + """ID of the verification session.""" + + session_number: int + """Sequential number of the session inside your application.""" + + status: str + """Status of the session, `Not Started` for a session that was just created.""" + + url: str + """The verification link Didit emailed to the person.""" + + workflow_id: str | None + """The workflow the session runs.""" + + email: str + """The address the link was sent to.""" + + +class UsersCreateResponse(BaseModel): + didit_internal_id: str | None = None + """Didit's stable internal UUID for this user.""" + + vendor_data: str | None = None + """Your unique identifier for this user (passed when creating sessions). This can be null when no vendor identifier was supplied.""" + + display_name: str | None = None + """Custom display name set by you""" + + full_name: str | None = None + """Full name extracted from verified documents""" + + date_of_birth: _dt.date | None = None + + effective_name: str | None = None + """Best available name: display_name if set, otherwise full_name""" + + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | None = None + """Lifecycle status of the user record (NOT a session status). `ACTIVE` is the default, `FLAGGED` marks the user for manual attention, `BLOCKED` prevents new sessions for this `vendor_data`.""" + + portrait_image_url: str | None = None + """Presigned URL of the user's portrait photo (expires after a few hours)""" + + session_count: int | None = None + """Total number of verification sessions for this user""" + + approved_count: int | None = None + """Number of approved sessions""" + + declined_count: int | None = None + """Number of declined sessions""" + + in_review_count: int | None = None + """Number of sessions in review""" + + issuing_states: list[str] | None = None + """ISO 3166-1 alpha-3 codes of issuing countries seen on this user's approved ID documents, e.g. `["USA", "ESP"]`. Empty array when none.""" + + approved_emails: list[str] | None = None + """Verified email addresses collected from this user's approved sessions, e.g. `["john@example.com"]`.""" + + approved_phones: list[str] | None = None + """Verified phone numbers collected from this user's approved sessions, e.g. `["+14155551234"]`.""" + + features: dict[str, object] | None = None + """Aggregated per-feature status across all of this user's sessions. Possible keys: `ID_VERIFICATION`, `NFC`, `LIVENESS`, `FACE_MATCH`, `POA`, `QUESTIONNAIRE`, `EMAIL_VERIFICATION`, `PHONE`, `AML`, `IP_ANALYSIS`, `AGE_ESTIMATION`, `DATABASE_VALIDATION`. Possible values: `Approved`, `Declined`, `In Review`, `Not Finished`, `Resub Requested`.""" + + features_list: list[UsersCreateResponseFeaturesList] | None = None + """Same data as `features`, as an ordered array of `{feature, status}` objects.""" + + last_session_at: _dt.datetime | None = None + """Timestamp of the most recent session""" + + first_session_at: _dt.datetime | None = None + """Timestamp of the first session""" + + last_activity_at: _dt.datetime | None = None + """Timestamp of the most recent activity on this user (session, transaction, status change, data edit, etc.).""" + + tags: list[UsersCreateResponseTags] | None = None + """Tag assignments. NOTE: on detail responses each entry is a tag *link* object (`{uuid, tag: {...}, added_by_email, added_by_name, created_at}`), unlike the flat `{uuid, name, color}` shape used on list responses.""" + + created_at: _dt.datetime | None = None + + metadata: dict[str, object] | None = None + """Custom metadata JSON you attached to this user. Defaults to `{}`.""" + + comments: list[UsersCreateResponseComments] | None = None + """Activity log and comments for this user (status changes, profile edits, manual notes).""" + + updated_at: _dt.datetime | None = None + + verification_link: UsersCreateResponseVerificationLink | None + """The verification session Didit created and emailed to the person. Always present: `null` when `send_verification_link` was not `true`.""" diff --git a/src/didit/types/users/set_aml_monitoring_response.py b/src/didit/types/users/set_aml_monitoring_response.py deleted file mode 100644 index f567974..0000000 --- a/src/didit/types/users/set_aml_monitoring_response.py +++ /dev/null @@ -1,299 +0,0 @@ -# Code generated by codegen from openapi/*.json. DO NOT EDIT. -from __future__ import annotations - -import datetime as _dt -from typing import Literal - -from didit._models import BaseModel - -__all__ = [ - "UsersSetAmlMonitoringResponse202", - "UsersSetAmlMonitoringResponse202Estimate", - "UsersSetAmlMonitoringResponse202Outcomes", - "UsersSetAmlMonitoringResponse202Results", - "UsersSetAmlMonitoringResponseVariant1", - "UsersSetAmlMonitoringResponseVariant2", - "UsersSetAmlMonitoringResponseVariant2Estimate", - "UsersSetAmlMonitoringResponseVariant2Outcomes", - "UsersSetAmlMonitoringResponseVariant2Results", -] - - -class UsersSetAmlMonitoringResponseVariant1(BaseModel): - """Cost preview. Returned directly when estimate_only is true (nothing is written).""" - - users_total: int - """Profiles matched by the selection.""" - - already_enabled: int - """Profiles already monitored (nothing to do when enabling; these are what disabling turns off).""" - - with_existing_aml: int - """Profiles whose existing AML screening will be switched to monitored (no new screening).""" - - needing_screening: int - """Profiles without any AML screening that will be screened first (billed as a regular AML screening).""" - - skipped_no_identity_data: int - """Profiles that cannot be screened because identity data is missing.""" - - skipped_blocked: int - """Blocked users that will be skipped (always 0 for businesses).""" - - estimated_screening_cost_usd: float - """One-time cost of the new screenings.""" - - estimated_annual_monitoring_cost_usd: float - """Yearly monitoring cost of the profiles that will become monitored.""" - - balance_sufficient: bool - """Whether the organization balance covers the screening plus the first year of monitoring.""" - - unmatched: list[str] - """Identifiers of the request that matched no profile in this application, exactly as sent.""" - - -class UsersSetAmlMonitoringResponseVariant2Estimate(BaseModel): - """Cost preview. Returned directly when estimate_only is true (nothing is written).""" - - users_total: int | None = None - """Profiles matched by the selection.""" - - already_enabled: int | None = None - """Profiles already monitored (nothing to do when enabling; these are what disabling turns off).""" - - with_existing_aml: int | None = None - """Profiles whose existing AML screening will be switched to monitored (no new screening).""" - - needing_screening: int | None = None - """Profiles without any AML screening that will be screened first (billed as a regular AML screening).""" - - skipped_no_identity_data: int | None = None - """Profiles that cannot be screened because identity data is missing.""" - - skipped_blocked: int | None = None - """Blocked users that will be skipped (always 0 for businesses).""" - - estimated_screening_cost_usd: float | None = None - """One-time cost of the new screenings.""" - - estimated_annual_monitoring_cost_usd: float | None = None - """Yearly monitoring cost of the profiles that will become monitored.""" - - balance_sufficient: bool | None = None - """Whether the organization balance covers the screening plus the first year of monitoring.""" - - -class UsersSetAmlMonitoringResponseVariant2Outcomes(BaseModel): - """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" - - enabled: int | None = None - """Monitoring enabled on an existing AML screening.""" - - screened_and_enabled: int | None = None - """A new AML screening was created (billed) and monitoring enabled on it.""" - - already_enabled: int | None = None - """Monitoring was already active; nothing changed and nothing was billed.""" - - disabled: int | None = None - """Monitoring turned off across every AML screening of the profile.""" - - skipped_no_identity_data: int | None = None - """Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen.""" - - skipped_blocked: int | None = None - """Skipped: the user is blocked.""" - - skipped_sandbox: int | None = None - """Skipped: sandbox application, no screening is created.""" - - failed_provider: int | None = None - """The screening could not be completed; retry later.""" - - failed_insufficient_balance: int | None = None - """Balance ran out while the job was running.""" - - -class UsersSetAmlMonitoringResponseVariant2Results(BaseModel): - didit_internal_id: str | None = None - """Didit internal id of the profile.""" - - vendor_data: str | None = None - """Your own identifier for the profile.""" - - outcome: ( - Literal[ - "enabled", - "screened_and_enabled", - "already_enabled", - "disabled", - "skipped_no_identity_data", - "skipped_blocked", - "skipped_sandbox", - "failed_provider", - "failed_insufficient_balance", - ] - | str - | None - ) = None - - -class UsersSetAmlMonitoringResponseVariant2(BaseModel): - job_id: str - - status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str - - is_enabled: bool - """true when the job enables monitoring, false when it disables it.""" - - target_type: Literal["user", "business"] | str - """Which profile family the job toggles.""" - - total: int - """Profiles the job processes.""" - - processed: int - """Profiles processed so far.""" - - outcomes: UsersSetAmlMonitoringResponseVariant2Outcomes - """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" - - results: list[UsersSetAmlMonitoringResponseVariant2Results] - """One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API.""" - - error: str | None - """Generic failure message when status is FAILED; the detailed cause is kept on the platform side.""" - - created_at: _dt.datetime - - completed_at: _dt.datetime | None - - estimate: UsersSetAmlMonitoringResponseVariant2Estimate - """Cost preview. Returned directly when estimate_only is true (nothing is written).""" - - unmatched: list[str] - """Identifiers of the request that matched no profile in this application, exactly as sent. They are not part of the job.""" - - -class UsersSetAmlMonitoringResponse202Estimate(BaseModel): - """Cost preview. Returned directly when estimate_only is true (nothing is written).""" - - users_total: int | None = None - """Profiles matched by the selection.""" - - already_enabled: int | None = None - """Profiles already monitored (nothing to do when enabling; these are what disabling turns off).""" - - with_existing_aml: int | None = None - """Profiles whose existing AML screening will be switched to monitored (no new screening).""" - - needing_screening: int | None = None - """Profiles without any AML screening that will be screened first (billed as a regular AML screening).""" - - skipped_no_identity_data: int | None = None - """Profiles that cannot be screened because identity data is missing.""" - - skipped_blocked: int | None = None - """Blocked users that will be skipped (always 0 for businesses).""" - - estimated_screening_cost_usd: float | None = None - """One-time cost of the new screenings.""" - - estimated_annual_monitoring_cost_usd: float | None = None - """Yearly monitoring cost of the profiles that will become monitored.""" - - balance_sufficient: bool | None = None - """Whether the organization balance covers the screening plus the first year of monitoring.""" - - -class UsersSetAmlMonitoringResponse202Outcomes(BaseModel): - """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" - - enabled: int | None = None - """Monitoring enabled on an existing AML screening.""" - - screened_and_enabled: int | None = None - """A new AML screening was created (billed) and monitoring enabled on it.""" - - already_enabled: int | None = None - """Monitoring was already active; nothing changed and nothing was billed.""" - - disabled: int | None = None - """Monitoring turned off across every AML screening of the profile.""" - - skipped_no_identity_data: int | None = None - """Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen.""" - - skipped_blocked: int | None = None - """Skipped: the user is blocked.""" - - skipped_sandbox: int | None = None - """Skipped: sandbox application, no screening is created.""" - - failed_provider: int | None = None - """The screening could not be completed; retry later.""" - - failed_insufficient_balance: int | None = None - """Balance ran out while the job was running.""" - - -class UsersSetAmlMonitoringResponse202Results(BaseModel): - didit_internal_id: str | None = None - """Didit internal id of the profile.""" - - vendor_data: str | None = None - """Your own identifier for the profile.""" - - outcome: ( - Literal[ - "enabled", - "screened_and_enabled", - "already_enabled", - "disabled", - "skipped_no_identity_data", - "skipped_blocked", - "skipped_sandbox", - "failed_provider", - "failed_insufficient_balance", - ] - | str - | None - ) = None - - -class UsersSetAmlMonitoringResponse202(BaseModel): - job_id: str | None = None - - status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None - - is_enabled: bool | None = None - """true when the job enables monitoring, false when it disables it.""" - - target_type: Literal["user", "business"] | str | None = None - """Which profile family the job toggles.""" - - total: int | None = None - """Profiles the job processes.""" - - processed: int | None = None - """Profiles processed so far.""" - - outcomes: UsersSetAmlMonitoringResponse202Outcomes | None = None - """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" - - results: list[UsersSetAmlMonitoringResponse202Results] | None = None - """One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API.""" - - error: str | None = None - """Generic failure message when status is FAILED; the detailed cause is kept on the platform side.""" - - created_at: _dt.datetime | None = None - - completed_at: _dt.datetime | None = None - - estimate: UsersSetAmlMonitoringResponse202Estimate | None = None - """Cost preview. Returned directly when estimate_only is true (nothing is written).""" - - unmatched: list[str] | None = None - """Identifiers of the request that matched no profile in this application, exactly as sent. They are not part of the job.""" diff --git a/src/didit/types/users/set_ongoing_monitoring_response.py b/src/didit/types/users/set_ongoing_monitoring_response.py deleted file mode 100644 index e61816b..0000000 --- a/src/didit/types/users/set_ongoing_monitoring_response.py +++ /dev/null @@ -1,193 +0,0 @@ -# Code generated by codegen from openapi/*.json. DO NOT EDIT. -from __future__ import annotations - -from typing import Literal - -from didit._models import BaseModel - -__all__ = [ - "UsersSetOngoingMonitoringResponse202", - "UsersSetOngoingMonitoringResponse202Estimate", - "UsersSetOngoingMonitoringResponse202Outcomes", - "UsersSetOngoingMonitoringResponseVariant1", - "UsersSetOngoingMonitoringResponseVariant2", - "UsersSetOngoingMonitoringResponseVariant2Estimate", - "UsersSetOngoingMonitoringResponseVariant2Outcomes", -] - - -class UsersSetOngoingMonitoringResponseVariant1(BaseModel): - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" - - users_total: int | None = None - """Entities in the selection.""" - - already_enabled: int | None = None - - with_existing_aml: int | None = None - """Entities whose existing AML screening will be switched to monitored.""" - - needing_screening: int | None = None - """Entities without any AML screening that will be screened first (billed).""" - - skipped_no_identity_data: int | None = None - - skipped_blocked: int | None = None - - estimated_screening_cost_usd: float | None = None - - estimated_annual_monitoring_cost_usd: float | None = None - - balance_sufficient: bool | None = None - - -class UsersSetOngoingMonitoringResponseVariant2Estimate(BaseModel): - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" - - users_total: int | None = None - """Entities in the selection.""" - - already_enabled: int | None = None - - with_existing_aml: int | None = None - """Entities whose existing AML screening will be switched to monitored.""" - - needing_screening: int | None = None - """Entities without any AML screening that will be screened first (billed).""" - - skipped_no_identity_data: int | None = None - - skipped_blocked: int | None = None - - estimated_screening_cost_usd: float | None = None - - estimated_annual_monitoring_cost_usd: float | None = None - - balance_sufficient: bool | None = None - - -class UsersSetOngoingMonitoringResponseVariant2Outcomes(BaseModel): - """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" - - enabled: int | None = None - """Monitoring enabled on an existing AML screening.""" - - screened_and_enabled: int | None = None - """A new AML screening was created (billed) and monitoring enabled.""" - - already_enabled: int | None = None - """Monitoring was already active (legacy duplicate rows are consolidated).""" - - disabled: int | None = None - """Monitoring disabled on every AML row of the entity.""" - - skipped_no_identity_data: int | None = None - """Skipped: no full name + date of birth + country available to screen.""" - - skipped_blocked: int | None = None - """Skipped: the user is blocked.""" - - skipped_sandbox: int | None = None - """Skipped: sandbox application.""" - - failed_provider: int | None = None - """The screening provider call failed.""" - - failed_insufficient_balance: int | None = None - """Balance ran out mid-job.""" - - -class UsersSetOngoingMonitoringResponseVariant2(BaseModel): - job_id: str | None = None - - status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None - - is_enabled: bool | None = None - - total: int | None = None - - processed: int | None = None - - outcomes: UsersSetOngoingMonitoringResponseVariant2Outcomes | None = None - """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" - - error: str | None = None - - estimate: UsersSetOngoingMonitoringResponseVariant2Estimate | None = None - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" - - -class UsersSetOngoingMonitoringResponse202Estimate(BaseModel): - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" - - users_total: int | None = None - """Entities in the selection.""" - - already_enabled: int | None = None - - with_existing_aml: int | None = None - """Entities whose existing AML screening will be switched to monitored.""" - - needing_screening: int | None = None - """Entities without any AML screening that will be screened first (billed).""" - - skipped_no_identity_data: int | None = None - - skipped_blocked: int | None = None - - estimated_screening_cost_usd: float | None = None - - estimated_annual_monitoring_cost_usd: float | None = None - - balance_sufficient: bool | None = None - - -class UsersSetOngoingMonitoringResponse202Outcomes(BaseModel): - """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" - - enabled: int | None = None - """Monitoring enabled on an existing AML screening.""" - - screened_and_enabled: int | None = None - """A new AML screening was created (billed) and monitoring enabled.""" - - already_enabled: int | None = None - """Monitoring was already active (legacy duplicate rows are consolidated).""" - - disabled: int | None = None - """Monitoring disabled on every AML row of the entity.""" - - skipped_no_identity_data: int | None = None - """Skipped: no full name + date of birth + country available to screen.""" - - skipped_blocked: int | None = None - """Skipped: the user is blocked.""" - - skipped_sandbox: int | None = None - """Skipped: sandbox application.""" - - failed_provider: int | None = None - """The screening provider call failed.""" - - failed_insufficient_balance: int | None = None - """Balance ran out mid-job.""" - - -class UsersSetOngoingMonitoringResponse202(BaseModel): - job_id: str | None = None - - status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None - - is_enabled: bool | None = None - - total: int | None = None - - processed: int | None = None - - outcomes: UsersSetOngoingMonitoringResponse202Outcomes | None = None - """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" - - error: str | None = None - - estimate: UsersSetOngoingMonitoringResponse202Estimate | None = None - """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" diff --git a/src/didit/types/users/update_params.py b/src/didit/types/users/update_params.py index f9e3ca5..799a972 100644 --- a/src/didit/types/users/update_params.py +++ b/src/didit/types/users/update_params.py @@ -20,7 +20,7 @@ class UsersUpdateParams(TypedDict, total=False): """Date of birth in YYYY-MM-DD format""" status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str - """User lifecycle status. Changing it via this generic PATCH does NOT sync the system blocklist or record a status-change activity; prefer the update-status endpoint.""" + """User lifecycle status. A change is recorded as a `STATUS_CHANGED` activity, and `BLOCKED` adds the `vendor_data` to the system blocklist (moving away from `BLOCKED` removes that entry).""" metadata: Mapping[str, object] | None """Arbitrary JSON object you attach to the user. **Fully replaces** the existing metadata on update - merge client-side if you only want to add keys.""" @@ -33,3 +33,6 @@ class UsersUpdateParams(TypedDict, total=False): issuing_states: Sequence[str] """Issuing countries (ISO 3166-1 alpha-3), e.g. `["USA"]`. Fully replaces the stored list.""" + + reason: str | None + """Why the status changed. Recorded on the status-change entry of the user's activity (`comments[].comment`, and **Activity** in the Business Console), only when the status actually changes. Webhooks do not carry it.""" diff --git a/src/didit/types/users/update_status_params.py b/src/didit/types/users/update_status_params.py index 2b0f161..6c19b11 100644 --- a/src/didit/types/users/update_status_params.py +++ b/src/didit/types/users/update_status_params.py @@ -11,3 +11,6 @@ class UsersUpdateStatusParams(TypedDict, total=False): status: Required[Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str] """New lifecycle status. `BLOCKED` also adds the `vendor_data` to the system blocklist.""" + + reason: str | None + """Why the status changed. Recorded on the status-change entry of the user's activity (`comments[].comment`, and **Activity** in the Business Console), only when the status actually changes. Webhooks do not carry it.""" diff --git a/src/didit/types/wallet_screening/screen_params.py b/src/didit/types/wallet_screening/screen_params.py index 1d27b5f..e2dc924 100644 --- a/src/didit/types/wallet_screening/screen_params.py +++ b/src/didit/types/wallet_screening/screen_params.py @@ -30,10 +30,13 @@ class WalletScreeningScreenParams(TypedDict, total=False): "BASE", "OP", "ARB", + "CELO", + "MODE", + "PLUME", ] | str ] - """Network identifier. Managed screening supports BTC, ETH, LTC, XRP, BCH, DOGE, TRX, SOL, MATIC, BNB, BASE (Base), OP (Optimism), and ARB (Arbitrum One). USDT and USDC are legacy Ethereum aliases, not multi-network searches. Connected-provider coverage varies. LIGHTNING requires a supported connected provider. Celo, Mode and Plume are not currently supported. Alias: `currency`.""" + """Network identifier. Managed screening supports BTC, ETH, LTC, XRP, BCH, DOGE, TRX, SOL, MATIC, BNB, BASE (Base), OP (Optimism), and ARB (Arbitrum One). USDT and USDC are legacy Ethereum aliases, not multi-network searches. Connected-provider coverage varies. LIGHTNING requires a supported connected provider. CELO (Celo), MODE (Mode) and PLUME (Plume) are screened through a connected TRM Labs account; managed screening answers 400 for them. Alias: `currency`.""" direction: Literal["inbound", "outbound", "deposit", "withdrawal"] | str """Optional. Whether the address is being screened as an inbound (deposit) or outbound (withdrawal) counterparty. Accepts `inbound`, `outbound`, `deposit`, or `withdrawal`. Defaults to a neutral pre-transfer screen when omitted. This is passed to the provider as context only; it does not change the returned `risk_score`, `severity`, or the `source_of_funds`/`destination_of_funds` breakdowns, which are always both returned regardless of `direction`. Changing or omitting `direction` therefore does not require re-screening an address.""" diff --git a/src/didit/types/webhooks/destinations/create_params.py b/src/didit/types/webhooks/destinations/create_params.py index 3e9f197..afa44f4 100644 --- a/src/didit/types/webhooks/destinations/create_params.py +++ b/src/didit/types/webhooks/destinations/create_params.py @@ -15,7 +15,7 @@ class WebhooksDestinationsCreateParams(TypedDict, total=False): """Human-readable name displayed in Console → Webhooks. Helps distinguish destinations when you have several (e.g. "Prod", "Staging mirror").""" url: Required[str] - """HTTPS URL that will receive `POST` requests. Must be unique per application among non-deleted destinations. Use a stable, hardened endpoint - Didit retries failed deliveries on a backoff schedule.""" + """HTTPS URL that will receive `POST` requests. Must be unique among the application's destinations. Use a stable, hardened endpoint: Didit retries failed deliveries on a backoff schedule.""" enabled: bool """When `false`, the destination is saved but Didit will not push any webhooks to it. Useful for staging configs you want to keep parked.""" diff --git a/src/didit/types/webhooks/destinations/create_response.py b/src/didit/types/webhooks/destinations/create_response.py index aaad01b..2481b0a 100644 --- a/src/didit/types/webhooks/destinations/create_response.py +++ b/src/didit/types/webhooks/destinations/create_response.py @@ -11,7 +11,7 @@ class WebhooksDestinationsCreateResponseSummary(BaseModel): - """Delivery health summary (all zero/null on a brand-new destination).""" + """Delivery metrics for the last `metrics_window_days` (30) days. `last_delivery_at` is the most recent attempt, whenever it was made.""" total_deliveries: int | None = None @@ -27,6 +27,9 @@ class WebhooksDestinationsCreateResponseSummary(BaseModel): last_delivery_at: _dt.datetime | None = None + metrics_window_days: int | None = None + """Length in days of the window the other summary metrics cover. Always `30`.""" + class WebhooksDestinationsCreateResponse(BaseModel): uuid: str | None = None @@ -43,12 +46,39 @@ class WebhooksDestinationsCreateResponse(BaseModel): secret_shared_key: str | None = None """Auto-generated HMAC signing secret bound to this destination (43-character URL-safe token). Use it to verify the signature headers on incoming webhooks. Always returned in full to API-key callers; Console users need the `read:webhooks` permission, otherwise they see an empty string.""" + secret_last4: str | None = None + """Last four characters of the destination's current signing secret, so you can tell secrets apart. Not enough to verify a signature.""" + + previous_secret_expires_at: _dt.datetime | None = None + """Set for 24 hours after the destination's signing secret is rotated in the Business Console. Until this time, every delivery to the destination also carries `X-Signature-Previous`, `X-Signature-V2-Previous` and `X-Signature-Simple-Previous`, made with the replaced secret (see [Signature verification](/integration/webhooks#signature-verification)). `null` when no rotation happened in the last 24 hours.""" + subscribed_events: WebhookSubscribedEvents | None = None """Event filter for this webhook destination. Didit delivers only webhooks whose event type exactly matches one of these values - there is no wildcard subscription. When sent, the list must contain at least one valid event (an explicit `[]` is rejected with 400); a destination whose list is empty (field omitted at create) receives nothing. On update the list is replaced wholesale, never merged.""" + created_by_email: str | None = None + """Email of the Business Console member who created the destination. `null` for a destination created with an API key.""" + + created_by_name: str | None = None + """Name of the Business Console member who created the destination. `null` for a destination created with an API key.""" + created_at: _dt.datetime | None = None updated_at: _dt.datetime | None = None + health_status: Literal["healthy", "degraded", "failing"] | str | None = None + """Delivery health. `failing`: 3 or more consecutive events failed their first delivery attempt since the destination last answered successfully (see `failing_streak`). `degraded`: not failing, but a delivery attempt failed in the last 24 hours and no later attempt has delivered that event; a retry or resend that delivers it clears this. `healthy`: neither. Independent of `enabled`. A failed attempt is one answered with HTTP 400 or above, or not answered at all.""" + + failing_streak: int | None = None + """How many consecutive events failed their first delivery attempt since the destination last answered successfully, within the last 30 days. Retries are not counted, and any successful attempt, including a retry or a resend, ends the streak. `0` when no event has failed since the last success.""" + + failing_since: _dt.datetime | None = None + """When the first failed attempt of the current `failing_streak` was made. `null` when `failing_streak` is `0`.""" + + last_success_at: _dt.datetime | None = None + """When the destination last answered a delivery with an HTTP status under 400, within the last 30 days. `null` if it has not in that window.""" + + environment: Literal["live", "sandbox"] | str | None = None + """Environment of the application that owns the destination.""" + summary: WebhooksDestinationsCreateResponseSummary | None = None - """Delivery health summary (all zero/null on a brand-new destination).""" + """Delivery metrics for the last `metrics_window_days` (30) days. `last_delivery_at` is the most recent attempt, whenever it was made.""" diff --git a/src/didit/types/webhooks/destinations/list_deliveries_response.py b/src/didit/types/webhooks/destinations/list_deliveries_response.py new file mode 100644 index 0000000..d5b1253 --- /dev/null +++ b/src/didit/types/webhooks/destinations/list_deliveries_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.webhook_destination_delivery import WebhookDestinationDelivery + +__all__ = ["WebhooksDestinationsListDeliveriesResponse"] + + +class WebhooksDestinationsListDeliveriesResponse(BaseModel): + count: int + """Matching deliveries, exact up to 100 and capped at 100 above that. Use `next` to know whether more pages exist.""" + + next: str | None + """Absolute URL of the next page, or `null` on the last page.""" + + previous: str | None + """Absolute URL of the previous page, or `null` on the first page.""" + + results: list[WebhookDestinationDelivery] diff --git a/src/didit/types/webhooks/destinations/list_response.py b/src/didit/types/webhooks/destinations/list_response.py index fdb3a21..ed5a345 100644 --- a/src/didit/types/webhooks/destinations/list_response.py +++ b/src/didit/types/webhooks/destinations/list_response.py @@ -7,7 +7,21 @@ from didit._models import BaseModel from didit.types.components.webhook_subscribed_events import WebhookSubscribedEvents -__all__ = ["WebhooksDestinationsListResponseItem"] +__all__ = [ + "WebhooksDestinationsListResponseItem", + "WebhooksDestinationsListResponseItemDailyPoints", +] + + +class WebhooksDestinationsListResponseItemDailyPoints(BaseModel): + date: _dt.date | None = None + """The UTC day.""" + + total_deliveries: int | None = None + """Delivery attempts made that day.""" + + failed_deliveries: int | None = None + """Attempts made that day that were answered with HTTP 400 or above, or not answered at all.""" class WebhooksDestinationsListResponseItem(BaseModel): @@ -33,20 +47,50 @@ class WebhooksDestinationsListResponseItem(BaseModel): """Cached length of `subscribed_events`, useful for UI rendering.""" deliveries_count: int | None = None - """Total delivery attempts (successful + failed) recorded for this destination.""" + """Delivery attempts (retries included) made to this destination in the last 30 days.""" failed_deliveries_count: int | None = None - """Delivery attempts whose target responded with HTTP ≥ 400 (or did not respond).""" + """Delivery attempts in the last 30 days that the destination answered with HTTP 400 or above, or did not answer (timeout, refused connection or blocked address).""" average_response_time_ms: int | None = None - """Average target response time in milliseconds, rounded to an integer. `null` if no deliveries yet.""" + """Average response time in milliseconds over the last 30 days, rounded to an integer. `null` when there were no deliveries in that window.""" error_rate_percentage: int | None = None - """`round(failed_deliveries_count / deliveries_count * 100)`; `0` when there are no deliveries.""" + """`round(failed_deliveries_count / deliveries_count * 100)` over the last 30 days; `0` when there are no deliveries.""" last_delivery_at: _dt.datetime | None = None """Timestamp of the most recent delivery attempt (success or failure). `null` if never delivered.""" + metrics_window_days: int | None = None + """Length in days of the window that `deliveries_count`, `failed_deliveries_count`, `average_response_time_ms`, `error_rate_percentage` and `daily_points` cover. Always `30`.""" + + health_status: Literal["healthy", "degraded", "failing"] | str | None = None + """Delivery health. `failing`: 3 or more consecutive events failed their first delivery attempt since the destination last answered successfully (see `failing_streak`). `degraded`: not failing, but a delivery attempt failed in the last 24 hours and no later attempt has delivered that event; a retry or resend that delivers it clears this. `healthy`: neither. Independent of `enabled`. A failed attempt is one answered with HTTP 400 or above, or not answered at all.""" + + failing_streak: int | None = None + """How many consecutive events failed their first delivery attempt since the destination last answered successfully, within the last 30 days. Retries are not counted, and any successful attempt, including a retry or a resend, ends the streak. `0` when no event has failed since the last success.""" + + failing_since: _dt.datetime | None = None + """When the first failed attempt of the current `failing_streak` was made. `null` when `failing_streak` is `0`.""" + + last_success_at: _dt.datetime | None = None + """When the destination last answered a delivery with an HTTP status under 400, within the last 30 days. `null` if it has not in that window.""" + + environment: Literal["live", "sandbox"] | str | None = None + """Environment of the application that owns the destination.""" + + daily_points: list[WebhooksDestinationsListResponseItemDailyPoints] | None = None + """One entry per UTC day of the last 30 days, oldest first, including days with no deliveries. Counts every attempt, retries included.""" + + secret_last4: str | None = None + """Last four characters of the destination's current signing secret, so you can tell secrets apart. Not enough to verify a signature.""" + + created_by_email: str | None = None + """Email of the Business Console member who created the destination. `null` for a destination created with an API key.""" + + created_by_name: str | None = None + """Name of the Business Console member who created the destination. `null` for a destination created with an API key.""" + created_at: _dt.datetime | None = None updated_at: _dt.datetime | None = None diff --git a/src/didit/types/webhooks/destinations/retrieve_analytics_response.py b/src/didit/types/webhooks/destinations/retrieve_analytics_response.py new file mode 100644 index 0000000..b5e0b1f --- /dev/null +++ b/src/didit/types/webhooks/destinations/retrieve_analytics_response.py @@ -0,0 +1,62 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = [ + "WebhooksDestinationsRetrieveAnalyticsResponse", + "WebhooksDestinationsRetrieveAnalyticsResponsePoints", + "WebhooksDestinationsRetrieveAnalyticsResponseSummary", +] + + +class WebhooksDestinationsRetrieveAnalyticsResponsePoints(BaseModel): + date: _dt.date + """UTC day, `YYYY-MM-DD`.""" + + total_deliveries: int + """Delivery attempts, including queued ones.""" + + failed_deliveries: int + """Attempts whose recorded HTTP status is 400 or above.""" + + min_response_time_ms: int | None + """Fastest attempt in milliseconds, or `null` when no attempt has a recorded time.""" + + avg_response_time_ms: int | None + """Mean attempt time in milliseconds, rounded, or `null` when no attempt has a recorded time.""" + + max_response_time_ms: int | None + """Slowest attempt in milliseconds, or `null` when no attempt has a recorded time.""" + + +class WebhooksDestinationsRetrieveAnalyticsResponseSummary(BaseModel): + """Totals over the requested range.""" + + total_deliveries: int + """Delivery attempts, including queued ones.""" + + failed_deliveries: int + """Attempts whose recorded HTTP status is 400 or above.""" + + min_response_time_ms: int | None + """Fastest attempt in milliseconds, or `null` when no attempt has a recorded time.""" + + avg_response_time_ms: int | None + """Mean attempt time in milliseconds, rounded, or `null` when no attempt has a recorded time.""" + + max_response_time_ms: int | None + """Slowest attempt in milliseconds, or `null` when no attempt has a recorded time.""" + + error_rate_percentage: int + """`failed_deliveries` / `total_deliveries` x 100, rounded to a whole number. `0` when there are no deliveries.""" + + +class WebhooksDestinationsRetrieveAnalyticsResponse(BaseModel): + summary: WebhooksDestinationsRetrieveAnalyticsResponseSummary + """Totals over the requested range.""" + + points: list[WebhooksDestinationsRetrieveAnalyticsResponsePoints] + """One entry per UTC day that had deliveries, oldest first. Days without deliveries are omitted, so fill the gaps with zeros when you chart them.""" diff --git a/src/didit/types/webhooks/destinations/retrieve_response.py b/src/didit/types/webhooks/destinations/retrieve_response.py index 7e2613b..beb1808 100644 --- a/src/didit/types/webhooks/destinations/retrieve_response.py +++ b/src/didit/types/webhooks/destinations/retrieve_response.py @@ -11,7 +11,7 @@ class WebhooksDestinationsRetrieveResponseSummary(BaseModel): - """Delivery health metrics aggregated over the destination's lifetime.""" + """Delivery metrics for the last `metrics_window_days` (30) days. `last_delivery_at` is the most recent attempt, whenever it was made.""" total_deliveries: int | None = None @@ -27,6 +27,9 @@ class WebhooksDestinationsRetrieveResponseSummary(BaseModel): last_delivery_at: _dt.datetime | None = None + metrics_window_days: int | None = None + """Length in days of the window the other summary metrics cover. Always `30`.""" + class WebhooksDestinationsRetrieveResponse(BaseModel): uuid: str | None = None @@ -42,12 +45,39 @@ class WebhooksDestinationsRetrieveResponse(BaseModel): secret_shared_key: str | None = None """HMAC signing secret (43-character URL-safe token). Always returned in full to API-key callers; Console users need `read:webhooks`, otherwise an empty string.""" + secret_last4: str | None = None + """Last four characters of the destination's current signing secret, so you can tell secrets apart. Not enough to verify a signature.""" + + previous_secret_expires_at: _dt.datetime | None = None + """Set for 24 hours after the destination's signing secret is rotated in the Business Console. Until this time, every delivery to the destination also carries `X-Signature-Previous`, `X-Signature-V2-Previous` and `X-Signature-Simple-Previous`, made with the replaced secret (see [Signature verification](/integration/webhooks#signature-verification)). `null` when no rotation happened in the last 24 hours.""" + subscribed_events: WebhookSubscribedEvents | None = None """Event filter for this webhook destination. Didit delivers only webhooks whose event type exactly matches one of these values - there is no wildcard subscription. When sent, the list must contain at least one valid event (an explicit `[]` is rejected with 400); a destination whose list is empty (field omitted at create) receives nothing. On update the list is replaced wholesale, never merged.""" + created_by_email: str | None = None + """Email of the Business Console member who created the destination. `null` for a destination created with an API key.""" + + created_by_name: str | None = None + """Name of the Business Console member who created the destination. `null` for a destination created with an API key.""" + created_at: _dt.datetime | None = None updated_at: _dt.datetime | None = None + health_status: Literal["healthy", "degraded", "failing"] | str | None = None + """Delivery health. `failing`: 3 or more consecutive events failed their first delivery attempt since the destination last answered successfully (see `failing_streak`). `degraded`: not failing, but a delivery attempt failed in the last 24 hours and no later attempt has delivered that event; a retry or resend that delivers it clears this. `healthy`: neither. Independent of `enabled`. A failed attempt is one answered with HTTP 400 or above, or not answered at all.""" + + failing_streak: int | None = None + """How many consecutive events failed their first delivery attempt since the destination last answered successfully, within the last 30 days. Retries are not counted, and any successful attempt, including a retry or a resend, ends the streak. `0` when no event has failed since the last success.""" + + failing_since: _dt.datetime | None = None + """When the first failed attempt of the current `failing_streak` was made. `null` when `failing_streak` is `0`.""" + + last_success_at: _dt.datetime | None = None + """When the destination last answered a delivery with an HTTP status under 400, within the last 30 days. `null` if it has not in that window.""" + + environment: Literal["live", "sandbox"] | str | None = None + """Environment of the application that owns the destination.""" + summary: WebhooksDestinationsRetrieveResponseSummary | None = None - """Delivery health metrics aggregated over the destination's lifetime.""" + """Delivery metrics for the last `metrics_window_days` (30) days. `last_delivery_at` is the most recent attempt, whenever it was made.""" diff --git a/src/didit/types/webhooks/destinations/update_response.py b/src/didit/types/webhooks/destinations/update_response.py index 7fc13e7..0be422b 100644 --- a/src/didit/types/webhooks/destinations/update_response.py +++ b/src/didit/types/webhooks/destinations/update_response.py @@ -11,6 +11,8 @@ class WebhooksDestinationsUpdateResponseSummary(BaseModel): + """Delivery metrics for the last `metrics_window_days` (30) days. `last_delivery_at` is the most recent attempt, whenever it was made.""" + total_deliveries: int | None = None failed_deliveries: int | None = None @@ -25,6 +27,9 @@ class WebhooksDestinationsUpdateResponseSummary(BaseModel): last_delivery_at: _dt.datetime | None = None + metrics_window_days: int | None = None + """Length in days of the window the other summary metrics cover. Always `30`.""" + class WebhooksDestinationsUpdateResponse(BaseModel): uuid: str | None = None @@ -39,11 +44,39 @@ class WebhooksDestinationsUpdateResponse(BaseModel): secret_shared_key: str | None = None + secret_last4: str | None = None + """Last four characters of the destination's current signing secret, so you can tell secrets apart. Not enough to verify a signature.""" + + previous_secret_expires_at: _dt.datetime | None = None + """Set for 24 hours after the destination's signing secret is rotated in the Business Console. Until this time, every delivery to the destination also carries `X-Signature-Previous`, `X-Signature-V2-Previous` and `X-Signature-Simple-Previous`, made with the replaced secret (see [Signature verification](/integration/webhooks#signature-verification)). `null` when no rotation happened in the last 24 hours.""" + subscribed_events: WebhookSubscribedEvents | None = None """Event filter for this webhook destination. Didit delivers only webhooks whose event type exactly matches one of these values - there is no wildcard subscription. When sent, the list must contain at least one valid event (an explicit `[]` is rejected with 400); a destination whose list is empty (field omitted at create) receives nothing. On update the list is replaced wholesale, never merged.""" + created_by_email: str | None = None + """Email of the Business Console member who created the destination. `null` for a destination created with an API key.""" + + created_by_name: str | None = None + """Name of the Business Console member who created the destination. `null` for a destination created with an API key.""" + created_at: _dt.datetime | None = None updated_at: _dt.datetime | None = None + health_status: Literal["healthy", "degraded", "failing"] | str | None = None + """Delivery health. `failing`: 3 or more consecutive events failed their first delivery attempt since the destination last answered successfully (see `failing_streak`). `degraded`: not failing, but a delivery attempt failed in the last 24 hours and no later attempt has delivered that event; a retry or resend that delivers it clears this. `healthy`: neither. Independent of `enabled`. A failed attempt is one answered with HTTP 400 or above, or not answered at all.""" + + failing_streak: int | None = None + """How many consecutive events failed their first delivery attempt since the destination last answered successfully, within the last 30 days. Retries are not counted, and any successful attempt, including a retry or a resend, ends the streak. `0` when no event has failed since the last success.""" + + failing_since: _dt.datetime | None = None + """When the first failed attempt of the current `failing_streak` was made. `null` when `failing_streak` is `0`.""" + + last_success_at: _dt.datetime | None = None + """When the destination last answered a delivery with an HTTP status under 400, within the last 30 days. `null` if it has not in that window.""" + + environment: Literal["live", "sandbox"] | str | None = None + """Environment of the application that owns the destination.""" + summary: WebhooksDestinationsUpdateResponseSummary | None = None + """Delivery metrics for the last `metrics_window_days` (30) days. `last_delivery_at` is the most recent attempt, whenever it was made.""" diff --git a/src/didit/types/webhooks/update_config_params.py b/src/didit/types/webhooks/update_config_params.py new file mode 100644 index 0000000..fcbbb40 --- /dev/null +++ b/src/didit/types/webhooks/update_config_params.py @@ -0,0 +1,36 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import TypedDict + +__all__ = ["WebhooksUpdateConfigParams"] + + +class WebhooksUpdateConfigParams(TypedDict, total=False): + """Send only the fields to change. Destinations are not managed here.""" + + webhook_url: str | None + """Legacy application-level webhook URL. Didit uses it only while the application has no enabled destination, and then only for `status.updated` and `data.updated`. It does not create a destination, and the next create, update or delete of a destination replaces it. To receive webhooks, create a destination with `POST /v3/webhook/destinations/` instead.""" + + webhook_version: Literal["v1", "v2", "v3"] | str + """Payload version of the legacy `webhook_url`.""" + + rotate_secret_key: bool + """`true` replaces the application-level `secret_shared_key` with a new random value at once; the new value is in the response. Destination secrets are not changed. Every call with `true` generates a new secret, so it is not idempotent.""" + + capture_method: Literal["MOBILE", "MOBILE_AND_DESKTOP"] | str + """Legacy application setting. The verification flow does not read it.""" + + data_retention_months: int | None + """Automatic deletion window in months (a month counts as 30 days), or `null` for no automatic deletion. A change applies to existing sessions too: every user verification session older than the new window is deleted automatically, and that deletion is irreversible.""" + + face_retention_policy: Literal["delete_with_session", "retain_until_user_deleted"] | str + """What happens to a session's face embedding when the session is deleted. `retain_until_user_deleted` needs `face_retention_days` in the same request or already set. A change applies to session deletions from then on; it creates or purges no existing biometric template.""" + + face_retention_days: int | None + """Retention period, in days, of biometric templates under `retain_until_user_deleted`.""" + + allowed_return_hosts: Sequence[str] + """Replaces the whole list of Digital ID wallet return hosts. Bare hostnames only, such as `shop.example.com`: no scheme, port, path or wildcard. Values are lowercased and duplicates are dropped. Send `[]` to clear the list.""" diff --git a/tests/generated/test_account.py b/tests/generated/test_account.py index 8d3eade..98acbcc 100644 --- a/tests/generated/test_account.py +++ b/tests/generated/test_account.py @@ -3,6 +3,8 @@ from __future__ import annotations +from didit import AsyncPage, SyncPage +from didit.types.account.resend_otp_response import AccountResendOtpResponse from didit.types.components.application import Application from didit.types.components.login_response import LoginResponse from didit.types.components.organization import Organization @@ -67,12 +69,15 @@ def test_account_resend_otp() -> None: auth="none", json_body={"email": "dev@example.com", "password": "sample-secret"}, status=200, - response_json={"detail": "A new verification code has been sent to your email."}, + response_json={ + "message": "A new verification code has been sent to your email.", + "email": "you@yourdomain.com", + }, ), lambda c: c.account.resend_otp(email="dev@example.com", password="sample-secret"), lambda c: c.account.resend_otp(email="dev@example.com", password="sample-secret"), - lambda r: True, - lambda r: True, + lambda r: isinstance(r, AccountResendOtpResponse), + lambda r: isinstance(r, AccountResendOtpResponse), ) @@ -136,6 +141,23 @@ def test_account_applications_create() -> None: "privacy_url": "https://acme.example/privacy", "description": None, "created_at": "2025-06-01T10:00:00Z", + "products": [ + { + "product": "identity", + "status": "enabled", + "modules": { + "user_verification": True, + "business_verification": True, + "case_management": True, + }, + }, + { + "product": "transactions", + "status": "disabled", + "modules": {"travel_rule": True, "case_management": True}, + }, + {"product": "authentication", "status": "disabled", "modules": {}}, + ], }, ), lambda c: c.account.applications.create( @@ -167,14 +189,53 @@ def test_account_applications_list() -> None: path="/organizations/me/a1b2c3d4-5678-90ab-cdef-111111111111/applications/", api="account", auth="account_token", + query={"limit": ["50"], "offset": ["0"]}, status=200, - response_json="value", - strict_type=object, + response_json={ + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "uuid": "b2c3d4e5-6789-01bc-defg-222222222222", + "name": "My App", + "client_id": "S9LIYGSoWNuGMLHsvEt9dQ", + "api_key": "sample-secret", + "website_url": "https://example.com", + "redirect_uris": ["https://example.com/callback"], + "terms_url": "https://example.com/terms", + "privacy_url": "https://example.com/privacy", + "description": "Production application", + "created_at": "2025-06-01T10:00:00Z", + "products": [ + { + "product": "identity", + "status": "enabled", + "modules": { + "user_verification": True, + "business_verification": True, + "case_management": True, + }, + }, + { + "product": "transactions", + "status": "disabled", + "modules": {"travel_rule": True, "case_management": True}, + }, + {"product": "authentication", "status": "disabled", "modules": {}}, + ], + } + ], + }, + ), + lambda c: c.account.applications.list( + "a1b2c3d4-5678-90ab-cdef-111111111111", limit=50, offset=0 ), - lambda c: c.account.applications.list("a1b2c3d4-5678-90ab-cdef-111111111111"), - lambda c: c.account.applications.list("a1b2c3d4-5678-90ab-cdef-111111111111"), - lambda r: True, - lambda r: True, + lambda c: c.account.applications.list( + "a1b2c3d4-5678-90ab-cdef-111111111111", limit=50, offset=0 + ), + lambda r: isinstance(r, SyncPage) and all(isinstance(x1, Application) for x1 in r.results), + lambda r: isinstance(r, AsyncPage) and all(isinstance(x1, Application) for x1 in r.results), ) @@ -198,6 +259,23 @@ def test_account_applications_retrieve() -> None: "privacy_url": "https://example.com/privacy", "description": "Production application", "created_at": "2025-06-01T10:00:00Z", + "products": [ + { + "product": "identity", + "status": "enabled", + "modules": { + "user_verification": True, + "business_verification": True, + "case_management": True, + }, + }, + { + "product": "transactions", + "status": "disabled", + "modules": {"travel_rule": True, "case_management": True}, + }, + {"product": "authentication", "status": "disabled", "modules": {}}, + ], }, ), lambda c: c.account.applications.retrieve( @@ -232,6 +310,23 @@ def test_account_applications_update() -> None: "privacy_url": "https://acme.example/privacy", "description": None, "created_at": "2025-06-01T10:00:00Z", + "products": [ + { + "product": "identity", + "status": "enabled", + "modules": { + "user_verification": True, + "business_verification": True, + "case_management": True, + }, + }, + { + "product": "transactions", + "status": "disabled", + "modules": {"travel_rule": True, "case_management": True}, + }, + {"product": "authentication", "status": "disabled", "modules": {}}, + ], }, ), lambda c: c.account.applications.update( @@ -259,18 +354,26 @@ def test_account_organizations_list() -> None: path="/organizations/me/", api="account", auth="account_token", + query={"limit": ["1"], "offset": ["1"]}, status=200, - response_json=[ - { - "uuid": "a1b2c3d4-5678-90ab-cdef-111111111111", - "name": "developer", - "contact_email": "developer@gmail.com", - "created_at": "2025-06-01T10:00:00Z", - } - ], + response_json={ + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "uuid": "a1b2c3d4-5678-90ab-cdef-111111111111", + "name": "developer", + "contact_email": "developer@gmail.com", + "created_at": "2025-06-01T10:00:00Z", + } + ], + }, + ), + lambda c: c.account.organizations.list(limit=1, offset=1), + lambda c: c.account.organizations.list(limit=1, offset=1), + lambda r: isinstance(r, SyncPage) and all(isinstance(x1, Organization) for x1 in r.results), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, Organization) for x1 in r.results) ), - lambda c: c.account.organizations.list(), - lambda c: c.account.organizations.list(), - lambda r: isinstance(r, list) and all(isinstance(x1, Organization) for x1 in r), - lambda r: isinstance(r, list) and all(isinstance(x1, Organization) for x1 in r), ) diff --git a/tests/generated/test_aml.py b/tests/generated/test_aml.py index bfc57e0..c50da4c 100644 --- a/tests/generated/test_aml.py +++ b/tests/generated/test_aml.py @@ -3,10 +3,50 @@ from __future__ import annotations +from didit.types.aml.retrieve_dataset_freshness_response import AmlRetrieveDatasetFreshnessResponse from didit.types.aml.screen_response import AmlScreenResponse from tests.harness import Expect, check_operation +def test_aml_retrieve_dataset_freshness() -> None: + """``GET /v3/aml/dataset-freshness/``""" + check_operation( + Expect( + method="GET", + path="/v3/aml/dataset-freshness/", + api="verification", + auth="api_key", + status=200, + response_json={ + "dataset_freshness": { + "captured_at": "2026-10-17T06:00:12.345678+00:00", + "provider": "aml_data_source", + "catalog_version": "20261017054500-abc", + "sources": { + "sanctions": { + "last_updated_at": "2026-10-17T04:10:00+00:00", + "oldest_updated_at": "2026-10-15T22:30:00+00:00", + "dataset_count": 48, + "dated_dataset_count": 48, + }, + "pep": { + "last_updated_at": "2026-10-16T23:05:00+00:00", + "oldest_updated_at": None, + "dataset_count": 12, + "dated_dataset_count": 11, + }, + }, + }, + "availability": "available", + }, + ), + lambda c: c.aml.retrieve_dataset_freshness(), + lambda c: c.aml.retrieve_dataset_freshness(), + lambda r: isinstance(r, AmlRetrieveDatasetFreshnessResponse), + lambda r: isinstance(r, AmlRetrieveDatasetFreshnessResponse), + ) + + def test_aml_screen() -> None: """``POST /v3/aml/``""" check_operation( diff --git a/tests/generated/test_analytics.py b/tests/generated/test_analytics.py new file mode 100644 index 0000000..7ad6d0e --- /dev/null +++ b/tests/generated/test_analytics.py @@ -0,0 +1,311 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``analytics`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.components.application_analytics_summary import ApplicationAnalyticsSummary +from didit.types.components.application_analytics_widget_response import ( + ApplicationAnalyticsWidgetResponse, +) +from tests.harness import Expect, check_operation + + +def test_analytics_retrieve() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/analytics/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/analytics/", + api="verification", + auth="api_key", + query={ + "start_date": ["2026-10-01"], + "end_date": ["2026-10-15"], + "scope": ["home"], + "widget_keys": ["tx_count_total,tx_count_daily"], + "workflow_ids": ["33333333-4444-5555-6666-777777777777"], + "versions": ["3,4"], + "branch_path_ids": ["default"], + "tag": ["sample"], + "transaction_type": ["finance"], + "action_type": ["withdrawal"], + }, + status=200, + response_json={ + "grouping_mode": "day", + "data_points": [ + { + "rounded_date": "2026-10-14", + "total": 12, + "approved": 8, + "declined": 1, + "canceled": 1, + "pending": 2, + "in_review": 1, + "expired": 1, + "abandoned": 0, + "in_progress": 1, + "not_started": 0, + "manually_approved": 1, + "manually_declined": 0, + }, + { + "rounded_date": "2026-10-15", + "total": 10, + "approved": 6, + "declined": 2, + "canceled": 0, + "pending": 2, + "in_review": 2, + "expired": 0, + "abandoned": 0, + "in_progress": 0, + "not_started": 0, + "manually_approved": 0, + "manually_declined": 1, + }, + ], + "request_breakdown": { + "total_approved": 14, + "total_in_review": 3, + "total_declined": 3, + "total_canceled": 1, + "total_pending": 4, + "total_expired": 1, + "total_abandoned": 0, + "total_in_progress": 1, + "total_not_started": 0, + "total_manually_approved": 1, + "total_manually_declined": 1, + }, + "conversion_rate": 70.0, + "location_stats": { + "countries": [ + {"session_count": 15, "country": "Spain", "country_code": "ES"}, + {"session_count": 7, "country": "Portugal", "country_code": "PT"}, + ], + "ip_countries": [ + {"session_count": 15, "country": "Spain", "country_code": "ES"}, + {"session_count": 7, "country": "Portugal", "country_code": "PT"}, + ], + "id_countries": [ + {"country": "Spain", "country_code": "ES", "session_count": 14}, + {"country": "Portugal", "country_code": "PT", "session_count": 6}, + ], + "conversion_by_country": [ + {"country": "Spain", "country_code": "ES", "finished": 13, "approved": 10}, + {"country": "Portugal", "country_code": "PT", "finished": 6, "approved": 4}, + ], + "cities": [ + { + "session_count": 9, + "location": "Madrid, Spain", + "latitude": 40.4168, + "longitude": -3.7038, + } + ], + }, + "demographic_stats": { + "age_distribution": { + "under_18": 0, + "18_24": 5, + "25_34": 8, + "35_44": 4, + "45_64": 2, + "65_plus": 1, + }, + "gender_distribution": {"M": 11, "F": 9}, + }, + "device_details": { + "device_models": [ + {"device": "Apple", "count": 12}, + {"device": "Android", "count": 10}, + ], + "browsers": [ + {"browser": "Safari", "count": 12}, + {"browser": "Chrome", "count": 10}, + ], + "operating_systems": [ + {"os": "iOS", "count": 12}, + {"os": "Android", "count": 10}, + ], + }, + "workflow_funnel_stats": { + "33333333-4444-5555-6666-777777777777": { + "total": 22, + "steps": { + "OCR": {"reached": 21}, + "LIVENESS": {"reached": 19}, + "FACE_MATCH": {"reached": 19}, + "AML": {"reached": 18}, + }, + } + }, + "verification_time_stats": { + "overall_avg_seconds": 64.0, + "overall_count": 18, + "per_feature": { + "OCR": {"avg_seconds": 41.2, "total_seconds": 741.6, "count": 18}, + "LIVENESS": {"avg_seconds": 22.8, "total_seconds": 410.4, "count": 18}, + }, + "per_sub_step": { + "OCR": { + "front_id_photo": { + "avg_seconds": 18.3, + "total_seconds": 329.4, + "count": 18, + } + } + }, + }, + "resubmission_stats": { + "first_pass": 17, + "second": 2, + "third_plus": 1, + "abandoned": 0, + }, + "warning_stats": { + "ID_VERIFICATION": { + "total": 21, + "risks": {"DOCUMENT_EXPIRED": {"error": 1, "warning": 0, "information": 0}}, + }, + "LIVENESS": { + "total": 19, + "risks": { + "LOW_LIVENESS_SCORE": {"error": 0, "warning": 2, "information": 0} + }, + }, + }, + "original_status_stats": {"approved": 13, "in_review": 5, "declined": 2}, + "demographic_time_series": [ + { + "date": "2026-10-14", + "age_distribution": { + "under_18": 0, + "18_24": 3, + "25_34": 4, + "35_44": 2, + "45_64": 1, + "65_plus": 0, + }, + "gender_distribution": {"M": 6, "F": 4}, + }, + { + "date": "2026-10-15", + "age_distribution": { + "under_18": 0, + "18_24": 2, + "25_34": 4, + "35_44": 2, + "45_64": 1, + "65_plus": 1, + }, + "gender_distribution": {"M": 5, "F": 5}, + }, + ], + "device_time_series": [ + { + "date": "2026-10-14", + "devices": {"Apple": 7, "Android": 5}, + "browsers": {"Safari": 7, "Chrome": 5}, + "operating_systems": {"iOS": 7, "Android": 5}, + }, + { + "date": "2026-10-15", + "devices": {"Apple": 5, "Android": 5}, + "browsers": {"Safari": 5, "Chrome": 5}, + "operating_systems": {"iOS": 5, "Android": 5}, + }, + ], + "verification_time_time_series": [ + { + "date": "2026-10-14", + "overall_avg": 66.1, + "per_feature": {"OCR": 42.0, "LIVENESS": 24.1}, + }, + { + "date": "2026-10-15", + "overall_avg": 61.5, + "per_feature": {"OCR": 40.2, "LIVENESS": 21.3}, + }, + ], + "resubmission_time_series": [ + { + "date": "2026-10-14", + "first_pass": 9, + "second": 1, + "third_plus": 0, + "abandoned": 0, + }, + { + "date": "2026-10-15", + "first_pass": 8, + "second": 1, + "third_plus": 1, + "abandoned": 0, + }, + ], + "original_status_time_series": [ + {"date": "2026-10-14", "approved": 7, "in_review": 3, "declined": 1}, + {"date": "2026-10-15", "approved": 6, "in_review": 2, "declined": 1}, + ], + "previous_data_points": [ + { + "rounded_date": "2026-10-12", + "total": 9, + "approved": 6, + "declined": 1, + "canceled": 1, + "pending": 1, + }, + { + "rounded_date": "2026-10-13", + "total": 11, + "approved": 7, + "declined": 2, + "canceled": 0, + "pending": 2, + }, + ], + "previous_request_breakdown": { + "total_approved": 13, + "total_in_review": 2, + "total_declined": 3, + "total_canceled": 1, + "total_pending": 3, + }, + }, + ), + lambda c: c.analytics.retrieve( + start_date="2026-10-01", + end_date="2026-10-15", + scope="home", + widget_keys="tx_count_total,tx_count_daily", + workflow_ids="33333333-4444-5555-6666-777777777777", + versions="3,4", + branch_path_ids="default", + tag="sample", + transaction_type="finance", + action_type="withdrawal", + ), + lambda c: c.analytics.retrieve( + start_date="2026-10-01", + end_date="2026-10-15", + scope="home", + widget_keys="tx_count_total,tx_count_daily", + workflow_ids="33333333-4444-5555-6666-777777777777", + versions="3,4", + branch_path_ids="default", + tag="sample", + transaction_type="finance", + action_type="withdrawal", + ), + lambda r: ( + (isinstance(r, ApplicationAnalyticsSummary)) + or (isinstance(r, ApplicationAnalyticsWidgetResponse)) + ), + lambda r: ( + (isinstance(r, ApplicationAnalyticsSummary)) + or (isinstance(r, ApplicationAnalyticsWidgetResponse)) + ), + ) diff --git a/tests/generated/test_billing.py b/tests/generated/test_billing.py index 397a91a..11a60b8 100644 --- a/tests/generated/test_billing.py +++ b/tests/generated/test_billing.py @@ -43,7 +43,7 @@ def test_billing_top_up() -> None: status=200, response_json={ "checkout_session_id": "cs_test_a1B2c3D4e5F6g7H8i9J0", - "checkout_session_url": "https://checkout.stripe.com/c/pay/cs_test_a1B2c3D4e5F6g7H8i9J0", + "checkout_session_url": "https://checkout.example.com/c/pay/cs_test_a1B2c3D4e5F6g7H8i9J0", }, ), lambda c: c.billing.top_up(amount_in_dollars=50), diff --git a/tests/generated/test_blocklist.py b/tests/generated/test_blocklist.py new file mode 100644 index 0000000..a9f33bc --- /dev/null +++ b/tests/generated/test_blocklist.py @@ -0,0 +1,127 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``blocklist`` resources: one per operation.""" + +from __future__ import annotations + +from didit import AsyncPage, SyncPage +from didit.types.blocklist.add_response import BlocklistAddResponse +from didit.types.blocklist.remove_response import BlocklistRemoveResponse +from didit.types.components.blocklisted_item import BlocklistedItem +from tests.harness import Expect, check_operation + + +def test_blocklist_add() -> None: + """``POST /v3/blocklist/add/``""" + check_operation( + Expect( + method="POST", + path="/v3/blocklist/add/", + api="verification", + auth="api_key", + json_body={ + "session_id": "33333333-4444-5555-6666-777777777777", + "blocklist_face": True, + "blocklist_document": True, + }, + status=200, + response_json={"message": "Successfully updated blocklist."}, + ), + lambda c: c.blocklist.add( + session_id="33333333-4444-5555-6666-777777777777", + blocklist_face=True, + blocklist_document=True, + ), + lambda c: c.blocklist.add( + session_id="33333333-4444-5555-6666-777777777777", + blocklist_face=True, + blocklist_document=True, + ), + lambda r: isinstance(r, BlocklistAddResponse), + lambda r: isinstance(r, BlocklistAddResponse), + ) + + +def test_blocklist_list() -> None: + """``GET /v3/blocklist/``""" + check_operation( + Expect( + method="GET", + path="/v3/blocklist/", + api="verification", + auth="api_key", + query={"item_type": ["face"], "limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "type": "document", + "session_id": "33333333-4444-5555-6666-777777777777", + "session_number": 1042, + "blocklisted_at": "2026-10-12T10:06:12.701553Z", + "face_image": None, + "document_type": "ID", + "document_number": "X1234567", + "front_image": "https://media.example.com/documents/front.jpg?expires=1760263938&signature=8b2e47", + "phone_number": None, + "country_code": None, + "email": None, + }, + { + "type": "phone", + "session_id": "77777777-8888-9999-0000-111111111111", + "session_number": 1017, + "blocklisted_at": "2026-10-08T15:31:09.400127Z", + "face_image": None, + "document_type": None, + "document_number": None, + "front_image": None, + "phone_number": "+14155552671", + "country_code": "US", + "email": None, + }, + ], + }, + ), + lambda c: c.blocklist.list(item_type="face", limit=1, offset=1), + lambda c: c.blocklist.list(item_type="face", limit=1, offset=1), + lambda r: ( + isinstance(r, SyncPage) and all(isinstance(x1, BlocklistedItem) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, BlocklistedItem) for x1 in r.results) + ), + ) + + +def test_blocklist_remove() -> None: + """``POST /v3/blocklist/remove/``""" + check_operation( + Expect( + method="POST", + path="/v3/blocklist/remove/", + api="verification", + auth="api_key", + json_body={ + "session_id": "33333333-4444-5555-6666-777777777777", + "unblock_face": True, + "unblock_document": True, + }, + status=200, + response_json={"message": "Successfully removed from blocklist."}, + ), + lambda c: c.blocklist.remove( + session_id="33333333-4444-5555-6666-777777777777", + unblock_face=True, + unblock_document=True, + ), + lambda c: c.blocklist.remove( + session_id="33333333-4444-5555-6666-777777777777", + unblock_face=True, + unblock_document=True, + ), + lambda r: isinstance(r, BlocklistRemoveResponse), + lambda r: isinstance(r, BlocklistRemoveResponse), + ) diff --git a/tests/generated/test_business_sessions.py b/tests/generated/test_business_sessions.py index 8db98ed..06edd51 100644 --- a/tests/generated/test_business_sessions.py +++ b/tests/generated/test_business_sessions.py @@ -3,9 +3,7 @@ from __future__ import annotations -from didit.types.business_sessions.list_networks_response import ( - BusinessSessionsListNetworksResponse, -) +from didit.types.components.entity_network_membership_list import EntityNetworkMembershipList from tests.harness import Expect, check_operation @@ -22,13 +20,13 @@ def test_business_sessions_list_networks() -> None: "results": [ { "network": { - "uuid": "00000000-0000-4000-8000-000000000007", + "uuid": "00000000-0000-4000-8000-000000000008", "network_id": "sample", "name": "sample", "status": "active", - "risk": {}, - "pattern_types": ["exact_same_device"], - "signal_types": ["ip_address"], + "risk": None, + "pattern_types": [], + "signal_types": [], "member_counts": {}, "member_status": {}, "signal_coverage": {}, @@ -43,10 +41,10 @@ def test_business_sessions_list_networks() -> None: } ] }, - strict_type=BusinessSessionsListNetworksResponse, + strict_type=EntityNetworkMembershipList, ), lambda c: c.business_sessions.list_networks("00000000-0000-4000-8000-000000000001"), lambda c: c.business_sessions.list_networks("00000000-0000-4000-8000-000000000001"), - lambda r: isinstance(r, BusinessSessionsListNetworksResponse), - lambda r: isinstance(r, BusinessSessionsListNetworksResponse), + lambda r: isinstance(r, EntityNetworkMembershipList), + lambda r: isinstance(r, EntityNetworkMembershipList), ) diff --git a/tests/generated/test_businesses.py b/tests/generated/test_businesses.py index 712a94a..da86877 100644 --- a/tests/generated/test_businesses.py +++ b/tests/generated/test_businesses.py @@ -5,31 +5,19 @@ from didit import AsyncPage, SyncPage from didit.types.businesses.batch_delete_response import BusinessesBatchDeleteResponse -from didit.types.businesses.list_networks_response import BusinessesListNetworksResponse -from didit.types.businesses.retrieve_ongoing_monitoring_job_response import ( - BusinessesRetrieveOngoingMonitoringJobResponse, -) -from didit.types.businesses.retrieve_session_ongoing_monitoring_job_response import ( - BusinessesRetrieveSessionOngoingMonitoringJobResponse, -) -from didit.types.businesses.set_aml_monitoring_response import ( - BusinessesSetAmlMonitoringResponse202, - BusinessesSetAmlMonitoringResponseVariant1, - BusinessesSetAmlMonitoringResponseVariant2, -) -from didit.types.businesses.set_ongoing_monitoring_response import ( - BusinessesSetOngoingMonitoringResponse202, - BusinessesSetOngoingMonitoringResponseVariant1, - BusinessesSetOngoingMonitoringResponseVariant2, -) -from didit.types.businesses.set_session_ongoing_monitoring_response import ( - BusinessesSetSessionOngoingMonitoringResponse202, - BusinessesSetSessionOngoingMonitoringResponseVariant1, - BusinessesSetSessionOngoingMonitoringResponseVariant2, +from didit.types.components.aml_monitoring_job import AMLMonitoringJob +from didit.types.components.aml_monitoring_toggle_result import ( + AMLMonitoringToggleResultVariant1, + AMLMonitoringToggleResultVariant2, ) from didit.types.components.business_detail_item import BusinessDetailItem from didit.types.components.business_list_item import BusinessListItem -from tests.harness import Expect, check_operation +from didit.types.components.entity_network_membership_list import EntityNetworkMembershipList +from didit.types.components.ongoing_monitoring_job import OngoingMonitoringJob +from didit.types.components.ongoing_monitoring_toggle_result import ( + OngoingMonitoringToggleResultVariant1, +) +from tests.harness import BINARY_BODY, Expect, check_operation def test_businesses_batch_delete() -> None: @@ -100,6 +88,25 @@ def test_businesses_create() -> None: ) +def test_businesses_generate_session_pdf() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/businesses/{business_session_id}/pdf/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/businesses/22222222-3333-4444-5555-666666666666/pdf/", + api="verification", + auth="api_key", + status=200, + response_bytes=BINARY_BODY, + response_content_type="application/pdf", + ), + lambda c: c.businesses.generate_session_pdf("22222222-3333-4444-5555-666666666666"), + lambda c: c.businesses.generate_session_pdf("22222222-3333-4444-5555-666666666666"), + lambda r: r == BINARY_BODY, + lambda r: r == BINARY_BODY, + ) + + def test_businesses_list() -> None: """``GET /v3/businesses/``""" check_operation( @@ -169,13 +176,13 @@ def test_businesses_list_networks() -> None: "results": [ { "network": { - "uuid": "00000000-0000-4000-8000-000000000007", + "uuid": "00000000-0000-4000-8000-000000000008", "network_id": "sample", "name": "sample", "status": "active", - "risk": {}, - "pattern_types": ["exact_same_device"], - "signal_types": ["ip_address"], + "risk": None, + "pattern_types": [], + "signal_types": [], "member_counts": {}, "member_status": {}, "signal_coverage": {}, @@ -190,12 +197,12 @@ def test_businesses_list_networks() -> None: } ] }, - strict_type=BusinessesListNetworksResponse, + strict_type=EntityNetworkMembershipList, ), lambda c: c.businesses.list_networks("00000000-0000-4000-8000-000000000001"), lambda c: c.businesses.list_networks("00000000-0000-4000-8000-000000000001"), - lambda r: isinstance(r, BusinessesListNetworksResponse), - lambda r: isinstance(r, BusinessesListNetworksResponse), + lambda r: isinstance(r, EntityNetworkMembershipList), + lambda r: isinstance(r, EntityNetworkMembershipList), ) @@ -255,7 +262,7 @@ def test_businesses_retrieve_ongoing_monitoring_job() -> None: auth="api_key", status=200, response_json={ - "job_id": "00000000-0000-4000-8000-000000000002", + "job_id": "00000000-0000-4000-8000-000000000003", "status": "PENDING", "is_enabled": True, "total": 1, @@ -284,7 +291,7 @@ def test_businesses_retrieve_ongoing_monitoring_job() -> None: "balance_sufficient": True, }, }, - strict_type=BusinessesRetrieveOngoingMonitoringJobResponse, + strict_type=OngoingMonitoringJob, ), lambda c: c.businesses.retrieve_ongoing_monitoring_job( "00000000-0000-4000-8000-000000000001" @@ -292,8 +299,8 @@ def test_businesses_retrieve_ongoing_monitoring_job() -> None: lambda c: c.businesses.retrieve_ongoing_monitoring_job( "00000000-0000-4000-8000-000000000001" ), - lambda r: isinstance(r, BusinessesRetrieveOngoingMonitoringJobResponse), - lambda r: isinstance(r, BusinessesRetrieveOngoingMonitoringJobResponse), + lambda r: isinstance(r, OngoingMonitoringJob), + lambda r: isinstance(r, OngoingMonitoringJob), ) @@ -307,7 +314,7 @@ def test_businesses_retrieve_session_ongoing_monitoring_job() -> None: auth="api_key", status=200, response_json={ - "job_id": "00000000-0000-4000-8000-000000000002", + "job_id": "00000000-0000-4000-8000-000000000003", "status": "PENDING", "is_enabled": True, "total": 1, @@ -336,7 +343,7 @@ def test_businesses_retrieve_session_ongoing_monitoring_job() -> None: "balance_sufficient": True, }, }, - strict_type=BusinessesRetrieveSessionOngoingMonitoringJobResponse, + strict_type=OngoingMonitoringJob, ), lambda c: c.businesses.retrieve_session_ongoing_monitoring_job( "00000000-0000-4000-8000-000000000001" @@ -344,8 +351,8 @@ def test_businesses_retrieve_session_ongoing_monitoring_job() -> None: lambda c: c.businesses.retrieve_session_ongoing_monitoring_job( "00000000-0000-4000-8000-000000000001" ), - lambda r: isinstance(r, BusinessesRetrieveSessionOngoingMonitoringJobResponse), - lambda r: isinstance(r, BusinessesRetrieveSessionOngoingMonitoringJobResponse), + lambda r: isinstance(r, OngoingMonitoringJob), + lambda r: isinstance(r, OngoingMonitoringJob), ) @@ -383,14 +390,18 @@ def test_businesses_set_aml_monitoring() -> None: is_enabled=True, estimate_only=True, vendor_data_list=["company-123", "company-456"] ), lambda r: ( - (isinstance(r, BusinessesSetAmlMonitoringResponseVariant1)) - or (isinstance(r, BusinessesSetAmlMonitoringResponseVariant2)) - or (isinstance(r, BusinessesSetAmlMonitoringResponse202)) + ( + (isinstance(r, AMLMonitoringToggleResultVariant1)) + or (isinstance(r, AMLMonitoringToggleResultVariant2)) + ) + or (isinstance(r, AMLMonitoringJob)) ), lambda r: ( - (isinstance(r, BusinessesSetAmlMonitoringResponseVariant1)) - or (isinstance(r, BusinessesSetAmlMonitoringResponseVariant2)) - or (isinstance(r, BusinessesSetAmlMonitoringResponse202)) + ( + (isinstance(r, AMLMonitoringToggleResultVariant1)) + or (isinstance(r, AMLMonitoringToggleResultVariant2)) + ) + or (isinstance(r, AMLMonitoringJob)) ), ) @@ -420,14 +431,18 @@ def test_businesses_set_ongoing_monitoring() -> None: lambda c: c.businesses.set_ongoing_monitoring(is_enabled=True), lambda c: c.businesses.set_ongoing_monitoring(is_enabled=True), lambda r: ( - (isinstance(r, BusinessesSetOngoingMonitoringResponseVariant1)) - or (isinstance(r, BusinessesSetOngoingMonitoringResponseVariant2)) - or (isinstance(r, BusinessesSetOngoingMonitoringResponse202)) + ( + (isinstance(r, OngoingMonitoringToggleResultVariant1)) + or (isinstance(r, OngoingMonitoringJob)) + ) + or (isinstance(r, OngoingMonitoringJob)) ), lambda r: ( - (isinstance(r, BusinessesSetOngoingMonitoringResponseVariant1)) - or (isinstance(r, BusinessesSetOngoingMonitoringResponseVariant2)) - or (isinstance(r, BusinessesSetOngoingMonitoringResponse202)) + ( + (isinstance(r, OngoingMonitoringToggleResultVariant1)) + or (isinstance(r, OngoingMonitoringJob)) + ) + or (isinstance(r, OngoingMonitoringJob)) ), ) @@ -457,14 +472,18 @@ def test_businesses_set_session_ongoing_monitoring() -> None: lambda c: c.businesses.set_session_ongoing_monitoring(is_enabled=True), lambda c: c.businesses.set_session_ongoing_monitoring(is_enabled=True), lambda r: ( - (isinstance(r, BusinessesSetSessionOngoingMonitoringResponseVariant1)) - or (isinstance(r, BusinessesSetSessionOngoingMonitoringResponseVariant2)) - or (isinstance(r, BusinessesSetSessionOngoingMonitoringResponse202)) + ( + (isinstance(r, OngoingMonitoringToggleResultVariant1)) + or (isinstance(r, OngoingMonitoringJob)) + ) + or (isinstance(r, OngoingMonitoringJob)) ), lambda r: ( - (isinstance(r, BusinessesSetSessionOngoingMonitoringResponseVariant1)) - or (isinstance(r, BusinessesSetSessionOngoingMonitoringResponseVariant2)) - or (isinstance(r, BusinessesSetSessionOngoingMonitoringResponse202)) + ( + (isinstance(r, OngoingMonitoringToggleResultVariant1)) + or (isinstance(r, OngoingMonitoringJob)) + ) + or (isinstance(r, OngoingMonitoringJob)) ), ) diff --git a/tests/generated/test_cases.py b/tests/generated/test_cases.py index f0d9c46..f25203e 100644 --- a/tests/generated/test_cases.py +++ b/tests/generated/test_cases.py @@ -4,12 +4,7 @@ from __future__ import annotations from didit import AsyncPage, SyncPage -from didit.types.cases.blueprints.install_presets_response import ( - CasesBlueprintsInstallPresetsResponse, -) from didit.types.cases.blueprints.list_presets_response import CasesBlueprintsListPresetsResponse -from didit.types.cases.bulk_assign_response import CasesBulkAssignResponse -from didit.types.cases.bulk_update_status_response import CasesBulkUpdateStatusResponse from didit.types.cases.list_reviewers_response import CasesListReviewersResponse from didit.types.cases.notes.create_attachment_upload_response import ( CasesNotesCreateAttachmentUploadResponse, @@ -18,7 +13,9 @@ CasesNotesRetrieveAttachmentUrlResponse, ) from didit.types.cases.print_response import CasesPrintResponse +from didit.types.cases.requests.remind_response import CasesRequestsRemindResponse from didit.types.cases.retrieve_statistics_response import CasesRetrieveStatisticsResponse +from didit.types.components.aml_screening import AMLScreening from didit.types.components.case_action_case_action_receipt import CaseActionCaseActionReceipt from didit.types.components.case_action_case_action_receipt_page import ( CaseActionCaseActionReceiptPage, @@ -28,6 +25,10 @@ ) from didit.types.components.case_blueprint_detail import CaseBlueprintDetail from didit.types.components.case_blueprint_list_item import CaseBlueprintListItem +from didit.types.components.case_blueprint_preset_install_result import ( + CaseBlueprintPresetInstallResult, +) +from didit.types.components.case_bulk_update_result import CaseBulkUpdateResult from didit.types.components.case_checklist_item import CaseChecklistItem from didit.types.components.case_collaboration_case_note import CaseCollaborationCaseNote from didit.types.components.case_detail import CaseDetail @@ -134,6 +135,8 @@ def test_cases_approve_resolution() -> None: "uuid": "00000000-0000-4000-8000-000000000005", "name": "sample", "external_id": "sample", + "country": "ESP", + "portrait": "https://example.com/resource", }, "assigned_to": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -141,6 +144,7 @@ def test_cases_approve_resolution() -> None: "email": "user@example.com", }, "assigned_at": "2026-01-01T00:00:00Z", + "team": {"uuid": "00000000-0000-4000-8000-000000000005", "name": "sample"}, "due_at": "2026-01-01T00:00:00Z", "resolved_at": "2026-01-01T00:00:00Z", "resolved_by_email": "sample", @@ -149,9 +153,15 @@ def test_cases_approve_resolution() -> None: "resolution_notes": "sample", "maker_email": "user@example.com", "maker_name": "sample", + "maker_user_id": "00000000-0000-4000-8000-000000000004", "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", "staged_at": "2026-01-01T00:00:00Z", }, + "checker": { + "member_uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "email": "user@example.com", + }, "metadata": {}, "blueprint": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -286,6 +296,8 @@ def test_cases_assign() -> None: "uuid": "00000000-0000-4000-8000-000000000005", "name": "sample", "external_id": "sample", + "country": "ESP", + "portrait": "https://example.com/resource", }, "assigned_to": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -293,6 +305,7 @@ def test_cases_assign() -> None: "email": "user@example.com", }, "assigned_at": "2026-01-01T00:00:00Z", + "team": {"uuid": "00000000-0000-4000-8000-000000000005", "name": "sample"}, "due_at": "2026-01-01T00:00:00Z", "resolved_at": "2026-01-01T00:00:00Z", "resolved_by_email": "sample", @@ -301,9 +314,15 @@ def test_cases_assign() -> None: "resolution_notes": "sample", "maker_email": "user@example.com", "maker_name": "sample", + "maker_user_id": "00000000-0000-4000-8000-000000000004", "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", "staged_at": "2026-01-01T00:00:00Z", }, + "checker": { + "member_uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "email": "user@example.com", + }, "metadata": {}, "blueprint": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -435,10 +454,10 @@ def test_cases_bulk_assign() -> None: response_json={ "updated": 1, "failures": [ - {"case_uuid": "00000000-0000-4000-8000-000000000004", "detail": "sample"} + {"case_uuid": "00000000-0000-4000-8000-000000000005", "detail": "sample"} ], }, - strict_type=CasesBulkAssignResponse, + strict_type=CaseBulkUpdateResult, ), lambda c: c.cases.bulk_assign( case_uuids=["00000000-0000-4000-8000-000000000002"], @@ -448,8 +467,8 @@ def test_cases_bulk_assign() -> None: case_uuids=["00000000-0000-4000-8000-000000000002"], assignee_user_id="00000000-0000-4000-8000-000000000001", ), - lambda r: isinstance(r, CasesBulkAssignResponse), - lambda r: isinstance(r, CasesBulkAssignResponse), + lambda r: isinstance(r, CaseBulkUpdateResult), + lambda r: isinstance(r, CaseBulkUpdateResult), ) @@ -466,10 +485,10 @@ def test_cases_bulk_update_status() -> None: response_json={ "updated": 1, "failures": [ - {"case_uuid": "00000000-0000-4000-8000-000000000004", "detail": "sample"} + {"case_uuid": "00000000-0000-4000-8000-000000000005", "detail": "sample"} ], }, - strict_type=CasesBulkUpdateStatusResponse, + strict_type=CaseBulkUpdateResult, ), lambda c: c.cases.bulk_update_status( case_uuids=["00000000-0000-4000-8000-000000000002"], status="OPEN" @@ -477,8 +496,8 @@ def test_cases_bulk_update_status() -> None: lambda c: c.cases.bulk_update_status( case_uuids=["00000000-0000-4000-8000-000000000002"], status="OPEN" ), - lambda r: isinstance(r, CasesBulkUpdateStatusResponse), - lambda r: isinstance(r, CasesBulkUpdateStatusResponse), + lambda r: isinstance(r, CaseBulkUpdateResult), + lambda r: isinstance(r, CaseBulkUpdateResult), ) @@ -507,6 +526,8 @@ def test_cases_create() -> None: "uuid": "00000000-0000-4000-8000-000000000005", "name": "sample", "external_id": "sample", + "country": "ESP", + "portrait": "https://example.com/resource", }, "assigned_to": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -514,6 +535,7 @@ def test_cases_create() -> None: "email": "user@example.com", }, "assigned_at": "2026-01-01T00:00:00Z", + "team": {"uuid": "00000000-0000-4000-8000-000000000005", "name": "sample"}, "due_at": "2026-01-01T00:00:00Z", "resolved_at": "2026-01-01T00:00:00Z", "resolved_by_email": "sample", @@ -522,9 +544,15 @@ def test_cases_create() -> None: "resolution_notes": "sample", "maker_email": "user@example.com", "maker_name": "sample", + "maker_user_id": "00000000-0000-4000-8000-000000000004", "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", "staged_at": "2026-01-01T00:00:00Z", }, + "checker": { + "member_uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "email": "user@example.com", + }, "metadata": {}, "blueprint": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -675,6 +703,8 @@ def test_cases_escalate() -> None: "uuid": "00000000-0000-4000-8000-000000000005", "name": "sample", "external_id": "sample", + "country": "ESP", + "portrait": "https://example.com/resource", }, "assigned_to": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -682,6 +712,7 @@ def test_cases_escalate() -> None: "email": "user@example.com", }, "assigned_at": "2026-01-01T00:00:00Z", + "team": {"uuid": "00000000-0000-4000-8000-000000000005", "name": "sample"}, "due_at": "2026-01-01T00:00:00Z", "resolved_at": "2026-01-01T00:00:00Z", "resolved_by_email": "sample", @@ -690,9 +721,15 @@ def test_cases_escalate() -> None: "resolution_notes": "sample", "maker_email": "user@example.com", "maker_name": "sample", + "maker_user_id": "00000000-0000-4000-8000-000000000004", "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", "staged_at": "2026-01-01T00:00:00Z", }, + "checker": { + "member_uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "email": "user@example.com", + }, "metadata": {}, "blueprint": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -876,6 +913,20 @@ def test_cases_list() -> None: "title": "sample", "status": "OPEN", "resolution": "FALSE_POSITIVE", + "pending_resolution": { + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "maker_email": "user@example.com", + "maker_name": "sample", + "maker_user_id": "00000000-0000-4000-8000-000000000006", + "maker_blueprint_id": "00000000-0000-4000-8000-000000000006", + "staged_at": "2026-01-01T00:00:00Z", + }, + "checker": { + "member_uuid": "00000000-0000-4000-8000-000000000007", + "name": "sample", + "email": "user@example.com", + }, "priority": "LOW", "source": "MANUAL", "tags": ["sample"], @@ -884,6 +935,8 @@ def test_cases_list() -> None: "uuid": "00000000-0000-4000-8000-000000000007", "name": "sample", "external_id": "sample", + "country": "ESP", + "portrait": "https://example.com/resource", }, "assigned_to": { "uuid": "00000000-0000-4000-8000-000000000007", @@ -891,6 +944,7 @@ def test_cases_list() -> None: "email": "user@example.com", }, "assigned_at": "2026-01-01T00:00:00Z", + "team": {"uuid": "00000000-0000-4000-8000-000000000007", "name": "sample"}, "due_at": "2026-01-01T00:00:00Z", "resolved_at": "2026-01-01T00:00:00Z", "created_by_email": "sample", @@ -998,13 +1052,205 @@ def test_cases_list_aml_hits() -> None: api="verification", auth="api_key", status=200, - response_json=[{}], - strict_type=list[dict[str, object]], + response_json=[ + { + "node_id": "sample", + "status": "Not Finished", + "total_hits": 1, + "entity_type": "sample", + "hits": [ + { + "id": "sample", + "url": "https://example.com/resource", + "match": True, + "score": 1.5, + "target": True, + "caption": "sample", + "datasets": ["PEP"], + "features": {}, + "rca_name": "sample", + "last_seen": "2026-01-01T00:00:00Z", + "risk_view": { + "crimes": { + "score": 1, + "weightage": 1, + "risk_level": "sample", + "risk_scores": {}, + }, + "countries": { + "score": 1, + "weightage": 1, + "risk_level": "sample", + "risk_scores": {}, + }, + "categories": { + "score": 1, + "weightage": 1, + "risk_level": "sample", + "risk_scores": {}, + }, + "custom_list": {}, + }, + "first_seen": "2026-01-01T00:00:00Z", + "properties": { + "name": ["sample"], + "alias": ["sample"], + "notes": ["sample"], + "title": "sample", + "gender": ["sample"], + "height": "sample", + "topics": "sample", + "weight": "sample", + "address": "sample", + "country": "sample", + "website": "sample", + "eyeColor": "sample", + "keywords": "sample", + "lastName": ["sample"], + "position": "sample", + "religion": "sample", + "birthDate": ["sample"], + "education": ["sample"], + "ethnicity": "sample", + "firstName": ["sample"], + "hairColor": "sample", + "weakAlias": "sample", + "birthPlace": ["sample"], + "modifiedAt": "sample", + "wikidataId": "sample", + "citizenship": "sample", + "nationality": "sample", + }, + "match_score": 1.5, + "risk_score": 1.5, + "review_status": "Unreviewed", + "score_breakdown": { + "name_score": 1, + "name_weight": 1, + "name_weight_normalized": 1.5, + "name_contribution": 1.5, + "dob_score": 1, + "dob_weight": 1, + "dob_weight_normalized": 1.5, + "dob_contribution": 1.5, + "country_score": 1, + "country_weight": 1, + "country_weight_normalized": 1.5, + "country_contribution": 1.5, + "document_number_match_type": "MATCH", + "document_number_effect": "sample", + "total_score": 1, + }, + "pep_matches": [ + { + "aliases": [], + "education": [], + "list_name": "sample", + "publisher": "sample", + "source_url": "https://example.com/resource", + "description": "sample", + "matched_name": "sample", + "pep_position": "sample", + "date_of_birth": "sample", + "other_sources": [], + "place_of_birth": "sample", + } + ], + "linked_entities": [ + { + "name": [], + "active": [], + "status": [], + "details": [], + "relation": [], + } + ], + "warning_matches": [ + { + "list_name": "sample", + "matched_name": "sample", + "description": "sample", + "publisher": "sample", + "source_url": "https://example.com/resource", + "other_sources": [], + "countries": [], + "created_at": "sample", + "updated_at": "sample", + "additional_data": {}, + } + ], + "sanction_matches": [ + { + "list_name": [], + "matched_name": "sample", + "description": "sample", + "reason": "sample", + "source_url": "https://example.com/resource", + "other_sources": [], + "details": [], + "legal_basis": [], + "listed_on": [], + "sanction_list": [], + "sanction_program": [], + "sanctioning_authority": [], + "updated_on": [], + "additional_data": {}, + } + ], + "adverse_media_details": { + "sentiment": "sample", + "entity_type": "sample", + "sentiment_score": 1, + "adverse_keywords": {"key": 1}, + }, + "adverse_media_matches": [ + { + "country": "sample", + "summary": "sample", + "headline": "sample", + "sentiment": "sample", + "thumbnail": "https://example.com/resource", + "source_url": "https://example.com/resource", + "author_name": "sample", + "other_sources": [], + "adverse_keywords": [], + "sentiment_score": 1, + "publication_date": "2026-01-01T00:00:00Z", + } + ], + "additional_information": {}, + } + ], + "score": 1.5, + "screened_data": { + "full_name": "sample", + "nationality": "sample", + "date_of_birth": "2026-01-01", + "document_number": "sample", + }, + "is_ongoing_monitoring_enabled": True, + "next_ongoing_monitoring_bill_date": "2026-01-01", + "last_ongoing_screening_at": "2026-01-01T00:00:00Z", + "last_ongoing_screening_datasets_updated_at": "2026-01-01T00:00:00Z", + "warnings": [ + { + "feature": "ID_VERIFICATION", + "risk": "sample", + "additional_data": {}, + "log_type": "sample", + "short_description": "sample", + "long_description": "sample", + "node_id": "sample", + } + ], + } + ], + strict_type=list[AMLScreening], ), lambda c: c.cases.list_aml_hits("c1a5e000-0000-4000-8000-000000000001"), lambda c: c.cases.list_aml_hits("c1a5e000-0000-4000-8000-000000000001"), - lambda r: isinstance(r, list) and all(isinstance(x1, dict) for x1 in r), - lambda r: isinstance(r, list) and all(isinstance(x1, dict) for x1 in r), + lambda r: isinstance(r, list) and all(isinstance(x1, AMLScreening) for x1 in r), + lambda r: isinstance(r, list) and all(isinstance(x1, AMLScreening) for x1 in r), ) @@ -1114,6 +1360,8 @@ def test_cases_reject_resolution() -> None: "uuid": "00000000-0000-4000-8000-000000000005", "name": "sample", "external_id": "sample", + "country": "ESP", + "portrait": "https://example.com/resource", }, "assigned_to": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -1121,6 +1369,7 @@ def test_cases_reject_resolution() -> None: "email": "user@example.com", }, "assigned_at": "2026-01-01T00:00:00Z", + "team": {"uuid": "00000000-0000-4000-8000-000000000005", "name": "sample"}, "due_at": "2026-01-01T00:00:00Z", "resolved_at": "2026-01-01T00:00:00Z", "resolved_by_email": "sample", @@ -1129,9 +1378,15 @@ def test_cases_reject_resolution() -> None: "resolution_notes": "sample", "maker_email": "user@example.com", "maker_name": "sample", + "maker_user_id": "00000000-0000-4000-8000-000000000004", "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", "staged_at": "2026-01-01T00:00:00Z", }, + "checker": { + "member_uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "email": "user@example.com", + }, "metadata": {}, "blueprint": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -1265,6 +1520,8 @@ def test_cases_reopen() -> None: "uuid": "00000000-0000-4000-8000-000000000005", "name": "sample", "external_id": "sample", + "country": "ESP", + "portrait": "https://example.com/resource", }, "assigned_to": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -1272,6 +1529,7 @@ def test_cases_reopen() -> None: "email": "user@example.com", }, "assigned_at": "2026-01-01T00:00:00Z", + "team": {"uuid": "00000000-0000-4000-8000-000000000005", "name": "sample"}, "due_at": "2026-01-01T00:00:00Z", "resolved_at": "2026-01-01T00:00:00Z", "resolved_by_email": "sample", @@ -1280,9 +1538,15 @@ def test_cases_reopen() -> None: "resolution_notes": "sample", "maker_email": "user@example.com", "maker_name": "sample", + "maker_user_id": "00000000-0000-4000-8000-000000000004", "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", "staged_at": "2026-01-01T00:00:00Z", }, + "checker": { + "member_uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "email": "user@example.com", + }, "metadata": {}, "blueprint": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -1416,6 +1680,8 @@ def test_cases_request_info() -> None: "uuid": "00000000-0000-4000-8000-000000000005", "name": "sample", "external_id": "sample", + "country": "ESP", + "portrait": "https://example.com/resource", }, "assigned_to": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -1423,6 +1689,7 @@ def test_cases_request_info() -> None: "email": "user@example.com", }, "assigned_at": "2026-01-01T00:00:00Z", + "team": {"uuid": "00000000-0000-4000-8000-000000000005", "name": "sample"}, "due_at": "2026-01-01T00:00:00Z", "resolved_at": "2026-01-01T00:00:00Z", "resolved_by_email": "sample", @@ -1431,9 +1698,15 @@ def test_cases_request_info() -> None: "resolution_notes": "sample", "maker_email": "user@example.com", "maker_name": "sample", + "maker_user_id": "00000000-0000-4000-8000-000000000004", "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", "staged_at": "2026-01-01T00:00:00Z", }, + "checker": { + "member_uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "email": "user@example.com", + }, "metadata": {}, "blueprint": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -1551,7 +1824,10 @@ def test_cases_resolve() -> None: path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/resolve/", api="verification", auth="api_key", - json_body={"resolution": "FALSE_POSITIVE"}, + json_body={ + "resolution": "FALSE_POSITIVE", + "note": "Confirmed legitimate activity after reviewing documents.", + }, status=200, response_json={ "uuid": "00000000-0000-4000-8000-000000000003", @@ -1568,6 +1844,8 @@ def test_cases_resolve() -> None: "uuid": "00000000-0000-4000-8000-000000000005", "name": "sample", "external_id": "sample", + "country": "ESP", + "portrait": "https://example.com/resource", }, "assigned_to": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -1575,6 +1853,7 @@ def test_cases_resolve() -> None: "email": "user@example.com", }, "assigned_at": "2026-01-01T00:00:00Z", + "team": {"uuid": "00000000-0000-4000-8000-000000000005", "name": "sample"}, "due_at": "2026-01-01T00:00:00Z", "resolved_at": "2026-01-01T00:00:00Z", "resolved_by_email": "sample", @@ -1583,9 +1862,15 @@ def test_cases_resolve() -> None: "resolution_notes": "sample", "maker_email": "user@example.com", "maker_name": "sample", + "maker_user_id": "00000000-0000-4000-8000-000000000004", "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", "staged_at": "2026-01-01T00:00:00Z", }, + "checker": { + "member_uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "email": "user@example.com", + }, "metadata": {}, "blueprint": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -1689,10 +1974,14 @@ def test_cases_resolve() -> None: strict_type=CaseDetail, ), lambda c: c.cases.resolve( - "c1a5e000-0000-4000-8000-000000000001", resolution="FALSE_POSITIVE" + "c1a5e000-0000-4000-8000-000000000001", + resolution="FALSE_POSITIVE", + note="Confirmed legitimate activity after reviewing documents.", ), lambda c: c.cases.resolve( - "c1a5e000-0000-4000-8000-000000000001", resolution="FALSE_POSITIVE" + "c1a5e000-0000-4000-8000-000000000001", + resolution="FALSE_POSITIVE", + note="Confirmed legitimate activity after reviewing documents.", ), lambda r: isinstance(r, CaseDetail), lambda r: isinstance(r, CaseDetail), @@ -1723,6 +2012,8 @@ def test_cases_retrieve() -> None: "uuid": "00000000-0000-4000-8000-000000000005", "name": "sample", "external_id": "sample", + "country": "ESP", + "portrait": "https://example.com/resource", }, "assigned_to": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -1730,6 +2021,7 @@ def test_cases_retrieve() -> None: "email": "user@example.com", }, "assigned_at": "2026-01-01T00:00:00Z", + "team": {"uuid": "00000000-0000-4000-8000-000000000005", "name": "sample"}, "due_at": "2026-01-01T00:00:00Z", "resolved_at": "2026-01-01T00:00:00Z", "resolved_by_email": "sample", @@ -1738,9 +2030,15 @@ def test_cases_retrieve() -> None: "resolution_notes": "sample", "maker_email": "user@example.com", "maker_name": "sample", + "maker_user_id": "00000000-0000-4000-8000-000000000004", "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", "staged_at": "2026-01-01T00:00:00Z", }, + "checker": { + "member_uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "email": "user@example.com", + }, "metadata": {}, "blueprint": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -2035,6 +2333,8 @@ def test_cases_transfer() -> None: "uuid": "00000000-0000-4000-8000-000000000005", "name": "sample", "external_id": "sample", + "country": "ESP", + "portrait": "https://example.com/resource", }, "assigned_to": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -2042,6 +2342,7 @@ def test_cases_transfer() -> None: "email": "user@example.com", }, "assigned_at": "2026-01-01T00:00:00Z", + "team": {"uuid": "00000000-0000-4000-8000-000000000005", "name": "sample"}, "due_at": "2026-01-01T00:00:00Z", "resolved_at": "2026-01-01T00:00:00Z", "resolved_by_email": "sample", @@ -2050,9 +2351,15 @@ def test_cases_transfer() -> None: "resolution_notes": "sample", "maker_email": "user@example.com", "maker_name": "sample", + "maker_user_id": "00000000-0000-4000-8000-000000000004", "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", "staged_at": "2026-01-01T00:00:00Z", }, + "checker": { + "member_uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "email": "user@example.com", + }, "metadata": {}, "blueprint": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -2176,7 +2483,7 @@ def test_cases_update() -> None: path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/", api="verification", auth="api_key", - json_body={"title": "sample"}, + json_body={"priority": "HIGH", "due_at": "2026-11-01T00:00:00Z"}, status=200, response_json={ "uuid": "00000000-0000-4000-8000-000000000003", @@ -2193,6 +2500,8 @@ def test_cases_update() -> None: "uuid": "00000000-0000-4000-8000-000000000005", "name": "sample", "external_id": "sample", + "country": "ESP", + "portrait": "https://example.com/resource", }, "assigned_to": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -2200,6 +2509,7 @@ def test_cases_update() -> None: "email": "user@example.com", }, "assigned_at": "2026-01-01T00:00:00Z", + "team": {"uuid": "00000000-0000-4000-8000-000000000005", "name": "sample"}, "due_at": "2026-01-01T00:00:00Z", "resolved_at": "2026-01-01T00:00:00Z", "resolved_by_email": "sample", @@ -2208,9 +2518,15 @@ def test_cases_update() -> None: "resolution_notes": "sample", "maker_email": "user@example.com", "maker_name": "sample", + "maker_user_id": "00000000-0000-4000-8000-000000000004", "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", "staged_at": "2026-01-01T00:00:00Z", }, + "checker": { + "member_uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "email": "user@example.com", + }, "metadata": {}, "blueprint": { "uuid": "00000000-0000-4000-8000-000000000005", @@ -2313,8 +2629,12 @@ def test_cases_update() -> None: }, strict_type=CaseDetail, ), - lambda c: c.cases.update("c1a5e000-0000-4000-8000-000000000001", title="sample"), - lambda c: c.cases.update("c1a5e000-0000-4000-8000-000000000001", title="sample"), + lambda c: c.cases.update( + "c1a5e000-0000-4000-8000-000000000001", priority="HIGH", due_at="2026-11-01T00:00:00Z" + ), + lambda c: c.cases.update( + "c1a5e000-0000-4000-8000-000000000001", priority="HIGH", due_at="2026-11-01T00:00:00Z" + ), lambda r: isinstance(r, CaseDetail), lambda r: isinstance(r, CaseDetail), ) @@ -2443,12 +2763,12 @@ def test_cases_blueprints_install_presets() -> None: json_body={"preset_keys": ["sample"]}, status=200, response_json={"created": ["sample"]}, - strict_type=CasesBlueprintsInstallPresetsResponse, + strict_type=CaseBlueprintPresetInstallResult, ), lambda c: c.cases.blueprints.install_presets(preset_keys=["sample"]), lambda c: c.cases.blueprints.install_presets(preset_keys=["sample"]), - lambda r: isinstance(r, CasesBlueprintsInstallPresetsResponse), - lambda r: isinstance(r, CasesBlueprintsInstallPresetsResponse), + lambda r: isinstance(r, CaseBlueprintPresetInstallResult), + lambda r: isinstance(r, CaseBlueprintPresetInstallResult), ) @@ -3278,8 +3598,8 @@ def test_cases_regulatory_reports_list() -> None: "previous": "sample", "results": [ { - "uuid": "00000000-0000-4000-8000-000000000005", - "case_uuid": "00000000-0000-4000-8000-000000000005", + "uuid": "00000000-0000-4000-8000-000000000006", + "case_uuid": "00000000-0000-4000-8000-000000000006", "case_number": "sample", "report_type": "SAR", "report_type_display": "sample", @@ -3310,7 +3630,7 @@ def test_cases_regulatory_reports_list() -> None: }, "report_indicators": ["sample"], "included_fields": ["sample"], - "transactions": ["00000000-0000-4000-8000-000000000006"], + "transactions": ["00000000-0000-4000-8000-000000000007"], "validation_errors": [{"field": "sample", "message": "sample"}], "validated_at": "2026-01-01T00:00:00Z", "finalized_at": "2026-01-01T00:00:00Z", @@ -3520,6 +3840,47 @@ def test_cases_requests_preview() -> None: ) +def test_cases_requests_remind() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/remind/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/00000000-0000-4000-8000-000000000001/requests/00000000-0000-4000-8000-000000000002/remind/", + api="verification", + auth="api_key", + status=200, + response_json={ + "request": { + "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "purpose": "confirm_details", + "language": "en", + "recipient": "customer@example.com", + "expires_at": "2026-10-20T09:00:00Z", + "actor_email": "reviewer@example.com", + "delivery_status": "SENT", + "delivered_at": "2026-10-13T09:00:04Z", + "cancelled_at": None, + "responded_at": None, + "response": None, + "has_attachment": False, + "created_at": "2026-10-13T09:00:00Z", + }, + "reminded_at": "2026-10-14T10:30:00Z", + "delivery_status": "SENT", + "next_reminder_at": "2026-10-15T10:30:00Z", + }, + ), + lambda c: c.cases.requests.remind( + "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" + ), + lambda c: c.cases.requests.remind( + "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" + ), + lambda r: isinstance(r, CasesRequestsRemindResponse), + lambda r: isinstance(r, CasesRequestsRemindResponse), + ) + + def test_cases_requests_retrieve_attachment() -> None: """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/attachment/``""" check_operation( diff --git a/tests/generated/test_customization.py b/tests/generated/test_customization.py index a4ec419..bb74764 100644 --- a/tests/generated/test_customization.py +++ b/tests/generated/test_customization.py @@ -37,6 +37,7 @@ def test_customization_retrieve() -> None: "font_url": "https://fonts.googleapis.com/css2?family=Inter", "logo_square": None, "logo_rectangular": None, + "logo_rectangular_dark": None, "favicon": None, "app_public_name": "Acme Verify", "privacy_policy_url": "https://acme.com/privacy", @@ -89,6 +90,7 @@ def test_customization_update() -> None: "font_url": "https://fonts.googleapis.com/css2?family=AR+One+Sans", "logo_square": None, "logo_rectangular": "https://cdn.didit.me/white_label/logo_rectangular/...", + "logo_rectangular_dark": None, "favicon": None, "app_public_name": "Acme Verify", "privacy_policy_url": "https://acme.com/privacy", diff --git a/tests/generated/test_investigator.py b/tests/generated/test_investigator.py new file mode 100644 index 0000000..ae60cb4 --- /dev/null +++ b/tests/generated/test_investigator.py @@ -0,0 +1,662 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``investigator`` resources: one per operation.""" + +from __future__ import annotations + +from didit import AsyncPage, SyncPage +from didit.types.components.investigator_batch_accept_response import ( + InvestigatorBatchAcceptResponse, +) +from didit.types.components.investigator_decision import InvestigatorDecision +from didit.types.components.investigator_queue_row import InvestigatorQueueRow +from didit.types.components.investigator_run_detail import InvestigatorRunDetail +from didit.types.components.investigator_settings import InvestigatorSettings +from didit.types.investigator.list_guardrails_response import InvestigatorListGuardrailsResponse +from didit.types.investigator.retrieve_dashboard_response import ( + InvestigatorRetrieveDashboardResponse, +) +from didit.types.investigator.retrieve_performance_response import ( + InvestigatorRetrievePerformanceResponse, +) +from didit.types.investigator.retrieve_summary_response import InvestigatorRetrieveSummaryResponse +from didit.types.investigator.retrieve_transaction_response import ( + InvestigatorRetrieveTransactionResponse, +) +from tests.harness import BINARY_BODY, Expect, check_operation + + +def test_investigator_accept_run() -> None: + """``POST /v3/investigator/runs/{run_id}/accept/``""" + check_operation( + Expect( + method="POST", + path="/v3/investigator/runs/00000000-0000-4000-8000-000000000001/accept/", + api="verification", + auth="api_key", + json_body={"narrative": "sample"}, + status=200, + response_json={ + "outcome": "accepted", + "action": "approve", + "actor_email": "sample", + "actor_name": "sample", + "override_category": "missing_context", + "override_reason": "sample", + "final_status": "sample", + "agreed": True, + "decided_at": "2026-01-01T00:00:00Z", + "billed": True, + "decided_by": "investigator", + "original_outcome": { + "action": "sample", + "final_status": "sample", + "decided_at": "2026-01-01T00:00:00Z", + }, + "overrides": [ + { + "action": "sample", + "actor_email": "sample", + "actor_name": "sample", + "override_category": "sample", + "override_reason": "sample", + "previous_status": "sample", + "final_status": "sample", + "agreed": True, + "overridden_at": "2026-01-01T00:00:00Z", + } + ], + }, + strict_type=InvestigatorDecision, + ), + lambda c: c.investigator.accept_run( + "00000000-0000-4000-8000-000000000001", narrative="sample" + ), + lambda c: c.investigator.accept_run( + "00000000-0000-4000-8000-000000000001", narrative="sample" + ), + lambda r: isinstance(r, InvestigatorDecision), + lambda r: isinstance(r, InvestigatorDecision), + ) + + +def test_investigator_batch_accept_runs() -> None: + """``POST /v3/investigator/runs/batch-accept/``""" + check_operation( + Expect( + method="POST", + path="/v3/investigator/runs/batch-accept/", + api="verification", + auth="api_key", + json_body={"run_ids": ["00000000-0000-4000-8000-000000000002"]}, + status=200, + response_json={ + "accepted": 1, + "skipped": 1, + "results": [ + { + "run_id": "00000000-0000-4000-8000-000000000005", + "result": "accepted", + "reason": "not_found", + "detail": "value", + "action": "sample", + "final_status": "sample", + } + ], + }, + strict_type=InvestigatorBatchAcceptResponse, + ), + lambda c: c.investigator.batch_accept_runs( + run_ids=["00000000-0000-4000-8000-000000000002"] + ), + lambda c: c.investigator.batch_accept_runs( + run_ids=["00000000-0000-4000-8000-000000000002"] + ), + lambda r: isinstance(r, InvestigatorBatchAcceptResponse), + lambda r: isinstance(r, InvestigatorBatchAcceptResponse), + ) + + +def test_investigator_export_activity() -> None: + """``GET /v3/investigator/activity/export/``""" + check_operation( + Expect( + method="GET", + path="/v3/investigator/activity/export/", + api="verification", + auth="api_key", + query={ + "scope": ["all"], + "group": ["agent"], + "actor": ["sample"], + "since": ["2026-01-01T00:00:00Z"], + "until": ["2026-01-01T00:00:00Z"], + }, + status=200, + response_bytes=BINARY_BODY, + response_content_type="text/csv", + ), + lambda c: c.investigator.export_activity( + scope="all", + group="agent", + actor="sample", + since="2026-01-01T00:00:00Z", + until="2026-01-01T00:00:00Z", + ), + lambda c: c.investigator.export_activity( + scope="all", + group="agent", + actor="sample", + since="2026-01-01T00:00:00Z", + until="2026-01-01T00:00:00Z", + ), + lambda r: r == BINARY_BODY, + lambda r: r == BINARY_BODY, + ) + + +def test_investigator_list_activity() -> None: + """``GET /v3/investigator/activity/``""" + check_operation( + Expect( + method="GET", + path="/v3/investigator/activity/", + api="verification", + auth="api_key", + query={ + "scope": ["all"], + "group": ["agent"], + "actor": ["sample"], + "since": ["2026-01-01T00:00:00Z"], + "until": ["2026-01-01T00:00:00Z"], + "limit": ["1"], + "offset": ["1"], + }, + status=200, + response_json={ + "count": 1, + "next": "sample", + "previous": "sample", + "counts": {}, + "actors": ["value"], + "results": [{}], + }, + ), + lambda c: c.investigator.list_activity( + scope="all", + group="agent", + actor="sample", + since="2026-01-01T00:00:00Z", + until="2026-01-01T00:00:00Z", + limit=1, + offset=1, + ), + lambda c: c.investigator.list_activity( + scope="all", + group="agent", + actor="sample", + since="2026-01-01T00:00:00Z", + until="2026-01-01T00:00:00Z", + limit=1, + offset=1, + ), + lambda r: isinstance(r, SyncPage) and all(isinstance(x1, dict) for x1 in r.results), + lambda r: isinstance(r, AsyncPage) and all(isinstance(x1, dict) for x1 in r.results), + ) + + +def test_investigator_list_guardrails() -> None: + """``GET /v3/investigator/guardrails/``""" + check_operation( + Expect( + method="GET", + path="/v3/investigator/guardrails/", + api="verification", + auth="api_key", + status=200, + response_json={ + "results": [ + { + "key": "sanctions_to_person", + "title": "sample", + "description": "sample", + "locked": True, + } + ] + }, + strict_type=InvestigatorListGuardrailsResponse, + ), + lambda c: c.investigator.list_guardrails(), + lambda c: c.investigator.list_guardrails(), + lambda r: isinstance(r, InvestigatorListGuardrailsResponse), + lambda r: isinstance(r, InvestigatorListGuardrailsResponse), + ) + + +def test_investigator_list_queue() -> None: + """``GET /v3/investigator/queue/``""" + check_operation( + Expect( + method="GET", + path="/v3/investigator/queue/", + api="verification", + auth="api_key", + query={ + "scope": ["all"], + "kind": ["sample"], + "queue": ["kyc"], + "recommendation": ["approve"], + "confidence": ["high"], + "ready": ["true"], + "search": ["sample"], + "limit": ["1"], + "offset": ["1"], + }, + status=200, + response_json={ + "count": 1, + "next": "sample", + "previous": "sample", + "counts": {}, + "results": [ + { + "run_id": "00000000-0000-4000-8000-000000000005", + "kind": "session", + "queue": "kyc", + "stage": "shadow", + "subject": {}, + "recommendation": "approve", + "recommendation_detail": "sample", + "confidence": "high", + "summary": "sample", + "sources_count": 1, + "ready_to_accept": True, + "guardrails": "value", + "waiting_since": "2026-01-01T00:00:00Z", + "due_at": "2026-01-01T00:00:00Z", + "prepared_at": "2026-01-01T00:00:00Z", + } + ], + }, + ), + lambda c: c.investigator.list_queue( + scope="all", + kind="sample", + queue="kyc", + recommendation="approve", + confidence="high", + ready=True, + search="sample", + limit=1, + offset=1, + ), + lambda c: c.investigator.list_queue( + scope="all", + kind="sample", + queue="kyc", + recommendation="approve", + confidence="high", + ready=True, + search="sample", + limit=1, + offset=1, + ), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, InvestigatorQueueRow) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, InvestigatorQueueRow) for x1 in r.results) + ), + ) + + +def test_investigator_override_run() -> None: + """``POST /v3/investigator/runs/{run_id}/override/``""" + check_operation( + Expect( + method="POST", + path="/v3/investigator/runs/00000000-0000-4000-8000-000000000001/override/", + api="verification", + auth="api_key", + json_body={"action": "approve", "category": "missing_context", "reason": "samplexxxx"}, + status=200, + response_json={ + "outcome": "accepted", + "action": "approve", + "actor_email": "sample", + "actor_name": "sample", + "override_category": "missing_context", + "override_reason": "sample", + "final_status": "sample", + "agreed": True, + "decided_at": "2026-01-01T00:00:00Z", + "billed": True, + "decided_by": "investigator", + "original_outcome": { + "action": "sample", + "final_status": "sample", + "decided_at": "2026-01-01T00:00:00Z", + }, + "overrides": [ + { + "action": "sample", + "actor_email": "sample", + "actor_name": "sample", + "override_category": "sample", + "override_reason": "sample", + "previous_status": "sample", + "final_status": "sample", + "agreed": True, + "overridden_at": "2026-01-01T00:00:00Z", + } + ], + }, + strict_type=InvestigatorDecision, + ), + lambda c: c.investigator.override_run( + "00000000-0000-4000-8000-000000000001", + action="approve", + category="missing_context", + reason="samplexxxx", + ), + lambda c: c.investigator.override_run( + "00000000-0000-4000-8000-000000000001", + action="approve", + category="missing_context", + reason="samplexxxx", + ), + lambda r: isinstance(r, InvestigatorDecision), + lambda r: isinstance(r, InvestigatorDecision), + ) + + +def test_investigator_retrieve_dashboard() -> None: + """``GET /v3/investigator/dashboard/``""" + check_operation( + Expect( + method="GET", + path="/v3/investigator/dashboard/", + api="verification", + auth="api_key", + query={"scope": ["all"]}, + status=200, + response_json={ + "stage": "sample", + "window_days": 1, + "reviews": 1, + "decided": 1, + "automated": 1, + "automation_rate": 1.5, + "hours_saved": 1.5, + }, + strict_type=InvestigatorRetrieveDashboardResponse, + ), + lambda c: c.investigator.retrieve_dashboard(scope="all"), + lambda c: c.investigator.retrieve_dashboard(scope="all"), + lambda r: isinstance(r, InvestigatorRetrieveDashboardResponse), + lambda r: isinstance(r, InvestigatorRetrieveDashboardResponse), + ) + + +def test_investigator_retrieve_performance() -> None: + """``GET /v3/investigator/performance/``""" + check_operation( + Expect( + method="GET", + path="/v3/investigator/performance/", + api="verification", + auth="api_key", + query={"scope": ["all"], "period": ["7"]}, + status=200, + response_json={ + "period": 1, + "start": "2026-01-01", + "end": "2026-01-01", + "investigated": 1, + }, + strict_type=InvestigatorRetrievePerformanceResponse, + ), + lambda c: c.investigator.retrieve_performance(scope="all", period="7"), + lambda c: c.investigator.retrieve_performance(scope="all", period="7"), + lambda r: isinstance(r, InvestigatorRetrievePerformanceResponse), + lambda r: isinstance(r, InvestigatorRetrievePerformanceResponse), + ) + + +def test_investigator_retrieve_run() -> None: + """``GET /v3/investigator/runs/{run_id}/``""" + check_operation( + Expect( + method="GET", + path="/v3/investigator/runs/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "run_id": "00000000-0000-4000-8000-000000000004", + "kind": "session", + "queue": "kyc", + "stage": "shadow", + "subject": {}, + "recommendation": "approve", + "recommendation_detail": "sample", + "confidence": "high", + "summary": "sample", + "sources_count": 1, + "ready_to_accept": True, + "guardrails": "value", + "waiting_since": "2026-01-01T00:00:00Z", + "due_at": "2026-01-01T00:00:00Z", + "prepared_at": "2026-01-01T00:00:00Z", + "status": "sample", + "is_open": True, + "lookups": 1, + "duration_ms": 1, + "agent_version": "sample", + "prepared_before_opened": True, + "report": {}, + "trail": "value", + "decision": { + "outcome": "accepted", + "action": "approve", + "actor_email": "sample", + "actor_name": "sample", + "override_category": "missing_context", + "override_reason": "sample", + "final_status": "sample", + "agreed": True, + "decided_at": "2026-01-01T00:00:00Z", + "billed": True, + "decided_by": "investigator", + "original_outcome": { + "action": "sample", + "final_status": "sample", + "decided_at": "2026-01-01T00:00:00Z", + }, + "overrides": [ + { + "action": "sample", + "actor_email": "sample", + "actor_name": "sample", + "override_category": "sample", + "override_reason": "sample", + "previous_status": "sample", + "final_status": "sample", + "agreed": True, + "overridden_at": "2026-01-01T00:00:00Z", + } + ], + }, + "autonomy": {}, + }, + strict_type=InvestigatorRunDetail, + ), + lambda c: c.investigator.retrieve_run("00000000-0000-4000-8000-000000000001"), + lambda c: c.investigator.retrieve_run("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, InvestigatorRunDetail), + lambda r: isinstance(r, InvestigatorRunDetail), + ) + + +def test_investigator_retrieve_settings() -> None: + """``GET /v3/investigator/settings/``""" + check_operation( + Expect( + method="GET", + path="/v3/investigator/settings/", + api="verification", + auth="api_key", + status=200, + response_json={ + "stage": "none", + "stage_changed_at": "2026-01-01T00:00:00Z", + "stage_changed_by": "sample", + "monthly_spend_limit": "sample", + "spend_limit": { + "enabled": True, + "limit": "sample", + "spent": "sample", + "reviews": 1, + "reserved": "sample", + "currency": "sample", + "paused": True, + }, + }, + strict_type=InvestigatorSettings, + ), + lambda c: c.investigator.retrieve_settings(), + lambda c: c.investigator.retrieve_settings(), + lambda r: isinstance(r, InvestigatorSettings), + lambda r: isinstance(r, InvestigatorSettings), + ) + + +def test_investigator_retrieve_summary() -> None: + """``GET /v3/investigator/summary/``""" + check_operation( + Expect( + method="GET", + path="/v3/investigator/summary/", + api="verification", + auth="api_key", + query={"scope": ["all"], "timezone": ["sample"]}, + status=200, + response_json={ + "stage": "sample", + "last_report_at": "2026-01-01T00:00:00Z", + "prepared_today": 1, + "ready_to_accept": 1, + "open": 1, + "decided_today": 1, + "automation_rate_month": 1.5, + "preparing": 1, + }, + strict_type=InvestigatorRetrieveSummaryResponse, + ), + lambda c: c.investigator.retrieve_summary(scope="all", timezone="sample"), + lambda c: c.investigator.retrieve_summary(scope="all", timezone="sample"), + lambda r: isinstance(r, InvestigatorRetrieveSummaryResponse), + lambda r: isinstance(r, InvestigatorRetrieveSummaryResponse), + ) + + +def test_investigator_retrieve_transaction() -> None: + """``GET /v3/investigator/transactions/{transaction_id}/``""" + check_operation( + Expect( + method="GET", + path="/v3/investigator/transactions/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "transaction_id": "00000000-0000-4000-8000-000000000002", + "investigator": { + "run_id": "00000000-0000-4000-8000-000000000004", + "status": "running", + "stage": "shadow", + "queue": "kyc", + "is_open": True, + "recommendation": "approve", + "recommendation_detail": "sample", + "confidence": "high", + "summary": "sample", + "ready_to_accept": True, + "prepared_at": "2026-01-01T00:00:00Z", + "automated": True, + "decided_by": "investigator", + "decision": { + "outcome": "accepted", + "action": "approve", + "actor_email": "sample", + "actor_name": "sample", + "override_category": "missing_context", + "override_reason": "sample", + "final_status": "sample", + "agreed": True, + "decided_at": "2026-01-01T00:00:00Z", + "billed": True, + "decided_by": "investigator", + "original_outcome": { + "action": "sample", + "final_status": "sample", + "decided_at": "2026-01-01T00:00:00Z", + }, + "overrides": [ + { + "action": "sample", + "actor_email": "sample", + "actor_name": "sample", + "override_category": "sample", + "override_reason": "sample", + "previous_status": "sample", + "final_status": "sample", + "agreed": True, + "overridden_at": "2026-01-01T00:00:00Z", + } + ], + }, + }, + }, + strict_type=InvestigatorRetrieveTransactionResponse, + ), + lambda c: c.investigator.retrieve_transaction("00000000-0000-4000-8000-000000000001"), + lambda c: c.investigator.retrieve_transaction("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, InvestigatorRetrieveTransactionResponse), + lambda r: isinstance(r, InvestigatorRetrieveTransactionResponse), + ) + + +def test_investigator_update_settings() -> None: + """``PATCH /v3/investigator/settings/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/investigator/settings/", + api="verification", + auth="api_key", + json_body={"stage": "none"}, + status=200, + response_json={ + "stage": "none", + "stage_changed_at": "2026-01-01T00:00:00Z", + "stage_changed_by": "sample", + "monthly_spend_limit": "sample", + "spend_limit": { + "enabled": True, + "limit": "sample", + "spent": "sample", + "reviews": 1, + "reserved": "sample", + "currency": "sample", + "paused": True, + }, + }, + strict_type=InvestigatorSettings, + ), + lambda c: c.investigator.update_settings(stage="none"), + lambda c: c.investigator.update_settings(stage="none"), + lambda r: isinstance(r, InvestigatorSettings), + lambda r: isinstance(r, InvestigatorSettings), + ) diff --git a/tests/generated/test_lists.py b/tests/generated/test_lists.py new file mode 100644 index 0000000..8654f97 --- /dev/null +++ b/tests/generated/test_lists.py @@ -0,0 +1,366 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``lists`` resources: one per operation.""" + +from __future__ import annotations + +from didit import AsyncPage, SyncPage +from didit.types.components.compliance_list import ComplianceList +from didit.types.components.compliance_list_entry import ComplianceListEntry +from didit.types.lists.update_response import ListsUpdateResponse +from tests.harness import Expect, check_operation + + +def test_lists_create() -> None: + """``POST /v3/lists/``""" + check_operation( + Expect( + method="POST", + path="/v3/lists/", + api="verification", + auth="api_key", + json_body={ + "name": "Trusted office IPs", + "list_type": "allowlist", + "entry_type": "ip_address", + "description": "Office egress ranges", + }, + status=201, + response_json={ + "uuid": "22222222-3333-4444-5555-666666666666", + "name": "Trusted office IPs", + "description": "Office egress ranges", + "list_type": "allowlist", + "entry_type": "ip_address", + "is_system": False, + "entry_count": 0, + "last_entry_added_at": None, + "created_at": "2026-10-12T09:15:44.031876Z", + "updated_at": "2026-10-12T09:15:44.031876Z", + }, + ), + lambda c: c.lists.create( + name="Trusted office IPs", + list_type="allowlist", + entry_type="ip_address", + description="Office egress ranges", + ), + lambda c: c.lists.create( + name="Trusted office IPs", + list_type="allowlist", + entry_type="ip_address", + description="Office egress ranges", + ), + lambda r: isinstance(r, ComplianceList), + lambda r: isinstance(r, ComplianceList), + ) + + +def test_lists_delete() -> None: + """``DELETE /v3/lists/{list_uuid}/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/lists/11111111-2222-3333-4444-555555555555/", + api="verification", + auth="api_key", + status=204, + ), + lambda c: c.lists.delete("11111111-2222-3333-4444-555555555555"), + lambda c: c.lists.delete("11111111-2222-3333-4444-555555555555"), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_lists_list() -> None: + """``GET /v3/lists/``""" + check_operation( + Expect( + method="GET", + path="/v3/lists/", + api="verification", + auth="api_key", + query={ + "list_type": ["blocklist"], + "entry_type": ["face"], + "ordering": ["-created_at"], + "limit": ["1"], + "offset": ["1"], + }, + status=200, + response_json={ + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "uuid": "22222222-3333-4444-5555-666666666666", + "name": "Trusted office IPs", + "description": "Office egress ranges", + "list_type": "allowlist", + "entry_type": "ip_address", + "is_system": False, + "entry_count": 3, + "last_entry_added_at": "2026-10-02T08:15:27.501334Z", + "created_at": "2026-09-30T13:20:45.889120Z", + "updated_at": "2026-09-30T13:20:45.889120Z", + }, + { + "uuid": "11111111-2222-3333-4444-555555555555", + "name": "Face Blocklist", + "description": "Blocked biometric faces. When a face matching an entry in this list is detected during liveness verification, the session will be automatically declined.", + "list_type": "blocklist", + "entry_type": "face", + "is_system": True, + "entry_count": 12, + "last_entry_added_at": "2026-10-09T16:42:03.118204Z", + "created_at": "2026-01-14T10:02:11.402917Z", + "updated_at": "2026-01-14T10:02:11.402917Z", + }, + ], + }, + ), + lambda c: c.lists.list( + list_type="blocklist", entry_type="face", ordering="-created_at", limit=1, offset=1 + ), + lambda c: c.lists.list( + list_type="blocklist", entry_type="face", ordering="-created_at", limit=1, offset=1 + ), + lambda r: ( + isinstance(r, SyncPage) and all(isinstance(x1, ComplianceList) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, ComplianceList) for x1 in r.results) + ), + ) + + +def test_lists_retrieve() -> None: + """``GET /v3/lists/{list_uuid}/``""" + check_operation( + Expect( + method="GET", + path="/v3/lists/11111111-2222-3333-4444-555555555555/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "11111111-2222-3333-4444-555555555555", + "name": "Face Blocklist", + "description": "Blocked biometric faces. When a face matching an entry in this list is detected during liveness verification, the session will be automatically declined.", + "list_type": "blocklist", + "entry_type": "face", + "is_system": True, + "entry_count": 12, + "last_entry_added_at": "2026-10-09T16:42:03.118204Z", + "created_at": "2026-01-14T10:02:11.402917Z", + "updated_at": "2026-01-14T10:02:11.402917Z", + }, + ), + lambda c: c.lists.retrieve("11111111-2222-3333-4444-555555555555"), + lambda c: c.lists.retrieve("11111111-2222-3333-4444-555555555555"), + lambda r: isinstance(r, ComplianceList), + lambda r: isinstance(r, ComplianceList), + ) + + +def test_lists_update() -> None: + """``PATCH /v3/lists/{list_uuid}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/lists/11111111-2222-3333-4444-555555555555/", + api="verification", + auth="api_key", + json_body={"name": "Trusted office and VPN IPs"}, + status=200, + response_json={ + "name": "Trusted office and VPN IPs", + "description": "Office egress ranges", + }, + ), + lambda c: c.lists.update( + "11111111-2222-3333-4444-555555555555", name="Trusted office and VPN IPs" + ), + lambda c: c.lists.update( + "11111111-2222-3333-4444-555555555555", name="Trusted office and VPN IPs" + ), + lambda r: isinstance(r, ListsUpdateResponse), + lambda r: isinstance(r, ListsUpdateResponse), + ) + + +def test_lists_entries_create() -> None: + """``POST /v3/lists/{list_uuid}/entries/``""" + check_operation( + Expect( + method="POST", + path="/v3/lists/11111111-2222-3333-4444-555555555555/entries/", + api="verification", + auth="api_key", + json_body={"value": "+14155552671", "comment": "Reported fraud"}, + status=201, + response_json={ + "uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "value": "+14155552671", + "display_label": "", + "comment": "Reported fraud", + "added_by": "api", + "reference_session_id": None, + "reference_session_number": None, + "reference_session_type": None, + "reference_object_uuid": None, + "metadata": None, + "created_at": "2026-10-12T10:04:51.220391Z", + }, + ), + lambda c: c.lists.entries.create( + "11111111-2222-3333-4444-555555555555", value="+14155552671", comment="Reported fraud" + ), + lambda c: c.lists.entries.create( + "11111111-2222-3333-4444-555555555555", value="+14155552671", comment="Reported fraud" + ), + lambda r: isinstance(r, ComplianceListEntry), + lambda r: isinstance(r, ComplianceListEntry), + ) + + +def test_lists_entries_delete() -> None: + """``DELETE /v3/lists/{list_uuid}/entries/{entry_uuid}/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/lists/11111111-2222-3333-4444-555555555555/entries/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/", + api="verification", + auth="api_key", + status=204, + ), + lambda c: c.lists.entries.delete( + "11111111-2222-3333-4444-555555555555", "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + ), + lambda c: c.lists.entries.delete( + "11111111-2222-3333-4444-555555555555", "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + ), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_lists_entries_list() -> None: + """``GET /v3/lists/{list_uuid}/entries/``""" + check_operation( + Expect( + method="GET", + path="/v3/lists/11111111-2222-3333-4444-555555555555/entries/", + api="verification", + auth="api_key", + query={ + "search": ["sample"], + "reference_session_id": ["00000000-0000-4000-8000-000000000001"], + "limit": ["1"], + "offset": ["1"], + }, + status=200, + response_json={ + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "uuid": "bbbbbbbb-cccc-dddd-eeee-ffffffffffff", + "value": "X1234567", + "display_label": "Jane Doe - X1234567", + "comment": "", + "added_by": "api", + "reference_session_id": "33333333-4444-5555-6666-777777777777", + "reference_session_number": 1042, + "reference_session_type": "kyc", + "reference_object_uuid": "66666666-7777-8888-9999-000000000000", + "metadata": { + "full_name": "Jane Doe", + "issuing_state": "ESP", + "document_type": "ID", + "region": "", + "document_number": "X1234567", + }, + "created_at": "2026-10-12T10:06:12.774915Z", + }, + { + "uuid": "eeeeeeee-ffff-0000-1111-222222222222", + "value": "y7654321", + "display_label": "", + "comment": "Reported by a partner", + "added_by": "api", + "reference_session_id": None, + "reference_session_number": None, + "reference_session_type": None, + "reference_object_uuid": None, + "metadata": None, + "created_at": "2026-10-03T07:48:30.552810Z", + }, + ], + }, + ), + lambda c: c.lists.entries.list( + "11111111-2222-3333-4444-555555555555", + search="sample", + reference_session_id="00000000-0000-4000-8000-000000000001", + limit=1, + offset=1, + ), + lambda c: c.lists.entries.list( + "11111111-2222-3333-4444-555555555555", + search="sample", + reference_session_id="00000000-0000-4000-8000-000000000001", + limit=1, + offset=1, + ), + lambda r: ( + isinstance(r, SyncPage) and all(isinstance(x1, ComplianceListEntry) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, ComplianceListEntry) for x1 in r.results) + ), + ) + + +def test_lists_entries_upload_face() -> None: + """``POST /v3/lists/{list_uuid}/entries/face-upload/``""" + check_operation( + Expect( + method="POST", + path="/v3/lists/11111111-2222-3333-4444-555555555555/entries/face-upload/", + api="verification", + auth="api_key", + json_body={"image": "/9j/4AAQSkZJRgABAQAAAQABAAD...", "comment": "Known fraud attempt"}, + status=201, + response_json={ + "uuid": "cccccccc-dddd-eeee-ffff-000000000000", + "value": "44444444-5555-6666-7777-888888888888", + "display_label": "Manually uploaded face", + "comment": "Known fraud attempt", + "added_by": "api", + "reference_session_id": None, + "reference_session_number": None, + "reference_session_type": None, + "reference_object_uuid": "44444444-5555-6666-7777-888888888888", + "metadata": { + "reference_image_url": "https://media.example.com/faces/reference.jpg?expires=1760263938&signature=3f9a1c" + }, + "created_at": "2026-10-12T10:12:18.904467Z", + }, + ), + lambda c: c.lists.entries.upload_face( + "11111111-2222-3333-4444-555555555555", + image="/9j/4AAQSkZJRgABAQAAAQABAAD...", + comment="Known fraud attempt", + ), + lambda c: c.lists.entries.upload_face( + "11111111-2222-3333-4444-555555555555", + image="/9j/4AAQSkZJRgABAQAAAQABAAD...", + comment="Known fraud attempt", + ), + lambda r: isinstance(r, ComplianceListEntry), + lambda r: isinstance(r, ComplianceListEntry), + ) diff --git a/tests/generated/test_monitoring_events.py b/tests/generated/test_monitoring_events.py new file mode 100644 index 0000000..a76a4b8 --- /dev/null +++ b/tests/generated/test_monitoring_events.py @@ -0,0 +1,746 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``monitoring_events`` resources: one per operation.""" + +from __future__ import annotations + +from didit import AsyncPage, SyncPage +from didit.types.components.monitoring_event import MonitoringEvent +from didit.types.components.monitoring_event_detail import MonitoringEventDetail +from didit.types.components.monitoring_event_ingest_result import MonitoringEventIngestResult +from didit.types.monitoring_events.create_batch_response import MonitoringEventsCreateBatchResponse +from didit.types.monitoring_events.create_sdk_token_response import ( + MonitoringEventsCreateSdkTokenResponse, +) +from didit.types.monitoring_events.retrieve_enrichment_metrics_response import ( + MonitoringEventsRetrieveEnrichmentMetricsResponse, +) +from tests.harness import Expect, check_operation + + +def test_monitoring_events_create() -> None: + """``POST /v3/events/``""" + check_operation( + Expect( + method="POST", + path="/v3/events/", + api="verification", + auth="api_key", + json_body={ + "event_id": "login-2026-10-17-000001", + "event_category": "auth", + "action_type": "login", + "occurred_at": "2026-10-17T09:15:02.481Z", + "subject": {"vendor_data": "user-42", "entity_type": "individual"}, + "time_zone": "Europe/Madrid", + "session": {"session_id": "web-session-abc", "sequence": 142}, + "device_context": { + "fingerprint": "fp-a8c1", + "network_context": {"ip_address": "203.0.113.42"}, + }, + "payload": {"method": "password"}, + "custom_values": {"vip_tier": "gold"}, + }, + status=200, + response_json={ + "event_uuid": "33333333-4444-5555-6666-777777777777", + "event_id": "login-2026-10-17-000001", + "status": "ACCEPTED", + "score": 0, + "rules_evaluated_count": 6, + "rules_matched_count": 0, + "correlation_pending": False, + "retention_class": "standard", + "expires_at": "2026-11-16T09:15:02.481000Z", + "idempotent_replay": True, + "rule_matches": [], + }, + ), + lambda c: c.monitoring_events.create( + event_id="login-2026-10-17-000001", + event_category="auth", + action_type="login", + occurred_at="2026-10-17T09:15:02.481Z", + subject={"vendor_data": "user-42", "entity_type": "individual"}, + time_zone="Europe/Madrid", + session={"session_id": "web-session-abc", "sequence": 142}, + device_context={ + "fingerprint": "fp-a8c1", + "network_context": {"ip_address": "203.0.113.42"}, + }, + payload={"method": "password"}, + custom_values={"vip_tier": "gold"}, + ), + lambda c: c.monitoring_events.create( + event_id="login-2026-10-17-000001", + event_category="auth", + action_type="login", + occurred_at="2026-10-17T09:15:02.481Z", + subject={"vendor_data": "user-42", "entity_type": "individual"}, + time_zone="Europe/Madrid", + session={"session_id": "web-session-abc", "sequence": 142}, + device_context={ + "fingerprint": "fp-a8c1", + "network_context": {"ip_address": "203.0.113.42"}, + }, + payload={"method": "password"}, + custom_values={"vip_tier": "gold"}, + ), + lambda r: isinstance(r, MonitoringEventIngestResult), + lambda r: isinstance(r, MonitoringEventIngestResult), + ) + + +def test_monitoring_events_create_batch() -> None: + """``POST /v3/events/batch/``""" + check_operation( + Expect( + method="POST", + path="/v3/events/batch/", + api="verification", + auth="api_key", + json_body={ + "events": [ + { + "event_id": "login-2026-10-17-000001", + "event_category": "auth", + "action_type": "login", + "occurred_at": "2026-10-17T09:15:02.481Z", + "subject": {"vendor_data": "user-42"}, + }, + { + "event_id": "deposit-2026-10-17-000002", + "event_category": "payment", + "action_type": "deposit_completed", + "occurred_at": "2026-10-17T09:16:40Z", + "subject": {"vendor_data": "user-42"}, + "transaction_context": { + "amount": "100.00", + "currency": "EUR", + "payment_reference_id": "dep_736", + }, + "payload": {"payment_method": "card", "status": "completed"}, + }, + ] + }, + status=201, + response_json={ + "accepted_count": 1, + "replayed_count": 1, + "rejected_count": 0, + "results": [ + { + "index": 0, + "outcome": "accepted", + "event_uuid": "33333333-4444-5555-6666-777777777777", + "event_id": "login-2026-10-17-000001", + "status": "ACCEPTED", + "score": 0, + "rules_evaluated_count": 6, + "rules_matched_count": 0, + "correlation_pending": False, + "retention_class": "standard", + "expires_at": "2026-11-16T09:15:02.481000Z", + "idempotent_replay": False, + "rule_matches": [], + }, + { + "index": 1, + "outcome": "replayed", + "event_uuid": "11111111-2222-3333-4444-555555555555", + "event_id": "deposit-2026-10-17-000002", + "status": "ACCEPTED", + "score": 0, + "rules_evaluated_count": 6, + "rules_matched_count": 0, + "correlation_pending": False, + "retention_class": "monetary_or_compliance", + "expires_at": None, + "idempotent_replay": True, + "rule_matches": [], + }, + ], + }, + ), + lambda c: c.monitoring_events.create_batch( + events=[ + { + "event_id": "login-2026-10-17-000001", + "event_category": "auth", + "action_type": "login", + "occurred_at": "2026-10-17T09:15:02.481Z", + "subject": {"vendor_data": "user-42"}, + }, + { + "event_id": "deposit-2026-10-17-000002", + "event_category": "payment", + "action_type": "deposit_completed", + "occurred_at": "2026-10-17T09:16:40Z", + "subject": {"vendor_data": "user-42"}, + "transaction_context": { + "amount": "100.00", + "currency": "EUR", + "payment_reference_id": "dep_736", + }, + "payload": {"payment_method": "card", "status": "completed"}, + }, + ] + ), + lambda c: c.monitoring_events.create_batch( + events=[ + { + "event_id": "login-2026-10-17-000001", + "event_category": "auth", + "action_type": "login", + "occurred_at": "2026-10-17T09:15:02.481Z", + "subject": {"vendor_data": "user-42"}, + }, + { + "event_id": "deposit-2026-10-17-000002", + "event_category": "payment", + "action_type": "deposit_completed", + "occurred_at": "2026-10-17T09:16:40Z", + "subject": {"vendor_data": "user-42"}, + "transaction_context": { + "amount": "100.00", + "currency": "EUR", + "payment_reference_id": "dep_736", + }, + "payload": {"payment_method": "card", "status": "completed"}, + }, + ] + ), + lambda r: isinstance(r, MonitoringEventsCreateBatchResponse), + lambda r: isinstance(r, MonitoringEventsCreateBatchResponse), + ) + + +def test_monitoring_events_create_sdk_token() -> None: + """``POST /v3/events/sdk-token/``""" + check_operation( + Expect( + method="POST", + path="/v3/events/sdk-token/", + api="verification", + auth="api_key", + json_body={ + "vendor_data": "user-42", + "allowed_event_categories": ["auth", "session", "gameplay"], + "ttl_seconds": 900, + "max_uses": 1000, + "bind_to_origin": "https://app.example.com", + }, + status=201, + response_json={ + "monitoring_token": "sample-token", + "expires_at": "2026-10-17T09:30:02.481000Z", + "allowed_event_categories": ["auth", "session", "gameplay"], + "max_uses": 1000, + "bind_to_origin": "https://app.example.com", + }, + ), + lambda c: c.monitoring_events.create_sdk_token( + vendor_data="user-42", + allowed_event_categories=["auth", "session", "gameplay"], + ttl_seconds=900, + max_uses=1000, + bind_to_origin="https://app.example.com", + ), + lambda c: c.monitoring_events.create_sdk_token( + vendor_data="user-42", + allowed_event_categories=["auth", "session", "gameplay"], + ttl_seconds=900, + max_uses=1000, + bind_to_origin="https://app.example.com", + ), + lambda r: isinstance(r, MonitoringEventsCreateSdkTokenResponse), + lambda r: isinstance(r, MonitoringEventsCreateSdkTokenResponse), + ) + + +def test_monitoring_events_delete() -> None: + """``DELETE /v3/events/{event_uuid}/delete/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/events/11111111-2222-3333-4444-555555555555/delete/", + api="verification", + auth="api_key", + status=204, + ), + lambda c: c.monitoring_events.delete("11111111-2222-3333-4444-555555555555"), + lambda c: c.monitoring_events.delete("11111111-2222-3333-4444-555555555555"), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_monitoring_events_list() -> None: + """``GET /v3/events/``""" + check_operation( + Expect( + method="GET", + path="/v3/events/", + api="verification", + auth="api_key", + query={ + "limit": ["50"], + "offset": ["0"], + "event_category": ["payment", "dispute"], + "action_type": ["deposit_completed", "chargeback_opened"], + "status": ["IN_REVIEW", "DECLINED"], + "retention_class": ["risk_relevant"], + "subject_vendor_data": ["user-42"], + "subject_entity_type": ["individual"], + "business_vendor_data": ["merchant-7"], + "session_id": ["web-session-abc"], + "device_fingerprint": ["fp-a8c1"], + "ip_address": ["203.0.113.42"], + "game_id": ["poker-holdem-01"], + "round_id": ["round-88231"], + "affiliate_code": ["aff-summer"], + "payment_reference_id": ["dep_735"], + "counterparty_vendor_data": ["user-77"], + "matched_only": ["true"], + "occurred_from": ["2026-10-01T00:00:00Z"], + "occurred_to": ["2026-10-17T23:59:59Z"], + "search": ["chargeback"], + }, + status=200, + response_json={ + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "event_uuid": "11111111-2222-3333-4444-555555555555", + "event_id": "chargeback-2026-10-16-000318", + "event_category": "dispute", + "action_type": "chargeback_opened", + "occurred_at": "2026-10-16T14:02:11Z", + "received_at": "2026-10-16T14:02:12.418230Z", + "status": "IN_REVIEW", + "score": 60, + "subject_vendor_data": "user-42", + "subject_entity_type": "individual", + "subject_uuid": "66666666-7777-8888-9999-000000000000", + "counterparty_vendor_data": None, + "session_id": None, + "device_fingerprint": None, + "ip_address": None, + "ip_country": None, + "amount": "250.000000000000000000", + "currency": "EUR", + "game_id": None, + "round_id": None, + "bonus_id": None, + "affiliate_code": None, + "payment_reference_id": "dep_735", + "retention_class": "monetary_or_compliance", + "rules_matched_count": 1, + "correlation_pending": False, + "is_droppable": False, + }, + { + "event_uuid": "22222222-3333-4444-5555-666666666666", + "event_id": "login-2026-10-15-000912", + "event_category": "auth", + "action_type": "login", + "occurred_at": "2026-10-15T08:30:00.120000Z", + "received_at": "2026-10-15T08:30:00.734512Z", + "status": "ACCEPTED", + "score": 0, + "subject_vendor_data": "user-42", + "subject_entity_type": "individual", + "subject_uuid": "66666666-7777-8888-9999-000000000000", + "counterparty_vendor_data": None, + "session_id": "web-session-abc", + "device_fingerprint": "fp-a8c1", + "ip_address": "203.0.113.42", + "ip_country": "ES", + "amount": None, + "currency": None, + "game_id": None, + "round_id": None, + "bonus_id": None, + "affiliate_code": None, + "payment_reference_id": None, + "retention_class": "standard", + "rules_matched_count": 0, + "correlation_pending": False, + "is_droppable": True, + }, + ], + }, + ), + lambda c: c.monitoring_events.list( + limit=50, + offset=0, + event_category=["payment", "dispute"], + action_type=["deposit_completed", "chargeback_opened"], + status=["IN_REVIEW", "DECLINED"], + retention_class=["risk_relevant"], + subject_vendor_data="user-42", + subject_entity_type="individual", + business_vendor_data="merchant-7", + session_id="web-session-abc", + device_fingerprint="fp-a8c1", + ip_address="203.0.113.42", + game_id="poker-holdem-01", + round_id="round-88231", + affiliate_code="aff-summer", + payment_reference_id="dep_735", + counterparty_vendor_data="user-77", + matched_only=True, + occurred_from="2026-10-01T00:00:00Z", + occurred_to="2026-10-17T23:59:59Z", + search="chargeback", + ), + lambda c: c.monitoring_events.list( + limit=50, + offset=0, + event_category=["payment", "dispute"], + action_type=["deposit_completed", "chargeback_opened"], + status=["IN_REVIEW", "DECLINED"], + retention_class=["risk_relevant"], + subject_vendor_data="user-42", + subject_entity_type="individual", + business_vendor_data="merchant-7", + session_id="web-session-abc", + device_fingerprint="fp-a8c1", + ip_address="203.0.113.42", + game_id="poker-holdem-01", + round_id="round-88231", + affiliate_code="aff-summer", + payment_reference_id="dep_735", + counterparty_vendor_data="user-77", + matched_only=True, + occurred_from="2026-10-01T00:00:00Z", + occurred_to="2026-10-17T23:59:59Z", + search="chargeback", + ), + lambda r: ( + isinstance(r, SyncPage) and all(isinstance(x1, MonitoringEvent) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, MonitoringEvent) for x1 in r.results) + ), + ) + + +def test_monitoring_events_retrieve() -> None: + """``GET /v3/events/{event_uuid}/``""" + check_operation( + Expect( + method="GET", + path="/v3/events/11111111-2222-3333-4444-555555555555/", + api="verification", + auth="api_key", + status=200, + response_json={ + "event_uuid": "33333333-4444-5555-6666-777777777777", + "event_id": "login-2026-10-17-000001", + "event_category": "auth", + "action_type": "login", + "occurred_at": "2026-10-17T09:15:02.481000Z", + "received_at": "2026-10-17T09:15:02.903114Z", + "status": "IN_REVIEW", + "score": 40, + "subject_vendor_data": "user-42", + "subject_entity_type": "individual", + "subject_uuid": "66666666-7777-8888-9999-000000000000", + "counterparty_vendor_data": None, + "session_id": "web-session-abc", + "device_fingerprint": "fp-a8c1", + "ip_address": "203.0.113.42", + "ip_country": "ES", + "amount": None, + "currency": None, + "game_id": None, + "round_id": None, + "bonus_id": None, + "affiliate_code": None, + "payment_reference_id": None, + "retention_class": "risk_relevant", + "rules_matched_count": 1, + "correlation_pending": False, + "is_droppable": True, + "payload": {"method": "password"}, + "custom_values": {"vip_tier": "gold", "vpn_or_proxy_detected": True}, + "device_context": { + "fingerprint": "fp-a8c1", + "network_context": { + "ip_address": "203.0.113.42", + "ip_country": "Spain", + "ip_country_code": "ES", + "ip_city": "Madrid", + "asn_number": 64500, + "asn_organization": "Example Networks", + "is_vpn_or_tor": True, + "is_data_center": False, + }, + }, + "field_sources": { + "subject.vendor_data": "customer", + "event_category": "customer", + "action_type": "customer", + "occurred_at": "customer", + "received_at": "server", + "payload.method": "customer", + "session.session_id": "customer", + "session.sequence": "customer", + "device_context.fingerprint": "customer", + "device_context.network_context.ip_address": "customer", + "device_context.network_context.ip_country": "server", + "device_context.network_context.ip_country_code": "server", + "device_context.network_context.ip_city": "server", + "device_context.network_context.asn_number": "server", + "device_context.network_context.asn_organization": "server", + "device_context.network_context.is_vpn_or_tor": "server", + "device_context.network_context.is_data_center": "server", + }, + "session_sequence": 142, + "table_id": None, + "hand_id": None, + "match_id": None, + "market_id": None, + "side": None, + "role": None, + "net_result_amount": None, + "payment_fingerprint": None, + "instrument_reference_hash": None, + "withdrawal_address_hash": None, + "dispute_id": None, + "chargeback_id": None, + "changed_field": None, + "identity_match_ref": None, + "schema_version": 1, + "expires_at": "2027-01-15T09:15:02.481000Z", + "rules_evaluated_count": 6, + "rule_runs": [ + { + "rule_uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "rule_title": "Login from VPN or Tor", + "library_key": None, + "matched": True, + "mode": "ACTIVE", + "is_async": False, + "severity": "MEDIUM", + "score_delta": 40, + "action_summary": "score +40; add_tag:VPN_LOGIN", + "matched_payload": [ + { + "field": "event.ip.is_vpn_or_tor", + "operator": "eq", + "expected_value": True, + "current_value": True, + "matched": True, + "group_index": None, + "group_logic": "AND", + "field_source": "server", + } + ], + "created_at": "2026-10-17T09:15:02.951207Z", + } + ], + "ip_enrichment": { + "ip_country_code": "ES", + "ip_city": "Madrid", + "asn_number": 64500, + "asn_organization": "Example Networks", + "is_vpn_or_tor": True, + "is_data_center": False, + "proxy_type": None, + "last_enriched_at": "2026-10-17T09:15:02.910442Z", + }, + "subject_profile": None, + "tags": ["VPN_LOGIN"], + }, + ), + lambda c: c.monitoring_events.retrieve("11111111-2222-3333-4444-555555555555"), + lambda c: c.monitoring_events.retrieve("11111111-2222-3333-4444-555555555555"), + lambda r: isinstance(r, MonitoringEventDetail), + lambda r: isinstance(r, MonitoringEventDetail), + ) + + +def test_monitoring_events_retrieve_enrichment_metrics() -> None: + """``GET /v3/events/enrichment-metrics/``""" + check_operation( + Expect( + method="GET", + path="/v3/events/enrichment-metrics/", + api="verification", + auth="api_key", + query={"days": ["30"]}, + status=200, + response_json={ + "events_ingested": 128450, + "events_rejected": 214, + "enrichment_lookups": 41230, + "enrichment_cache_hits": 40118, + "enrichment_provider_calls": 1112, + "enrichment_skipped_low_value": 74320, + "unique_ips_seen": 1112, + "period_days": 30, + "cache_hit_rate": 0.973, + "unique_ip_ratio": 0.0087, + "provider_calls_per_1k_events": 8.66, + }, + ), + lambda c: c.monitoring_events.retrieve_enrichment_metrics(days=30), + lambda c: c.monitoring_events.retrieve_enrichment_metrics(days=30), + lambda r: isinstance(r, MonitoringEventsRetrieveEnrichmentMetricsResponse), + lambda r: isinstance(r, MonitoringEventsRetrieveEnrichmentMetricsResponse), + ) + + +def test_monitoring_events_retrieve_subject_timeline() -> None: + """``GET /v3/events/subjects/{vendor_data}/``""" + check_operation( + Expect( + method="GET", + path="/v3/events/subjects/user-42/", + api="verification", + auth="api_key", + query={ + "entity_type": ["individual"], + "limit": ["50"], + "offset": ["0"], + "event_category": ["payment", "dispute"], + "action_type": ["deposit_completed", "chargeback_opened"], + "status": ["IN_REVIEW", "DECLINED"], + "retention_class": ["risk_relevant"], + "business_vendor_data": ["merchant-7"], + "session_id": ["web-session-abc"], + "device_fingerprint": ["fp-a8c1"], + "ip_address": ["203.0.113.42"], + "game_id": ["poker-holdem-01"], + "round_id": ["round-88231"], + "affiliate_code": ["aff-summer"], + "payment_reference_id": ["dep_735"], + "counterparty_vendor_data": ["user-77"], + "matched_only": ["true"], + "occurred_from": ["2026-10-01T00:00:00Z"], + "occurred_to": ["2026-10-17T23:59:59Z"], + "search": ["chargeback"], + }, + status=200, + response_json={ + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "event_uuid": "11111111-2222-3333-4444-555555555555", + "event_id": "chargeback-2026-10-16-000318", + "event_category": "dispute", + "action_type": "chargeback_opened", + "occurred_at": "2026-10-16T14:02:11Z", + "received_at": "2026-10-16T14:02:12.418230Z", + "status": "IN_REVIEW", + "score": 60, + "subject_vendor_data": "user-42", + "subject_entity_type": "individual", + "subject_uuid": "66666666-7777-8888-9999-000000000000", + "counterparty_vendor_data": None, + "session_id": None, + "device_fingerprint": None, + "ip_address": None, + "ip_country": None, + "amount": "250.000000000000000000", + "currency": "EUR", + "game_id": None, + "round_id": None, + "bonus_id": None, + "affiliate_code": None, + "payment_reference_id": "dep_735", + "retention_class": "monetary_or_compliance", + "rules_matched_count": 1, + "correlation_pending": False, + "is_droppable": False, + }, + { + "event_uuid": "22222222-3333-4444-5555-666666666666", + "event_id": "login-2026-10-15-000912", + "event_category": "auth", + "action_type": "login", + "occurred_at": "2026-10-15T08:30:00.120000Z", + "received_at": "2026-10-15T08:30:00.734512Z", + "status": "ACCEPTED", + "score": 0, + "subject_vendor_data": "user-42", + "subject_entity_type": "individual", + "subject_uuid": "66666666-7777-8888-9999-000000000000", + "counterparty_vendor_data": None, + "session_id": "web-session-abc", + "device_fingerprint": "fp-a8c1", + "ip_address": "203.0.113.42", + "ip_country": "ES", + "amount": None, + "currency": None, + "game_id": None, + "round_id": None, + "bonus_id": None, + "affiliate_code": None, + "payment_reference_id": None, + "retention_class": "standard", + "rules_matched_count": 0, + "correlation_pending": False, + "is_droppable": True, + }, + ], + }, + ), + lambda c: c.monitoring_events.retrieve_subject_timeline( + "user-42", + entity_type="individual", + limit=50, + offset=0, + event_category=["payment", "dispute"], + action_type=["deposit_completed", "chargeback_opened"], + status=["IN_REVIEW", "DECLINED"], + retention_class=["risk_relevant"], + business_vendor_data="merchant-7", + session_id="web-session-abc", + device_fingerprint="fp-a8c1", + ip_address="203.0.113.42", + game_id="poker-holdem-01", + round_id="round-88231", + affiliate_code="aff-summer", + payment_reference_id="dep_735", + counterparty_vendor_data="user-77", + matched_only=True, + occurred_from="2026-10-01T00:00:00Z", + occurred_to="2026-10-17T23:59:59Z", + search="chargeback", + ), + lambda c: c.monitoring_events.retrieve_subject_timeline( + "user-42", + entity_type="individual", + limit=50, + offset=0, + event_category=["payment", "dispute"], + action_type=["deposit_completed", "chargeback_opened"], + status=["IN_REVIEW", "DECLINED"], + retention_class=["risk_relevant"], + business_vendor_data="merchant-7", + session_id="web-session-abc", + device_fingerprint="fp-a8c1", + ip_address="203.0.113.42", + game_id="poker-holdem-01", + round_id="round-88231", + affiliate_code="aff-summer", + payment_reference_id="dep_735", + counterparty_vendor_data="user-77", + matched_only=True, + occurred_from="2026-10-01T00:00:00Z", + occurred_to="2026-10-17T23:59:59Z", + search="chargeback", + ), + lambda r: ( + isinstance(r, SyncPage) and all(isinstance(x1, MonitoringEvent) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, MonitoringEvent) for x1 in r.results) + ), + ) diff --git a/tests/generated/test_networks.py b/tests/generated/test_networks.py index 68ac226..a7fee15 100644 --- a/tests/generated/test_networks.py +++ b/tests/generated/test_networks.py @@ -4,17 +4,119 @@ from __future__ import annotations from didit import AsyncPage, SyncPage -from didit.types.components.network_detail import NetworkDetail +from didit.types.components.network_detail_response import NetworkDetailResponse from didit.types.components.network_graph import NetworkGraph from didit.types.components.network_list_item import NetworkListItem from didit.types.components.network_map import NetworkMap from didit.types.components.network_note import NetworkNote +from didit.types.components.network_timeline import NetworkTimeline +from didit.types.components.network_timeline_page import NetworkTimelinePage from didit.types.networks.list_members_response import NetworksListMembersResponse from didit.types.networks.list_signals_response import NetworksListSignalsResponse -from didit.types.networks.retrieve_timeline_response import NetworksRetrieveTimelineResponse from tests.harness import BINARY_BODY, Expect, check_operation +def test_networks_add_tag() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/tags/add/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/networks/3fa85f64-5717-4562-b3fc-2c963f66afa6/tags/add/", + api="verification", + auth="api_key", + json_body={"tag_uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7"}, + status=200, + response_json={ + "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "network_id": "N-0042", + "name": None, + "status": "in_review", + "risk": {"score": 82, "band": "high", "bar": 0.82}, + "pattern_types": ["exact_same_device", "same_email"], + "signal_types": ["device", "email"], + "member_counts": { + "total": 3, + "users": 3, + "businesses": 0, + "transaction_parties": 0, + }, + "member_status": {"approved": 1, "declined": 1, "in_review": 0, "not_completed": 1}, + "signal_coverage": { + "device": {"signals": 1, "subjects": 3, "coverage": 1.0}, + "email": {"signals": 1, "subjects": 2, "coverage": 0.6667}, + }, + "first_activity_at": "2026-10-01T09:12:44Z", + "last_activity_at": "2026-10-12T16:03:10Z", + "stats": { + "members": {"total": 3, "users": 3, "businesses": 0, "transaction_parties": 0}, + "member_status": { + "approved": 1, + "declined": 1, + "in_review": 0, + "not_completed": 1, + }, + "signals": { + "total": 2, + "by_type": { + "device": {"signals": 1, "subjects": 3, "coverage": 1.0}, + "email": {"signals": 1, "subjects": 2, "coverage": 0.6667}, + }, + }, + }, + "fraud_tags": [], + "risk_signals": { + "residential_proxy_reuse": { + "detected": False, + "signal_count": 0, + "max_subject_count": 0, + }, + "email_intelligence": { + "enriched_signal_count": 1, + "high_risk_count": 0, + "social_profile_count": 0, + }, + "phone_intelligence": {"enriched_signal_count": 0, "high_risk_count": 0}, + }, + "reviewed_at": "2026-10-12T16:30:00Z", + "reviewed_by": "reviewer@example.com", + "review_reason": "Shared device across three new accounts.", + "notes_count": 0, + "sharing_mode": "global", + "cross_org_available": True, + "cross_org_unavailable_reason": None, + "tags": [ + { + "uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "name": "SUSPICIOUS", + "color": "#6366F1", + "source": "custom", + "added_by": None, + "added_at": "2026-10-13T08:15:00Z", + } + ], + "outcomes": {"approved": 1, "declined": 1, "in_review": 0, "total": 3}, + "patterns": [ + { + "type": "background", + "score": 0.8125, + "members": 3, + "reason": None, + "computed_at": "2026-10-13T08:00:00Z", + } + ], + }, + ), + lambda c: c.networks.add_tag( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", tag_uuid="7c9e6679-7425-40de-944b-e07fc1f90ae7" + ), + lambda c: c.networks.add_tag( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", tag_uuid="7c9e6679-7425-40de-944b-e07fc1f90ae7" + ), + lambda r: isinstance(r, NetworkDetailResponse), + lambda r: isinstance(r, NetworkDetailResponse), + ) + + def test_networks_download_report() -> None: """``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/report/``""" check_operation( @@ -44,17 +146,23 @@ def test_networks_list() -> None: auth="api_key", query={ "status": ["sample"], + "scope": ["identity"], "signal_type": ["sample"], "pattern_type": ["sample"], "risk_band": ["sample"], "min_size": ["1"], "max_size": ["1"], + "assignee": ["sample"], + "priority": ["sample"], + "risk_min": ["1"], + "has_case": ["true"], "date_from": ["2026-01-01"], "date_to": ["2026-01-01"], "q": ["sample"], "ordering": ["sample"], "limit": ["1"], "offset": ["1"], + "summary": ["true"], }, status=200, response_json={ @@ -84,35 +192,91 @@ def test_networks_list() -> None: "detected_this_period": {}, "false_positive_rate": {}, }, + "counts": { + "scopes": {"all": 1, "identity": 1, "transactions": 1}, + "views": { + "all": 1, + "active": 1, + "in_review": 1, + "resolved": 1, + "dismissed": 1, + "mine": 1, + "high_risk": 1, + }, + "facets": { + "total": 1, + "last_activity_days": {"7": 1, "30": 1, "90": 1}, + "risk_min": {"50": 1, "80": 1}, + "signal_types": { + "ip_address": 1, + "device": 1, + "face": 1, + "document_number": 1, + "phone": 1, + "email": 1, + "address": 1, + }, + "priority": {"low": 1, "medium": 1, "high": 1, "none": 1}, + "assignee": { + "me": 1, + "unassigned": 1, + "users": [ + { + "uuid": "00000000-0000-4000-8000-000000000008", + "full_name": "sample", + "email": "sample", + "count": 1, + } + ], + }, + "has_case": 1, + "min_size": {"10": 1}, + }, + }, + "sharing_mode": "off", + "cross_org_available": True, + "cross_org_unavailable_reason": "plan", }, ), lambda c: c.networks.list( status="sample", + scope="identity", signal_type="sample", pattern_type="sample", risk_band="sample", min_size=1, max_size=1, + assignee="sample", + priority="sample", + risk_min=1, + has_case=True, date_from="2026-01-01", date_to="2026-01-01", q="sample", ordering="sample", limit=1, offset=1, + summary=True, ), lambda c: c.networks.list( status="sample", + scope="identity", signal_type="sample", pattern_type="sample", risk_band="sample", min_size=1, max_size=1, + assignee="sample", + priority="sample", + risk_min=1, + has_case=True, date_from="2026-01-01", date_to="2026-01-01", q="sample", ordering="sample", limit=1, offset=1, + summary=True, ), lambda r: ( isinstance(r, SyncPage) and all(isinstance(x1, NetworkListItem) for x1 in r.results) @@ -213,6 +377,98 @@ def test_networks_list_signals() -> None: ) +def test_networks_remove_tag() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/tags/remove/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/networks/3fa85f64-5717-4562-b3fc-2c963f66afa6/tags/remove/", + api="verification", + auth="api_key", + json_body={"tag_uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7"}, + status=200, + response_json={ + "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "network_id": "N-0042", + "name": None, + "status": "in_review", + "risk": {"score": 82, "band": "high", "bar": 0.82}, + "pattern_types": ["exact_same_device", "same_email"], + "signal_types": ["device", "email"], + "member_counts": { + "total": 3, + "users": 3, + "businesses": 0, + "transaction_parties": 0, + }, + "member_status": {"approved": 1, "declined": 1, "in_review": 0, "not_completed": 1}, + "signal_coverage": { + "device": {"signals": 1, "subjects": 3, "coverage": 1.0}, + "email": {"signals": 1, "subjects": 2, "coverage": 0.6667}, + }, + "first_activity_at": "2026-10-01T09:12:44Z", + "last_activity_at": "2026-10-12T16:03:10Z", + "stats": { + "members": {"total": 3, "users": 3, "businesses": 0, "transaction_parties": 0}, + "member_status": { + "approved": 1, + "declined": 1, + "in_review": 0, + "not_completed": 1, + }, + "signals": { + "total": 2, + "by_type": { + "device": {"signals": 1, "subjects": 3, "coverage": 1.0}, + "email": {"signals": 1, "subjects": 2, "coverage": 0.6667}, + }, + }, + }, + "fraud_tags": [], + "risk_signals": { + "residential_proxy_reuse": { + "detected": False, + "signal_count": 0, + "max_subject_count": 0, + }, + "email_intelligence": { + "enriched_signal_count": 1, + "high_risk_count": 0, + "social_profile_count": 0, + }, + "phone_intelligence": {"enriched_signal_count": 0, "high_risk_count": 0}, + }, + "reviewed_at": "2026-10-12T16:30:00Z", + "reviewed_by": "reviewer@example.com", + "review_reason": "Shared device across three new accounts.", + "notes_count": 0, + "sharing_mode": "global", + "cross_org_available": True, + "cross_org_unavailable_reason": None, + "tags": [], + "outcomes": {"approved": 1, "declined": 1, "in_review": 0, "total": 3}, + "patterns": [ + { + "type": "background", + "score": 0.8125, + "members": 3, + "reason": None, + "computed_at": "2026-10-13T08:00:00Z", + } + ], + }, + ), + lambda c: c.networks.remove_tag( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", tag_uuid="7c9e6679-7425-40de-944b-e07fc1f90ae7" + ), + lambda c: c.networks.remove_tag( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", tag_uuid="7c9e6679-7425-40de-944b-e07fc1f90ae7" + ), + lambda r: isinstance(r, NetworkDetailResponse), + lambda r: isinstance(r, NetworkDetailResponse), + ) + + def test_networks_retrieve() -> None: """``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/``""" check_operation( @@ -223,32 +479,89 @@ def test_networks_retrieve() -> None: auth="api_key", status=200, response_json={ - "uuid": "00000000-0000-4000-8000-000000000004", - "network_id": "sample", - "name": "sample", - "status": "active", - "risk": {"score": 1, "band": "low", "bar": 1.5}, - "pattern_types": ["exact_same_device"], - "signal_types": ["ip_address"], - "member_counts": {}, - "member_status": {}, - "signal_coverage": {}, - "first_activity_at": "2026-01-01T00:00:00Z", - "last_activity_at": "2026-01-01T00:00:00Z", - "stats": {}, - "fraud_tags": [{}], - "risk_signals": {}, - "reviewed_at": "2026-01-01T00:00:00Z", - "reviewed_by": "sample", - "review_reason": "sample", - "notes_count": 1, + "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "network_id": "N-0042", + "name": None, + "status": "in_review", + "risk": {"score": 82, "band": "high", "bar": 0.82}, + "pattern_types": ["exact_same_device", "same_email"], + "signal_types": ["device", "email"], + "member_counts": { + "total": 3, + "users": 3, + "businesses": 0, + "transaction_parties": 0, + }, + "member_status": {"approved": 1, "declined": 1, "in_review": 0, "not_completed": 1}, + "signal_coverage": { + "device": {"signals": 1, "subjects": 3, "coverage": 1.0}, + "email": {"signals": 1, "subjects": 2, "coverage": 0.6667}, + }, + "first_activity_at": "2026-10-01T09:12:44Z", + "last_activity_at": "2026-10-12T16:03:10Z", + "stats": { + "members": {"total": 3, "users": 3, "businesses": 0, "transaction_parties": 0}, + "member_status": { + "approved": 1, + "declined": 1, + "in_review": 0, + "not_completed": 1, + }, + "signals": { + "total": 2, + "by_type": { + "device": {"signals": 1, "subjects": 3, "coverage": 1.0}, + "email": {"signals": 1, "subjects": 2, "coverage": 0.6667}, + }, + }, + }, + "fraud_tags": [], + "risk_signals": { + "residential_proxy_reuse": { + "detected": False, + "signal_count": 0, + "max_subject_count": 0, + }, + "email_intelligence": { + "enriched_signal_count": 1, + "high_risk_count": 0, + "social_profile_count": 0, + }, + "phone_intelligence": {"enriched_signal_count": 0, "high_risk_count": 0}, + }, + "reviewed_at": "2026-10-12T16:30:00Z", + "reviewed_by": "reviewer@example.com", + "review_reason": "Shared device across three new accounts.", + "notes_count": 0, + "sharing_mode": "global", + "cross_org_available": True, + "cross_org_unavailable_reason": None, + "tags": [ + { + "uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "name": "SUSPICIOUS", + "color": "#6366F1", + "source": "custom", + "added_by": None, + "added_at": "2026-10-13T08:15:00Z", + } + ], + "outcomes": {"approved": 1, "declined": 1, "in_review": 0, "total": 3}, + "patterns": [ + { + "type": "background", + "score": 0.8125, + "members": 3, + "reason": None, + "computed_at": "2026-10-13T08:00:00Z", + } + ], }, - strict_type=NetworkDetail, ), lambda c: c.networks.retrieve("n3a5e000-0000-4000-8000-000000000001"), lambda c: c.networks.retrieve("n3a5e000-0000-4000-8000-000000000001"), - lambda r: isinstance(r, NetworkDetail), - lambda r: isinstance(r, NetworkDetail), + lambda r: isinstance(r, NetworkDetailResponse), + lambda r: isinstance(r, NetworkDetailResponse), ) @@ -387,23 +700,52 @@ def test_networks_retrieve_timeline() -> None: path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/networks/n3a5e000-0000-4000-8000-000000000001/timeline/", api="verification", auth="api_key", + query={ + "contract": ["2"], + "limit": ["1"], + "cursor": ["sample"], + "event_type": ["network_detected"], + }, status=200, response_json={ "results": [ { - "event_type": "sample", - "occurred_at": "2026-01-01T00:00:00Z", - "label": "sample", - "metadata": {}, - } + "event_type": "network_detected", + "occurred_at": "2026-10-01T09:12:44Z", + "label": "Network detected", + "metadata": {"network_id": "N-0042"}, + }, + { + "event_type": "signal_observed", + "occurred_at": "2026-10-12T16:03:10Z", + "label": "device observed", + "metadata": {"signal_type": "device", "subject_count": 3, "is_hub": False}, + }, + { + "event_type": "network_in_review", + "occurred_at": "2026-10-12T16:30:00Z", + "label": "Network in_review", + "metadata": {"reason": "Shared device across three new accounts."}, + }, ] }, - strict_type=NetworksRetrieveTimelineResponse, ), - lambda c: c.networks.retrieve_timeline("n3a5e000-0000-4000-8000-000000000001"), - lambda c: c.networks.retrieve_timeline("n3a5e000-0000-4000-8000-000000000001"), - lambda r: isinstance(r, NetworksRetrieveTimelineResponse), - lambda r: isinstance(r, NetworksRetrieveTimelineResponse), + lambda c: c.networks.retrieve_timeline( + "n3a5e000-0000-4000-8000-000000000001", + contract="2", + limit=1, + cursor="sample", + event_type=["network_detected"], + ), + lambda c: c.networks.retrieve_timeline( + "n3a5e000-0000-4000-8000-000000000001", + contract="2", + limit=1, + cursor="sample", + event_type=["network_detected"], + ), + lambda r: (isinstance(r, NetworkTimeline)) or (isinstance(r, NetworkTimelinePage)), + lambda r: (isinstance(r, NetworkTimeline)) or (isinstance(r, NetworkTimelinePage)), ) diff --git a/tests/generated/test_regulatory_reports.py b/tests/generated/test_regulatory_reports.py index 0ef2c60..786e061 100644 --- a/tests/generated/test_regulatory_reports.py +++ b/tests/generated/test_regulatory_reports.py @@ -181,8 +181,8 @@ def test_regulatory_reports_list() -> None: "previous": "sample", "results": [ { - "uuid": "00000000-0000-4000-8000-000000000005", - "case_uuid": "00000000-0000-4000-8000-000000000005", + "uuid": "00000000-0000-4000-8000-000000000006", + "case_uuid": "00000000-0000-4000-8000-000000000006", "case_number": "sample", "report_type": "SAR", "report_type_display": "sample", @@ -213,7 +213,7 @@ def test_regulatory_reports_list() -> None: }, "report_indicators": ["sample"], "included_fields": ["sample"], - "transactions": ["00000000-0000-4000-8000-000000000006"], + "transactions": ["00000000-0000-4000-8000-000000000007"], "validation_errors": [{"field": "sample", "message": "sample"}], "validated_at": "2026-01-01T00:00:00Z", "finalized_at": "2026-01-01T00:00:00Z", diff --git a/tests/generated/test_sessions.py b/tests/generated/test_sessions.py index e2d6dc7..22a921e 100644 --- a/tests/generated/test_sessions.py +++ b/tests/generated/test_sessions.py @@ -4,26 +4,43 @@ from __future__ import annotations from didit import AsyncPage, SyncPage +from didit.types.components.aml_screening_run import AMLScreeningRun +from didit.types.components.business_session_list_item import BusinessSessionListItem +from didit.types.components.entity_network_membership_list import EntityNetworkMembershipList +from didit.types.components.feature_status_override import FeatureStatusOverride +from didit.types.components.kyb_key_people_party import KYBKeyPeopleParty from didit.types.components.review_item import ReviewItem +from didit.types.components.sandbox_simulate_response import SandboxSimulateResponse +from didit.types.components.session_decision import SessionDecision +from didit.types.components.session_deletion_job import SessionDeletionJob +from didit.types.components.session_event import SessionEvent from didit.types.components.session_import_error import SessionImportError from didit.types.components.session_import_job import SessionImportJob -from didit.types.sessions.batch_delete_response import ( - SessionsBatchDeleteResponse, - SessionsBatchDeleteResponse202, +from didit.types.components.session_webhook_delivery import SessionWebhookDelivery +from didit.types.components.user_session_list_item import UserSessionListItem +from didit.types.sessions.add_images_response import SessionsAddImagesResponse +from didit.types.sessions.batch_delete_response import SessionsBatchDeleteResponse +from didit.types.sessions.bulk_update_aml_hit_status_response import ( + SessionsBulkUpdateAmlHitStatusResponse, ) from didit.types.sessions.create_response import SessionsCreateResponse from didit.types.sessions.delete_response import SessionsDeleteResponse -from didit.types.sessions.list_networks_response import SessionsListNetworksResponse -from didit.types.sessions.list_response import ( - SessionsListResponseResultsKybSessionRowSessionKindBusiness, - SessionsListResponseResultsKycSessionRowSessionKindUser, +from didit.types.sessions.list_feature_overrides_response import ( + SessionsListFeatureOverridesResponse, +) +from didit.types.sessions.preview_decision_email_response import ( + SessionsPreviewDecisionEmailResponse, ) from didit.types.sessions.preview_resubmission_response import SessionsPreviewResubmissionResponse -from didit.types.sessions.retrieve_decision_response import SessionsRetrieveDecisionResponse -from didit.types.sessions.retrieve_deletion_job_response import SessionsRetrieveDeletionJobResponse from didit.types.sessions.reviews.create_response import SessionsReviewsCreateResponse from didit.types.sessions.share_response import SessionsShareResponse +from didit.types.sessions.signatures.validate_response import SessionsSignaturesValidateResponse +from didit.types.sessions.update_aml_hit_status_response import SessionsUpdateAmlHitStatusResponse +from didit.types.sessions.update_aml_status_response import SessionsUpdateAmlStatusResponse from didit.types.sessions.update_data_response import SessionsUpdateDataResponse +from didit.types.sessions.update_document_ai_data_response import ( + SessionsUpdateDocumentAiDataResponse, +) from didit.types.sessions.update_feature_status_response import SessionsUpdateFeatureStatusResponse from didit.types.sessions.update_kyb_company_data_response import ( SessionsUpdateKybCompanyDataResponse, @@ -34,6 +51,34 @@ from tests.harness import BINARY_BODY, Expect, check_operation +def test_sessions_add_images() -> None: + """``PATCH /v3/session/{sessionId}/add-images/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/session/11111111-2222-3333-4444-555555555555/add-images/", + api="verification", + auth="api_key", + multipart_fields=["model_type"], + multipart_files=["file"], + status=200, + response_json={ + "extra_files": [ + "https://media.example.com/session/11111111-2222-3333-4444-555555555555/kyc-extra-files/66666666-7777-8888-9999-000000000000.jpg?expires=1789383731&signature=abc123" + ] + }, + ), + lambda c: c.sessions.add_images( + "11111111-2222-3333-4444-555555555555", file=b"file-content", model_type="kyc" + ), + lambda c: c.sessions.add_images( + "11111111-2222-3333-4444-555555555555", file=b"file-content", model_type="kyc" + ), + lambda r: isinstance(r, SessionsAddImagesResponse), + lambda r: isinstance(r, SessionsAddImagesResponse), + ) + + def test_sessions_batch_delete() -> None: """``POST /v3/sessions/delete/``""" check_operation( @@ -84,12 +129,10 @@ def test_sessions_batch_delete() -> None: lambda c: c.sessions.batch_delete(session_numbers=["1001", "1002", "1003"]), lambda c: c.sessions.batch_delete(session_numbers=["1001", "1002", "1003"]), lambda r: ( - (isinstance(r, SessionsBatchDeleteResponse)) - or (isinstance(r, SessionsBatchDeleteResponse202)) + (isinstance(r, SessionsBatchDeleteResponse)) or (isinstance(r, SessionDeletionJob)) ), lambda r: ( - (isinstance(r, SessionsBatchDeleteResponse)) - or (isinstance(r, SessionsBatchDeleteResponse202)) + (isinstance(r, SessionsBatchDeleteResponse)) or (isinstance(r, SessionDeletionJob)) ), ) @@ -120,8 +163,49 @@ def test_sessions_bulk_update_aml_hit_status() -> None: hit_updates=[{"hit_id": "abc123", "review_status": "Unreviewed"}], node_id_query="feature_aml_1", ), - lambda r: True, - lambda r: True, + lambda r: isinstance(r, SessionsBulkUpdateAmlHitStatusResponse), + lambda r: isinstance(r, SessionsBulkUpdateAmlHitStatusResponse), + ) + + +def test_sessions_cancel_feature_override() -> None: + """``POST /v3/session/{sessionId}/features/overrides/{overrideId}/cancel/``""" + check_operation( + Expect( + method="POST", + path="/v3/session/3fa85f64-5717-4562-b3fc-2c963f66afa6/features/overrides/9b2f4c1e-8d3a-4e5f-a6b7-c8d9e0f1a2b3/cancel/", + api="verification", + auth="api_key", + status=200, + response_json={ + "id": "9b2f4c1e-8d3a-4e5f-a6b7-c8d9e0f1a2b3", + "node_id": "feature_liveness_1", + "feature_type": "", + "feature_model": "Face", + "previous_status": "Declined", + "new_status": "Approved", + "state": "cancelled", + "forced": False, + "reason": "Liveness retried with a reviewer present.", + "requested_by": { + "email": "reviewer@example.com", + "name": "Alex Reviewer", + "type": "CONSOLE_USER", + }, + "requested_at": "2026-10-13T09:15:00Z", + "applied_at": None, + "cancelled_at": "2026-10-13T09:20:00Z", + "cancel_reason": "cancelled_by_reviewer", + }, + ), + lambda c: c.sessions.cancel_feature_override( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", "9b2f4c1e-8d3a-4e5f-a6b7-c8d9e0f1a2b3" + ), + lambda c: c.sessions.cancel_feature_override( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", "9b2f4c1e-8d3a-4e5f-a6b7-c8d9e0f1a2b3" + ), + lambda r: isinstance(r, FeatureStatusOverride), + lambda r: isinstance(r, FeatureStatusOverride), ) @@ -258,6 +342,30 @@ def test_sessions_delete() -> None: ) +def test_sessions_export_aml_screening_history() -> None: + """``GET /v3/session/{sessionId}/aml-screening-history/export/``""" + check_operation( + Expect( + method="GET", + path="/v3/session/11111111-2222-3333-4444-555555555555/aml-screening-history/export/", + api="verification", + auth="api_key", + query={"node_id": ["feature_aml_1"]}, + status=200, + response_bytes=BINARY_BODY, + response_content_type="text/csv", + ), + lambda c: c.sessions.export_aml_screening_history( + "11111111-2222-3333-4444-555555555555", node_id="feature_aml_1" + ), + lambda c: c.sessions.export_aml_screening_history( + "11111111-2222-3333-4444-555555555555", node_id="feature_aml_1" + ), + lambda r: r == BINARY_BODY, + lambda r: r == BINARY_BODY, + ) + + def test_sessions_generate_pdf() -> None: """``GET /v3/session/{sessionId}/generate-pdf/``""" check_operation( @@ -317,8 +425,8 @@ def test_sessions_import_shared() -> None: workflow_id="9f9b1234-aaaa-bbbb-cccc-1234567890ab", vendor_data="user-1", ), - lambda r: True, - lambda r: True, + lambda r: isinstance(r, SessionDecision), + lambda r: isinstance(r, SessionDecision), ) @@ -470,22 +578,251 @@ def test_sessions_list() -> None: lambda r: ( isinstance(r, SyncPage) and all( - (isinstance(x1, SessionsListResponseResultsKycSessionRowSessionKindUser)) - or (isinstance(x1, SessionsListResponseResultsKybSessionRowSessionKindBusiness)) + (isinstance(x1, UserSessionListItem)) or (isinstance(x1, BusinessSessionListItem)) for x1 in r.results ) ), lambda r: ( isinstance(r, AsyncPage) and all( - (isinstance(x1, SessionsListResponseResultsKycSessionRowSessionKindUser)) - or (isinstance(x1, SessionsListResponseResultsKybSessionRowSessionKindBusiness)) + (isinstance(x1, UserSessionListItem)) or (isinstance(x1, BusinessSessionListItem)) for x1 in r.results ) ), ) +def test_sessions_list_aml_screening_history() -> None: + """``GET /v3/session/{sessionId}/aml-screening-history/``""" + check_operation( + Expect( + method="GET", + path="/v3/session/11111111-2222-3333-4444-555555555555/aml-screening-history/", + api="verification", + auth="api_key", + query={ + "node_id": ["feature_aml_1"], + "limit": ["50"], + "offset": ["0"], + "ordering": ["completed_at"], + }, + status=200, + response_json={ + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "uuid": "dddddddd-1111-2222-3333-444444444444", + "completed_at": "2026-09-14T04:12:33.481207Z", + "started_at": "2026-09-14T04:00:01.002311Z", + "outcome": "NO_CHANGE", + "trigger": "ONGOING_MONITORING", + "provider": "aml_data_source", + "status": "Approved", + "score": 0.0, + "total_hits": 0, + "dataset_freshness": { + "captured_at": "2026-09-14T03:58:02.114820+00:00", + "provider": "aml_data_source", + "catalog_version": "2026-09-13", + "sources": { + "sanctions": { + "last_updated_at": "2026-09-13T21:10:00+00:00", + "oldest_updated_at": "2026-09-11T06:00:00+00:00", + "dataset_count": 120, + "dated_dataset_count": 120, + }, + "pep": { + "last_updated_at": "2026-09-13T08:00:00+00:00", + "oldest_updated_at": None, + "dataset_count": 45, + "dated_dataset_count": 44, + }, + "warnings": { + "last_updated_at": "2026-09-12T18:45:00+00:00", + "oldest_updated_at": "2026-09-01T00:00:00+00:00", + "dataset_count": 30, + "dated_dataset_count": 30, + }, + }, + }, + }, + { + "uuid": "eeeeeeee-1111-2222-3333-444444444444", + "completed_at": "2026-09-13T04:11:58.020914Z", + "started_at": "2026-09-13T04:00:00.871022Z", + "outcome": "FAILED", + "trigger": "ONGOING_MONITORING", + "provider": "aml_data_source", + "status": "Approved", + "score": 0.0, + "total_hits": 0, + "dataset_freshness": None, + }, + ], + }, + ), + lambda c: c.sessions.list_aml_screening_history( + "11111111-2222-3333-4444-555555555555", + node_id="feature_aml_1", + limit=50, + offset=0, + ordering="completed_at", + ), + lambda c: c.sessions.list_aml_screening_history( + "11111111-2222-3333-4444-555555555555", + node_id="feature_aml_1", + limit=50, + offset=0, + ordering="completed_at", + ), + lambda r: ( + isinstance(r, SyncPage) and all(isinstance(x1, AMLScreeningRun) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, AMLScreeningRun) for x1 in r.results) + ), + ) + + +def test_sessions_list_events() -> None: + """``GET /v3/session/{sessionId}/events/``""" + check_operation( + Expect( + method="GET", + path="/v3/session/11111111-2222-3333-4444-555555555555/events/", + api="verification", + auth="api_key", + query={"limit": ["50"], "offset": ["0"], "ordering": ["sent_at"]}, + status=200, + response_json={ + "count": 3, + "next": None, + "previous": None, + "results": [ + { + "uuid": "aaaaaaaa-1111-2222-3333-444444444444", + "event_type": "SESSION_STATUS_CHANGED", + "created_at": "2026-09-14T10:02:13.904512Z", + "component": None, + "feature_step": None, + "step": None, + "details": {"old_status": "In Progress", "current_status": "Approved"}, + "ip_address": None, + "user_agent": None, + "device_info": None, + "file_url": None, + "sent_at": 1789380131482, + }, + { + "uuid": "bbbbbbbb-1111-2222-3333-444444444444", + "event_type": "FEATURE_STEP_CHANGED", + "created_at": "2026-09-14T10:01:49.210377Z", + "component": None, + "feature_step": None, + "step": None, + "details": {"old_feature_step": "OCR", "current_feature_step": "LIVENESS"}, + "ip_address": None, + "user_agent": None, + "device_info": None, + "file_url": None, + "sent_at": 1789380107103, + }, + { + "uuid": "cccccccc-1111-2222-3333-444444444444", + "event_type": "STEP_RENDERED", + "created_at": "2026-09-14T10:01:00.581930Z", + "component": "DocumentCapture", + "feature_step": "OCR", + "step": "DOCUMENT_CAPTURE_FRONT", + "details": {}, + "ip_address": "203.0.113.24", + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1", + "device_info": {"device_model": "iPhone"}, + "file_url": None, + "sent_at": 1789380058776, + }, + ], + }, + ), + lambda c: c.sessions.list_events( + "11111111-2222-3333-4444-555555555555", limit=50, offset=0, ordering="sent_at" + ), + lambda c: c.sessions.list_events( + "11111111-2222-3333-4444-555555555555", limit=50, offset=0, ordering="sent_at" + ), + lambda r: isinstance(r, SyncPage) and all(isinstance(x1, SessionEvent) for x1 in r.results), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, SessionEvent) for x1 in r.results) + ), + ) + + +def test_sessions_list_feature_overrides() -> None: + """``GET /v3/session/{sessionId}/features/overrides/``""" + check_operation( + Expect( + method="GET", + path="/v3/session/3fa85f64-5717-4562-b3fc-2c963f66afa6/features/overrides/", + api="verification", + auth="api_key", + status=200, + response_json={ + "session_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "person_in_flow": True, + "flow_finished_at": None, + "overrides": [ + { + "id": "9b2f4c1e-8d3a-4e5f-a6b7-c8d9e0f1a2b3", + "node_id": "feature_liveness_1", + "feature_type": "", + "feature_model": "Face", + "previous_status": "Declined", + "new_status": "Approved", + "state": "pending_until_flow_finishes", + "forced": False, + "reason": "Liveness retried with a reviewer present.", + "requested_by": { + "email": "reviewer@example.com", + "name": "Alex Reviewer", + "type": "CONSOLE_USER", + }, + "requested_at": "2026-10-13T09:15:00Z", + "applied_at": None, + "cancelled_at": None, + "cancel_reason": None, + }, + { + "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "node_id": "feature_ocr_1", + "feature_type": "", + "feature_model": "KYC", + "previous_status": "In Review", + "new_status": "Approved", + "state": "applied", + "forced": True, + "reason": "Document manually verified by compliance.", + "requested_by": { + "email": "reviewer@example.com", + "name": "Alex Reviewer", + "type": "CONSOLE_USER", + }, + "requested_at": "2026-10-13T09:15:00Z", + "applied_at": "2026-10-13T09:15:00Z", + "cancelled_at": None, + "cancel_reason": None, + }, + ], + }, + ), + lambda c: c.sessions.list_feature_overrides("3fa85f64-5717-4562-b3fc-2c963f66afa6"), + lambda c: c.sessions.list_feature_overrides("3fa85f64-5717-4562-b3fc-2c963f66afa6"), + lambda r: isinstance(r, SessionsListFeatureOverridesResponse), + lambda r: isinstance(r, SessionsListFeatureOverridesResponse), + ) + + def test_sessions_list_networks() -> None: """``GET /v3/organization/{organization_id}/application/{application_id}/sessions/{session_id}/networks/``""" check_operation( @@ -499,13 +836,13 @@ def test_sessions_list_networks() -> None: "results": [ { "network": { - "uuid": "00000000-0000-4000-8000-000000000007", + "uuid": "00000000-0000-4000-8000-000000000008", "network_id": "sample", "name": "sample", "status": "active", - "risk": {}, - "pattern_types": ["exact_same_device"], - "signal_types": ["ip_address"], + "risk": None, + "pattern_types": [], + "signal_types": [], "member_counts": {}, "member_status": {}, "signal_coverage": {}, @@ -520,12 +857,48 @@ def test_sessions_list_networks() -> None: } ] }, - strict_type=SessionsListNetworksResponse, + strict_type=EntityNetworkMembershipList, ), lambda c: c.sessions.list_networks("00000000-0000-4000-8000-000000000001"), lambda c: c.sessions.list_networks("00000000-0000-4000-8000-000000000001"), - lambda r: isinstance(r, SessionsListNetworksResponse), - lambda r: isinstance(r, SessionsListNetworksResponse), + lambda r: isinstance(r, EntityNetworkMembershipList), + lambda r: isinstance(r, EntityNetworkMembershipList), + ) + + +def test_sessions_preview_decision_email() -> None: + """``POST /v3/session/{sessionId}/decision-email-preview/``""" + check_operation( + Expect( + method="POST", + path="/v3/session/3fa85f64-5717-4562-b3fc-2c963f66afa6/decision-email-preview/", + api="verification", + auth="api_key", + json_body={"new_status": "Approved"}, + status=200, + response_json={ + "recipient": "person@example.com", + "recipient_source": "email_verification", + "sender": "Example App ", + "sender_name": "Example App", + "sender_email": "no-reply@mail.example.com", + "sender_kind": "customer", + "subject": "Your verification has been approved in Example App", + "html": "...", + "language": "en", + "will_be_sent": True, + "not_sent_reason": None, + "preview_token": "sample-token", + }, + ), + lambda c: c.sessions.preview_decision_email( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", new_status="Approved" + ), + lambda c: c.sessions.preview_decision_email( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", new_status="Approved" + ), + lambda r: isinstance(r, SessionsPreviewDecisionEmailResponse), + lambda r: isinstance(r, SessionsPreviewDecisionEmailResponse), ) @@ -537,30 +910,101 @@ def test_sessions_preview_resubmission() -> None: path="/v3/session/11111111-2222-3333-4444-555555555555/resubmission-preview/", api="verification", auth="api_key", - json_body={"email_address": "user@example.com"}, + json_body={ + "nodes_to_resubmit": [{"node_id": "feature_poa_1", "feature": "POA"}], + "email_language": "en", + }, status=200, response_json={ - "recipient": "user@example.com", - "sender": "sample", - "subject": "sample", - "html": "sample", - "nodes_to_resubmit": [{"node_id": "feature_ocr", "feature": "OCR"}], + "recipient": "person@example.com", + "recipient_source": "email_verification", + "sender": "Example App ", + "sender_name": "Example App", + "sender_email": "no-reply@mail.example.com", + "sender_kind": "customer", + "subject": "Action Required: Complete your verification in Example App", + "html": "...", + "language": "en", + "will_be_sent": True, + "not_sent_reason": None, + "nodes_to_resubmit": [{"node_id": "feature_poa_1", "feature": "PROOF_OF_ADDRESS"}], "requires_user_action": True, "preview_token": "sample-token", }, - strict_type=SessionsPreviewResubmissionResponse, ), lambda c: c.sessions.preview_resubmission( - "11111111-2222-3333-4444-555555555555", email_address="user@example.com" + "11111111-2222-3333-4444-555555555555", + nodes_to_resubmit=[{"node_id": "feature_poa_1", "feature": "POA"}], + email_language="en", ), lambda c: c.sessions.preview_resubmission( - "11111111-2222-3333-4444-555555555555", email_address="user@example.com" + "11111111-2222-3333-4444-555555555555", + nodes_to_resubmit=[{"node_id": "feature_poa_1", "feature": "POA"}], + email_language="en", ), lambda r: isinstance(r, SessionsPreviewResubmissionResponse), lambda r: isinstance(r, SessionsPreviewResubmissionResponse), ) +def test_sessions_regenerate_kyb_party() -> None: + """``POST /v3/session/{sessionId}/kyb/parties/{partyUuid}/regenerate/``""" + check_operation( + Expect( + method="POST", + path="/v3/session/33333333-4444-5555-6666-777777777777/kyb/parties/0a3f1e2c-7c2f-4a1b-9d55-2f8d2a5d6b41/regenerate/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "0a3f1e2c-7c2f-4a1b-9d55-2f8d2a5d6b41", + "entity_type": "person", + "source": "USER", + "name": "Jane Smith", + "first_name": "Jane", + "last_name": "Smith", + "company_name": None, + "registration_number": None, + "email": "user@example.com", + "nationality": "GB", + "phone_number": None, + "date_of_birth": "1980-05-04", + "position": "Director", + "custom_fields": {}, + "is_active": True, + "is_skipped": False, + "requires_verification": True, + "roles": [{"role": "director", "ownership_percent": None, "voting_percent": None}], + "kyc_session_id": "55555555-6666-7777-8888-999999999999", + "kyc_session_status": "Not Started", + "kyc_session_url": "https://verify.didit.me/session/abc123def456", + "kyc_session_deleted": False, + "kyb_sub_session_id": None, + "kyb_sub_session_status": None, + "kyb_sub_session_url": None, + "kyb_sub_session_deleted": False, + "verification_workflow_type": "kyc", + "verification_workflow_id": "12121212-3434-5656-7878-909090909090", + "verification_workflow_label": "Key People KYC", + "verification_workflow_features": [ + {"feature": "ID_VERIFICATION", "status": "Not Finished"}, + {"feature": "LIVENESS", "status": "Not Finished"}, + {"feature": "AML", "status": "Not Finished"}, + ], + "didit_internal_id": None, + }, + ), + lambda c: c.sessions.regenerate_kyb_party( + "33333333-4444-5555-6666-777777777777", "0a3f1e2c-7c2f-4a1b-9d55-2f8d2a5d6b41" + ), + lambda c: c.sessions.regenerate_kyb_party( + "33333333-4444-5555-6666-777777777777", "0a3f1e2c-7c2f-4a1b-9d55-2f8d2a5d6b41" + ), + lambda r: isinstance(r, KYBKeyPeopleParty), + lambda r: isinstance(r, KYBKeyPeopleParty), + ) + + def test_sessions_retrieve_decision() -> None: """``GET /v3/session/{sessionId}/decision/``""" check_operation( @@ -930,6 +1374,8 @@ def test_sessions_retrieve_decision() -> None: "status": "In Review", "is_nfc_skipped": False, "skip_reason": None, + "chip_protocol": "icao", + "jpki": None, "portrait_image": "https:///portrait.jpg", "signature_image": "https:///signature.jpg", "chip_data": { @@ -2075,8 +2521,8 @@ def test_sessions_retrieve_decision() -> None: lambda c: c.sessions.retrieve_decision( "11111111-2222-3333-4444-555555555555", include="events" ), - lambda r: isinstance(r, SessionsRetrieveDecisionResponse), - lambda r: isinstance(r, SessionsRetrieveDecisionResponse), + lambda r: isinstance(r, SessionDecision), + lambda r: isinstance(r, SessionDecision), ) @@ -2110,8 +2556,8 @@ def test_sessions_retrieve_deletion_job() -> None: ), lambda c: c.sessions.retrieve_deletion_job("00000000-0000-4000-8000-000000000001"), lambda c: c.sessions.retrieve_deletion_job("00000000-0000-4000-8000-000000000001"), - lambda r: isinstance(r, SessionsRetrieveDeletionJobResponse), - lambda r: isinstance(r, SessionsRetrieveDeletionJobResponse), + lambda r: isinstance(r, SessionDeletionJob), + lambda r: isinstance(r, SessionDeletionJob), ) @@ -2149,6 +2595,36 @@ def test_sessions_share() -> None: ) +def test_sessions_simulate() -> None: + """``POST /v3/session/{sessionId}/simulate/``""" + check_operation( + Expect( + method="POST", + path="/v3/session/3fa85f64-5717-4562-b3fc-2c963f66afa6/simulate/", + api="verification", + auth="api_key", + json_body={"scenario": "decline_aml_hit", "send_webhooks": False}, + status=200, + response_json={ + "session_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "previous_status": "In Progress", + "status": "Declined", + "scenario": "decline_aml_hit", + "risk": "POSSIBLE_MATCH_FOUND", + "send_webhooks": False, + }, + ), + lambda c: c.sessions.simulate( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", scenario="decline_aml_hit", send_webhooks=False + ), + lambda c: c.sessions.simulate( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", scenario="decline_aml_hit", send_webhooks=False + ), + lambda r: isinstance(r, SandboxSimulateResponse), + lambda r: isinstance(r, SandboxSimulateResponse), + ) + + def test_sessions_update_aml_hit_status() -> None: """``PATCH /v3/session/{sessionId}/update-aml-hit-status/``""" check_operation( @@ -2178,8 +2654,36 @@ def test_sessions_update_aml_hit_status() -> None: review_status="Unreviewed", node_id_query="feature_aml_1", ), - lambda r: True, - lambda r: True, + lambda r: isinstance(r, SessionsUpdateAmlHitStatusResponse), + lambda r: isinstance(r, SessionsUpdateAmlHitStatusResponse), + ) + + +def test_sessions_update_aml_status() -> None: + """``PATCH /v3/session/{sessionId}/update-aml-status/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/session/11111111-2222-3333-4444-555555555555/update-aml-status/", + api="verification", + auth="api_key", + query={"node_id": ["feature_aml_1"]}, + json_body={"status": "Approved"}, + status=200, + response_json={ + "message": "AML status updated successfully", + "status": "Approved", + "previous_status": "In Review", + }, + ), + lambda c: c.sessions.update_aml_status( + "11111111-2222-3333-4444-555555555555", status="Approved", node_id_query="feature_aml_1" + ), + lambda c: c.sessions.update_aml_status( + "11111111-2222-3333-4444-555555555555", status="Approved", node_id_query="feature_aml_1" + ), + lambda r: isinstance(r, SessionsUpdateAmlStatusResponse), + lambda r: isinstance(r, SessionsUpdateAmlStatusResponse), ) @@ -2270,6 +2774,46 @@ def test_sessions_update_data() -> None: ) +def test_sessions_update_document_ai_data() -> None: + """``PATCH /v3/document-ai/{sessionId}/documents/{documentUuid}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/document-ai/11111111-2222-3333-4444-555555555555/documents/44444444-5555-6666-7777-888888888888/", + api="verification", + auth="api_key", + json_body={"extracted_data": {"invoice_number": "INV-2026-0042"}, "status": "Approved"}, + status=200, + response_json={ + "uuid": "44444444-5555-6666-7777-888888888888", + "document_key": "invoice", + "original_filename": "invoice-2026-09.pdf", + "file_url": "document_ai/11111111-2222-3333-4444-555555555555-66666666-7777-8888-9999-000000000000.pdf", + "status": "Approved", + "extracted_data": { + "invoice_number": "INV-2026-0042", + "total_amount": 1250.0, + "issue_date": "2026-09-01", + }, + }, + ), + lambda c: c.sessions.update_document_ai_data( + "11111111-2222-3333-4444-555555555555", + "44444444-5555-6666-7777-888888888888", + extracted_data={"invoice_number": "INV-2026-0042"}, + status="Approved", + ), + lambda c: c.sessions.update_document_ai_data( + "11111111-2222-3333-4444-555555555555", + "44444444-5555-6666-7777-888888888888", + extracted_data={"invoice_number": "INV-2026-0042"}, + status="Approved", + ), + lambda r: isinstance(r, SessionsUpdateDocumentAiDataResponse), + lambda r: isinstance(r, SessionsUpdateDocumentAiDataResponse), + ) + + def test_sessions_update_feature_status() -> None: """``PATCH /v3/session/{sessionId}/features/{nodeId}/update-status/``""" check_operation( @@ -2278,20 +2822,49 @@ def test_sessions_update_feature_status() -> None: path="/v3/session/11111111-2222-3333-4444-555555555555/features/feature_ocr_1/update-status/", api="verification", auth="api_key", - json_body={"new_status": "Approved"}, + json_body={ + "new_status": "Approved", + "comment": "Document manually verified by compliance.", + }, status=200, response_json={ - "session_id": "11111111-2222-3333-4444-555555555555", + "session_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "node_id": "feature_ocr_1", "feature_type": "KYC", "new_status": "Approved", + "override": { + "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "node_id": "feature_ocr_1", + "feature_type": "", + "feature_model": "KYC", + "previous_status": "In Review", + "new_status": "Approved", + "state": "applied", + "forced": False, + "reason": "Document manually verified by compliance.", + "requested_by": { + "email": "reviewer@example.com", + "name": "Alex Reviewer", + "type": "CONSOLE_USER", + }, + "requested_at": "2026-10-13T09:15:00Z", + "applied_at": "2026-10-13T09:15:00Z", + "cancelled_at": None, + "cancel_reason": None, + }, }, ), lambda c: c.sessions.update_feature_status( - "11111111-2222-3333-4444-555555555555", "feature_ocr_1", new_status="Approved" + "11111111-2222-3333-4444-555555555555", + "feature_ocr_1", + new_status="Approved", + comment="Document manually verified by compliance.", ), lambda c: c.sessions.update_feature_status( - "11111111-2222-3333-4444-555555555555", "feature_ocr_1", new_status="Approved" + "11111111-2222-3333-4444-555555555555", + "feature_ocr_1", + new_status="Approved", + comment="Document manually verified by compliance.", ), lambda r: isinstance(r, SessionsUpdateFeatureStatusResponse), lambda r: isinstance(r, SessionsUpdateFeatureStatusResponse), @@ -2847,3 +3420,326 @@ def test_sessions_reviews_list() -> None: lambda r: isinstance(r, SyncPage) and all(isinstance(x1, ReviewItem) for x1 in r.results), lambda r: isinstance(r, AsyncPage) and all(isinstance(x1, ReviewItem) for x1 in r.results), ) + + +def test_sessions_signatures_download_document() -> None: + """``GET /v3/session/{sessionId}/signatures/{nodeId}/documents/{documentId}/file/``""" + check_operation( + Expect( + method="GET", + path="/v3/session/11111111-2222-3333-4444-555555555555/signatures/feature_signature/documents/00000000-0000-4000-8000-000000000003/file/", + api="verification", + auth="api_key", + status=200, + response_bytes=BINARY_BODY, + response_content_type="application/pdf", + ), + lambda c: c.sessions.signatures.download_document( + "11111111-2222-3333-4444-555555555555", + "feature_signature", + "00000000-0000-4000-8000-000000000003", + ), + lambda c: c.sessions.signatures.download_document( + "11111111-2222-3333-4444-555555555555", + "feature_signature", + "00000000-0000-4000-8000-000000000003", + ), + lambda r: r == BINARY_BODY, + lambda r: r == BINARY_BODY, + ) + + +def test_sessions_signatures_download_evidence() -> None: + """``GET /v3/session/{sessionId}/signatures/{nodeId}/evidence/``""" + check_operation( + Expect( + method="GET", + path="/v3/session/11111111-2222-3333-4444-555555555555/signatures/feature_signature/evidence/", + api="verification", + auth="api_key", + query={"signature_id": ["00000000-0000-4000-8000-000000000001"]}, + status=200, + response_bytes=BINARY_BODY, + response_content_type="application/pdf", + ), + lambda c: c.sessions.signatures.download_evidence( + "11111111-2222-3333-4444-555555555555", + "feature_signature", + signature_id="00000000-0000-4000-8000-000000000001", + ), + lambda c: c.sessions.signatures.download_evidence( + "11111111-2222-3333-4444-555555555555", + "feature_signature", + signature_id="00000000-0000-4000-8000-000000000001", + ), + lambda r: r == BINARY_BODY, + lambda r: r == BINARY_BODY, + ) + + +def test_sessions_signatures_download_field_image() -> None: + """``GET /v3/session/{sessionId}/signatures/{nodeId}/fields/{fieldId}/image/``""" + check_operation( + Expect( + method="GET", + path="/v3/session/11111111-2222-3333-4444-555555555555/signatures/feature_signature/fields/00000000-0000-4000-8000-000000000003/image/", + api="verification", + auth="api_key", + status=200, + response_bytes=BINARY_BODY, + response_content_type="image/png", + ), + lambda c: c.sessions.signatures.download_field_image( + "11111111-2222-3333-4444-555555555555", + "feature_signature", + "00000000-0000-4000-8000-000000000003", + ), + lambda c: c.sessions.signatures.download_field_image( + "11111111-2222-3333-4444-555555555555", + "feature_signature", + "00000000-0000-4000-8000-000000000003", + ), + lambda r: r == BINARY_BODY, + lambda r: r == BINARY_BODY, + ) + + +def test_sessions_signatures_validate() -> None: + """``POST /v3/session/{sessionId}/signatures/{nodeId}/validate/``""" + check_operation( + Expect( + method="POST", + path="/v3/session/11111111-2222-3333-4444-555555555555/signatures/feature_signature/validate/", + api="verification", + auth="api_key", + json_body={"signature_id": "0e6c2a8e-6c2f-4b8e-9a51-3f2f6a7d9b10"}, + status=200, + response_json={ + "signatures": [ + { + "id": "00000000-0000-4000-8000-000000000005", + "node_id": "sample", + "status": "intro", + "verdict": "Not Finished", + "level": "aes", + "format": "pades", + "profile": "sample", + "signer": { + "name": "sample", + "capacity": "self", + "company": "sample", + "role": "sample", + "roles": ["sample"], + "identity_session_id": "00000000-0000-4000-8000-000000000006", + }, + "identity_used": { + "session_id": "00000000-0000-4000-8000-000000000006", + "kyc_id": "00000000-0000-4000-8000-000000000006", + "verified": True, + "expected_name": "sample", + }, + "certificate": { + "issuer": "sample", + "issued_to": "sample", + "serial": "sample", + "valid_from": "2026-01-01T00:00:00Z", + "valid_until": "2026-01-01T00:00:00Z", + }, + "confirmation": { + "channel": "email", + "destination": "sample", + "confirmed_at": "2026-01-01T00:00:00Z", + "attempts": 1, + }, + "timestamp": { + "time": "2026-01-01T00:00:00Z", + "qualified": True, + "valid": True, + }, + "documents": [ + { + "id": "00000000-0000-4000-8000-000000000007", + "uuid": "00000000-0000-4000-8000-000000000007", + "title": "sample", + "file_name": "sample", + "pages": 1, + "size_bytes": 1, + "sha256_original": "sample", + "sha256_signed": "sample", + "status": "signed", + "opened_at": "2026-01-01T00:00:00Z", + "read_at": "2026-01-01T00:00:00Z", + "read_to_end": True, + "consented_at": "2026-01-01T00:00:00Z", + "consent": True, + "signed_file": "sample", + "mode": "fields", + "source_format": "pdf", + "conversion": { + "from": "docx", + "file_name": "sample", + "sha256": "sample", + "size_bytes": 1, + "converter": "sample", + }, + "fields": [None], + } + ], + "adopted_signature": { + "method": "typed", + "font": "sample", + "adopted_at": "2026-01-01T00:00:00Z", + }, + "electronic_signature_consent_at": "2026-01-01T00:00:00Z", + "evidence_file": "sample", + "warnings": [{"code": "SIGNATURE_DECLINED", "node_id": "sample"}], + "validation": { + "valid": True, + "reason": "sample", + "checked_at": "2026-01-01T00:00:00Z", + "documents": [ + { + "id": "00000000-0000-4000-8000-000000000008", + "checked_at": "2026-01-01T00:00:00Z", + "valid": True, + "integrity": True, + "trusted": True, + "revoked": True, + "trusted_list": {}, + "certificate": {}, + "timestamp": {}, + } + ], + }, + "declined_at": "2026-01-01T00:00:00Z", + "declined_action": "DECLINE", + "signed_at": "2026-01-01T00:00:00Z", + "expires_at": "2026-01-01T00:00:00Z", + "email_sent_at": "2026-01-01T00:00:00Z", + "email_unavailable_reason": "sample", + "ip_address": "sample", + "place": "sample", + "place_unavailable_reason": "not_collected", + } + ] + }, + strict_type=SessionsSignaturesValidateResponse, + ), + lambda c: c.sessions.signatures.validate( + "11111111-2222-3333-4444-555555555555", + "feature_signature", + signature_id="0e6c2a8e-6c2f-4b8e-9a51-3f2f6a7d9b10", + ), + lambda c: c.sessions.signatures.validate( + "11111111-2222-3333-4444-555555555555", + "feature_signature", + signature_id="0e6c2a8e-6c2f-4b8e-9a51-3f2f6a7d9b10", + ), + lambda r: isinstance(r, SessionsSignaturesValidateResponse), + lambda r: isinstance(r, SessionsSignaturesValidateResponse), + ) + + +def test_sessions_webhooks_list() -> None: + """``GET /v3/session/{sessionId}/webhooks/``""" + check_operation( + Expect( + method="GET", + path="/v3/session/22222222-3333-4444-5555-666666666666/webhooks/", + api="verification", + auth="api_key", + status=200, + response_json=[ + { + "uuid": "66666666-7777-8888-9999-000000000002", + "session_id": "22222222-3333-4444-5555-666666666666", + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": 200, + "status": "In Progress", + "webhook_type": "status.updated", + "request_header": '{"Content-Type": "application/json", "X-Signature": "9188851d160b5b3aacfc946e4dde1f061b7e568662518942c927ddaf507f533d", "X-Signature-V2": "381236af514d3fb538579365ac5a24f2464ce1b8224c6e770c60d8201c43ecc5", "X-Signature-Simple": "4740131dc38f29e78a8508371979be74967d5307464494bd3148a03b4d6c8716", "X-Timestamp": "1792056139", "User-Agent": "DiditWebhook/2.0 +https://didit.me"}', + "request_body": '{"application_id":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee","created_at":1792056057,"environment":"live","event_id":"33333333-4444-5555-6666-777777777777","sandbox_scenario":null,"session_id":"22222222-3333-4444-5555-666666666666","status":"In Progress","timestamp":1792056139,"vendor_data":"user-123","webhook_type":"status.updated","workflow_id":"44444444-5555-6666-7777-888888888888","workflow_version":3}', + "created_at": "2026-10-15T09:22:19.104233Z", + }, + { + "uuid": "66666666-7777-8888-9999-000000000001", + "session_id": "22222222-3333-4444-5555-666666666666", + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": 503, + "status": "In Progress", + "webhook_type": "status.updated", + "request_header": '{"Content-Type": "application/json", "X-Signature": "a5e02a03404e1337a0198e6be15d34abbd053c658a0e51e5d0e78554213ef29b", "X-Signature-V2": "927e7cb770c94ce745939bff73f04aa200015f3bc64795ba46c2aaf78a6cedc5", "X-Signature-Simple": "e37b0d4c716c72bb5ae7127b9cc455d28ab4686e9e22312b4ec2327409a73302", "X-Timestamp": "1792056078", "User-Agent": "DiditWebhook/2.0 +https://didit.me"}', + "request_body": '{"application_id":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee","created_at":1792056057,"environment":"live","event_id":"33333333-4444-5555-6666-777777777777","sandbox_scenario":null,"session_id":"22222222-3333-4444-5555-666666666666","status":"In Progress","timestamp":1792056078,"vendor_data":"user-123","webhook_type":"status.updated","workflow_id":"44444444-5555-6666-7777-888888888888","workflow_version":3}', + "created_at": "2026-10-15T09:21:18.412345Z", + }, + ], + ), + lambda c: c.sessions.webhooks.list("22222222-3333-4444-5555-666666666666"), + lambda c: c.sessions.webhooks.list("22222222-3333-4444-5555-666666666666"), + lambda r: isinstance(r, list) and all(isinstance(x1, SessionWebhookDelivery) for x1 in r), + lambda r: isinstance(r, list) and all(isinstance(x1, SessionWebhookDelivery) for x1 in r), + ) + + +def test_sessions_webhooks_resend() -> None: + """``POST /v3/session/{sessionId}/webhook/{webhook_uuid}/resend``""" + check_operation( + Expect( + method="POST", + path="/v3/session/22222222-3333-4444-5555-666666666666/webhook/66666666-7777-8888-9999-000000000001/resend", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "66666666-7777-8888-9999-000000000004", + "session_id": "22222222-3333-4444-5555-666666666666", + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": 200, + "status": "In Progress", + "webhook_type": "status.updated", + "request_header": '{"Content-Type": "application/json", "X-Signature": "b8eb1e376517ee0cad8ba98ff7f8fd35fc72eca231000963a96a335f82328835", "X-Signature-V2": "9efd2ce9ed1303e1d05273ef9608a59363ed9789a05253db4748b3e8e50cb109", "X-Signature-Simple": "bf3a9dc04b2c20ab539203b0cd5cca2a10f630bf62d8feb6d68d82af80f64774", "X-Timestamp": "1792159542", "User-Agent": "DiditWebhook/2.0 +https://didit.me"}', + "request_body": '{"application_id":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee","created_at":1792056057,"environment":"live","event_id":"33333333-4444-5555-6666-777777777777","sandbox_scenario":null,"session_id":"22222222-3333-4444-5555-666666666666","status":"In Progress","timestamp":1792159542,"vendor_data":"user-123","webhook_type":"status.updated","workflow_id":"44444444-5555-6666-7777-888888888888","workflow_version":3}', + "created_at": "2026-10-16T14:05:42.330981Z", + }, + ), + lambda c: c.sessions.webhooks.resend( + "22222222-3333-4444-5555-666666666666", "66666666-7777-8888-9999-000000000001" + ), + lambda c: c.sessions.webhooks.resend( + "22222222-3333-4444-5555-666666666666", "66666666-7777-8888-9999-000000000001" + ), + lambda r: isinstance(r, SessionWebhookDelivery), + lambda r: isinstance(r, SessionWebhookDelivery), + ) + + +def test_sessions_webhooks_retrieve() -> None: + """``GET /v3/session/{sessionId}/webhook/{webhook_uuid}``""" + check_operation( + Expect( + method="GET", + path="/v3/session/22222222-3333-4444-5555-666666666666/webhook/66666666-7777-8888-9999-000000000001", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "66666666-7777-8888-9999-000000000001", + "session_id": "22222222-3333-4444-5555-666666666666", + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": 503, + "status": "In Progress", + "webhook_type": "status.updated", + "request_header": '{"Content-Type": "application/json", "X-Signature": "a5e02a03404e1337a0198e6be15d34abbd053c658a0e51e5d0e78554213ef29b", "X-Signature-V2": "927e7cb770c94ce745939bff73f04aa200015f3bc64795ba46c2aaf78a6cedc5", "X-Signature-Simple": "e37b0d4c716c72bb5ae7127b9cc455d28ab4686e9e22312b4ec2327409a73302", "X-Timestamp": "1792056078", "User-Agent": "DiditWebhook/2.0 +https://didit.me"}', + "request_body": '{"application_id":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee","created_at":1792056057,"environment":"live","event_id":"33333333-4444-5555-6666-777777777777","sandbox_scenario":null,"session_id":"22222222-3333-4444-5555-666666666666","status":"In Progress","timestamp":1792056078,"vendor_data":"user-123","webhook_type":"status.updated","workflow_id":"44444444-5555-6666-7777-888888888888","workflow_version":3}', + "created_at": "2026-10-15T09:21:18.412345Z", + }, + ), + lambda c: c.sessions.webhooks.retrieve( + "22222222-3333-4444-5555-666666666666", "66666666-7777-8888-9999-000000000001" + ), + lambda c: c.sessions.webhooks.retrieve( + "22222222-3333-4444-5555-666666666666", "66666666-7777-8888-9999-000000000001" + ), + lambda r: isinstance(r, SessionWebhookDelivery), + lambda r: isinstance(r, SessionWebhookDelivery), + ) diff --git a/tests/generated/test_transactions.py b/tests/generated/test_transactions.py index 874b2c8..9a766d3 100644 --- a/tests/generated/test_transactions.py +++ b/tests/generated/test_transactions.py @@ -4,19 +4,22 @@ from __future__ import annotations from didit import AsyncPage, SyncPage +from didit.types.components.entity_network_membership_list import EntityNetworkMembershipList from didit.types.components.transaction_detail import TransactionDetail +from didit.types.components.transaction_note import TransactionNote from didit.types.components.transaction_rule_backtest_result import TransactionRuleBacktestResult from didit.types.components.transaction_rule_detail import TransactionRuleDetail from didit.types.components.transaction_rule_install_result import TransactionRuleInstallResult from didit.types.components.transaction_rule_library_entry import TransactionRuleLibraryEntry +from didit.types.components.transaction_summary import TransactionSummary +from didit.types.components.travel_rule_message import TravelRuleMessage +from didit.types.components.travel_rule_test_vasp_inbound_response import ( + TravelRuleTestVaspInboundResponse, +) from didit.types.components.travel_rule_transfer_detail import TravelRuleTransferDetail from didit.types.transactions.create_sdk_token_response import TransactionsCreateSdkTokenResponse -from didit.types.transactions.list_networks_response import TransactionsListNetworksResponse -from didit.types.transactions.list_party_networks_response import ( - TransactionsListPartyNetworksResponse, -) from didit.types.transactions.list_response import TransactionsListResponseResults -from tests.harness import Expect, check_operation +from tests.harness import BINARY_BODY, Expect, check_operation def test_transactions_create() -> None: @@ -120,6 +123,7 @@ def test_transactions_create() -> None: "travel_rule": None, "network_snapshot": None, "remediation": None, + "action_required": None, "cost_breakdown": None, }, ), @@ -181,6 +185,25 @@ def test_transactions_create_sdk_token() -> None: ) +def test_transactions_generate_pdf() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/pdf/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/transactions/abcdef12-3456-7890-abcd-ef1234567890/pdf/", + api="verification", + auth="api_key", + status=200, + response_bytes=BINARY_BODY, + response_content_type="application/pdf", + ), + lambda c: c.transactions.generate_pdf("abcdef12-3456-7890-abcd-ef1234567890"), + lambda c: c.transactions.generate_pdf("abcdef12-3456-7890-abcd-ef1234567890"), + lambda r: r == BINARY_BODY, + lambda r: r == BINARY_BODY, + ) + + def test_transactions_list() -> None: """``GET /v3/transactions/``""" check_operation( @@ -189,7 +212,7 @@ def test_transactions_list() -> None: path="/v3/transactions/", api="verification", auth="api_key", - query={"limit": ["1"], "offset": ["1"]}, + query={"limit": ["1"], "offset": ["1"], "travel_rule_status": ["sample"]}, status=200, response_json={ "count": 1, @@ -232,12 +255,14 @@ def test_transactions_list() -> None: "counterparty_country": None, "subject_country": None, "assigned_to_name": "Alex Reviewer", + "travel_rule_status": None, + "travel_rule": None, } ], }, ), - lambda c: c.transactions.list(limit=1, offset=1), - lambda c: c.transactions.list(limit=1, offset=1), + lambda c: c.transactions.list(limit=1, offset=1, travel_rule_status="sample"), + lambda c: c.transactions.list(limit=1, offset=1, travel_rule_status="sample"), lambda r: ( isinstance(r, SyncPage) and all(isinstance(x1, TransactionsListResponseResults) for x1 in r.results) @@ -262,13 +287,13 @@ def test_transactions_list_networks() -> None: "results": [ { "network": { - "uuid": "00000000-0000-4000-8000-000000000007", + "uuid": "00000000-0000-4000-8000-000000000008", "network_id": "sample", "name": "sample", "status": "active", - "risk": {}, - "pattern_types": ["exact_same_device"], - "signal_types": ["ip_address"], + "risk": None, + "pattern_types": [], + "signal_types": [], "member_counts": {}, "member_status": {}, "signal_coverage": {}, @@ -283,12 +308,12 @@ def test_transactions_list_networks() -> None: } ] }, - strict_type=TransactionsListNetworksResponse, + strict_type=EntityNetworkMembershipList, ), lambda c: c.transactions.list_networks("00000000-0000-4000-8000-000000000001"), lambda c: c.transactions.list_networks("00000000-0000-4000-8000-000000000001"), - lambda r: isinstance(r, TransactionsListNetworksResponse), - lambda r: isinstance(r, TransactionsListNetworksResponse), + lambda r: isinstance(r, EntityNetworkMembershipList), + lambda r: isinstance(r, EntityNetworkMembershipList), ) @@ -305,13 +330,13 @@ def test_transactions_list_party_networks() -> None: "results": [ { "network": { - "uuid": "00000000-0000-4000-8000-000000000007", + "uuid": "00000000-0000-4000-8000-000000000008", "network_id": "sample", "name": "sample", "status": "active", - "risk": {}, - "pattern_types": ["exact_same_device"], - "signal_types": ["ip_address"], + "risk": None, + "pattern_types": [], + "signal_types": [], "member_counts": {}, "member_status": {}, "signal_coverage": {}, @@ -326,7 +351,7 @@ def test_transactions_list_party_networks() -> None: } ] }, - strict_type=TransactionsListPartyNetworksResponse, + strict_type=EntityNetworkMembershipList, ), lambda c: c.transactions.list_party_networks( "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" @@ -334,8 +359,78 @@ def test_transactions_list_party_networks() -> None: lambda c: c.transactions.list_party_networks( "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" ), - lambda r: isinstance(r, TransactionsListPartyNetworksResponse), - lambda r: isinstance(r, TransactionsListPartyNetworksResponse), + lambda r: isinstance(r, EntityNetworkMembershipList), + lambda r: isinstance(r, EntityNetworkMembershipList), + ) + + +def test_transactions_list_related() -> None: + """``GET /v3/transactions/{transaction_id}/related/``""" + check_operation( + Expect( + method="GET", + path="/v3/transactions/abcdef12-3456-7890-abcd-ef1234567890/related/", + api="verification", + auth="api_key", + query={"limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "uuid": "abcdef99-3456-4890-abcd-ef1234567890", + "transaction_number": 4087, + "txn_id": "your-txn-2026-10-02-017", + "transaction_type": "finance", + "action_type": "deposit", + "status": "APPROVED", + "source": "LIVE", + "direction": "INBOUND", + "amount": "250", + "currency": "EUR", + "currency_type": "fiat", + "preferred_currency_amount": "270.08", + "preferred_currency_code": "USD", + "payment_details": "Card top-up", + "txn_date": "2026-10-02T14:10:00Z", + "created_at": "2026-10-02T14:10:03.512244Z", + "score": 12, + "severity": "LOW", + "decision_reason_code": None, + "decision_reason_label": None, + "tags": [], + "subject_name": "Maria Garcia", + "subject_vendor_data": "user-12345", + "subject_type": "user", + "subject_uuid": "e1111111-2222-4333-8444-555555555555", + "counterparty_name": None, + "counterparty_vendor_data": None, + "counterparty_country": None, + "subject_country": "ESP", + "assigned_to_name": None, + "travel_rule_status": None, + "application_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "application_name": "Production app", + "organization_id": "11111111-2222-3333-4444-555555555555", + "organization_name": "Example Org", + } + ], + }, + ), + lambda c: c.transactions.list_related( + "abcdef12-3456-7890-abcd-ef1234567890", limit=1, offset=1 + ), + lambda c: c.transactions.list_related( + "abcdef12-3456-7890-abcd-ef1234567890", limit=1, offset=1 + ), + lambda r: ( + isinstance(r, SyncPage) and all(isinstance(x1, TransactionSummary) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, TransactionSummary) for x1 in r.results) + ), ) @@ -426,6 +521,7 @@ def test_transactions_retrieve() -> None: "travel_rule": None, "network_snapshot": None, "remediation": None, + "action_required": None, "cost_breakdown": None, }, ), @@ -436,6 +532,109 @@ def test_transactions_retrieve() -> None: ) +def test_transactions_notes_create() -> None: + """``POST /v3/transactions/{transaction_id}/notes/``""" + check_operation( + Expect( + method="POST", + path="/v3/transactions/abcdef12-3456-7890-abcd-ef1234567890/notes/", + api="verification", + auth="api_key", + json_body={ + "note": "Customer confirmed the withdrawal by phone.", + "mentioned_emails": ["analyst@example.com"], + }, + status=201, + response_json={ + "uuid": "c1111111-2222-4333-8444-555555555555", + "note": "Customer confirmed the withdrawal by phone.", + "actor_email": None, + "actor_name": None, + "metadata": {"mentioned_emails": ["analyst@example.com"]}, + "created_at": "2026-10-16T10:05:12.481203Z", + }, + ), + lambda c: c.transactions.notes.create( + "abcdef12-3456-7890-abcd-ef1234567890", + note="Customer confirmed the withdrawal by phone.", + mentioned_emails=["analyst@example.com"], + ), + lambda c: c.transactions.notes.create( + "abcdef12-3456-7890-abcd-ef1234567890", + note="Customer confirmed the withdrawal by phone.", + mentioned_emails=["analyst@example.com"], + ), + lambda r: isinstance(r, TransactionNote), + lambda r: isinstance(r, TransactionNote), + ) + + +def test_transactions_notes_list() -> None: + """``GET /v3/transactions/{transaction_id}/notes/``""" + check_operation( + Expect( + method="GET", + path="/v3/transactions/abcdef12-3456-7890-abcd-ef1234567890/notes/", + api="verification", + auth="api_key", + query={"limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 3, + "next": None, + "previous": None, + "results": [ + { + "uuid": "c1111111-2222-4333-8444-555555555555", + "note": "Customer confirmed the withdrawal by phone.", + "actor_email": None, + "actor_name": None, + "metadata": {"mentioned_emails": ["analyst@example.com"]}, + "created_at": "2026-10-16T10:05:12.481203Z", + }, + { + "uuid": "c2222222-3333-4444-8555-666666666666", + "note": "Status changed from IN_REVIEW to APPROVED. Reason: Source of funds verified", + "actor_email": "reviewer@example.com", + "actor_name": "Alex Reviewer", + "metadata": { + "type": "status_change", + "previous_status": "IN_REVIEW", + "new_status": "APPROVED", + "reason_code": "SOF_VERIFIED", + "reason_label": "Source of funds verified", + }, + "created_at": "2026-10-15T17:22:40.103911Z", + }, + { + "uuid": "c3333333-4444-4555-8666-777777777777", + "note": "Large withdrawal to a new wallet: check the source of funds.", + "actor_email": None, + "actor_name": "Rule Engine", + "metadata": { + "rule_uuid": "d1111111-2222-4333-8444-555555555555", + "source": "rule_action", + }, + "created_at": "2026-10-15T08:42:12.004518Z", + }, + ], + }, + ), + lambda c: c.transactions.notes.list( + "abcdef12-3456-7890-abcd-ef1234567890", limit=1, offset=1 + ), + lambda c: c.transactions.notes.list( + "abcdef12-3456-7890-abcd-ef1234567890", limit=1, offset=1 + ), + lambda r: ( + isinstance(r, SyncPage) and all(isinstance(x1, TransactionNote) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, TransactionNote) for x1 in r.results) + ), + ) + + def test_transactions_rules_backtest() -> None: """``POST /v3/transactions/rules/backtest/``""" check_operation( @@ -853,7 +1052,7 @@ def test_transactions_rules_list() -> None: "filters": {}, } ], - "actions": [{"type": "add_score", "value": 1}], + "actions": [None], "metadata": {}, "severity": "UNKNOWN", "source": "PRESET", @@ -958,7 +1157,7 @@ def test_transactions_rules_list_library() -> None: "filters": {}, } ], - "actions": [{"type": "add_score", "value": 1}], + "actions": [None], "metadata": {}, "bundle": "sample", "tags": ["sample"], @@ -1169,12 +1368,97 @@ def test_transactions_travel_rule_confirm_ownership() -> None: "ownership_confirmed": True, "deadline_at": "2026-01-01T00:00:00Z", "timeline": [ - {"status": "sample", "at": "2026-01-01T00:00:00Z", "detail": "sample"} + { + "status": "sample", + "at": "2026-01-01T00:00:00Z", + "detail": "sample", + "name_match": { + "score": 1, + "matched": True, + "method": "none", + "strictness": "NONE", + "compared_with": "wallet_holder", + "transfer_name": "sample", + "compared_name": "sample", + "matched_names": ["sample"], + "mismatched_names": ["sample"], + }, + "reason": "sample", + } ], - "counterparty_vasp": {"name": "sample", "lei": "sample", "dd_score": 1}, + "counterparty_vasp": { + "name": "sample", + "lei": "sample", + "dd_score": 1, + "dd_score_breakdown": { + "key": {"points": 1.5, "max": 1, "reason": "sample", "detail": "sample"} + }, + "dd_assessed_at": "2026-01-01T00:00:00Z", + }, "originator_data": {}, "beneficiary_data": {}, + "name_match": { + "score": 1, + "matched": True, + "method": "none", + "strictness": "NONE", + "compared_with": "wallet_holder", + "transfer_name": "sample", + "compared_name": "sample", + "matched_names": ["sample"], + "mismatched_names": ["sample"], + }, + "data_match": { + "matched": True, + "mismatched_fields": ["name"], + "fields": {"key": {"result": "matched", "strictness": "NONE"}}, + }, + "mismatched_fields": ["sample"], + "data_unmasked": { + "at": "2026-01-01T00:00:00Z", + "reason": "ownership_confirmed", + "actor": "sample", + }, + "data_masked": True, "created_at": "2026-01-01T00:00:00Z", + "delivery": { + "kind": "SEND", + "status": "QUEUED", + "rail": "INTERNAL", + "attempts": 1, + "last_error": "email undeliverable", + "next_attempt_at": "2026-01-01T00:00:00Z", + "delivered_at": "2026-01-01T00:00:00Z", + }, + "discovery": [ + { + "step": "internal_address_book", + "outcome": "match", + "reason": "sample", + "vasp": "00000000-0000-4000-8000-000000000006", + "rail": "sample", + "at": "2026-01-01T00:00:00Z", + } + ], + "reason": "sample", + "next_actions": [ + {"action": "select_counterparty_vasp", "url": "https://example.com/resource"} + ], + "chain_details": { + "chain": "sample", + "shape": "utxo", + "originator": {"address": "sample", "memo": "sample", "source": "declared"}, + "beneficiary": {"address": "sample", "memo": "sample", "source": "declared"}, + "utxo": { + "inputs": [{"address": "sample"}], + "outputs": [{"address": "sample"}], + "change_output_indexes": [1], + "beneficiary_output_index": 1, + }, + "lightning": {}, + "warnings": ["declared_originator_not_an_input"], + "not_applicable_reason": "LIGHTNING_NO_PAYEE_IDENTIFIER", + }, }, strict_type=TravelRuleTransferDetail, ), @@ -1189,6 +1473,249 @@ def test_transactions_travel_rule_confirm_ownership() -> None: ) +def test_transactions_travel_rule_create_message() -> None: + """``POST /v3/transactions/{transaction_id}/travel-rule/messages/``""" + check_operation( + Expect( + method="POST", + path="/v3/transactions/00000000-0000-4000-8000-000000000001/travel-rule/messages/", + api="verification", + auth="api_key", + json_body={ + "kind": "DOCUMENT_REQUEST", + "body": "Please confirm the date of birth of the beneficiary.", + "requested_documents": ["Proof of address"], + }, + status=201, + response_json={ + "id": "00000000-0000-4000-8000-000000000003", + "kind": "MESSAGE", + "direction": "INBOUND", + "author": "sample", + "body": "sample", + "requested_documents": ["sample"], + "reply_to": "00000000-0000-4000-8000-000000000003", + "fulfilled_at": "2026-01-01T00:00:00Z", + "attachment": { + "uuid": "00000000-0000-4000-8000-000000000005", + "label": "sample", + "note": "sample", + "uploaded_by": "sample", + "created_at": "2026-01-01T00:00:00Z", + "files": [ + { + "uuid": "00000000-0000-4000-8000-000000000007", + "file_url": "sample", + "original_filename": "sample", + "file_size": 1, + } + ], + }, + "created_at": "2026-01-01T00:00:00Z", + }, + strict_type=TravelRuleMessage, + ), + lambda c: c.transactions.travel_rule.create_message( + "00000000-0000-4000-8000-000000000001", + kind="DOCUMENT_REQUEST", + body="Please confirm the date of birth of the beneficiary.", + requested_documents=["Proof of address"], + ), + lambda c: c.transactions.travel_rule.create_message( + "00000000-0000-4000-8000-000000000001", + kind="DOCUMENT_REQUEST", + body="Please confirm the date of birth of the beneficiary.", + requested_documents=["Proof of address"], + ), + lambda r: isinstance(r, TravelRuleMessage), + lambda r: isinstance(r, TravelRuleMessage), + ) + + +def test_transactions_travel_rule_list_messages() -> None: + """``GET /v3/transactions/{transaction_id}/travel-rule/messages/``""" + check_operation( + Expect( + method="GET", + path="/v3/transactions/00000000-0000-4000-8000-000000000001/travel-rule/messages/", + api="verification", + auth="api_key", + status=200, + response_json=[ + { + "id": "00000000-0000-4000-8000-000000000004", + "kind": "MESSAGE", + "direction": "INBOUND", + "author": "sample", + "body": "sample", + "requested_documents": ["sample"], + "reply_to": "00000000-0000-4000-8000-000000000004", + "fulfilled_at": "2026-01-01T00:00:00Z", + "attachment": { + "uuid": "00000000-0000-4000-8000-000000000006", + "label": "sample", + "note": "sample", + "uploaded_by": "sample", + "created_at": "2026-01-01T00:00:00Z", + "files": [ + { + "uuid": "00000000-0000-4000-8000-000000000008", + "file_url": "sample", + "original_filename": "sample", + "file_size": 1, + } + ], + }, + "created_at": "2026-01-01T00:00:00Z", + } + ], + strict_type=list[TravelRuleMessage], + ), + lambda c: c.transactions.travel_rule.list_messages("00000000-0000-4000-8000-000000000001"), + lambda c: c.transactions.travel_rule.list_messages("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, list) and all(isinstance(x1, TravelRuleMessage) for x1 in r), + lambda r: isinstance(r, list) and all(isinstance(x1, TravelRuleMessage) for x1 in r), + ) + + +def test_transactions_travel_rule_trigger_test_vasp_inbound() -> None: + """``POST /v3/travel-rule/test-vasp/inbound/``""" + check_operation( + Expect( + method="POST", + path="/v3/travel-rule/test-vasp/inbound/", + api="verification", + auth="api_key", + json_body={ + "beneficiary_wallet_address": "0xYourSandboxWallet", + "scenario": "accept", + "amount": "1", + "reference": "demo-001", + }, + status=201, + response_json={ + "transaction_id": "00000000-0000-4000-8000-000000000003", + "txn_id": "sample", + "scenario": "sample", + "travel_rule_status": "sample", + "detail": "sample", + "test_vasp": True, + "travel_rule": { + "uuid": "00000000-0000-4000-8000-000000000005", + "status": "AWAITING_COUNTERPARTY", + "direction": "INBOUND", + "rail": "INTERNAL", + "protocol": "sample", + "ivms_version": "ivms101.2023", + "required": True, + "ownership_confirmed": True, + "deadline_at": "2026-01-01T00:00:00Z", + "timeline": [ + { + "status": "sample", + "at": "2026-01-01T00:00:00Z", + "detail": "sample", + "name_match": {}, + "reason": "sample", + } + ], + "counterparty_vasp": { + "name": "sample", + "lei": "sample", + "dd_score": 1, + "dd_score_breakdown": { + "key": {"points": 1.5, "max": 1, "reason": "sample", "detail": "sample"} + }, + "dd_assessed_at": "2026-01-01T00:00:00Z", + }, + "originator_data": {}, + "beneficiary_data": {}, + "name_match": { + "score": 1, + "matched": True, + "method": "none", + "strictness": "NONE", + "compared_with": "wallet_holder", + "transfer_name": "sample", + "compared_name": "sample", + "matched_names": ["sample"], + "mismatched_names": ["sample"], + }, + "data_match": { + "matched": True, + "mismatched_fields": ["name"], + "fields": {"key": {}}, + }, + "mismatched_fields": ["sample"], + "data_unmasked": { + "at": "2026-01-01T00:00:00Z", + "reason": "ownership_confirmed", + "actor": "sample", + }, + "data_masked": True, + "created_at": "2026-01-01T00:00:00Z", + "delivery": { + "kind": "SEND", + "status": "QUEUED", + "rail": "INTERNAL", + "attempts": 1, + "last_error": "email undeliverable", + "next_attempt_at": "2026-01-01T00:00:00Z", + "delivered_at": "2026-01-01T00:00:00Z", + }, + "discovery": [ + { + "step": "internal_address_book", + "outcome": "match", + "reason": "sample", + "vasp": "00000000-0000-4000-8000-000000000008", + "rail": "sample", + "at": "2026-01-01T00:00:00Z", + } + ], + "reason": "sample", + "next_actions": [ + { + "action": "select_counterparty_vasp", + "url": "https://example.com/resource", + } + ], + "chain_details": { + "chain": "sample", + "shape": "utxo", + "originator": {}, + "beneficiary": {}, + "utxo": { + "inputs": [], + "outputs": [], + "change_output_indexes": [], + "beneficiary_output_index": 1, + }, + "lightning": {}, + "warnings": ["declared_originator_not_an_input"], + "not_applicable_reason": "LIGHTNING_NO_PAYEE_IDENTIFIER", + }, + }, + }, + strict_type=TravelRuleTestVaspInboundResponse, + ), + lambda c: c.transactions.travel_rule.trigger_test_vasp_inbound( + beneficiary_wallet_address="0xYourSandboxWallet", + scenario="accept", + amount="1", + reference="demo-001", + ), + lambda c: c.transactions.travel_rule.trigger_test_vasp_inbound( + beneficiary_wallet_address="0xYourSandboxWallet", + scenario="accept", + amount="1", + reference="demo-001", + ), + lambda r: isinstance(r, TravelRuleTestVaspInboundResponse), + lambda r: isinstance(r, TravelRuleTestVaspInboundResponse), + ) + + def test_transactions_travel_rule_update_transfer() -> None: """``PATCH /v3/transactions/{transaction_id}/travel-rule/``""" check_operation( @@ -1210,12 +1737,97 @@ def test_transactions_travel_rule_update_transfer() -> None: "ownership_confirmed": True, "deadline_at": "2026-01-01T00:00:00Z", "timeline": [ - {"status": "sample", "at": "2026-01-01T00:00:00Z", "detail": "sample"} + { + "status": "sample", + "at": "2026-01-01T00:00:00Z", + "detail": "sample", + "name_match": { + "score": 1, + "matched": True, + "method": "none", + "strictness": "NONE", + "compared_with": "wallet_holder", + "transfer_name": "sample", + "compared_name": "sample", + "matched_names": ["sample"], + "mismatched_names": ["sample"], + }, + "reason": "sample", + } ], - "counterparty_vasp": {"name": "sample", "lei": "sample", "dd_score": 1}, + "counterparty_vasp": { + "name": "sample", + "lei": "sample", + "dd_score": 1, + "dd_score_breakdown": { + "key": {"points": 1.5, "max": 1, "reason": "sample", "detail": "sample"} + }, + "dd_assessed_at": "2026-01-01T00:00:00Z", + }, "originator_data": {}, "beneficiary_data": {}, + "name_match": { + "score": 1, + "matched": True, + "method": "none", + "strictness": "NONE", + "compared_with": "wallet_holder", + "transfer_name": "sample", + "compared_name": "sample", + "matched_names": ["sample"], + "mismatched_names": ["sample"], + }, + "data_match": { + "matched": True, + "mismatched_fields": ["name"], + "fields": {"key": {"result": "matched", "strictness": "NONE"}}, + }, + "mismatched_fields": ["sample"], + "data_unmasked": { + "at": "2026-01-01T00:00:00Z", + "reason": "ownership_confirmed", + "actor": "sample", + }, + "data_masked": True, "created_at": "2026-01-01T00:00:00Z", + "delivery": { + "kind": "SEND", + "status": "QUEUED", + "rail": "INTERNAL", + "attempts": 1, + "last_error": "email undeliverable", + "next_attempt_at": "2026-01-01T00:00:00Z", + "delivered_at": "2026-01-01T00:00:00Z", + }, + "discovery": [ + { + "step": "internal_address_book", + "outcome": "match", + "reason": "sample", + "vasp": "00000000-0000-4000-8000-000000000006", + "rail": "sample", + "at": "2026-01-01T00:00:00Z", + } + ], + "reason": "sample", + "next_actions": [ + {"action": "select_counterparty_vasp", "url": "https://example.com/resource"} + ], + "chain_details": { + "chain": "sample", + "shape": "utxo", + "originator": {"address": "sample", "memo": "sample", "source": "declared"}, + "beneficiary": {"address": "sample", "memo": "sample", "source": "declared"}, + "utxo": { + "inputs": [{"address": "sample"}], + "outputs": [{"address": "sample"}], + "change_output_indexes": [1], + "beneficiary_output_index": 1, + }, + "lightning": {}, + "warnings": ["declared_originator_not_an_input"], + "not_applicable_reason": "LIGHTNING_NO_PAYEE_IDENTIFIER", + }, }, strict_type=TravelRuleTransferDetail, ), diff --git a/tests/generated/test_travel_rule.py b/tests/generated/test_travel_rule.py index 886f3db..f2c6d2c 100644 --- a/tests/generated/test_travel_rule.py +++ b/tests/generated/test_travel_rule.py @@ -4,17 +4,60 @@ from __future__ import annotations from didit import AsyncPage, SyncPage +from didit.types.components.travel_rule_counterparty_contact import TravelRuleCounterpartyContact +from didit.types.components.travel_rule_counterparty_decision_response import ( + TravelRuleCounterpartyDecisionResponse, +) +from didit.types.components.travel_rule_counterparty_questionnaire import ( + TravelRuleCounterpartyQuestionnaire, +) +from didit.types.components.travel_rule_counterparty_questionnaire_list import ( + TravelRuleCounterpartyQuestionnaireList, +) +from didit.types.components.travel_rule_counterparty_screening_result import ( + TravelRuleCounterpartyScreeningResult, +) +from didit.types.components.travel_rule_counterparty_screening_status import ( + TravelRuleCounterpartyScreeningStatus, +) from didit.types.components.travel_rule_inbound_response import TravelRuleInboundResponse from didit.types.components.travel_rule_settings_detail import TravelRuleSettingsDetail +from didit.types.components.travel_rule_transfer_row import TravelRuleTransferRow from didit.types.components.travel_rule_widget_session_response import ( TravelRuleWidgetSessionResponse, ) +from didit.types.components.travel_rule_workspace import TravelRuleWorkspace +from didit.types.components.travel_rule_workspace_domain_code_response import ( + TravelRuleWorkspaceDomainCodeResponse, +) from didit.types.components.vasp_directory_entry import VaspDirectoryEntry from didit.types.components.wallet_address_entry import WalletAddressEntry from didit.types.components.wallet_address_entry_update import WalletAddressEntryUpdate +from didit.types.components.wallet_address_import_report import WalletAddressImportReport +from didit.types.travel_rule.send_compliance_email_confirmation_response import ( + TravelRuleSendComplianceEmailConfirmationResponse, +) from tests.harness import Expect, check_operation +def test_travel_rule_clear_vasp_decision() -> None: + """``DELETE /v3/travel-rule/vasps/{vasp_id}/decision/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/travel-rule/vasps/00000000-0000-4000-8000-000000000001/decision/", + api="verification", + auth="api_key", + status=200, + response_json={"decision": None}, + ), + lambda c: c.travel_rule.clear_vasp_decision("00000000-0000-4000-8000-000000000001"), + lambda c: c.travel_rule.clear_vasp_decision("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, TravelRuleCounterpartyDecisionResponse), + lambda r: isinstance(r, TravelRuleCounterpartyDecisionResponse), + ) + + def test_travel_rule_create_widget_session() -> None: """``POST /v3/travel-rule/widget-session/``""" check_operation( @@ -24,11 +67,11 @@ def test_travel_rule_create_widget_session() -> None: api="verification", auth="api_key", json_body={ - "wallet_address": "0xBeneficiaryWallet01", - "chain": "ethereum", - "holder_name": "Ana Diaz", + "wallet_address": "0xd3b62acb76fb27a7f8961ec40fa11746b113aca9", + "chain": "ETH", + "holder_name": "Jane Doe", "vendor_data": "user-042", - "transaction_id": "22222222-3333-4444-5555-666666666666", + "transaction_id": "99999999-aaaa-bbbb-cccc-dddddddddddd", "expires_in_minutes": 60, }, status=201, @@ -41,19 +84,19 @@ def test_travel_rule_create_widget_session() -> None: strict_type=TravelRuleWidgetSessionResponse, ), lambda c: c.travel_rule.create_widget_session( - wallet_address="0xBeneficiaryWallet01", - chain="ethereum", - holder_name="Ana Diaz", + wallet_address="0xd3b62acb76fb27a7f8961ec40fa11746b113aca9", + chain="ETH", + holder_name="Jane Doe", vendor_data="user-042", - transaction_id="22222222-3333-4444-5555-666666666666", + transaction_id="99999999-aaaa-bbbb-cccc-dddddddddddd", expires_in_minutes=60, ), lambda c: c.travel_rule.create_widget_session( - wallet_address="0xBeneficiaryWallet01", - chain="ethereum", - holder_name="Ana Diaz", + wallet_address="0xd3b62acb76fb27a7f8961ec40fa11746b113aca9", + chain="ETH", + holder_name="Jane Doe", vendor_data="user-042", - transaction_id="22222222-3333-4444-5555-666666666666", + transaction_id="99999999-aaaa-bbbb-cccc-dddddddddddd", expires_in_minutes=60, ), lambda r: isinstance(r, TravelRuleWidgetSessionResponse), @@ -61,6 +104,64 @@ def test_travel_rule_create_widget_session() -> None: ) +def test_travel_rule_list_vasp_questionnaires() -> None: + """``GET /v3/travel-rule/vasps/{vasp_id}/questionnaires/``""" + check_operation( + Expect( + method="GET", + path="/v3/travel-rule/vasps/00000000-0000-4000-8000-000000000001/questionnaires/", + api="verification", + auth="api_key", + status=200, + response_json={ + "questionnaires": [ + { + "id": "3f9e8d7c-6b5a-4c3d-9e2f-1a0b9c8d7e6f", + "status": "SENT", + "form_version": "1", + "sent_to": "travelrule@counterparty.example", + "sent_at": "2026-10-14T10:00:00Z", + "expires_at": "2026-11-13T10:00:00Z", + "answered_at": None, + "valid_until": None, + "current": False, + "answers": {}, + }, + { + "id": "8b7a6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d", + "status": "ANSWERED", + "form_version": "1", + "sent_to": "travelrule@counterparty.example", + "sent_at": "2026-09-02T08:00:00Z", + "expires_at": "2026-10-02T08:00:00Z", + "answered_at": "2026-09-04T15:20:00Z", + "valid_until": "2027-09-04T15:20:00Z", + "current": True, + "answers": { + "legal_name": "Example Counterparty VASP, incorporated in the example jurisdiction.", + "licensing": "Crypto-asset service provider authorised under MiCA by its national regulator, licence active.", + "jurisdictions": "EU and EEA. We do not serve the United States.", + "aml_program": "Written AML/CFT policy, annual independent review, last completed in March 2026.", + "compliance_officer": "Head of Compliance, reachable at compliance@counterparty.example.", + "sanctions_screening": "Customers at onboarding and daily; counterparties on every transfer.", + "transaction_monitoring": "Rule-based monitoring with analyst review; suspicious activity reports filed with the FIU.", + "travel_rule": "TRP and email; we answer requests within one business day.", + "custody": "Segregated customer wallets held with a qualified custodian, insured.", + "ownership": "No shareholder holds 25 percent or more; parent company Example Holding Group.", + "record_keeping": "Five-year record-keeping for Travel Rule and transaction data, as our regulator requires.", + "data_protection": "Encrypted at rest and in transit, stored in the EU.", + }, + }, + ] + }, + ), + lambda c: c.travel_rule.list_vasp_questionnaires("00000000-0000-4000-8000-000000000001"), + lambda c: c.travel_rule.list_vasp_questionnaires("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, TravelRuleCounterpartyQuestionnaireList), + lambda r: isinstance(r, TravelRuleCounterpartyQuestionnaireList), + ) + + def test_travel_rule_list_vasps() -> None: """``GET /v3/travel-rule/vasps/``""" check_operation( @@ -69,7 +170,17 @@ def test_travel_rule_list_vasps() -> None: path="/v3/travel-rule/vasps/", api="verification", auth="api_key", - query={"search": ["sample"], "limit": ["1"], "offset": ["1"]}, + query={ + "search": ["sample"], + "network": ["GTR"], + "jurisdiction": ["sample"], + "reachable": ["true"], + "kind": ["directory"], + "risk": ["LOW"], + "responsiveness": ["RESPONSIVE"], + "limit": ["1"], + "offset": ["1"], + }, status=200, response_json={ "count": 1, @@ -77,21 +188,178 @@ def test_travel_rule_list_vasps() -> None: "previous": "sample", "results": [ { - "name": "sample", - "lei": "sample", - "jurisdiction": "sample", + "id": "0d6f3c2a-6a1e-4c55-9a39-1f2b0c7d8e91", + "kind": "directory", + "name": "Example Counterparty VASP", + "lei": "529900EXAMPLECPTY001", + "jurisdiction": "DE", + "domains": ["counterparty.example"], + "reachable": True, + "aliases": [{"name": "Example Counterparty", "kind": "trading_name"}], + "networks": [ + { + "network": "GTR", + "code": "EXAMPLE01", + "listed_name": "Example Counterparty VASP", + "country": "DE", + "verified_at": "2026-10-12T09:30:00Z", + "last_seen_at": "2026-10-12T09:30:00Z", + "source": "staff", + } + ], + "routes": [ + { + "rail": "GTR", + "status": "unavailable", + "reason": "no_membership", + "via": "", + }, + { + "rail": "EMAIL", + "status": "available", + "reason": "", + "via": "contact", + }, + ], + "umbrella": None, + "entities": [], + "contacts": [ + { + "id": "6c1f9b8e-2d4a-4f7e-8b3c-5a9d0e1f2a3b", + "email": "travelrule@counterparty.example", + "name": "Travel Rule desk", + "source": "staff", + "scope": "platform", + "verified_at": None, + } + ], "dd_status": "NOT_STARTED", - "dd_score": 1, - "reachable_rails": ["INTERNAL"], - "is_didit_customer": True, - "dd_score_breakdown": {"key": 1}, - "dd_assessed_at": "2026-01-01T00:00:00Z", + "dd_score": 62, + "dd_score_breakdown": { + "jurisdiction": { + "points": 25.0, + "max": 25, + "reason": "jurisdiction_not_high_risk", + "detail": "DEU is not on the high-risk jurisdiction list", + }, + "verification": { + "points": 3.8, + "max": 15, + "reason": "dd_status_not_started", + "detail": "due diligence status NOT_STARTED", + }, + "protocol": { + "points": 6.0, + "max": 15, + "reason": "email_only", + "detail": "reachable over the email fallback only", + }, + "lei": { + "points": 10.0, + "max": 10, + "reason": "lei_valid", + "detail": "valid LEI on record", + }, + "history": { + "points": 5.0, + "max": 10, + "reason": "no_history", + "detail": "no exchange history", + }, + "essentials": { + "points": 7.5, + "max": 15, + "reason": "no_essential_checks", + "detail": "no decided essential check yet", + }, + "responsiveness": { + "points": 5.0, + "max": 10, + "reason": "responsiveness_unknown", + "detail": "0 decided recent requests, too few to judge", + }, + }, + "dd_assessed_at": "2026-10-12T09:30:00Z", + "dd_score_semantic": "higher_is_safer", + "risk_level": "MEDIUM", + "responsiveness": { + "status": "UNKNOWN", + "answered": 0, + "total": 0, + "computed_at": "2026-10-12T09:30:00Z", + }, + "essential_checks": {}, + "reachable_rails": ["GTR", "EMAIL"], + "is_didit_customer": False, + "decision": { + "decision": "APPROVED", + "reason": "Licensed in DE, questionnaire reviewed", + "decided_by": "", + "updated_at": "2026-10-14T10:05:00Z", + "inherited_from": None, + }, + "entity_screening": { + "id": "7a1d2c3b-4e5f-4a6b-8c7d-9e0f1a2b3c4d", + "status": "Approved", + "total_hits": 0, + "score": None, + "screened_at": "2026-10-14T09:58:00Z", + "fresh": True, + "flagged": False, + }, + "questionnaires": [ + { + "id": "8b7a6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d", + "status": "ANSWERED", + "form_version": "1", + "sent_to": "travelrule@counterparty.example", + "sent_at": "2026-09-02T08:00:00Z", + "expires_at": "2026-10-02T08:00:00Z", + "answered_at": "2026-09-04T15:20:00Z", + "valid_until": "2027-09-04T15:20:00Z", + "current": True, + "answers": { + "legal_name": "Example Counterparty VASP, incorporated in the example jurisdiction.", + "licensing": "Crypto-asset service provider authorised under MiCA by its national regulator, licence active.", + "jurisdictions": "EU and EEA. We do not serve the United States.", + "aml_program": "Written AML/CFT policy, annual independent review, last completed in March 2026.", + "compliance_officer": "Head of Compliance, reachable at compliance@counterparty.example.", + "sanctions_screening": "Customers at onboarding and daily; counterparties on every transfer.", + "transaction_monitoring": "Rule-based monitoring with analyst review; suspicious activity reports filed with the FIU.", + "travel_rule": "TRP and email; we answer requests within one business day.", + "custody": "Segregated customer wallets held with a qualified custodian, insured.", + "ownership": "No shareholder holds 25 percent or more; parent company Example Holding Group.", + "record_keeping": "Five-year record-keeping for Travel Rule and transaction data, as our regulator requires.", + "data_protection": "Encrypted at rest and in transit, stored in the EU.", + }, + } + ], } ], }, ), - lambda c: c.travel_rule.list_vasps(search="sample", limit=1, offset=1), - lambda c: c.travel_rule.list_vasps(search="sample", limit=1, offset=1), + lambda c: c.travel_rule.list_vasps( + search="sample", + network="GTR", + jurisdiction="sample", + reachable=True, + kind="directory", + risk="LOW", + responsiveness="RESPONSIVE", + limit=1, + offset=1, + ), + lambda c: c.travel_rule.list_vasps( + search="sample", + network="GTR", + jurisdiction="sample", + reachable=True, + kind="directory", + risk="LOW", + responsiveness="RESPONSIVE", + limit=1, + offset=1, + ), lambda r: ( isinstance(r, SyncPage) and all(isinstance(x1, VaspDirectoryEntry) for x1 in r.results) ), @@ -110,7 +378,7 @@ def test_travel_rule_register_inbound() -> None: api="verification", auth="api_key", json_body={ - "chain": "ethereum", + "chain": "ETH", "txid": "0xdeposit-hash", "wallet_address": "0xYourDepositWallet", "amount": "500.00", @@ -120,7 +388,7 @@ def test_travel_rule_register_inbound() -> None: "originating_vasp": { "name": "Origin CASP SL", "lei": "", - "travel_address": "ta...", + "travel_address": "ta9JbXbsBSuS5ozArJRFVgvBHMVnDGUnrPPChSZYQsuZTn3kPrJXUQ7RSLUEJ5KjyPvr8C9sAxmPtZfGsQVKCi4n8ShxP9BkbDdbtjtA9aumNybAZ1BxC", }, }, status=200, @@ -139,35 +407,121 @@ def test_travel_rule_register_inbound() -> None: "ownership_confirmed": True, "deadline_at": "2026-01-01T00:00:00Z", "timeline": [ - {"status": "sample", "at": "2026-01-01T00:00:00Z", "detail": "sample"} + { + "status": "sample", + "at": "2026-01-01T00:00:00Z", + "detail": "sample", + "name_match": {}, + "reason": "sample", + } ], - "counterparty_vasp": {"name": "sample", "lei": "sample", "dd_score": 1}, + "counterparty_vasp": { + "name": "sample", + "lei": "sample", + "dd_score": 1, + "dd_score_breakdown": { + "key": {"points": 1.5, "max": 1, "reason": "sample", "detail": "sample"} + }, + "dd_assessed_at": "2026-01-01T00:00:00Z", + }, "originator_data": {}, "beneficiary_data": {}, + "name_match": { + "score": 1, + "matched": True, + "method": "none", + "strictness": "NONE", + "compared_with": "wallet_holder", + "transfer_name": "sample", + "compared_name": "sample", + "matched_names": ["sample"], + "mismatched_names": ["sample"], + }, + "data_match": { + "matched": True, + "mismatched_fields": ["name"], + "fields": {"key": {}}, + }, + "mismatched_fields": ["sample"], + "data_unmasked": { + "at": "2026-01-01T00:00:00Z", + "reason": "ownership_confirmed", + "actor": "sample", + }, + "data_masked": True, "created_at": "2026-01-01T00:00:00Z", + "delivery": { + "kind": "SEND", + "status": "QUEUED", + "rail": "INTERNAL", + "attempts": 1, + "last_error": "email undeliverable", + "next_attempt_at": "2026-01-01T00:00:00Z", + "delivered_at": "2026-01-01T00:00:00Z", + }, + "discovery": [ + { + "step": "internal_address_book", + "outcome": "match", + "reason": "sample", + "vasp": "00000000-0000-4000-8000-000000000008", + "rail": "sample", + "at": "2026-01-01T00:00:00Z", + } + ], + "reason": "sample", + "next_actions": [ + { + "action": "select_counterparty_vasp", + "url": "https://example.com/resource", + } + ], + "chain_details": { + "chain": "sample", + "shape": "utxo", + "originator": {}, + "beneficiary": {}, + "utxo": { + "inputs": [], + "outputs": [], + "change_output_indexes": [], + "beneficiary_output_index": 1, + }, + "lightning": {}, + "warnings": ["declared_originator_not_an_input"], + "not_applicable_reason": "LIGHTNING_NO_PAYEE_IDENTIFIER", + }, }, }, strict_type=TravelRuleInboundResponse, ), lambda c: c.travel_rule.register_inbound( - chain="ethereum", + chain="ETH", txid="0xdeposit-hash", wallet_address="0xYourDepositWallet", amount="500.00", currency="USDC", originator_data={"name": "Origin CASP SL"}, beneficiary_data={"name": "Ana Diaz"}, - originating_vasp={"name": "Origin CASP SL", "lei": "", "travel_address": "ta..."}, + originating_vasp={ + "name": "Origin CASP SL", + "lei": "", + "travel_address": "ta9JbXbsBSuS5ozArJRFVgvBHMVnDGUnrPPChSZYQsuZTn3kPrJXUQ7RSLUEJ5KjyPvr8C9sAxmPtZfGsQVKCi4n8ShxP9BkbDdbtjtA9aumNybAZ1BxC", + }, ), lambda c: c.travel_rule.register_inbound( - chain="ethereum", + chain="ETH", txid="0xdeposit-hash", wallet_address="0xYourDepositWallet", amount="500.00", currency="USDC", originator_data={"name": "Origin CASP SL"}, beneficiary_data={"name": "Ana Diaz"}, - originating_vasp={"name": "Origin CASP SL", "lei": "", "travel_address": "ta..."}, + originating_vasp={ + "name": "Origin CASP SL", + "lei": "", + "travel_address": "ta9JbXbsBSuS5ozArJRFVgvBHMVnDGUnrPPChSZYQsuZTn3kPrJXUQ7RSLUEJ5KjyPvr8C9sAxmPtZfGsQVKCi4n8ShxP9BkbDdbtjtA9aumNybAZ1BxC", + }, ), lambda r: isinstance(r, TravelRuleInboundResponse), lambda r: isinstance(r, TravelRuleInboundResponse), @@ -190,10 +544,20 @@ def test_travel_rule_retrieve_settings() -> None: "confirmation_timeout_hours": 48, "timeout_outcome": "HOLD", "threshold_amount": "0.00", + "inbound_auto_accept": True, + "allow_self_declaration": True, + "allow_screenshot_proof": True, + "auto_wallet_verification": True, + "vasp_attribution_enabled": True, "legal_name": "", "lei": "", "compliance_email": "", "is_discoverable": True, + "counterparty_screening_enabled": False, + "sunrise_policy": "", + "sunrise_jurisdictions": ["CO", "KH", "VN"], + "non_responsive_policy": "", + "travel_address": "", "disabled_networks": [], "networks": { "gtr": { @@ -221,6 +585,15 @@ def test_travel_rule_retrieve_settings() -> None: "connected": False, }, }, + "didit_network": { + "listed": False, + "missing": [ + "travel_rule_off", + "legal_name_missing", + "compliance_email_missing", + ], + "compliance_email_confirmed": False, + }, }, ), lambda c: c.travel_rule.retrieve_settings(), @@ -230,6 +603,300 @@ def test_travel_rule_retrieve_settings() -> None: ) +def test_travel_rule_retrieve_vasp() -> None: + """``GET /v3/travel-rule/vasps/{vasp_id}/``""" + check_operation( + Expect( + method="GET", + path="/v3/travel-rule/vasps/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "id": "0d6f3c2a-6a1e-4c55-9a39-1f2b0c7d8e91", + "kind": "directory", + "name": "Example Counterparty VASP", + "lei": "529900EXAMPLECPTY001", + "jurisdiction": "DE", + "domains": ["counterparty.example"], + "reachable": True, + "aliases": [{"name": "Example Counterparty", "kind": "trading_name"}], + "networks": [ + { + "network": "GTR", + "code": "EXAMPLE01", + "listed_name": "Example Counterparty VASP", + "country": "DE", + "verified_at": "2026-10-12T09:30:00Z", + "last_seen_at": "2026-10-12T09:30:00Z", + "source": "staff", + } + ], + "routes": [ + {"rail": "GTR", "status": "unavailable", "reason": "no_membership", "via": ""}, + {"rail": "EMAIL", "status": "available", "reason": "", "via": "contact"}, + ], + "umbrella": None, + "entities": [], + "contacts": [ + { + "id": "6c1f9b8e-2d4a-4f7e-8b3c-5a9d0e1f2a3b", + "email": "travelrule@counterparty.example", + "name": "Travel Rule desk", + "source": "staff", + "scope": "platform", + "verified_at": None, + } + ], + "dd_status": "NOT_STARTED", + "dd_score": 62, + "dd_score_breakdown": { + "jurisdiction": { + "points": 25.0, + "max": 25, + "reason": "jurisdiction_not_high_risk", + "detail": "DEU is not on the high-risk jurisdiction list", + }, + "verification": { + "points": 3.8, + "max": 15, + "reason": "dd_status_not_started", + "detail": "due diligence status NOT_STARTED", + }, + "protocol": { + "points": 6.0, + "max": 15, + "reason": "email_only", + "detail": "reachable over the email fallback only", + }, + "lei": { + "points": 10.0, + "max": 10, + "reason": "lei_valid", + "detail": "valid LEI on record", + }, + "history": { + "points": 5.0, + "max": 10, + "reason": "no_history", + "detail": "no exchange history", + }, + "essentials": { + "points": 7.5, + "max": 15, + "reason": "no_essential_checks", + "detail": "no decided essential check yet", + }, + "responsiveness": { + "points": 5.0, + "max": 10, + "reason": "responsiveness_unknown", + "detail": "0 decided recent requests, too few to judge", + }, + }, + "dd_assessed_at": "2026-10-12T09:30:00Z", + "dd_score_semantic": "higher_is_safer", + "risk_level": "MEDIUM", + "responsiveness": { + "status": "UNKNOWN", + "answered": 0, + "total": 0, + "computed_at": "2026-10-12T09:30:00Z", + }, + "essential_checks": {}, + "reachable_rails": ["GTR", "EMAIL"], + "is_didit_customer": False, + "decision": { + "decision": "APPROVED", + "reason": "Licensed in DE, questionnaire reviewed", + "decided_by": "", + "updated_at": "2026-10-14T10:05:00Z", + "inherited_from": None, + }, + "entity_screening": { + "id": "7a1d2c3b-4e5f-4a6b-8c7d-9e0f1a2b3c4d", + "status": "Approved", + "total_hits": 0, + "score": None, + "screened_at": "2026-10-14T09:58:00Z", + "fresh": True, + "flagged": False, + }, + "questionnaires": [ + { + "id": "8b7a6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d", + "status": "ANSWERED", + "form_version": "1", + "sent_to": "travelrule@counterparty.example", + "sent_at": "2026-09-02T08:00:00Z", + "expires_at": "2026-10-02T08:00:00Z", + "answered_at": "2026-09-04T15:20:00Z", + "valid_until": "2027-09-04T15:20:00Z", + "current": True, + "answers": { + "legal_name": "Example Counterparty VASP, incorporated in the example jurisdiction.", + "licensing": "Crypto-asset service provider authorised under MiCA by its national regulator, licence active.", + "jurisdictions": "EU and EEA. We do not serve the United States.", + "aml_program": "Written AML/CFT policy, annual independent review, last completed in March 2026.", + "compliance_officer": "Head of Compliance, reachable at compliance@counterparty.example.", + "sanctions_screening": "Customers at onboarding and daily; counterparties on every transfer.", + "transaction_monitoring": "Rule-based monitoring with analyst review; suspicious activity reports filed with the FIU.", + "travel_rule": "TRP and email; we answer requests within one business day.", + "custody": "Segregated customer wallets held with a qualified custodian, insured.", + "ownership": "No shareholder holds 25 percent or more; parent company Example Holding Group.", + "record_keeping": "Five-year record-keeping for Travel Rule and transaction data, as our regulator requires.", + "data_protection": "Encrypted at rest and in transit, stored in the EU.", + }, + } + ], + }, + ), + lambda c: c.travel_rule.retrieve_vasp("00000000-0000-4000-8000-000000000001"), + lambda c: c.travel_rule.retrieve_vasp("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, VaspDirectoryEntry), + lambda r: isinstance(r, VaspDirectoryEntry), + ) + + +def test_travel_rule_retrieve_vasp_decision() -> None: + """``GET /v3/travel-rule/vasps/{vasp_id}/decision/``""" + check_operation( + Expect( + method="GET", + path="/v3/travel-rule/vasps/00000000-0000-4000-8000-000000000001/decision/", + api="verification", + auth="api_key", + status=200, + response_json={ + "decision": { + "decision": "APPROVED", + "reason": "Licensed in DE, questionnaire reviewed", + "decided_by": "", + "updated_at": "2026-10-14T10:05:00Z", + "inherited_from": None, + } + }, + ), + lambda c: c.travel_rule.retrieve_vasp_decision("00000000-0000-4000-8000-000000000001"), + lambda c: c.travel_rule.retrieve_vasp_decision("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, TravelRuleCounterpartyDecisionResponse), + lambda r: isinstance(r, TravelRuleCounterpartyDecisionResponse), + ) + + +def test_travel_rule_retrieve_vasp_screening() -> None: + """``GET /v3/travel-rule/vasps/{vasp_id}/screening/``""" + check_operation( + Expect( + method="GET", + path="/v3/travel-rule/vasps/00000000-0000-4000-8000-000000000001/screening/", + api="verification", + auth="api_key", + status=200, + response_json={ + "enabled": True, + "price": "0.20", + "fresh_days": 30, + "screening": { + "id": "7a1d2c3b-4e5f-4a6b-8c7d-9e0f1a2b3c4d", + "status": "Approved", + "total_hits": 0, + "score": None, + "screened_at": "2026-10-14T09:58:00Z", + "fresh": True, + "flagged": False, + }, + }, + ), + lambda c: c.travel_rule.retrieve_vasp_screening("00000000-0000-4000-8000-000000000001"), + lambda c: c.travel_rule.retrieve_vasp_screening("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, TravelRuleCounterpartyScreeningStatus), + lambda r: isinstance(r, TravelRuleCounterpartyScreeningStatus), + ) + + +def test_travel_rule_screen_vasp() -> None: + """``POST /v3/travel-rule/vasps/{vasp_id}/screening/``""" + check_operation( + Expect( + method="POST", + path="/v3/travel-rule/vasps/00000000-0000-4000-8000-000000000001/screening/", + api="verification", + auth="api_key", + json_body={"refresh": False}, + status=200, + response_json={ + "outcome": "reused", + "screening": { + "id": "7a1d2c3b-4e5f-4a6b-8c7d-9e0f1a2b3c4d", + "status": "Approved", + "total_hits": 0, + "score": None, + "screened_at": "2026-10-14T09:58:00Z", + "fresh": True, + "flagged": False, + }, + }, + ), + lambda c: c.travel_rule.screen_vasp("00000000-0000-4000-8000-000000000001", refresh=False), + lambda c: c.travel_rule.screen_vasp("00000000-0000-4000-8000-000000000001", refresh=False), + lambda r: isinstance(r, TravelRuleCounterpartyScreeningResult), + lambda r: isinstance(r, TravelRuleCounterpartyScreeningResult), + ) + + +def test_travel_rule_send_compliance_email_confirmation() -> None: + """``POST /v3/travel-rule/settings/compliance-email/confirmation/``""" + check_operation( + Expect( + method="POST", + path="/v3/travel-rule/settings/compliance-email/confirmation/", + api="verification", + auth="api_key", + status=202, + response_json={"sent_to": "compliance@your-vasp.example"}, + ), + lambda c: c.travel_rule.send_compliance_email_confirmation(), + lambda c: c.travel_rule.send_compliance_email_confirmation(), + lambda r: isinstance(r, TravelRuleSendComplianceEmailConfirmationResponse), + lambda r: isinstance(r, TravelRuleSendComplianceEmailConfirmationResponse), + ) + + +def test_travel_rule_send_vasp_questionnaire() -> None: + """``POST /v3/travel-rule/vasps/{vasp_id}/questionnaires/``""" + check_operation( + Expect( + method="POST", + path="/v3/travel-rule/vasps/00000000-0000-4000-8000-000000000001/questionnaires/", + api="verification", + auth="api_key", + json_body={"sent_to": "travelrule@counterparty.example"}, + status=202, + response_json={ + "id": "3f9e8d7c-6b5a-4c3d-9e2f-1a0b9c8d7e6f", + "status": "SENT", + "form_version": "1", + "sent_to": "travelrule@counterparty.example", + "sent_at": "2026-10-14T10:00:00Z", + "expires_at": "2026-11-13T10:00:00Z", + "answered_at": None, + "valid_until": None, + "current": False, + "answers": {}, + }, + ), + lambda c: c.travel_rule.send_vasp_questionnaire( + "00000000-0000-4000-8000-000000000001", sent_to="travelrule@counterparty.example" + ), + lambda c: c.travel_rule.send_vasp_questionnaire( + "00000000-0000-4000-8000-000000000001", sent_to="travelrule@counterparty.example" + ), + lambda r: isinstance(r, TravelRuleCounterpartyQuestionnaire), + lambda r: isinstance(r, TravelRuleCounterpartyQuestionnaire), + ) + + def test_travel_rule_update_settings() -> None: """``PUT /v3/travel-rule/settings/``""" check_operation( @@ -255,6 +922,18 @@ def test_travel_rule_update_settings() -> None: "confirmation_timeout_hours": 1, "timeout_outcome": "HOLD", "threshold_amount": "sample", + "ownership_validation_mode": "FULL", + "simplified_validation_threshold": "sample", + "data_sharing_fields": {"individual": ["name"], "company": ["name"]}, + "matching_rules": { + "individual": { + "name": "NONE", + "date_of_birth": "NONE", + "address": "NONE", + "account": "NONE", + }, + "company": {"name": "NONE", "address": "NONE", "account": "NONE"}, + }, "legal_name": "sample", "lei": "sample", "compliance_email": "user@example.com", @@ -264,6 +943,10 @@ def test_travel_rule_update_settings() -> None: "allow_screenshot_proof": True, "auto_wallet_verification": True, "vasp_attribution_enabled": True, + "counterparty_screening_enabled": True, + "sunrise_policy": "", + "sunrise_jurisdictions": ["sample"], + "non_responsive_policy": "", "travel_address": "sample", "disabled_networks": ["GTR"], "networks": { @@ -292,6 +975,11 @@ def test_travel_rule_update_settings() -> None: "connected": True, }, }, + "didit_network": { + "listed": True, + "missing": ["travel_rule_off"], + "compliance_email_confirmed": True, + }, }, strict_type=TravelRuleSettingsDetail, ), @@ -318,6 +1006,298 @@ def test_travel_rule_update_settings() -> None: ) +def test_travel_rule_update_vasp_decision() -> None: + """``PUT /v3/travel-rule/vasps/{vasp_id}/decision/``""" + check_operation( + Expect( + method="PUT", + path="/v3/travel-rule/vasps/00000000-0000-4000-8000-000000000001/decision/", + api="verification", + auth="api_key", + json_body={"decision": "BLOCKED", "reason": "Unlicensed in its home jurisdiction"}, + status=200, + response_json={ + "decision": { + "decision": "BLOCKED", + "reason": "Unlicensed in its home jurisdiction", + "decided_by": "", + "updated_at": "2026-10-14T10:05:00Z", + "inherited_from": None, + } + }, + ), + lambda c: c.travel_rule.update_vasp_decision( + "00000000-0000-4000-8000-000000000001", + decision="BLOCKED", + reason="Unlicensed in its home jurisdiction", + ), + lambda c: c.travel_rule.update_vasp_decision( + "00000000-0000-4000-8000-000000000001", + decision="BLOCKED", + reason="Unlicensed in its home jurisdiction", + ), + lambda r: isinstance(r, TravelRuleCounterpartyDecisionResponse), + lambda r: isinstance(r, TravelRuleCounterpartyDecisionResponse), + ) + + +def test_travel_rule_counterparty_contacts_create() -> None: + """``POST /v3/travel-rule/counterparty-contacts/``""" + check_operation( + Expect( + method="POST", + path="/v3/travel-rule/counterparty-contacts/", + api="verification", + auth="api_key", + json_body={ + "counterparty": "0d6f3c2a-6a1e-4c55-9a39-1f2b0c7d8e91", + "email": "compliance@counterparty.example", + "name": "Compliance desk", + }, + status=201, + response_json={ + "uuid": "6c1f9b8e-2d4a-4f7e-8b3c-5a9d0e1f2a3b", + "counterparty": "0d6f3c2a-6a1e-4c55-9a39-1f2b0c7d8e91", + "counterparty_name": "Example Counterparty VASP", + "email": "compliance@counterparty.example", + "name": "Compliance desk", + "source": "application", + "scope": "application", + "created_by": "", + "verified_at": None, + "is_invalid": False, + "invalid_reason": "", + "invalid_detail": "", + "invalid_at": None, + "created_at": "2026-10-19T09:00:00Z", + }, + ), + lambda c: c.travel_rule.counterparty_contacts.create( + counterparty="0d6f3c2a-6a1e-4c55-9a39-1f2b0c7d8e91", + email="compliance@counterparty.example", + name="Compliance desk", + ), + lambda c: c.travel_rule.counterparty_contacts.create( + counterparty="0d6f3c2a-6a1e-4c55-9a39-1f2b0c7d8e91", + email="compliance@counterparty.example", + name="Compliance desk", + ), + lambda r: isinstance(r, TravelRuleCounterpartyContact), + lambda r: isinstance(r, TravelRuleCounterpartyContact), + ) + + +def test_travel_rule_counterparty_contacts_delete() -> None: + """``DELETE /v3/travel-rule/counterparty-contacts/{contact_uuid}/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/travel-rule/counterparty-contacts/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=204, + ), + lambda c: c.travel_rule.counterparty_contacts.delete( + "00000000-0000-4000-8000-000000000001" + ), + lambda c: c.travel_rule.counterparty_contacts.delete( + "00000000-0000-4000-8000-000000000001" + ), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_travel_rule_counterparty_contacts_list() -> None: + """``GET /v3/travel-rule/counterparty-contacts/``""" + check_operation( + Expect( + method="GET", + path="/v3/travel-rule/counterparty-contacts/", + api="verification", + auth="api_key", + query={ + "counterparty": ["00000000-0000-4000-8000-000000000001"], + "limit": ["1"], + "offset": ["1"], + }, + status=200, + response_json={ + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "uuid": "6c1f9b8e-2d4a-4f7e-8b3c-5a9d0e1f2a3b", + "counterparty": "0d6f3c2a-6a1e-4c55-9a39-1f2b0c7d8e91", + "counterparty_name": "Example Counterparty VASP", + "email": "compliance@counterparty.example", + "name": "Compliance desk", + "source": "application", + "scope": "application", + "created_by": "", + "verified_at": None, + "is_invalid": False, + "invalid_reason": "", + "invalid_detail": "", + "invalid_at": None, + "created_at": "2026-10-19T09:00:00Z", + } + ], + }, + ), + lambda c: c.travel_rule.counterparty_contacts.list( + counterparty="00000000-0000-4000-8000-000000000001", limit=1, offset=1 + ), + lambda c: c.travel_rule.counterparty_contacts.list( + counterparty="00000000-0000-4000-8000-000000000001", limit=1, offset=1 + ), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, TravelRuleCounterpartyContact) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, TravelRuleCounterpartyContact) for x1 in r.results) + ), + ) + + +def test_travel_rule_transfers_list() -> None: + """``GET /v3/travel-rule/transfers/``""" + check_operation( + Expect( + method="GET", + path="/v3/travel-rule/transfers/", + api="verification", + auth="api_key", + query={ + "direction": ["sample"], + "status": ["sample"], + "rail": ["sample"], + "kind": ["sample"], + "currency": ["sample"], + "start_date": ["2026-01-01"], + "end_date": ["2026-01-01"], + "counterparty": ["00000000-0000-4000-8000-000000000001"], + "limit": ["1"], + "offset": ["1"], + }, + status=200, + response_json={ + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "id": "5b0f6c2e-2a4d-4f0e-9a41-0c3f9a7e1d22", + "transaction_id": "a3c1e8f4-7b62-4d1a-8e0b-2f9d6c4b7a10", + "txn_id": "wd-2026-10-19-0042", + "txn_date": "2026-10-19T08:41:12Z", + "kind": "withdrawal", + "direction": "OUTBOUND", + "status": "AWAITING_COUNTERPARTY", + "rail": "TRP", + "asset": "ETH", + "amount": "1.25", + "originator_vasp": "Origin CASP SL", + "beneficiary_vasp": "Example Counterparty VASP", + "originator_wallet": "0x77ccff0cc624a53cd8b8d8703ced826710211ec7", + "beneficiary_wallet": "0xede2d7e4f5b097e6391ab6bbcf2f47a53f4778cb", + "originator_memo": "", + "beneficiary_memo": "", + "originator_wallet_type": "vasp", + "beneficiary_wallet_type": "vasp", + "matched_transaction_id": None, + "deadline_at": "2026-10-21T08:41:13Z", + "created_at": "2026-10-19T08:41:13Z", + } + ], + }, + ), + lambda c: c.travel_rule.transfers.list( + direction="sample", + status="sample", + rail="sample", + kind="sample", + currency="sample", + start_date="2026-01-01", + end_date="2026-01-01", + counterparty="00000000-0000-4000-8000-000000000001", + limit=1, + offset=1, + ), + lambda c: c.travel_rule.transfers.list( + direction="sample", + status="sample", + rail="sample", + kind="sample", + currency="sample", + start_date="2026-01-01", + end_date="2026-01-01", + counterparty="00000000-0000-4000-8000-000000000001", + limit=1, + offset=1, + ), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, TravelRuleTransferRow) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, TravelRuleTransferRow) for x1 in r.results) + ), + ) + + +def test_travel_rule_wallet_addresses_bulk_import() -> None: + """``POST /v3/travel-rule/wallet-addresses/import/``""" + check_operation( + Expect( + method="POST", + path="/v3/travel-rule/wallet-addresses/import/", + api="verification", + auth="api_key", + json_body={}, + status=200, + response_json={ + "total": 3, + "created": 2, + "existing": 1, + "duplicate": 0, + "failed": 0, + "rows": [ + { + "row": 1, + "status": "existing", + "entry_id": "11111111-2222-3333-4444-555555555555", + "errors": {}, + "duplicate_of_row": None, + }, + { + "row": 2, + "status": "created", + "entry_id": "22222222-3333-4444-5555-666666666666", + "errors": {}, + "duplicate_of_row": None, + }, + { + "row": 3, + "status": "created", + "entry_id": "33333333-4444-5555-6666-777777777777", + "errors": {}, + "duplicate_of_row": None, + }, + ], + }, + ), + lambda c: c.travel_rule.wallet_addresses.bulk_import(), + lambda c: c.travel_rule.wallet_addresses.bulk_import(), + lambda r: isinstance(r, WalletAddressImportReport), + lambda r: isinstance(r, WalletAddressImportReport), + ) + + def test_travel_rule_wallet_addresses_create() -> None: """``POST /v3/travel-rule/wallet-addresses/``""" check_operation( @@ -328,7 +1308,7 @@ def test_travel_rule_wallet_addresses_create() -> None: auth="api_key", json_body={ "address": "0xBeneficiaryWallet01", - "chain": "ethereum", + "chain": "ETH", "holder_vendor_data": "user-042", "holder_name": "Ana Diaz", "self_declared": True, @@ -343,21 +1323,23 @@ def test_travel_rule_wallet_addresses_create() -> None: "entity_type": "sample", "is_ownership_verified": True, "created_at": "2026-01-01T00:00:00Z", - "self_declared": True, "travel_address": "sample", + "address_hash": "sample", + "memo": "sample", + "receiving_travel_address": "sample", }, strict_type=WalletAddressEntry, ), lambda c: c.travel_rule.wallet_addresses.create( address="0xBeneficiaryWallet01", - chain="ethereum", + chain="ETH", holder_vendor_data="user-042", holder_name="Ana Diaz", self_declared=True, ), lambda c: c.travel_rule.wallet_addresses.create( address="0xBeneficiaryWallet01", - chain="ethereum", + chain="ETH", holder_vendor_data="user-042", holder_name="Ana Diaz", self_declared=True, @@ -408,8 +1390,10 @@ def test_travel_rule_wallet_addresses_list() -> None: "entity_type": "sample", "is_ownership_verified": True, "created_at": "2026-01-01T00:00:00Z", - "self_declared": True, "travel_address": "sample", + "address_hash": "sample", + "memo": "sample", + "receiving_travel_address": "sample", } ], }, @@ -433,7 +1417,10 @@ def test_travel_rule_wallet_addresses_update() -> None: path="/v3/travel-rule/wallet-addresses/00000000-0000-4000-8000-000000000001/", api="verification", auth="api_key", - json_body={"holder_name": "Ana Updated", "travel_address": "ta..."}, + json_body={ + "holder_name": "Ana Updated", + "travel_address": "ta2ih1MicegQ3JqiKz9xT89KsSuwaDJYw8DgzmeSEp1h9tduJcNDZ5SgKLEHeyGMcS1s2HgbM6undFrMQknhM5aTXw3G9KBzi16hx", + }, status=200, response_json={ "holder_name": "sample", @@ -446,13 +1433,579 @@ def test_travel_rule_wallet_addresses_update() -> None: lambda c: c.travel_rule.wallet_addresses.update( "00000000-0000-4000-8000-000000000001", holder_name="Ana Updated", - travel_address="ta...", + travel_address="ta2ih1MicegQ3JqiKz9xT89KsSuwaDJYw8DgzmeSEp1h9tduJcNDZ5SgKLEHeyGMcS1s2HgbM6undFrMQknhM5aTXw3G9KBzi16hx", ), lambda c: c.travel_rule.wallet_addresses.update( "00000000-0000-4000-8000-000000000001", holder_name="Ana Updated", - travel_address="ta...", + travel_address="ta2ih1MicegQ3JqiKz9xT89KsSuwaDJYw8DgzmeSEp1h9tduJcNDZ5SgKLEHeyGMcS1s2HgbM6undFrMQknhM5aTXw3G9KBzi16hx", ), lambda r: isinstance(r, WalletAddressEntryUpdate), lambda r: isinstance(r, WalletAddressEntryUpdate), ) + + +def test_travel_rule_workspace_claim() -> None: + """``POST /v3/travel-rule/workspace/claim/``""" + check_operation( + Expect( + method="POST", + path="/v3/travel-rule/workspace/claim/", + api="verification", + auth="api_key", + json_body={"vasp_id": "0b0c9d3e-5a53-4a3e-9d6e-1f2f6f6a1c11"}, + status=200, + response_json={ + "joined_free": True, + "work_email": "user@example.com", + "joined_at": "2026-01-01T00:00:00Z", + "claim": { + "status": "unclaimed", + "proved_by_domain": True, + "claimed_at": "2026-01-01T00:00:00Z", + "reviewed_at": "2026-01-01T00:00:00Z", + "review_note": "sample", + "vasp_id": "00000000-0000-4000-8000-000000000004", + "vasp_name": "sample", + }, + "prefill": {}, + "domain": { + "domain": "sample", + "status": "unverified", + "verified_at": "2026-01-01T00:00:00Z", + "txt": {"host": "didit-tr-verify.example.com", "value": "sample"}, + }, + "billing_enabled": True, + "billing_enabled_at": "2026-01-01T00:00:00Z", + "didit_network": {}, + "settings": { + "is_enabled": True, + "jurisdiction": "sample", + "name_matching_strictness": "NONE", + "confirmation_timeout_hours": 1, + "timeout_outcome": "HOLD", + "threshold_amount": "sample", + "ownership_validation_mode": "FULL", + "simplified_validation_threshold": "sample", + "data_sharing_fields": {"individual": ["name"], "company": ["name"]}, + "matching_rules": { + "individual": { + "name": "NONE", + "date_of_birth": "NONE", + "address": "NONE", + "account": "NONE", + }, + "company": {"name": "NONE", "address": "NONE", "account": "NONE"}, + }, + "legal_name": "sample", + "lei": "sample", + "compliance_email": "user@example.com", + "is_discoverable": True, + "inbound_auto_accept": True, + "allow_self_declaration": True, + "allow_screenshot_proof": True, + "auto_wallet_verification": True, + "vasp_attribution_enabled": True, + "counterparty_screening_enabled": True, + "sunrise_policy": "", + "sunrise_jurisdictions": ["sample"], + "non_responsive_policy": "", + "travel_address": "sample", + "disabled_networks": ["GTR"], + "networks": { + "gtr": { + "rail": "GTR", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "trust": { + "rail": "TRUST", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "verifyvasp": { + "rail": "VERIFYVASP", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "sygna": { + "rail": "SYGNA", + "mode": "own", + "platform_available": True, + "connected": True, + }, + }, + "didit_network": { + "listed": True, + "missing": ["travel_rule_off"], + "compliance_email_confirmed": True, + }, + }, + }, + strict_type=TravelRuleWorkspace, + ), + lambda c: c.travel_rule.workspace.claim(vasp_id="0b0c9d3e-5a53-4a3e-9d6e-1f2f6f6a1c11"), + lambda c: c.travel_rule.workspace.claim(vasp_id="0b0c9d3e-5a53-4a3e-9d6e-1f2f6f6a1c11"), + lambda r: isinstance(r, TravelRuleWorkspace), + lambda r: isinstance(r, TravelRuleWorkspace), + ) + + +def test_travel_rule_workspace_confirm_domain() -> None: + """``POST /v3/travel-rule/workspace/domain/confirm/``""" + check_operation( + Expect( + method="POST", + path="/v3/travel-rule/workspace/domain/confirm/", + api="verification", + auth="api_key", + json_body={"code": "123456"}, + status=200, + response_json={ + "joined_free": True, + "work_email": "user@example.com", + "joined_at": "2026-01-01T00:00:00Z", + "claim": { + "status": "unclaimed", + "proved_by_domain": True, + "claimed_at": "2026-01-01T00:00:00Z", + "reviewed_at": "2026-01-01T00:00:00Z", + "review_note": "sample", + "vasp_id": "00000000-0000-4000-8000-000000000004", + "vasp_name": "sample", + }, + "prefill": {}, + "domain": { + "domain": "sample", + "status": "unverified", + "verified_at": "2026-01-01T00:00:00Z", + "txt": {"host": "didit-tr-verify.example.com", "value": "sample"}, + }, + "billing_enabled": True, + "billing_enabled_at": "2026-01-01T00:00:00Z", + "didit_network": {}, + "settings": { + "is_enabled": True, + "jurisdiction": "sample", + "name_matching_strictness": "NONE", + "confirmation_timeout_hours": 1, + "timeout_outcome": "HOLD", + "threshold_amount": "sample", + "ownership_validation_mode": "FULL", + "simplified_validation_threshold": "sample", + "data_sharing_fields": {"individual": ["name"], "company": ["name"]}, + "matching_rules": { + "individual": { + "name": "NONE", + "date_of_birth": "NONE", + "address": "NONE", + "account": "NONE", + }, + "company": {"name": "NONE", "address": "NONE", "account": "NONE"}, + }, + "legal_name": "sample", + "lei": "sample", + "compliance_email": "user@example.com", + "is_discoverable": True, + "inbound_auto_accept": True, + "allow_self_declaration": True, + "allow_screenshot_proof": True, + "auto_wallet_verification": True, + "vasp_attribution_enabled": True, + "counterparty_screening_enabled": True, + "sunrise_policy": "", + "sunrise_jurisdictions": ["sample"], + "non_responsive_policy": "", + "travel_address": "sample", + "disabled_networks": ["GTR"], + "networks": { + "gtr": { + "rail": "GTR", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "trust": { + "rail": "TRUST", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "verifyvasp": { + "rail": "VERIFYVASP", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "sygna": { + "rail": "SYGNA", + "mode": "own", + "platform_available": True, + "connected": True, + }, + }, + "didit_network": { + "listed": True, + "missing": ["travel_rule_off"], + "compliance_email_confirmed": True, + }, + }, + }, + strict_type=TravelRuleWorkspace, + ), + lambda c: c.travel_rule.workspace.confirm_domain(code="123456"), + lambda c: c.travel_rule.workspace.confirm_domain(code="123456"), + lambda r: isinstance(r, TravelRuleWorkspace), + lambda r: isinstance(r, TravelRuleWorkspace), + ) + + +def test_travel_rule_workspace_enable_billing() -> None: + """``POST /v3/travel-rule/workspace/billing/``""" + check_operation( + Expect( + method="POST", + path="/v3/travel-rule/workspace/billing/", + api="verification", + auth="api_key", + json_body={"enabled": True}, + status=200, + response_json={ + "joined_free": True, + "work_email": "user@example.com", + "joined_at": "2026-01-01T00:00:00Z", + "claim": { + "status": "unclaimed", + "proved_by_domain": True, + "claimed_at": "2026-01-01T00:00:00Z", + "reviewed_at": "2026-01-01T00:00:00Z", + "review_note": "sample", + "vasp_id": "00000000-0000-4000-8000-000000000004", + "vasp_name": "sample", + }, + "prefill": {}, + "domain": { + "domain": "sample", + "status": "unverified", + "verified_at": "2026-01-01T00:00:00Z", + "txt": {"host": "didit-tr-verify.example.com", "value": "sample"}, + }, + "billing_enabled": True, + "billing_enabled_at": "2026-01-01T00:00:00Z", + "didit_network": {}, + "settings": { + "is_enabled": True, + "jurisdiction": "sample", + "name_matching_strictness": "NONE", + "confirmation_timeout_hours": 1, + "timeout_outcome": "HOLD", + "threshold_amount": "sample", + "ownership_validation_mode": "FULL", + "simplified_validation_threshold": "sample", + "data_sharing_fields": {"individual": ["name"], "company": ["name"]}, + "matching_rules": { + "individual": { + "name": "NONE", + "date_of_birth": "NONE", + "address": "NONE", + "account": "NONE", + }, + "company": {"name": "NONE", "address": "NONE", "account": "NONE"}, + }, + "legal_name": "sample", + "lei": "sample", + "compliance_email": "user@example.com", + "is_discoverable": True, + "inbound_auto_accept": True, + "allow_self_declaration": True, + "allow_screenshot_proof": True, + "auto_wallet_verification": True, + "vasp_attribution_enabled": True, + "counterparty_screening_enabled": True, + "sunrise_policy": "", + "sunrise_jurisdictions": ["sample"], + "non_responsive_policy": "", + "travel_address": "sample", + "disabled_networks": ["GTR"], + "networks": { + "gtr": { + "rail": "GTR", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "trust": { + "rail": "TRUST", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "verifyvasp": { + "rail": "VERIFYVASP", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "sygna": { + "rail": "SYGNA", + "mode": "own", + "platform_available": True, + "connected": True, + }, + }, + "didit_network": { + "listed": True, + "missing": ["travel_rule_off"], + "compliance_email_confirmed": True, + }, + }, + }, + strict_type=TravelRuleWorkspace, + ), + lambda c: c.travel_rule.workspace.enable_billing(enabled=True), + lambda c: c.travel_rule.workspace.enable_billing(enabled=True), + lambda r: isinstance(r, TravelRuleWorkspace), + lambda r: isinstance(r, TravelRuleWorkspace), + ) + + +def test_travel_rule_workspace_get() -> None: + """``GET /v3/travel-rule/workspace/``""" + check_operation( + Expect( + method="GET", + path="/v3/travel-rule/workspace/", + api="verification", + auth="api_key", + status=200, + response_json={ + "joined_free": True, + "work_email": "user@example.com", + "joined_at": "2026-01-01T00:00:00Z", + "claim": { + "status": "unclaimed", + "proved_by_domain": True, + "claimed_at": "2026-01-01T00:00:00Z", + "reviewed_at": "2026-01-01T00:00:00Z", + "review_note": "sample", + "vasp_id": "00000000-0000-4000-8000-000000000004", + "vasp_name": "sample", + }, + "prefill": {}, + "domain": { + "domain": "sample", + "status": "unverified", + "verified_at": "2026-01-01T00:00:00Z", + "txt": {"host": "didit-tr-verify.example.com", "value": "sample"}, + }, + "billing_enabled": True, + "billing_enabled_at": "2026-01-01T00:00:00Z", + "didit_network": {}, + "settings": { + "is_enabled": True, + "jurisdiction": "sample", + "name_matching_strictness": "NONE", + "confirmation_timeout_hours": 1, + "timeout_outcome": "HOLD", + "threshold_amount": "sample", + "ownership_validation_mode": "FULL", + "simplified_validation_threshold": "sample", + "data_sharing_fields": {"individual": ["name"], "company": ["name"]}, + "matching_rules": { + "individual": { + "name": "NONE", + "date_of_birth": "NONE", + "address": "NONE", + "account": "NONE", + }, + "company": {"name": "NONE", "address": "NONE", "account": "NONE"}, + }, + "legal_name": "sample", + "lei": "sample", + "compliance_email": "user@example.com", + "is_discoverable": True, + "inbound_auto_accept": True, + "allow_self_declaration": True, + "allow_screenshot_proof": True, + "auto_wallet_verification": True, + "vasp_attribution_enabled": True, + "counterparty_screening_enabled": True, + "sunrise_policy": "", + "sunrise_jurisdictions": ["sample"], + "non_responsive_policy": "", + "travel_address": "sample", + "disabled_networks": ["GTR"], + "networks": { + "gtr": { + "rail": "GTR", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "trust": { + "rail": "TRUST", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "verifyvasp": { + "rail": "VERIFYVASP", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "sygna": { + "rail": "SYGNA", + "mode": "own", + "platform_available": True, + "connected": True, + }, + }, + "didit_network": { + "listed": True, + "missing": ["travel_rule_off"], + "compliance_email_confirmed": True, + }, + }, + }, + strict_type=TravelRuleWorkspace, + ), + lambda c: c.travel_rule.workspace.get(), + lambda c: c.travel_rule.workspace.get(), + lambda r: isinstance(r, TravelRuleWorkspace), + lambda r: isinstance(r, TravelRuleWorkspace), + ) + + +def test_travel_rule_workspace_send_domain_code() -> None: + """``POST /v3/travel-rule/workspace/domain/code/``""" + check_operation( + Expect( + method="POST", + path="/v3/travel-rule/workspace/domain/code/", + api="verification", + auth="api_key", + status=200, + response_json={ + "domain": "sample", + "status": "unverified", + "txt": {"host": "didit-tr-verify.example.com", "value": "sample"}, + }, + strict_type=TravelRuleWorkspaceDomainCodeResponse, + ), + lambda c: c.travel_rule.workspace.send_domain_code(), + lambda c: c.travel_rule.workspace.send_domain_code(), + lambda r: isinstance(r, TravelRuleWorkspaceDomainCodeResponse), + lambda r: isinstance(r, TravelRuleWorkspaceDomainCodeResponse), + ) + + +def test_travel_rule_workspace_verify_domain_dns() -> None: + """``POST /v3/travel-rule/workspace/domain/dns/``""" + check_operation( + Expect( + method="POST", + path="/v3/travel-rule/workspace/domain/dns/", + api="verification", + auth="api_key", + status=200, + response_json={ + "joined_free": True, + "work_email": "user@example.com", + "joined_at": "2026-01-01T00:00:00Z", + "claim": { + "status": "unclaimed", + "proved_by_domain": True, + "claimed_at": "2026-01-01T00:00:00Z", + "reviewed_at": "2026-01-01T00:00:00Z", + "review_note": "sample", + "vasp_id": "00000000-0000-4000-8000-000000000004", + "vasp_name": "sample", + }, + "prefill": {}, + "domain": { + "domain": "sample", + "status": "unverified", + "verified_at": "2026-01-01T00:00:00Z", + "txt": {"host": "didit-tr-verify.example.com", "value": "sample"}, + }, + "billing_enabled": True, + "billing_enabled_at": "2026-01-01T00:00:00Z", + "didit_network": {}, + "settings": { + "is_enabled": True, + "jurisdiction": "sample", + "name_matching_strictness": "NONE", + "confirmation_timeout_hours": 1, + "timeout_outcome": "HOLD", + "threshold_amount": "sample", + "ownership_validation_mode": "FULL", + "simplified_validation_threshold": "sample", + "data_sharing_fields": {"individual": ["name"], "company": ["name"]}, + "matching_rules": { + "individual": { + "name": "NONE", + "date_of_birth": "NONE", + "address": "NONE", + "account": "NONE", + }, + "company": {"name": "NONE", "address": "NONE", "account": "NONE"}, + }, + "legal_name": "sample", + "lei": "sample", + "compliance_email": "user@example.com", + "is_discoverable": True, + "inbound_auto_accept": True, + "allow_self_declaration": True, + "allow_screenshot_proof": True, + "auto_wallet_verification": True, + "vasp_attribution_enabled": True, + "counterparty_screening_enabled": True, + "sunrise_policy": "", + "sunrise_jurisdictions": ["sample"], + "non_responsive_policy": "", + "travel_address": "sample", + "disabled_networks": ["GTR"], + "networks": { + "gtr": { + "rail": "GTR", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "trust": { + "rail": "TRUST", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "verifyvasp": { + "rail": "VERIFYVASP", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "sygna": { + "rail": "SYGNA", + "mode": "own", + "platform_available": True, + "connected": True, + }, + }, + "didit_network": { + "listed": True, + "missing": ["travel_rule_off"], + "compliance_email_confirmed": True, + }, + }, + }, + strict_type=TravelRuleWorkspace, + ), + lambda c: c.travel_rule.workspace.verify_domain_dns(), + lambda c: c.travel_rule.workspace.verify_domain_dns(), + lambda r: isinstance(r, TravelRuleWorkspace), + lambda r: isinstance(r, TravelRuleWorkspace), + ) diff --git a/tests/generated/test_users.py b/tests/generated/test_users.py index 0d4595e..9831f88 100644 --- a/tests/generated/test_users.py +++ b/tests/generated/test_users.py @@ -4,23 +4,20 @@ from __future__ import annotations from didit import AsyncPage, SyncPage +from didit.types.components.aml_monitoring_job import AMLMonitoringJob +from didit.types.components.aml_monitoring_toggle_result import ( + AMLMonitoringToggleResultVariant1, + AMLMonitoringToggleResultVariant2, +) +from didit.types.components.entity_network_membership_list import EntityNetworkMembershipList +from didit.types.components.ongoing_monitoring_job import OngoingMonitoringJob +from didit.types.components.ongoing_monitoring_toggle_result import ( + OngoingMonitoringToggleResultVariant1, +) from didit.types.components.user_detail_item import UserDetailItem from didit.types.components.user_list_item import UserListItem from didit.types.users.batch_delete_response import UsersBatchDeleteResponse -from didit.types.users.list_networks_response import UsersListNetworksResponse -from didit.types.users.retrieve_ongoing_monitoring_job_response import ( - UsersRetrieveOngoingMonitoringJobResponse, -) -from didit.types.users.set_aml_monitoring_response import ( - UsersSetAmlMonitoringResponse202, - UsersSetAmlMonitoringResponseVariant1, - UsersSetAmlMonitoringResponseVariant2, -) -from didit.types.users.set_ongoing_monitoring_response import ( - UsersSetOngoingMonitoringResponse202, - UsersSetOngoingMonitoringResponseVariant1, - UsersSetOngoingMonitoringResponseVariant2, -) +from didit.types.users.create_response import UsersCreateResponse from didit.types.users.upload_face_response import UsersUploadFaceResponse from tests.harness import BINARY_BODY, Expect, check_operation @@ -80,12 +77,13 @@ def test_users_create() -> None: "comments": [], "created_at": "2025-06-15T10:30:00Z", "updated_at": "2025-06-15T10:30:00Z", + "verification_link": None, }, ), lambda c: c.users.create(vendor_data="user-abc-123", full_name="John Doe"), lambda c: c.users.create(vendor_data="user-abc-123", full_name="John Doe"), - lambda r: isinstance(r, UserDetailItem), - lambda r: isinstance(r, UserDetailItem), + lambda r: isinstance(r, UsersCreateResponse), + lambda r: isinstance(r, UsersCreateResponse), ) @@ -212,13 +210,13 @@ def test_users_list_networks() -> None: "results": [ { "network": { - "uuid": "00000000-0000-4000-8000-000000000007", + "uuid": "00000000-0000-4000-8000-000000000008", "network_id": "sample", "name": "sample", "status": "active", - "risk": {}, - "pattern_types": ["exact_same_device"], - "signal_types": ["ip_address"], + "risk": None, + "pattern_types": [], + "signal_types": [], "member_counts": {}, "member_status": {}, "signal_coverage": {}, @@ -233,12 +231,12 @@ def test_users_list_networks() -> None: } ] }, - strict_type=UsersListNetworksResponse, + strict_type=EntityNetworkMembershipList, ), lambda c: c.users.list_networks("00000000-0000-4000-8000-000000000001"), lambda c: c.users.list_networks("00000000-0000-4000-8000-000000000001"), - lambda r: isinstance(r, UsersListNetworksResponse), - lambda r: isinstance(r, UsersListNetworksResponse), + lambda r: isinstance(r, EntityNetworkMembershipList), + lambda r: isinstance(r, EntityNetworkMembershipList), ) @@ -338,7 +336,7 @@ def test_users_retrieve_ongoing_monitoring_job() -> None: auth="api_key", status=200, response_json={ - "job_id": "00000000-0000-4000-8000-000000000002", + "job_id": "00000000-0000-4000-8000-000000000003", "status": "PENDING", "is_enabled": True, "total": 1, @@ -367,12 +365,12 @@ def test_users_retrieve_ongoing_monitoring_job() -> None: "balance_sufficient": True, }, }, - strict_type=UsersRetrieveOngoingMonitoringJobResponse, + strict_type=OngoingMonitoringJob, ), lambda c: c.users.retrieve_ongoing_monitoring_job("00000000-0000-4000-8000-000000000001"), lambda c: c.users.retrieve_ongoing_monitoring_job("00000000-0000-4000-8000-000000000001"), - lambda r: isinstance(r, UsersRetrieveOngoingMonitoringJobResponse), - lambda r: isinstance(r, UsersRetrieveOngoingMonitoringJobResponse), + lambda r: isinstance(r, OngoingMonitoringJob), + lambda r: isinstance(r, OngoingMonitoringJob), ) @@ -410,14 +408,18 @@ def test_users_set_aml_monitoring() -> None: is_enabled=True, estimate_only=True, vendor_data_list=["user-123", "user-456"] ), lambda r: ( - (isinstance(r, UsersSetAmlMonitoringResponseVariant1)) - or (isinstance(r, UsersSetAmlMonitoringResponseVariant2)) - or (isinstance(r, UsersSetAmlMonitoringResponse202)) + ( + (isinstance(r, AMLMonitoringToggleResultVariant1)) + or (isinstance(r, AMLMonitoringToggleResultVariant2)) + ) + or (isinstance(r, AMLMonitoringJob)) ), lambda r: ( - (isinstance(r, UsersSetAmlMonitoringResponseVariant1)) - or (isinstance(r, UsersSetAmlMonitoringResponseVariant2)) - or (isinstance(r, UsersSetAmlMonitoringResponse202)) + ( + (isinstance(r, AMLMonitoringToggleResultVariant1)) + or (isinstance(r, AMLMonitoringToggleResultVariant2)) + ) + or (isinstance(r, AMLMonitoringJob)) ), ) @@ -447,14 +449,18 @@ def test_users_set_ongoing_monitoring() -> None: lambda c: c.users.set_ongoing_monitoring(is_enabled=True), lambda c: c.users.set_ongoing_monitoring(is_enabled=True), lambda r: ( - (isinstance(r, UsersSetOngoingMonitoringResponseVariant1)) - or (isinstance(r, UsersSetOngoingMonitoringResponseVariant2)) - or (isinstance(r, UsersSetOngoingMonitoringResponse202)) + ( + (isinstance(r, OngoingMonitoringToggleResultVariant1)) + or (isinstance(r, OngoingMonitoringJob)) + ) + or (isinstance(r, OngoingMonitoringJob)) ), lambda r: ( - (isinstance(r, UsersSetOngoingMonitoringResponseVariant1)) - or (isinstance(r, UsersSetOngoingMonitoringResponseVariant2)) - or (isinstance(r, UsersSetOngoingMonitoringResponse202)) + ( + (isinstance(r, OngoingMonitoringToggleResultVariant1)) + or (isinstance(r, OngoingMonitoringJob)) + ) + or (isinstance(r, OngoingMonitoringJob)) ), ) @@ -593,7 +599,7 @@ def test_users_update_status() -> None: { "uuid": "c1111111-2222-4333-8444-555555555555", "comment_type": "STATUS_CHANGED", - "comment": None, + "comment": "Chargeback fraud pattern confirmed", "actor_email": None, "actor_name": None, "previous_status": "ACTIVE", diff --git a/tests/generated/test_webhooks.py b/tests/generated/test_webhooks.py index e2de341..525e939 100644 --- a/tests/generated/test_webhooks.py +++ b/tests/generated/test_webhooks.py @@ -3,13 +3,142 @@ from __future__ import annotations +from didit import AsyncPage, SyncPage +from didit.types.components.webhook_config import WebhookConfig +from didit.types.components.webhook_destination_delivery import WebhookDestinationDelivery +from didit.types.components.webhook_destination_delivery_detail import ( + WebhookDestinationDeliveryDetail, +) from didit.types.webhooks.destinations.create_response import WebhooksDestinationsCreateResponse from didit.types.webhooks.destinations.list_response import WebhooksDestinationsListResponseItem +from didit.types.webhooks.destinations.retrieve_analytics_response import ( + WebhooksDestinationsRetrieveAnalyticsResponse, +) from didit.types.webhooks.destinations.retrieve_response import WebhooksDestinationsRetrieveResponse from didit.types.webhooks.destinations.update_response import WebhooksDestinationsUpdateResponse from tests.harness import Expect, check_operation +def test_webhooks_retrieve_config() -> None: + """``GET /v3/webhook/``""" + check_operation( + Expect( + method="GET", + path="/v3/webhook/", + api="verification", + auth="api_key", + status=200, + response_json={ + "webhook_url": "https://yourapp.com/webhooks/didit", + "webhook_version": "v3", + "secret_shared_key": "sample-secret", + "capture_method": "MOBILE", + "data_retention_months": 24, + "face_retention_policy": "delete_with_session", + "face_retention_days": None, + "available_events": [ + {"value": "status.updated", "label": "Session status updated"}, + {"value": "data.updated", "label": "Session data updated"}, + {"value": "user.status.updated", "label": "User status updated"}, + {"value": "user.data.updated", "label": "User data updated"}, + {"value": "business.status.updated", "label": "Business status updated"}, + {"value": "business.data.updated", "label": "Business data updated"}, + {"value": "activity.created", "label": "Activity created"}, + {"value": "transaction.created", "label": "Transaction created"}, + {"value": "transaction.status.updated", "label": "Transaction status updated"}, + {"value": "travel_rule.status.updated", "label": "Travel Rule Status Updated"}, + { + "value": "workflow.compliance_changed", + "label": "Workflow Compliance Changed", + }, + ], + "allowed_return_hosts": ["shop.example.com"], + "endpoints": [ + { + "uuid": "11111111-2222-3333-4444-555555555555", + "label": "Production session webhooks", + "url": "https://yourapp.com/webhooks/didit", + "enabled": True, + "webhook_version": "v3", + "secret_shared_key": "sample-secret", + "subscribed_events": ["status.updated", "data.updated"], + "created_at": "2026-01-04T08:00:00.000000Z", + "updated_at": "2026-04-22T14:09:11.523671Z", + } + ], + }, + ), + lambda c: c.webhooks.retrieve_config(), + lambda c: c.webhooks.retrieve_config(), + lambda r: isinstance(r, WebhookConfig), + lambda r: isinstance(r, WebhookConfig), + ) + + +def test_webhooks_update_config() -> None: + """``PATCH /v3/webhook/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/webhook/", + api="verification", + auth="api_key", + json_body={ + "face_retention_policy": "retain_until_user_deleted", + "face_retention_days": 365, + }, + status=200, + response_json={ + "webhook_url": "https://yourapp.com/webhooks/didit", + "webhook_version": "v3", + "secret_shared_key": "sample-secret", + "capture_method": "MOBILE", + "data_retention_months": 24, + "face_retention_policy": "retain_until_user_deleted", + "face_retention_days": 365, + "available_events": [ + {"value": "status.updated", "label": "Session status updated"}, + {"value": "data.updated", "label": "Session data updated"}, + {"value": "user.status.updated", "label": "User status updated"}, + {"value": "user.data.updated", "label": "User data updated"}, + {"value": "business.status.updated", "label": "Business status updated"}, + {"value": "business.data.updated", "label": "Business data updated"}, + {"value": "activity.created", "label": "Activity created"}, + {"value": "transaction.created", "label": "Transaction created"}, + {"value": "transaction.status.updated", "label": "Transaction status updated"}, + {"value": "travel_rule.status.updated", "label": "Travel Rule Status Updated"}, + { + "value": "workflow.compliance_changed", + "label": "Workflow Compliance Changed", + }, + ], + "allowed_return_hosts": ["shop.example.com"], + "endpoints": [ + { + "uuid": "11111111-2222-3333-4444-555555555555", + "label": "Production session webhooks", + "url": "https://yourapp.com/webhooks/didit", + "enabled": True, + "webhook_version": "v3", + "secret_shared_key": "sample-secret", + "subscribed_events": ["status.updated", "data.updated"], + "created_at": "2026-01-04T08:00:00.000000Z", + "updated_at": "2026-04-22T14:09:11.523671Z", + } + ], + }, + ), + lambda c: c.webhooks.update_config( + face_retention_policy="retain_until_user_deleted", face_retention_days=365 + ), + lambda c: c.webhooks.update_config( + face_retention_policy="retain_until_user_deleted", face_retention_days=365 + ), + lambda r: isinstance(r, WebhookConfig), + lambda r: isinstance(r, WebhookConfig), + ) + + def test_webhooks_destinations_create() -> None: """``POST /v3/webhook/destinations/``""" check_operation( @@ -32,9 +161,18 @@ def test_webhooks_destinations_create() -> None: "enabled": True, "webhook_version": "v3", "secret_shared_key": "sample-secret", + "secret_last4": "sample-secret", + "previous_secret_expires_at": None, "subscribed_events": ["status.updated", "data.updated"], + "created_by_email": None, + "created_by_name": None, "created_at": "2026-05-17T10:00:00Z", "updated_at": "2026-05-17T10:00:00Z", + "health_status": "healthy", + "failing_streak": 0, + "failing_since": None, + "last_success_at": None, + "environment": "live", "summary": { "total_deliveries": 0, "failed_deliveries": 0, @@ -43,6 +181,7 @@ def test_webhooks_destinations_create() -> None: "avg_response_time_ms": None, "max_response_time_ms": None, "last_delivery_at": None, + "metrics_window_days": 30, }, }, ), @@ -103,6 +242,47 @@ def test_webhooks_destinations_list() -> None: "average_response_time_ms": 187, "error_rate_percentage": 0, "last_delivery_at": "2026-05-17T09:21:18Z", + "metrics_window_days": 30, + "health_status": "healthy", + "failing_streak": 0, + "failing_since": None, + "last_success_at": "2026-05-17T09:21:18Z", + "environment": "live", + "daily_points": [ + {"date": "2026-04-18", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-04-19", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-04-20", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-04-21", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-04-22", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-04-23", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-04-24", "total_deliveries": 415, "failed_deliveries": 9}, + {"date": "2026-04-25", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-04-26", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-04-27", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-04-28", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-04-29", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-04-30", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-05-01", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-05-02", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-05-03", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-05-04", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-05-05", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-05-06", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-05-07", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-05-08", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-05-09", "total_deliveries": 415, "failed_deliveries": 5}, + {"date": "2026-05-10", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-05-11", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-05-12", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-05-13", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-05-14", "total_deliveries": 415, "failed_deliveries": 0}, + {"date": "2026-05-15", "total_deliveries": 416, "failed_deliveries": 0}, + {"date": "2026-05-16", "total_deliveries": 416, "failed_deliveries": 0}, + {"date": "2026-05-17", "total_deliveries": 416, "failed_deliveries": 0}, + ], + "secret_last4": "sample-secret", + "created_by_email": None, + "created_by_name": None, "created_at": "2026-01-04T08:00:00Z", "updated_at": "2026-04-22T14:09:11Z", }, @@ -119,6 +299,47 @@ def test_webhooks_destinations_list() -> None: "average_response_time_ms": None, "error_rate_percentage": 0, "last_delivery_at": None, + "metrics_window_days": 30, + "health_status": "healthy", + "failing_streak": 0, + "failing_since": None, + "last_success_at": None, + "environment": "live", + "daily_points": [ + {"date": "2026-04-18", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-04-19", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-04-20", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-04-21", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-04-22", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-04-23", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-04-24", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-04-25", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-04-26", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-04-27", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-04-28", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-04-29", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-04-30", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-01", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-02", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-03", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-04", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-05", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-06", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-07", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-08", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-09", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-10", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-11", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-12", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-13", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-14", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-15", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-16", "total_deliveries": 0, "failed_deliveries": 0}, + {"date": "2026-05-17", "total_deliveries": 0, "failed_deliveries": 0}, + ], + "secret_last4": "sample-secret", + "created_by_email": "reviewer@example.com", + "created_by_name": "Example Reviewer", "created_at": "2026-03-12T10:15:02Z", "updated_at": "2026-03-12T10:15:02Z", }, @@ -137,6 +358,117 @@ def test_webhooks_destinations_list() -> None: ) +def test_webhooks_destinations_list_deliveries() -> None: + """``GET /v3/webhook/destinations/{destination_uuid}/deliveries/``""" + check_operation( + Expect( + method="GET", + path="/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/deliveries/", + api="verification", + auth="api_key", + query={ + "limit": ["50"], + "offset": ["0"], + "date_from": ["2026-10-01"], + "date_to": ["2026-10-16"], + }, + status=200, + response_json={ + "count": 3, + "next": None, + "previous": None, + "results": [ + { + "uuid": "66666666-7777-8888-9999-000000000003", + "session_id": None, + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": None, + "response_time_ms": None, + "status": "APPROVED", + "webhook_type": "transaction.created", + "created_at": "2026-10-15T09:23:02.871190Z", + }, + { + "uuid": "66666666-7777-8888-9999-000000000002", + "session_id": "22222222-3333-4444-5555-666666666666", + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": 200, + "response_time_ms": 143, + "status": "In Progress", + "webhook_type": "status.updated", + "created_at": "2026-10-15T09:22:19.104233Z", + }, + { + "uuid": "66666666-7777-8888-9999-000000000001", + "session_id": "22222222-3333-4444-5555-666666666666", + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": 503, + "response_time_ms": 412, + "status": "In Progress", + "webhook_type": "status.updated", + "created_at": "2026-10-15T09:21:18.412345Z", + }, + ], + }, + ), + lambda c: c.webhooks.destinations.list_deliveries( + "11111111-2222-3333-4444-555555555555", + limit=50, + offset=0, + date_from="2026-10-01", + date_to="2026-10-16", + ), + lambda c: c.webhooks.destinations.list_deliveries( + "11111111-2222-3333-4444-555555555555", + limit=50, + offset=0, + date_from="2026-10-01", + date_to="2026-10-16", + ), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, WebhookDestinationDelivery) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, WebhookDestinationDelivery) for x1 in r.results) + ), + ) + + +def test_webhooks_destinations_resend_delivery() -> None: + """``POST /v3/webhook/destinations/{destination_uuid}/deliveries/{delivery_uuid}/resend/``""" + check_operation( + Expect( + method="POST", + path="/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/deliveries/66666666-7777-8888-9999-000000000001/resend/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "66666666-7777-8888-9999-000000000004", + "session_id": "22222222-3333-4444-5555-666666666666", + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": 200, + "response_time_ms": 151, + "status": "In Progress", + "webhook_type": "status.updated", + "created_at": "2026-10-16T14:05:42.330981Z", + "request_header": '{"Content-Type": "application/json", "X-Signature": "b8eb1e376517ee0cad8ba98ff7f8fd35fc72eca231000963a96a335f82328835", "X-Signature-V2": "9efd2ce9ed1303e1d05273ef9608a59363ed9789a05253db4748b3e8e50cb109", "X-Signature-Simple": "bf3a9dc04b2c20ab539203b0cd5cca2a10f630bf62d8feb6d68d82af80f64774", "X-Timestamp": "1792159542", "User-Agent": "DiditWebhook/2.0 +https://didit.me"}', + "request_body": '{"application_id":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee","created_at":1792056057,"environment":"live","event_id":"33333333-4444-5555-6666-777777777777","sandbox_scenario":null,"session_id":"22222222-3333-4444-5555-666666666666","status":"In Progress","timestamp":1792159542,"vendor_data":"user-123","webhook_type":"status.updated","workflow_id":"44444444-5555-6666-7777-888888888888","workflow_version":3}', + }, + ), + lambda c: c.webhooks.destinations.resend_delivery( + "11111111-2222-3333-4444-555555555555", "66666666-7777-8888-9999-000000000001" + ), + lambda c: c.webhooks.destinations.resend_delivery( + "11111111-2222-3333-4444-555555555555", "66666666-7777-8888-9999-000000000001" + ), + lambda r: isinstance(r, WebhookDestinationDeliveryDetail), + lambda r: isinstance(r, WebhookDestinationDeliveryDetail), + ) + + def test_webhooks_destinations_retrieve() -> None: """``GET /v3/webhook/destinations/{destination_uuid}/``""" check_operation( @@ -153,9 +485,18 @@ def test_webhooks_destinations_retrieve() -> None: "enabled": True, "webhook_version": "v3", "secret_shared_key": "sample-secret", + "secret_last4": "sample-secret", + "previous_secret_expires_at": None, "subscribed_events": ["status.updated", "data.updated"], + "created_by_email": None, + "created_by_name": None, "created_at": "2026-01-04T08:00:00Z", "updated_at": "2026-04-22T14:09:11Z", + "health_status": "healthy", + "failing_streak": 0, + "failing_since": None, + "last_success_at": "2026-05-17T09:21:18Z", + "environment": "live", "summary": { "total_deliveries": 12453, "failed_deliveries": 14, @@ -164,6 +505,7 @@ def test_webhooks_destinations_retrieve() -> None: "avg_response_time_ms": 187, "max_response_time_ms": 2103, "last_delivery_at": "2026-05-17T09:21:18Z", + "metrics_window_days": 30, }, }, ), @@ -174,6 +516,97 @@ def test_webhooks_destinations_retrieve() -> None: ) +def test_webhooks_destinations_retrieve_analytics() -> None: + """``GET /v3/webhook/destinations/{destination_uuid}/analytics/``""" + check_operation( + Expect( + method="GET", + path="/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/analytics/", + api="verification", + auth="api_key", + query={"date_from": ["2026-10-01"], "date_to": ["2026-10-16"]}, + status=200, + response_json={ + "summary": { + "total_deliveries": 1250, + "failed_deliveries": 12, + "min_response_time_ms": 38, + "avg_response_time_ms": 164, + "max_response_time_ms": 5004, + "error_rate_percentage": 1, + }, + "points": [ + { + "date": "2026-10-14", + "total_deliveries": 410, + "failed_deliveries": 0, + "min_response_time_ms": 38, + "avg_response_time_ms": 152, + "max_response_time_ms": 911, + }, + { + "date": "2026-10-15", + "total_deliveries": 395, + "failed_deliveries": 12, + "min_response_time_ms": 41, + "avg_response_time_ms": 190, + "max_response_time_ms": 5004, + }, + { + "date": "2026-10-16", + "total_deliveries": 445, + "failed_deliveries": 0, + "min_response_time_ms": 40, + "avg_response_time_ms": 151, + "max_response_time_ms": 1220, + }, + ], + }, + ), + lambda c: c.webhooks.destinations.retrieve_analytics( + "11111111-2222-3333-4444-555555555555", date_from="2026-10-01", date_to="2026-10-16" + ), + lambda c: c.webhooks.destinations.retrieve_analytics( + "11111111-2222-3333-4444-555555555555", date_from="2026-10-01", date_to="2026-10-16" + ), + lambda r: isinstance(r, WebhooksDestinationsRetrieveAnalyticsResponse), + lambda r: isinstance(r, WebhooksDestinationsRetrieveAnalyticsResponse), + ) + + +def test_webhooks_destinations_retrieve_delivery() -> None: + """``GET /v3/webhook/destinations/{destination_uuid}/deliveries/{delivery_uuid}/``""" + check_operation( + Expect( + method="GET", + path="/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/deliveries/66666666-7777-8888-9999-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "66666666-7777-8888-9999-000000000001", + "session_id": "22222222-3333-4444-5555-666666666666", + "request_url": "https://yourapp.com/webhooks/didit", + "response_status": 503, + "response_time_ms": 412, + "status": "In Progress", + "webhook_type": "status.updated", + "created_at": "2026-10-15T09:21:18.412345Z", + "request_header": '{"Content-Type": "application/json", "X-Signature": "a5e02a03404e1337a0198e6be15d34abbd053c658a0e51e5d0e78554213ef29b", "X-Signature-V2": "927e7cb770c94ce745939bff73f04aa200015f3bc64795ba46c2aaf78a6cedc5", "X-Signature-Simple": "e37b0d4c716c72bb5ae7127b9cc455d28ab4686e9e22312b4ec2327409a73302", "X-Timestamp": "1792056078", "User-Agent": "DiditWebhook/2.0 +https://didit.me"}', + "request_body": '{"application_id":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee","created_at":1792056057,"environment":"live","event_id":"33333333-4444-5555-6666-777777777777","sandbox_scenario":null,"session_id":"22222222-3333-4444-5555-666666666666","status":"In Progress","timestamp":1792056078,"vendor_data":"user-123","webhook_type":"status.updated","workflow_id":"44444444-5555-6666-7777-888888888888","workflow_version":3}', + }, + ), + lambda c: c.webhooks.destinations.retrieve_delivery( + "11111111-2222-3333-4444-555555555555", "66666666-7777-8888-9999-000000000001" + ), + lambda c: c.webhooks.destinations.retrieve_delivery( + "11111111-2222-3333-4444-555555555555", "66666666-7777-8888-9999-000000000001" + ), + lambda r: isinstance(r, WebhookDestinationDeliveryDetail), + lambda r: isinstance(r, WebhookDestinationDeliveryDetail), + ) + + def test_webhooks_destinations_update() -> None: """``PATCH /v3/webhook/destinations/{destination_uuid}/``""" check_operation( @@ -191,9 +624,18 @@ def test_webhooks_destinations_update() -> None: "enabled": False, "webhook_version": "v3", "secret_shared_key": "sample-secret", + "secret_last4": "sample-secret", + "previous_secret_expires_at": None, "subscribed_events": ["status.updated", "data.updated"], + "created_by_email": None, + "created_by_name": None, "created_at": "2026-01-04T08:00:00Z", "updated_at": "2026-05-17T11:02:55Z", + "health_status": "healthy", + "failing_streak": 0, + "failing_since": None, + "last_success_at": "2026-05-17T09:21:18Z", + "environment": "live", "summary": { "total_deliveries": 12453, "failed_deliveries": 14, @@ -202,6 +644,7 @@ def test_webhooks_destinations_update() -> None: "avg_response_time_ms": 187, "max_response_time_ms": 2103, "last_delivery_at": "2026-05-17T09:21:18Z", + "metrics_window_days": 30, }, }, ), diff --git a/tests/generated/test_workflows.py b/tests/generated/test_workflows.py index f512eb0..e252f29 100644 --- a/tests/generated/test_workflows.py +++ b/tests/generated/test_workflows.py @@ -4,6 +4,7 @@ from __future__ import annotations from didit import AsyncPage, SyncPage +from didit.types.components.workflow_detail import WorkflowDetail from didit.types.components.workflow_list_item import WorkflowListItem from tests.harness import Expect, check_operation @@ -87,8 +88,8 @@ def test_workflows_create() -> None: ], workflow_label="Standard KYC", ), - lambda r: isinstance(r, dict), - lambda r: isinstance(r, dict), + lambda r: isinstance(r, WorkflowDetail), + lambda r: isinstance(r, WorkflowDetail), ) @@ -250,8 +251,8 @@ def test_workflows_retrieve() -> None: ), lambda c: c.workflows.retrieve("00000000-0000-4000-8000-000000000001"), lambda c: c.workflows.retrieve("00000000-0000-4000-8000-000000000001"), - lambda r: True, - lambda r: True, + lambda r: isinstance(r, WorkflowDetail), + lambda r: isinstance(r, WorkflowDetail), ) @@ -265,8 +266,34 @@ def test_workflows_update() -> None: auth="api_key", json_body={"features": [{"feature": "OCR"}, {"feature": "AML"}]}, status=200, - response_json={}, - strict_type=dict[str, object], + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "workflow_id": "00000000-0000-4000-8000-000000000003", + "workflow_label": "sample", + "workflow_type": "kyc", + "is_default": True, + "is_archived": True, + "version": 1, + "status": "draft", + "published_at": "2026-01-01T00:00:00Z", + "is_simple_workflow": True, + "is_editable": True, + "workflow_url": "https://example.com/resource", + "workflow_graph": {}, + "features": "sample", + "total_price": 1.5, + "min_price": 1.5, + "max_price": 1.5, + "special_add_on_prices": {}, + "max_retry_attempts": 1, + "retry_window_days": 1, + "session_expiration_time": 1, + "questionnaire": {}, + "kyb_role_settings": [{}], + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=WorkflowDetail, ), lambda c: c.workflows.update( "00000000-0000-4000-8000-000000000001", @@ -276,6 +303,6 @@ def test_workflows_update() -> None: "00000000-0000-4000-8000-000000000001", features=[{"feature": "OCR"}, {"feature": "AML"}], ), - lambda r: isinstance(r, dict), - lambda r: isinstance(r, dict), + lambda r: isinstance(r, WorkflowDetail), + lambda r: isinstance(r, WorkflowDetail), ) From 78f16e09b1d00bb6a2be7b3e48c74409d4a5b49c Mon Sep 17 00:00:00 2001 From: Alejandro Rosas Date: Fri, 23 Oct 2026 12:21:52 +0400 Subject: [PATCH 2/2] fix(codegen): build and test against the 2026-10-23 spec - Webhook events use the SessionDecision component (the decision response is no longer an inline type). - A resource class that shares its name with a type takes a Resource suffix (travel_rule.workspace vs the TravelRuleWorkspace response): the type import shadowed the class. - Operations with a body that is not a property map (wallet_addresses.bulk_import: object or array, or a CSV file) get a generated test, and the client sends a JSON array body. - The generated tests' sample response no longer puts a null in a list cut off by the depth limit. - organizations.list is paginated upstream now: the client test mock returns a page. - test_import_is_light counts the SessionDecision component and the dozen component models it references. Co-Authored-By: Claude Sonnet 5.5 --- codegen/didit_codegen/generate.py | 2 +- codegen/didit_codegen/render.py | 15 +++-- codegen/didit_codegen/render_tests.py | 20 +++++- src/didit/_base_client.py | 38 ++++++----- src/didit/resources/travel_rule/__init__.py | 52 ++++++++------- src/didit/resources/travel_rule/workspace.py | 48 +++++++------- src/didit/webhooks/_events.py | 4 +- tests/generated/test_sessions.py | 2 +- tests/generated/test_transactions.py | 4 +- tests/generated/test_travel_rule.py | 70 +++++++++++++++++++- tests/test_client.py | 14 +++- tests/test_types.py | 5 +- 12 files changed, 193 insertions(+), 81 deletions(-) diff --git a/codegen/didit_codegen/generate.py b/codegen/didit_codegen/generate.py index e84e8e8..6d0d97a 100644 --- a/codegen/didit_codegen/generate.py +++ b/codegen/didit_codegen/generate.py @@ -88,7 +88,7 @@ def generate(openapi_dir: pathlib.Path) -> dict[str, str]: files: dict[str, str] = {} for rel, text in render_types(builder.registry).items(): files[f"src/didit/types/{rel}"] = text - roots = build_tree(methods) + roots = build_tree(methods, frozenset(builder.registry.defs)) for rel, text in render_resources(roots, builder.registry).items(): files[f"src/didit/resources/{rel}"] = text for rel, text in render_tests(methods, builder.registry).items(): diff --git a/codegen/didit_codegen/render.py b/codegen/didit_codegen/render.py index c380bf0..0d581f7 100644 --- a/codegen/didit_codegen/render.py +++ b/codegen/didit_codegen/render.py @@ -268,14 +268,19 @@ def _render_types_init(registry: Registry) -> str: class ResourceNode: - def __init__(self, path: str) -> None: + def __init__(self, path: str, type_names: frozenset[str] = frozenset()) -> None: self.path = path + self.type_names = type_names self.methods: list[Method] = [] self.children: dict[str, ResourceNode] = {} @property def cls(self) -> str: - return resource_class(self.path) + """The resource class. A type of the same name (resource ``travel_rule.workspace`` and the + ``TravelRuleWorkspace`` response type) would be shadowed in the resource's module, so the + resource takes a ``Resource`` suffix.""" + name = resource_class(self.path) + return f"{name}Resource" if name in self.type_names else name @property def module(self) -> str: @@ -292,7 +297,9 @@ def file(self) -> str: return "/".join(parts) + ".py" -def build_tree(methods: list[Method]) -> dict[str, ResourceNode]: +def build_tree( + methods: list[Method], type_names: frozenset[str] = frozenset() +) -> dict[str, ResourceNode]: roots: dict[str, ResourceNode] = {} nodes: dict[str, ResourceNode] = {} for method in methods: @@ -300,7 +307,7 @@ def build_tree(methods: list[Method]) -> dict[str, ResourceNode]: for i in range(1, len(parts) + 1): path = ".".join(parts[:i]) if path not in nodes: - nodes[path] = ResourceNode(path) + nodes[path] = ResourceNode(path, type_names) if i == 1: roots[path] = nodes[path] else: diff --git a/codegen/didit_codegen/render_tests.py b/codegen/didit_codegen/render_tests.py index f79dee5..8459698 100644 --- a/codegen/didit_codegen/render_tests.py +++ b/codegen/didit_codegen/render_tests.py @@ -102,6 +102,9 @@ def sample(spec: Spec, schema: Json, *, request: bool, depth: int = 0, full: boo if depth > MAX_DEPTH: return [] item = sample(spec, schema.get("items", {}), request=request, depth=depth + 1, full=full) + if item is None and not spec.resolve(schema.get("items", {})).get("nullable"): + # The items sit past the depth limit: an empty list stays valid, a null item would not. + return [] min_items = schema.get("minItems") or 1 return [item] * max(1, min(int(min_items), 3)) if kind == "object" or "properties" in schema: @@ -338,6 +341,13 @@ def _kwargs_for(method: Method) -> list[tuple[Arg, Json]]: for arg in body_args: if arg.wire in chosen: out.append((arg, scrub(chosen[arg.wire], arg.wire))) + whole = method.whole_body + if whole is not None: + # A body that is not a property map (a oneOf, an array) is passed whole as ``body=``. + if method.has_body_example and conforms(spec, whole.schema, example, request=True): + out.append((whole, scrub(example, "body"))) + elif whole.required: + out.append((whole, scrub(sample(spec, whole.schema, request=True, full=False), "body"))) for arg in (a for a in method.kwargs if a.location == "query"): if ( arg.has_example @@ -386,11 +396,14 @@ def _render_test(method: Method, registry: Registry, imports: set[tuple[str, str for arg, value in path_values: path = path.replace("{" + arg.wire + "}", urllib.parse.quote(value, safe="")) query: dict[str, list[str]] = defaultdict(list) - json_body: dict[str, Json] = {} + json_body: Json = {} for arg, value in kwargs: if arg.location == "query": query[arg.wire].extend(_query_text(value)) + elif arg is method.whole_body: + json_body = value else: + assert isinstance(json_body, dict) json_body[arg.wire] = value call_args = [literal(v) for _, v in path_values] call_args += [f"{a.py}={literal(v)}" for a, v in kwargs] @@ -399,7 +412,9 @@ def _render_test(method: Method, registry: Registry, imports: set[tuple[str, str status, response, strict = _response_value(method) multipart = method.body_mode == "multipart" or ( - method.body_mode == "auto" and any(name in json_body for name in method.file_fields) + method.body_mode == "auto" + and isinstance(json_body, dict) + and any(name in json_body for name in method.file_fields) ) resp = method.response if resp.kind == "page": @@ -437,6 +452,7 @@ def _render_test(method: Method, registry: Registry, imports: set[tuple[str, str fields.append(f"query={literal(dict(query))}") if method.body_mode != "none": if multipart: + assert isinstance(json_body, dict) text_fields = sorted(k for k in json_body if k not in method.file_fields) file_fields = sorted(k for k in json_body if k in method.file_fields) fields.append(f"multipart_fields={literal(text_fields)}") diff --git a/src/didit/_base_client.py b/src/didit/_base_client.py index c0ba1d6..8d0c192 100644 --- a/src/didit/_base_client.py +++ b/src/didit/_base_client.py @@ -12,7 +12,7 @@ import logging import random import time -from collections.abc import Callable, Mapping +from collections.abc import Callable, Mapping, Sequence from dataclasses import dataclass, field, replace from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast @@ -223,7 +223,7 @@ def _prepare( auth: AuthKind, api: Literal["verification", "account"], query: Mapping[str, object] | None, - body: Mapping[str, object] | None, + body: Mapping[str, object] | Sequence[object] | None, multipart: Multipart, file_fields: tuple[str, ...], headers: Mapping[str, str | NotGiven | None] | None, @@ -253,18 +253,26 @@ def _prepare( json_content: bytes | None = None data: dict[str, str] | None = None files: list[tuple[str, HttpxFile]] | None = None - if body is not None or extra_body: - merged: dict[str, object] = dict(body or {}) + if body is None or isinstance(body, Mapping): + if body is not None or extra_body: + merged: dict[str, object] = dict(body or {}) + if extra_body: + merged.update(extra_body) + use_multipart = multipart is True or ( + multipart == "auto" + and any(merged.get(name) not in (None, NOT_GIVEN) for name in file_fields) + ) + if use_multipart: + data, files = multipart_fields(merged, file_fields) + else: + json_content = encode_json(merged) + else: + # A JSON array body (an operation whose body is a oneOf of an object and an array). if extra_body: - merged.update(extra_body) - use_multipart = multipart is True or ( - multipart == "auto" - and any(merged.get(name) not in (None, NOT_GIVEN) for name in file_fields) - ) - if use_multipart: - data, files = multipart_fields(merged, file_fields) - else: - json_content = encode_json(merged) + raise ConfigurationError( + "extra_body cannot be merged into a request body that is a JSON array." + ) + json_content = encode_json(body) merged_headers: dict[str, str | None] = { k: v for k, v in (headers or {}).items() if not isinstance(v, NotGiven) @@ -462,7 +470,7 @@ def request( auth: AuthKind = "api_key", api: Literal["verification", "account"] = "verification", query: Mapping[str, object] | None = None, - body: Mapping[str, object] | None = None, + body: Mapping[str, object] | Sequence[object] | None = None, multipart: Multipart = False, file_fields: tuple[str, ...] = (), headers: Mapping[str, str | NotGiven | None] | None = None, @@ -580,7 +588,7 @@ async def request( auth: AuthKind = "api_key", api: Literal["verification", "account"] = "verification", query: Mapping[str, object] | None = None, - body: Mapping[str, object] | None = None, + body: Mapping[str, object] | Sequence[object] | None = None, multipart: Multipart = False, file_fields: tuple[str, ...] = (), headers: Mapping[str, str | NotGiven | None] | None = None, diff --git a/src/didit/resources/travel_rule/__init__.py b/src/didit/resources/travel_rule/__init__.py index b7b41dc..8086afb 100644 --- a/src/didit/resources/travel_rule/__init__.py +++ b/src/didit/resources/travel_rule/__init__.py @@ -75,12 +75,12 @@ TravelRuleWalletAddressesWithStreamingResponse, ) from didit.resources.travel_rule.workspace import ( - AsyncTravelRuleWorkspace, - AsyncTravelRuleWorkspaceWithRawResponse, - AsyncTravelRuleWorkspaceWithStreamingResponse, - TravelRuleWorkspace, - TravelRuleWorkspaceWithRawResponse, - TravelRuleWorkspaceWithStreamingResponse, + AsyncTravelRuleWorkspaceResource, + AsyncTravelRuleWorkspaceResourceWithRawResponse, + AsyncTravelRuleWorkspaceResourceWithStreamingResponse, + TravelRuleWorkspaceResource, + TravelRuleWorkspaceResourceWithRawResponse, + TravelRuleWorkspaceResourceWithStreamingResponse, ) __all__ = [ @@ -120,10 +120,10 @@ def wallet_addresses(self) -> TravelRuleWalletAddresses: return TravelRuleWalletAddresses(self._client) @cached_property - def workspace(self) -> TravelRuleWorkspace: - from didit.resources.travel_rule.workspace import TravelRuleWorkspace + def workspace(self) -> TravelRuleWorkspaceResource: + from didit.resources.travel_rule.workspace import TravelRuleWorkspaceResource - return TravelRuleWorkspace(self._client) + return TravelRuleWorkspaceResource(self._client) @cached_property def with_raw_response(self) -> TravelRuleWithRawResponse: @@ -846,10 +846,10 @@ def wallet_addresses(self) -> AsyncTravelRuleWalletAddresses: return AsyncTravelRuleWalletAddresses(self._client) @cached_property - def workspace(self) -> AsyncTravelRuleWorkspace: - from didit.resources.travel_rule.workspace import AsyncTravelRuleWorkspace + def workspace(self) -> AsyncTravelRuleWorkspaceResource: + from didit.resources.travel_rule.workspace import AsyncTravelRuleWorkspaceResource - return AsyncTravelRuleWorkspace(self._client) + return AsyncTravelRuleWorkspaceResource(self._client) @cached_property def with_raw_response(self) -> AsyncTravelRuleWithRawResponse: @@ -1586,10 +1586,10 @@ def wallet_addresses(self) -> TravelRuleWalletAddressesWithRawResponse: return TravelRuleWalletAddressesWithRawResponse(self._resource.wallet_addresses) @cached_property - def workspace(self) -> TravelRuleWorkspaceWithRawResponse: - from didit.resources.travel_rule.workspace import TravelRuleWorkspaceWithRawResponse + def workspace(self) -> TravelRuleWorkspaceResourceWithRawResponse: + from didit.resources.travel_rule.workspace import TravelRuleWorkspaceResourceWithRawResponse - return TravelRuleWorkspaceWithRawResponse(self._resource.workspace) + return TravelRuleWorkspaceResourceWithRawResponse(self._resource.workspace) class AsyncTravelRuleWithRawResponse: @@ -1643,10 +1643,12 @@ def wallet_addresses(self) -> AsyncTravelRuleWalletAddressesWithRawResponse: return AsyncTravelRuleWalletAddressesWithRawResponse(self._resource.wallet_addresses) @cached_property - def workspace(self) -> AsyncTravelRuleWorkspaceWithRawResponse: - from didit.resources.travel_rule.workspace import AsyncTravelRuleWorkspaceWithRawResponse + def workspace(self) -> AsyncTravelRuleWorkspaceResourceWithRawResponse: + from didit.resources.travel_rule.workspace import ( + AsyncTravelRuleWorkspaceResourceWithRawResponse, + ) - return AsyncTravelRuleWorkspaceWithRawResponse(self._resource.workspace) + return AsyncTravelRuleWorkspaceResourceWithRawResponse(self._resource.workspace) class TravelRuleWithStreamingResponse: @@ -1700,10 +1702,12 @@ def wallet_addresses(self) -> TravelRuleWalletAddressesWithStreamingResponse: return TravelRuleWalletAddressesWithStreamingResponse(self._resource.wallet_addresses) @cached_property - def workspace(self) -> TravelRuleWorkspaceWithStreamingResponse: - from didit.resources.travel_rule.workspace import TravelRuleWorkspaceWithStreamingResponse + def workspace(self) -> TravelRuleWorkspaceResourceWithStreamingResponse: + from didit.resources.travel_rule.workspace import ( + TravelRuleWorkspaceResourceWithStreamingResponse, + ) - return TravelRuleWorkspaceWithStreamingResponse(self._resource.workspace) + return TravelRuleWorkspaceResourceWithStreamingResponse(self._resource.workspace) class AsyncTravelRuleWithStreamingResponse: @@ -1765,9 +1769,9 @@ def wallet_addresses(self) -> AsyncTravelRuleWalletAddressesWithStreamingRespons return AsyncTravelRuleWalletAddressesWithStreamingResponse(self._resource.wallet_addresses) @cached_property - def workspace(self) -> AsyncTravelRuleWorkspaceWithStreamingResponse: + def workspace(self) -> AsyncTravelRuleWorkspaceResourceWithStreamingResponse: from didit.resources.travel_rule.workspace import ( - AsyncTravelRuleWorkspaceWithStreamingResponse, + AsyncTravelRuleWorkspaceResourceWithStreamingResponse, ) - return AsyncTravelRuleWorkspaceWithStreamingResponse(self._resource.workspace) + return AsyncTravelRuleWorkspaceResourceWithStreamingResponse(self._resource.workspace) diff --git a/src/didit/resources/travel_rule/workspace.py b/src/didit/resources/travel_rule/workspace.py index a3907b5..cef86cc 100644 --- a/src/didit/resources/travel_rule/workspace.py +++ b/src/didit/resources/travel_rule/workspace.py @@ -22,30 +22,30 @@ ) __all__ = [ - "TravelRuleWorkspace", - "AsyncTravelRuleWorkspace", - "TravelRuleWorkspaceWithRawResponse", - "AsyncTravelRuleWorkspaceWithRawResponse", - "TravelRuleWorkspaceWithStreamingResponse", - "AsyncTravelRuleWorkspaceWithStreamingResponse", + "TravelRuleWorkspaceResource", + "AsyncTravelRuleWorkspaceResource", + "TravelRuleWorkspaceResourceWithRawResponse", + "AsyncTravelRuleWorkspaceResourceWithRawResponse", + "TravelRuleWorkspaceResourceWithStreamingResponse", + "AsyncTravelRuleWorkspaceResourceWithStreamingResponse", ] -class TravelRuleWorkspace(SyncAPIResource): +class TravelRuleWorkspaceResource(SyncAPIResource): """The ``travel_rule.workspace`` resource of the Didit API. Methods: ``claim``, ``confirm_domain``, ``enable_billing``, ``get``, ``send_domain_code``, ``verify_domain_dns``. """ @cached_property - def with_raw_response(self) -> TravelRuleWorkspaceWithRawResponse: + def with_raw_response(self) -> TravelRuleWorkspaceResourceWithRawResponse: """Every method of this resource, returning the raw HTTP response.""" - return TravelRuleWorkspaceWithRawResponse(self) + return TravelRuleWorkspaceResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> TravelRuleWorkspaceWithStreamingResponse: + def with_streaming_response(self) -> TravelRuleWorkspaceResourceWithStreamingResponse: """Every method of this resource, returning the HTTP response before reading its body.""" - return TravelRuleWorkspaceWithStreamingResponse(self) + return TravelRuleWorkspaceResourceWithStreamingResponse(self) def get( self, @@ -261,21 +261,21 @@ def enable_billing( ) -class AsyncTravelRuleWorkspace(AsyncAPIResource): +class AsyncTravelRuleWorkspaceResource(AsyncAPIResource): """The ``travel_rule.workspace`` resource of the Didit API. Methods: ``claim``, ``confirm_domain``, ``enable_billing``, ``get``, ``send_domain_code``, ``verify_domain_dns``. """ @cached_property - def with_raw_response(self) -> AsyncTravelRuleWorkspaceWithRawResponse: + def with_raw_response(self) -> AsyncTravelRuleWorkspaceResourceWithRawResponse: """Every method of this resource, returning the raw HTTP response.""" - return AsyncTravelRuleWorkspaceWithRawResponse(self) + return AsyncTravelRuleWorkspaceResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> AsyncTravelRuleWorkspaceWithStreamingResponse: + def with_streaming_response(self) -> AsyncTravelRuleWorkspaceResourceWithStreamingResponse: """Every method of this resource, returning the HTTP response before reading its body.""" - return AsyncTravelRuleWorkspaceWithStreamingResponse(self) + return AsyncTravelRuleWorkspaceResourceWithStreamingResponse(self) async def get( self, @@ -491,8 +491,8 @@ async def enable_billing( ) -class TravelRuleWorkspaceWithRawResponse: - def __init__(self, resource: TravelRuleWorkspace) -> None: +class TravelRuleWorkspaceResourceWithRawResponse: + def __init__(self, resource: TravelRuleWorkspaceResource) -> None: self._resource = resource self.get = to_raw_response_wrapper(resource.get) self.claim = to_raw_response_wrapper(resource.claim) @@ -502,8 +502,8 @@ def __init__(self, resource: TravelRuleWorkspace) -> None: self.enable_billing = to_raw_response_wrapper(resource.enable_billing) -class AsyncTravelRuleWorkspaceWithRawResponse: - def __init__(self, resource: AsyncTravelRuleWorkspace) -> None: +class AsyncTravelRuleWorkspaceResourceWithRawResponse: + def __init__(self, resource: AsyncTravelRuleWorkspaceResource) -> None: self._resource = resource self.get = async_to_raw_response_wrapper(resource.get) self.claim = async_to_raw_response_wrapper(resource.claim) @@ -513,8 +513,8 @@ def __init__(self, resource: AsyncTravelRuleWorkspace) -> None: self.enable_billing = async_to_raw_response_wrapper(resource.enable_billing) -class TravelRuleWorkspaceWithStreamingResponse: - def __init__(self, resource: TravelRuleWorkspace) -> None: +class TravelRuleWorkspaceResourceWithStreamingResponse: + def __init__(self, resource: TravelRuleWorkspaceResource) -> None: self._resource = resource self.get = to_streamed_response_wrapper(resource.get) self.claim = to_streamed_response_wrapper(resource.claim) @@ -524,8 +524,8 @@ def __init__(self, resource: TravelRuleWorkspace) -> None: self.enable_billing = to_streamed_response_wrapper(resource.enable_billing) -class AsyncTravelRuleWorkspaceWithStreamingResponse: - def __init__(self, resource: AsyncTravelRuleWorkspace) -> None: +class AsyncTravelRuleWorkspaceResourceWithStreamingResponse: + def __init__(self, resource: AsyncTravelRuleWorkspaceResource) -> None: self._resource = resource self.get = async_to_streamed_response_wrapper(resource.get) self.claim = async_to_streamed_response_wrapper(resource.claim) diff --git a/src/didit/webhooks/_events.py b/src/didit/webhooks/_events.py index 9990ce3..3eae3ff 100644 --- a/src/didit/webhooks/_events.py +++ b/src/didit/webhooks/_events.py @@ -7,7 +7,7 @@ from pydantic import Field, PrivateAttr from .._models import BaseModel, validate_response -from ..types.sessions.retrieve_decision_response import SessionsRetrieveDecisionResponse +from ..types.components.session_decision import SessionDecision __all__ = [ "ActivityCreatedEvent", @@ -66,7 +66,7 @@ class _SessionEvent(WebhookEventBase): workflow_version: int | None = None vendor_data: str | None = None metadata: object = None - decision: SessionsRetrieveDecisionResponse | None = None + decision: SessionDecision | None = None resubmit_info: object = None diff --git a/tests/generated/test_sessions.py b/tests/generated/test_sessions.py index 22a921e..bd7195d 100644 --- a/tests/generated/test_sessions.py +++ b/tests/generated/test_sessions.py @@ -3582,7 +3582,7 @@ def test_sessions_signatures_validate() -> None: "size_bytes": 1, "converter": "sample", }, - "fields": [None], + "fields": [], } ], "adopted_signature": { diff --git a/tests/generated/test_transactions.py b/tests/generated/test_transactions.py index 9a766d3..647d793 100644 --- a/tests/generated/test_transactions.py +++ b/tests/generated/test_transactions.py @@ -1052,7 +1052,7 @@ def test_transactions_rules_list() -> None: "filters": {}, } ], - "actions": [None], + "actions": [], "metadata": {}, "severity": "UNKNOWN", "source": "PRESET", @@ -1157,7 +1157,7 @@ def test_transactions_rules_list_library() -> None: "filters": {}, } ], - "actions": [None], + "actions": [], "metadata": {}, "bundle": "sample", "tags": ["sample"], diff --git a/tests/generated/test_travel_rule.py b/tests/generated/test_travel_rule.py index f2c6d2c..b8e5b7d 100644 --- a/tests/generated/test_travel_rule.py +++ b/tests/generated/test_travel_rule.py @@ -1258,7 +1258,27 @@ def test_travel_rule_wallet_addresses_bulk_import() -> None: path="/v3/travel-rule/wallet-addresses/import/", api="verification", auth="api_key", - json_body={}, + json_body={ + "entries": [ + { + "address": "0xBeneficiaryWallet01", + "chain": "ETH", + "holder_name": "Ana Diaz", + "holder_vendor_data": "user-042", + }, + { + "address_hash": "4f7d2e1c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e", + "chain": "bitcoin", + "holder_vendor_data": "user-043", + }, + { + "address": "rExchangeSharedAddress01", + "chain": "XRP", + "memo": "104729", + "holder_name": "Ana Diaz", + }, + ] + }, status=200, response_json={ "total": 3, @@ -1291,8 +1311,52 @@ def test_travel_rule_wallet_addresses_bulk_import() -> None: ], }, ), - lambda c: c.travel_rule.wallet_addresses.bulk_import(), - lambda c: c.travel_rule.wallet_addresses.bulk_import(), + lambda c: c.travel_rule.wallet_addresses.bulk_import( + body={ + "entries": [ + { + "address": "0xBeneficiaryWallet01", + "chain": "ETH", + "holder_name": "Ana Diaz", + "holder_vendor_data": "user-042", + }, + { + "address_hash": "4f7d2e1c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e", + "chain": "bitcoin", + "holder_vendor_data": "user-043", + }, + { + "address": "rExchangeSharedAddress01", + "chain": "XRP", + "memo": "104729", + "holder_name": "Ana Diaz", + }, + ] + } + ), + lambda c: c.travel_rule.wallet_addresses.bulk_import( + body={ + "entries": [ + { + "address": "0xBeneficiaryWallet01", + "chain": "ETH", + "holder_name": "Ana Diaz", + "holder_vendor_data": "user-042", + }, + { + "address_hash": "4f7d2e1c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e", + "chain": "bitcoin", + "holder_vendor_data": "user-043", + }, + { + "address": "rExchangeSharedAddress01", + "chain": "XRP", + "memo": "104729", + "holder_name": "Ana Diaz", + }, + ] + } + ), lambda r: isinstance(r, WalletAddressImportReport), lambda r: isinstance(r, WalletAddressImportReport), ) diff --git a/tests/test_client.py b/tests/test_client.py index 5a61b3a..30391ee 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -11,6 +11,7 @@ import didit from didit import AsyncDidit, ConfigurationError, Didit +from didit.types.components.wallet_address_import_row import WalletAddressImportRow from tests.mock_api import API_KEY, APP_ID, ORG_ID, SESSION, MockAPI @@ -211,7 +212,7 @@ def handler(request: httpx.Request) -> httpx.Response: def test_account_api_uses_its_own_base_url_and_token() -> None: api = MockAPI() api.json(200, {"access_token": "acct-token", "refresh_token": "r", "expires_in": 86400}) - api.json(200, []) + api.json(200, {"count": 0, "next": None, "previous": None, "results": []}) client = api.client() login = client.account.login(email="dev@example.com", password="pw") request = api.last @@ -311,3 +312,14 @@ def test_didit_log_environment_variable(monkeypatch: pytest.MonkeyPatch) -> None monkeypatch.setenv("DIDIT_LOG", "off") setup_logging() assert logger.handlers == before + + +def test_a_json_array_body_is_sent_as_an_array() -> None: + api = MockAPI().json( + 200, {"total": 1, "created": 1, "existing": 0, "duplicate": 0, "failed": 0, "rows": []} + ) + rows: list[WalletAddressImportRow] = [{"address": "0xBeneficiaryWallet01", "chain": "ETH"}] + api.client().travel_rule.wallet_addresses.bulk_import(body=rows) + assert api.last_json() == rows + with pytest.raises(ConfigurationError, match="JSON array"): + api.client().travel_rule.wallet_addresses.bulk_import(body=rows, extra_body={"x": 1}) diff --git a/tests/test_types.py b/tests/test_types.py index 2cde5ee..4888dd9 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -49,6 +49,7 @@ def test_import_is_light() -> None: out = subprocess.run( [sys.executable, "-c", code], capture_output=True, text=True, check=True ).stdout.split() - # Only the decision model the webhook events reuse is loaded up front. - assert int(out[0]) < 10 + # Only the SessionDecision component the webhook events reuse is loaded up front, with the dozen + # component models it references; the other ~700 generated modules stay unloaded. + assert int(out[0]) < 20 assert out[1] == "False"