# Myaza Trust > Integration guides for identity verification, workflows, Risk Intelligence and Travel Rule. Read product decisions and failure handling as well as endpoint schemas. ## Discovery - [Machine index](https://trust.myaza.co/documentation/index.json): all guides, integration skills and content digests. - [Public v1 OpenAPI](https://trust.myaza.app/api/openapi.json): /api/v1 operations only; KYC and Identity APIs use separate guide contracts. - [Agent guide](https://trust.myaza.co/documentation/ai-integration/markdown): API boundaries, security and acceptance. ## Integration skills - [Identity verification](https://trust.myaza.co/documentation/skills/verification/SKILL.md): Capture identity evidence and return an actionable verification outcome. - [Workflows and decisioning](https://trust.myaza.co/documentation/skills/workflows/SKILL.md): Design a customer journey and apply the organisation's published decisions. - [Business verification](https://trust.myaza.co/documentation/skills/business-verification/SKILL.md): Verify businesses and the people associated with them. - [Risk Intelligence](https://trust.myaza.co/documentation/skills/risk-intelligence/SKILL.md): Connect customer evidence to reviewable, measurable risk decisions. - [Screening](https://trust.myaza.co/documentation/skills/screening/SKILL.md): Screen people, businesses and wallets and respond to potential matches. - [Events and continuous monitoring](https://trust.myaza.co/documentation/skills/monitoring/SKILL.md): Observe changing customer risk and assess account activity or custom events. - [Transactions and fraud decisions](https://trust.myaza.co/documentation/skills/transactions/SKILL.md): Assess fiat and crypto transactions before the customer's system acts. - [Travel Rule](https://trust.myaza.co/documentation/skills/travel-rule/SKILL.md): Exchange transfer information securely and obtain an enforceable current decision. - [Crypto operations](https://trust.myaza.co/documentation/skills/crypto-operations/SKILL.md): Manage crypto institutions, saved destinations and deposit observations. - [Address Intelligence](https://trust.myaza.co/documentation/skills/address-intelligence/SKILL.md): Assess address evidence and maintain address confidence over time. - [Investigations and reporting](https://trust.myaza.co/documentation/skills/investigations/SKILL.md): Triage findings, document decisions and measure policy quality. - [Webhooks and operations](https://trust.myaza.co/documentation/skills/webhooks/SKILL.md): Deliver results reliably to the customer's system. ## Guides - [Introduction](https://trust.myaza.co/documentation/introduction/markdown): Choose the Myaza Trust identity-verification or Risk Intelligence integration journey. - [Quickstart](https://trust.myaza.co/documentation/quickstart/markdown): Run a complete identity verification from your terminal in a few minutes. - [Authentication](https://trust.myaza.co/documentation/authentication/markdown): Create API keys and authenticate every request with a Bearer token. - [Environments](https://trust.myaza.co/documentation/environments/markdown): Sandbox vs production, test vs live keys, and going live. - [Integrate with AI](https://trust.myaza.co/documentation/ai-integration/markdown): Give your coding agent the contracts, product context and tests needed to build a reliable integration. - [Status and reliability](https://trust.myaza.co/documentation/status-and-reliability/markdown): Where to check live service status, and how to get Myaza Trust's measured availability for your security review. - [Get configuration](https://trust.myaza.co/documentation/api-config/markdown): GET /config: discover the ID types and features enabled for your organisation. - [Upload media](https://trust.myaza.co/documentation/api-upload/markdown): POST /upload: store a document, selfie, or liveness file and get a mediaId. - [Create a verification](https://trust.myaza.co/documentation/api-create-verification/markdown): POST /verify, submit an identity for verification. The core endpoint. - [Get verification status](https://trust.myaza.co/documentation/api-verification-status/markdown): GET /status/:verificationId, minimal, publishable-safe submission status (no PII). - [Get verification result](https://trust.myaza.co/documentation/api-verification-result/markdown): GET /verifications/:id, full verification result with PII. Secret key only. - [Face match](https://trust.myaza.co/documentation/api-face-match/markdown): POST /face/compare: compare two photos 1:1 and get a verdict, synchronously. - [Create a session](https://trust.myaza.co/documentation/api-create-session/markdown): POST /sessions, mint a hosted verification link for one applicant, server to server. - [Errors](https://trust.myaza.co/documentation/errors/markdown): HTTP status codes, error response shapes, and the health check. - [Rate limits](https://trust.myaza.co/documentation/rate-limits/markdown): Request limits and how to handle 429 responses. - [Verify again](https://trust.myaza.co/documentation/api-verify-again/markdown): POST /verifications/:id/rerun, ask the applicant to redo a finished verification, whole or in part, server to server. - [Decide a verification](https://trust.myaza.co/documentation/api-review-verification/markdown): POST /verifications/:id/review, approve, decline or send back a finished verification from your backend, or change an earlier decision. - [List verifications](https://trust.myaza.co/documentation/api-list-verifications/markdown): GET /verifications: page through your verification history server to server. - [Spot checks](https://trust.myaza.co/documentation/api-spot-checks/markdown): POST /checks: run one-off individual and business checks server to server, and read their history. - [Delete, expire & cancel](https://trust.myaza.co/documentation/api-manage-records/markdown): DELETE /verifications/:id, DELETE /checks/*/:id, DELETE /face/checks/:id, POST /sessions/:id/expire and POST /sessions/:id/cancel: remove a record, end a live session early, or stop a session or a processing verification and take that back. - [Email & SMS verification](https://trust.myaza.co/documentation/api-contact-verification/markdown): Send a one-time code to an email address or phone number, then check it. Synchronous, no SDK. - [API explorer](https://trust.myaza.co/documentation/api-explorer/markdown): Search public v1 endpoints and inspect request and response fields. - [On-Demand Wallet Screening API](https://trust.myaza.co/documentation/api-wallet-screenings/markdown): Screen a wallet address with a secret API key and retrieve its risk result. - [Supported countries](https://trust.myaza.co/documentation/countries/markdown): Every country Myaza Trust covers, the government-database KYC markets, global document verification, and the business (KYB) registry list. - [Address Intelligence](https://trust.myaza.co/documentation/address-intelligence/markdown): Smart-address capture (map pin, entrance photo, directions) and how the platform corroborates it against the evidence it already holds. - [Background Location Declarations](https://trust.myaza.co/documentation/background-location-declarations/markdown): A store-review kit for apps that turn on background presence verification: how to frame it for your product, the Play Console answers, the in-app disclosure, a 30-second video script, Apple review notes and what to do if you are rejected. - [Hosted link events](https://trust.myaza.co/documentation/hosted-events/markdown): Listen to a hosted verification's lifecycle from a React Native or Flutter WebView, or from an iframe. - [Webhooks](https://trust.myaza.co/documentation/webhooks/markdown): Receive signed event notifications and verify their authenticity. - [Sandbox & test IDs](https://trust.myaza.co/documentation/sandbox-testing/markdown): Test your full integration with canned test IDs, every outcome and no external check cost. - [Workflows](https://trust.myaza.co/documentation/workflows/markdown): Build a verification flow visually once, then embed it by id or share it as a hosted link, with no redeploys. - [Decisioning](https://trust.myaza.co/documentation/decisioning/markdown): Turn verification data into an approve / decline / review decision with a visual rules graph. - [Lists](https://trust.myaza.co/documentation/lists/markdown): Blocklists, the face allowlist, the High-risk countries list, and the custom lists you create for your workflow rules. - [SDKs](https://trust.myaza.co/documentation/sdks/markdown): The server-side Risk Intelligence SDK and client verification SDKs. - [Web SDK (React)](https://trust.myaza.co/documentation/sdk-react/markdown): Drop-in button and modal verification flow for React (≥ 18). - [React Native SDK](https://trust.myaza.co/documentation/sdk-react-native/markdown): On-device native liveness for React Native / Expo, with the same API as the web SDK. - [Flutter SDK](https://trust.myaza.co/documentation/sdk-flutter/markdown): MyazaKYC.show() modal verification flow for Flutter / Dart with on-device liveness. - [User Verification](https://trust.myaza.co/documentation/user-verification/markdown): Launch your first individual verification with a hosted link or an SDK. - [Verification lifecycle](https://trust.myaza.co/documentation/verifications/markdown): How a verification moves from pending to a final status, and how to handle each. - [Countries & ID types](https://trust.myaza.co/documentation/id-types/markdown): Government-database markets, global document coverage, the ID types in each, and their capture requirements. - [NFC chip verification](https://trust.myaza.co/documentation/nfc-chip/markdown): Reading an eMRTD chip (e-passports and chip eID cards), what data is extracted, and how it is authenticated server-side. - [Business verification quickstart](https://trust.myaza.co/documentation/business-verification-quickstart/markdown): Create a KYB workflow, send a company verification link, and receive its result. - [Business verification (KYB)](https://trust.myaza.co/documentation/business-verification/markdown): Verify a company against the official registry, plus documents, declarations, and due diligence on its owners. - [Key people & ownership](https://trust.myaza.co/documentation/key-people/markdown): Due diligence on the people who own or control a business, including corporate shareholders and ownership chains. - [What Risk Intelligence is](https://trust.myaza.co/documentation/risk-intelligence/markdown): Real-time decisions, ongoing customer monitoring, Travel Rule controls and investigations. - [Risk Intelligence quickstart](https://trust.myaza.co/documentation/risk-intelligence-quickstart/markdown): Submit your first Sandbox transaction and read its risk decision. - [Read customer risk](https://trust.myaza.co/documentation/customer-risk-reads/markdown): Read one customer assessment or filter a customer book with consistent risk, monitoring and totals. - [Risk overview](https://trust.myaza.co/documentation/compliance/markdown): Monitor entities, screen against watchlists, and investigate suspicious activity (FRAML). - [Link customer identities](https://trust.myaza.co/documentation/identity-hub/markdown): Sync your users as monitored entities and read their risk state, server to server. - [Customer screening](https://trust.myaza.co/documentation/screening/markdown): Sanctions, PEP, and adverse-media watchlist screening for your entities. - [Crypto transfers and deposits](https://trust.myaza.co/documentation/crypto-operations/markdown): Review crypto transfers, match deposit observations and maintain your address book. - [Transaction monitoring](https://trust.myaza.co/documentation/monitoring-events/markdown): Send complete transaction relationships and customer activity for real-time screening and risk decisions. - [Fraud decision settings](https://trust.myaza.co/documentation/monitoring-rules/markdown): Configure how events are scored, thresholds, money limits, built-in rules, and custom rules. - [Import past transactions](https://trust.myaza.co/documentation/import-past-transactions/markdown): Preview a historical batch, then submit it through Transaction Monitoring. - [Ongoing customer monitoring](https://trust.myaza.co/documentation/monitoring-policies/markdown): Configure policy defaults, then manage each customer's monitoring state from their Entity. - [Fraud rules](https://trust.myaza.co/documentation/transaction-rules/markdown): Decide when supported activities should be allowed, reviewed or blocked. - [Investigations and reporting](https://trust.myaza.co/documentation/investigations/markdown): Triage alerts, investigate evidence, and file suspicious-activity reports. - [Measure decision effectiveness](https://trust.myaza.co/documentation/decision-effectiveness/markdown): Connect downstream outcomes to Myaza decisions, measure fraud capture, detect drift, and review controlled tuning recommendations. - [On-Demand Wallet Screening](https://trust.myaza.co/documentation/wallet-screening/markdown): Check one wallet address, understand its exposure, and act on your rules. - [Test in simulator](https://trust.myaza.co/documentation/platform-simulator/markdown): Run deterministic Myaza Trust risk stories in the sandbox. - [Travel Rule overview](https://trust.myaza.co/documentation/travel-rule/markdown): Exchange required transfer information and act on the result. - [Set up Travel Rule](https://trust.myaza.co/documentation/travel-rule-setup/markdown): Your company, institutions and application. - [Transfer requirements](https://trust.myaza.co/documentation/travel-rule-policies/markdown): Choose rules, information packages and publication. - [Create a transfer](https://trust.myaza.co/documentation/travel-rule-transactions/markdown): Send the information you already hold in one request. - [Supply missing information](https://trust.myaza.co/documentation/travel-rule-information/markdown): Recover incomplete transfers without creating duplicates. - [Exchange and authorise](https://trust.myaza.co/documentation/travel-rule-execution/markdown): Secure messaging and the customer wallet's execution gate. - [Receive a transfer](https://trust.myaza.co/documentation/travel-rule-incoming/markdown): Review incoming information and admit a verified deposit. - [Confirm an outgoing transfer](https://trust.myaza.co/documentation/travel-rule-confirmation/markdown): Report wallet confirmation and track message delivery. - [Receive Travel Rule updates](https://trust.myaza.co/documentation/travel-rule-webhooks/markdown): Events, current status and safe notification handling. - [Self-hosted wallets](https://trust.myaza.co/documentation/travel-rule-self-hosted/markdown): Signed control proof and separate address review. - [Test and troubleshoot](https://trust.myaza.co/documentation/travel-rule-testing/markdown): Exercise information gaps, review and stopped transfers. - [Authentication](https://trust.myaza.co/documentation/webhook-authentication/markdown): Verify V2 signatures, reject replays and rotate endpoint secrets safely. - [Verification](https://trust.myaza.co/documentation/webhook-verification/markdown): Handle individual verification lifecycle events and their complete public result contract. - [Business verification](https://trust.myaza.co/documentation/webhook-business-verification/markdown): Integrate entity, identity, key-person and workflow lifecycle events. - [Risk Intelligence](https://trust.myaza.co/documentation/webhook-risk-intelligence/markdown): Consume risk assessments, immutable signals, level changes and decisions. - [Activity decisions](https://trust.myaza.co/documentation/webhook-event-monitoring/markdown): Receive decisions for logins, account changes and other customer activity. - [Transaction decisions](https://trust.myaza.co/documentation/webhook-transaction-monitoring/markdown): Receive the outcome and next action for an assessed transaction. - [Screening](https://trust.myaza.co/documentation/webhook-screening/markdown): Handle sanctions, PEP, adverse-media and wallet screening outcomes. - [Continuous Monitoring](https://trust.myaza.co/documentation/webhook-continuous-monitoring/markdown): Receive monitoring state changes, failures and material customer-risk deltas. - [Alerts & Investigations](https://trust.myaza.co/documentation/webhook-alerts-cases/markdown): Integrate alert and investigation lifecycle events without exposing analyst evidence. - [Wallet Intelligence](https://trust.myaza.co/documentation/webhook-wallet-intelligence/markdown): Handle wallet and crypto transaction screening events. - [Billing](https://trust.myaza.co/documentation/webhook-billing/markdown): Track credit balance changes and API-key lifecycle events. - [Webhook Testing](https://trust.myaza.co/documentation/webhook-testing/markdown): Test signed deliveries, retries, aliases and idempotency before going live. ## Blog Articles from the Myaza Trust team: https://trust.myaza.co/blog (RSS: https://trust.myaza.co/blog/rss.xml) - [How to manage workspace access and costs with Myaza Trust](https://trust.myaza.co/blog/how-to-workspace-operations): Manage team access, environments and cost controls deliberately. Give each teammate the permissions needed for their job and keep Sandbox preparation separate from Production activity. (2026-10-04, by Charles Archibong) - [How to test and troubleshoot an integration with Myaza Trust](https://trust.myaza.co/blog/how-to-integration-testing): Test the complete integration, including failures and recovery. Choose the supported fixture for the API family you use, then inspect both Myaza’s record and your application’s behaviour. (2026-10-04, by Charles Archibong) - [How to receive results reliably with Myaza Trust](https://trust.myaza.co/blog/how-to-result-delivery): Receive Myaza results reliably with a signed webhook receiver. Verify the exact bytes, store each event durably and recover retries without repeating the customer action. (2026-10-04, by Charles Archibong) - [How to connect your customer records with Myaza Trust](https://trust.myaza.co/blog/how-to-customer-book): Connect the customers already in your application to Myaza records using stable references. Retain what you know about their verification source rather than upgrading imported claims into verified facts. (2026-10-04, by Charles Archibong) - [How to add verification to your app with Myaza Trust](https://trust.myaza.co/blog/how-to-capture-integration): Add verification to your app using a published workflow and a supported SDK or hosted session. Keep capture in the customer journey and final result handling on your backend. (2026-10-04, by Charles Archibong) - [How to verify a business with Myaza Trust](https://trust.myaza.co/blog/how-to-verify-business): Verify a business against a supported registry and choose the additional evidence your onboarding policy needs. Company existence and authority to represent it are different questions. (2026-10-04, by Charles Archibong) - [How to check and monitor address presence with Myaza Trust](https://trust.myaza.co/blog/how-to-address-presence): Verify address presence over a supported observation window. Configure capture, native reporting and customer consent together, then distinguish verified, failed and inconclusive evidence. (2026-10-04, by Charles Archibong) - [How to review existing customer addresses with Myaza Trust](https://trust.myaza.co/blog/how-to-address-triage): Review the address evidence already in your customer book. Upload a permitted CSV, inspect the quote and use the resulting grades to choose the right next capture or presence check. (2026-10-04, by Charles Archibong) - [How to manage crypto destinations and observations with Myaza Trust](https://trust.myaza.co/blog/how-to-crypto-operations): Keep crypto destination records and deposit observations organised. A deposit observation, customer match, screening decision and settled funds are separate facts. (2026-10-04, by Charles Archibong) - [How to set up Travel Rule exchange with Myaza Trust](https://trust.myaza.co/blog/how-to-travel-rule): Set up your organisation’s Travel Rule information-exchange journey, test it and follow the displayed activation requirements. Every transfer still needs its own current authorisation before your wallet acts. (2026-10-04, by Charles Archibong) - [How to screen a crypto wallet with Myaza Trust](https://trust.myaza.co/blog/how-to-wallet-screening): Screen a saved crypto wallet and inspect the decision with its exposure evidence. Keep blockchain risk, address approval, signed control and transfer clearance separate. (2026-10-04, by Charles Archibong) - [How to measure decision quality with Myaza Trust](https://trust.myaza.co/blog/how-to-decision-quality): Measure decision quality using confirmed downstream outcomes. Review fraud capture and customer friction without silently changing the live policy. (2026-10-04, by Charles Archibong) - [How to review and investigate findings with Myaza Trust](https://trust.myaza.co/blog/how-to-investigate-findings): Turn a risk alert into owned casework. Review the reason, gather evidence and record a clear outcome that the next team member can understand. (2026-10-04, by Charles Archibong) - [How to understand customer risk with Myaza Trust](https://trust.myaza.co/blog/how-to-customer-risk): Read customer risk with its evidence and assessment basis. Customer risk helps explain a customer’s recorded state; it is different from the risk decision for a specific payment. (2026-10-04, by Charles Archibong) - [How to assess transaction risk with Myaza Trust](https://trust.myaza.co/blog/how-to-assess-transactions): Assess transaction risk before your application moves money. Submit an exact amount, the right direction and the parties involved, then use the decision and its explanation. (2026-10-04, by Charles Archibong) - [How to assess account activity with Myaza Trust](https://trust.myaza.co/blog/how-to-assess-activity): Assess logins, account recovery and other non-payment activity. Send the actual customer and device context, read the returned decision and enforce it in your own app. (2026-10-04, by Charles Archibong) - [How to keep customer screening up to date with Myaza Trust](https://trust.myaza.co/blog/how-to-monitor-customers): Continuous monitoring rechecks selected customer evidence over time. Configure a policy, enrol eligible customers and give material changes a clear review owner. (2026-10-04, by Charles Archibong) - [How to screen customers for risk with Myaza Trust](https://trust.myaza.co/blog/how-to-screen-customers): Screen customers for sanctions, politically exposed person and adverse-media findings. Start with a correctly linked customer, run the supported checks, then review possible matches with the evidence available. (2026-10-04, by Charles Archibong) - [How to investigate connected customers with Myaza Trust](https://trust.myaza.co/blog/how-to-connected-customers): Investigate connections that may explain suspicious activity. Shared devices, instruments and locations help you ask better questions; they do not establish that every connected customer is the same person. (2026-10-04, by Charles Archibong) - [How to manage reviewed lists with Myaza Trust](https://trust.myaza.co/blog/how-to-review-lists): Use organisation lists to apply reviewed evidence consistently. Keep the decision to add an entry separate from the rules that decide what a list match does. (2026-10-04, by Charles Archibong) - [How to search for matching customers with Myaza Trust](https://trust.myaza.co/blog/how-to-face-search): Search an authorised face photo against the eligible faces in your organisation. Use this for investigation and review, and keep it distinct from comparing two photos or proving a login. (2026-10-04, by Charles Archibong) - [How to verify a customer again with Myaza Trust](https://trust.myaza.co/blog/how-to-reverify-customer): Ask an existing applicant to redo a finished verification, either fully or only for selected steps. Re-verification keeps the verification ID and records a new attempt so your team can explain what changed. (2026-10-04, by Charles Archibong) - [How to compare two faces with Myaza Trust](https://trust.myaza.co/blog/how-to-face-comparison): Compare two face photos when you need a 1:1 similarity verdict. This check is separate from liveness and from searching a customer book. (2026-10-04, by Charles Archibong) - [How to confirm a returning customer with Myaza Trust](https://trust.myaza.co/blog/how-to-returning-customer): Protect a returning-customer journey with a subject-bound face re-authentication workflow. Compare the current live capture with an eligible enrolment or an authorised reference photo, then let your backend decide whether to continue the login. (2026-10-04, by Charles Archibong) - [How to confirm address evidence with Myaza Trust](https://trust.myaza.co/blog/how-to-confirm-address): Collect a map pin and the address details your team needs. Address Intelligence corroborates available evidence and explains its strength; a customer’s typed address remains their claim. (2026-10-04, by Charles Archibong) - [How to collect customer declarations with Myaza Trust](https://trust.myaza.co/blog/how-to-customer-declarations): Collect the declarations your policy needs, such as source of funds or expected activity. A questionnaire gives you recorded customer answers; it does not independently verify those answers. (2026-10-04, by Charles Archibong) - [How to confirm contact details with Myaza Trust](https://trust.myaza.co/blog/how-to-contact-verification): Verify contact access with an email or phone code. Use the workflow step for capture journeys, or the standalone API when your app needs a separate contact check. (2026-10-04, by Charles Archibong) - [How to add device intelligence with Myaza Trust](https://trust.myaza.co/blog/how-to-device-intelligence): Device Intelligence adds context from the device and network used during capture. Configure the findings your team cares about and use them with other evidence rather than treating every shared device as fraud. (2026-10-04, by Charles Archibong) - [How to strengthen identity evidence with Myaza Trust](https://trust.myaza.co/blog/how-to-identity-evidence): NFC verification adds authenticated chip evidence from a supported passport or identity card. Set it up in a mobile capture workflow and understand when the result can, and cannot, raise assurance. (2026-10-04, by Charles Archibong) - [How to confirm a live person with Myaza Trust](https://trust.myaza.co/blog/how-to-live-person): Liveness helps establish that a real person is in front of the camera. Enable it in the workflow customers use, choose a method, and route captures your policy cannot accept. (2026-10-04, by Charles Archibong) - [How to verify a new customer with Myaza Trust](https://trust.myaza.co/blog/how-to-verify-customer): Verify a new customer with a published individual workflow. The customer completes the document and selfie journey; your backend receives the outcome and applies your onboarding policy. (2026-10-04, by Charles Archibong) - [How to detect duplicate customers with Myaza Trust](https://trust.myaza.co/blog/how-to-duplicate-customers): Set up face search in your onboarding workflow, decide what happens when one person uses another account, and check that your app handles the result. (2026-10-04, by Charles Archibong) - [Myaza Trust joins NVIDIA Inception](https://trust.myaza.co/blog/myaza-trust-joins-nvidia-inception): Matching a face to one document is a contained problem. Searching it against every identity a business has verified is not. Why our fraud work is becoming a compute problem, and why Myaza Trust has joined NVIDIA Inception. (2026-09-29, by Charles Archibong) - [White-label verification: putting your own logo on every verification screen](https://trust.myaza.co/blog/white-label-verification-branding): You can replace the “Protected by Myaza” footer on every verification screen with your own logo. It costs $0.10 per production verification, and your organisation needs $750 of paid credit the first time a custom logo goes live. After that, anyone on your team can change it at any balance. (2026-09-28, by Charles Archibong) - [Perpetual KYC: keeping customer records current without re-onboarding everyone](https://trust.myaza.co/blog/perpetual-kyc): Perpetual KYC means refreshing a customer's record when something changes, not re-checking everyone on a calendar. The FATF asks that CDD data be kept up to date, particularly for higher-risk customers; events and risk decide when and how much you ask for. (2026-09-27, by Charles Archibong) - [Screening crypto wallet addresses: what it catches and where it stops](https://trust.myaza.co/blog/crypto-wallet-screening): Wallet screening catches addresses that a sanctions authority has published, matched exactly. It misses unlisted addresses controlled by the same person, funds that passed through intermediaries, and anything published after your last check. (2026-09-27, by Charles Archibong) - [PEP screening: what a match means and what to do next](https://trust.myaza.co/blog/pep-screening-what-a-match-means): A PEP match means you must confirm the person and classify them, not refuse them. Foreign PEPs always need senior management approval, source of wealth and funds, and enhanced monitoring; domestic PEPs need it only when your risk assessment says the relationship is higher risk. (2026-09-27, by Charles Archibong) - [Cutting sanctions screening false positives without missing real matches](https://trust.myaza.co/blog/reducing-sanctions-false-positives): Most sanctions false positives come from thin customer data, not oversensitive matching. Screen with verified date of birth, nationality and ID numbers, record why each match was cleared so it does not return, and leave fuzzy matching alone. (2026-09-27, by Charles Archibong) - [FATF's payment transparency changes: what payment firms should prepare for](https://trust.myaza.co/blog/fatf-payment-transparency-changes): In June 2025 the FATF revised Recommendation 16: standard originator and beneficiary data above USD/EUR 1,000, a defined start to the payment chain and new checks against misdirected payments. Countries are expected to implement by the end of 2030. (2026-09-27, by Charles Archibong) - [The Travel Rule explained for teams launching crypto transfers](https://trust.myaza.co/blog/travel-rule-explained): The Travel Rule requires the sending VASP to collect originator and beneficiary information, pass it to the receiving VASP immediately and securely, and make it available to authorities. The hard parts are finding the counterparty, handling self-hosted wallets and jurisdictions that disagree. (2026-09-27, by Charles Archibong) - [FATF's 2026 virtual assets update: what VASP compliance teams should take from it](https://trust.myaza.co/blog/fatf-virtual-assets-update-2026): The FATF's seventh targeted update, published 16 July 2026, finds most jurisdictions now have Travel Rule laws but few enforce them, and asks VASPs to strengthen wallet screening, unhosted-wallet due diligence and scrutiny of offshore platforms. (2026-09-27, by Charles Archibong) - [FATF's June 2026 grey list: what it means for onboarding in affected markets](https://trust.myaza.co/blog/fatf-grey-list-june-2026): On 19 June 2026 the FATF added Bosnia and Herzegovina and Iraq to its grey list and removed Algeria and Namibia. A grey listing is a risk factor to weigh, not an instruction to apply enhanced due diligence or to exit a market. (2026-09-27, by Charles Archibong) - [Where verification flows lose people, and how to fix each step](https://trust.myaza.co/blog/reducing-verification-drop-off): People abandon verification at predictable moments: an unexplained start, the camera permission prompt, document capture, the desktop that has no camera, and being asked to redo everything after one failure. Measure abandonment per step, then fix the step, not the whole flow. (2026-09-27, by Charles Archibong) - [When one ID is not enough: verifying two or three IDs in one run](https://trust.myaza.co/blog/multi-id-verification): Ask for more than one ID when a single ID's weakness is exactly your risk, and choose IDs that come from independent sources. Then set the pass policy (all, at least two, or any one) by asking what you would accept if one check could not confirm. (2026-09-27, by Charles Archibong) - [Age checks that hold up: from a birthday field to a verified date of birth](https://trust.myaza.co/blog/age-verification-from-government-id): A reliable age check uses a date of birth established by an identity check (the government record, the document or its chip), compares it with a per-country limit, and fails closed when no date can be read. A typed birthday proves nothing, and a face gives an estimate with a margin of error. (2026-09-27, by Charles Archibong) - [Verifying addresses where streets have no numbers](https://trust.myaza.co/blog/verifying-addresses-without-street-numbers): Where streets have no numbers, stop matching address strings. Capture a location (a map pin with the applicant's own directions and details), then corroborate it at the level of the area against independent records, and grade how strongly it is supported. (2026-09-27, by Charles Archibong) - [Proof of address: what a utility bill does and does not prove](https://trust.myaza.co/blog/proof-of-address-documents): A proof-of-address document shows that an address was printed on a document, often with a name, at a date. Give it the weight of corroborating evidence, not verification, and set rules for name, recency and document type that fit each market. (2026-09-27, by Charles Archibong) - [Proving the person applying can act for the business](https://trust.myaza.co/blog/verifying-kyb-applicant): Registration numbers are public, so anyone can type a real company's number. To know the applicant represents the business, verify who they are, check them against the officers the register names, and, where they are not named, ask for evidence of their authority. (2026-09-27, by Charles Archibong) - [When the owner is another company: handling layered ownership in KYB](https://trust.myaza.co/blog/layered-company-ownership): A corporate shareholder is never the beneficial owner; it is a branch of the ownership chain you have not finished. Screen it as an entity, multiply stakes down the chain to find effective ownership, trace the branches that can matter, and escalate the ones you cannot resolve. (2026-09-27, by Charles Archibong) - [Finding the people behind a company: a practical guide to beneficial ownership](https://trust.myaza.co/blog/beneficial-ownership-checks): Identify beneficial owners by reconciling two imperfect lists, the register and the applicant's own disclosure, then testing each person against an ownership threshold and, where nobody crosses it, against control by other means. Verify each person to a depth set by their role. (2026-09-27, by Charles Archibong) - [What a business registry check tells you, and what it leaves out](https://trust.myaza.co/blog/business-registry-verification): A registry lookup confirms that a company with that number exists and shows what it last filed. It does not prove the applicant represents it, who really owns it, or that it trades as it says. Each gap needs its own check. (2026-09-27, by Charles Archibong) - [Not every pass is equal: using assurance levels in onboarding decisions](https://trust.myaza.co/blog/kyc-assurance-levels): A chip pass, a government database pass and a document-only pass are different strengths of evidence. Decide in advance what each tier may do, route on the tier together with the photo the face was matched against, and let customers move up a tier rather than failing them. (2026-09-27, by Charles Archibong) - [Setting a face-match threshold without guessing](https://trust.myaza.co/blog/setting-face-match-thresholds): A face-match threshold is a policy choice between wrongly accepting impostors and wrongly rejecting real customers. Set the pass mark from labelled pairs of your own traffic, add a bounded review band just below it, and revisit both as your users and cameras change. (2026-09-27, by Charles Archibong) - [What reading an ePassport chip proves, and what it can't](https://trust.myaza.co/blog/epassport-nfc-chip-verification): Reading an ePassport chip can prove the data was signed by the issuing state and has not been altered, and with Active Authentication that the chip is not a copy. It cannot prove the person holding the passport is its owner; only a face match against the chip photo does that. (2026-09-27, by Charles Archibong) - [Why a liveness result from the phone is not enough](https://trust.myaza.co/blog/server-side-liveness-verification): A liveness result produced on the phone is a claim made by a device the attacker may control. Treat it as input, keep the recorded evidence, and re-check that evidence on the server before the result counts. (2026-09-27, by Charles Archibong) - [Gesture or screen-flash liveness: choosing the right challenge](https://trust.myaza.co/blog/gesture-vs-flash-liveness): Gesture challenges suit broad consumer populations and defeat photos and pre-recorded video. Screen-flash challenges add a random colour sequence that a replayed or injected video cannot contain. Use both where the account is valuable enough to justify the extra seconds. (2026-09-27, by Charles Archibong) - [NIN and BVN checks in Nigeria: what each one proves](https://trust.myaza.co/blog/nin-bvn-verification-nigeria): A NIN check confirms a national identity recorded by NIMC; a BVN check confirms an identity enrolled in the Nigerian banking system; a vNIN is a privacy-preserving token for a NIN. Each proves the record exists, and only a live selfie matched to the record photo proves the applicant owns it. (2026-09-27, by Charles Archibong) - [Document checks or government database checks: which does your onboarding need?](https://trust.myaza.co/blog/document-vs-government-database-checks): Reading a document tells you it looks genuine; checking the government record tells you the identity was issued. Use the record wherever one exists, and remember that neither proves the applicant owns the identity until a live face is matched to a trusted photo. (2026-09-27, by Charles Archibong) - [What identity verification actually checks, and what it doesn't](https://trust.myaza.co/blog/what-identity-verification-checks): A KYC check proves three narrow things at one moment: the identity exists, the evidence for it is genuine, and the person in front of the camera is its owner. It says nothing about intent, and it starts ageing the day it passes. (2026-09-27, by Charles Archibong) - [Sending events for real-time risk scoring: an integration guide](https://trust.myaza.co/blog/sending-events-for-real-time-risk-scoring): Call the transactions endpoint for money movement and the activities endpoint for logins and account changes, from your backend, before you act. Send complete parties and stable IDs, then route on the summary outcome: allow, review or block. (2026-09-27, by Charles Archibong) - [Adding identity verification to a React Native or Flutter app](https://trust.myaza.co/blog/mobile-identity-verification-react-native-flutter): The component call is the easy part. A mobile integration is mostly native build setup, camera permission, mounting a dashboard workflow so changes skip the app store, and keeping results on your server rather than on the phone. (2026-09-27, by Charles Archibong) - [Mapping verification statuses to your product's states](https://trust.myaza.co/blog/mapping-verification-statuses): Drive your product from status, the outcome, and store checkStatus beside it as the explanation. Map each of the ten status values to one state in your own system, and take the latest status from verification.status_updated or polling. (2026-09-27, by Charles Archibong) - [Idempotent verification requests: avoiding duplicate checks and duplicate charges](https://trust.myaza.co/blog/idempotent-verification-requests): Give every logical verification one requestId, save it before you send the request, and reuse it on every retry. A repeated requestId returns the existing verification instead of creating and charging a second one. (2026-09-27, by Charles Archibong) - [Publishable and secret keys: keeping verification data off the client](https://trust.myaza.co/blog/publishable-and-secret-api-keys): Put the publishable key in your app and keep the secret key on your server. A publishable key can start a verification and read minimal status, never identity data, so plan for it to leak and limit what a leak costs. (2026-09-27, by Charles Archibong) - [Testing a verification integration end to end in sandbox](https://trust.myaza.co/blog/testing-verification-in-sandbox): Use the published sandbox test IDs, whose last digits pick the outcome, to drive every result your code must handle, then test your webhook receiver with signed simulated events before switching to live keys. (2026-09-27, by Charles Archibong) - [Changing a verification flow without shipping an app release](https://trust.myaza.co/blog/changing-verification-flows-without-a-release): Mount the SDK with a workflow ID and the flow's configuration is fetched from the server on each verification. Re-publishing changes the steps, checks, copy and decision rules for the next verification, with no app release. (2026-09-27, by Charles Archibong) - [How decision graphs turn checks into approve, review or decline](https://trust.myaza.co/blog/decision-graphs-explained): When a verification finishes, the workflow's decision graph gathers the results, walks branches over a closed set of fields, and lands on approve, review or decline. The outcome sets the customer's disposition without rewriting what the checks found. (2026-09-27, by Charles Archibong) - [Designing your product around asynchronous verification results](https://trust.myaza.co/blog/designing-for-asynchronous-verification): Treat submission as the start of a state machine, not the end of a request. Show a pending state keyed on the verification ID, update it from webhooks, and let the status change more than once. (2026-09-27, by Charles Archibong) - [Verifying webhook signatures and handling retries correctly](https://trust.myaza.co/blog/verifying-webhook-signatures): Verify the V2 signature over the raw request bytes, reject timestamps outside the replay window, record the event ID under a unique constraint before doing any work, and return 2xx only once the event is durably stored. (2026-09-27, by Charles Archibong) - [SDK embed or hosted link: choosing how to integrate verification](https://trust.myaza.co/blog/sdk-or-hosted-link): Embed the SDK when verification happens inside your product; send a per-applicant hosted link when it happens outside it. Either way, run a published workflow so the choice is about delivery, not about the flow. (2026-09-27, by Charles Archibong) - [Handling biometric data under African data protection laws](https://trust.myaza.co/blog/biometric-data-protection-africa): All three laws treat biometric data as a special category. Nigeria names facial images and limits sensitive data to listed grounds, Kenya treats biometric processing as high risk, and POPIA prohibits it unless an authorisation applies. Plan a lawful ground, a DPIA and minimisation. (2026-09-27, by Charles Archibong) - [What device and IP signals can and cannot tell you](https://trust.myaza.co/blog/device-and-ip-signals-limits): Device and IP signals describe the handset and connection, not the person. Emulators and datacentre IPs are strong warnings; shared devices, IP velocity and IP location are weak. Use them as corroboration, and trust IP location at country level only. (2026-09-27, by Charles Archibong) - [Account takeover: step up the challenge instead of blocking the customer](https://trust.myaza.co/blog/account-takeover-step-up): When a login looks like account takeover, do not guess. Let the session continue with limited rights, and ask for proof that only the verified person can give, such as a fresh selfie matched to their enrolment, before any payout, beneficiary change or password reset goes through. (2026-09-27, by Charles Archibong) - [Finding the same person behind many accounts](https://trust.myaza.co/blog/finding-duplicate-accounts): Rank shared artefacts by what they prove. A repeated verified ID number means the same person. A repeated face is strong evidence for review. A shared device, phone or address is common in families, so treat it as corroboration and count distinct people before acting. (2026-09-27, by Charles Archibong) - [Deepfakes and injected camera feeds: how identity checks are attacked](https://trust.myaza.co/blog/deepfakes-and-injection-attacks): Attackers either hold fake media up to a real camera (a presentation attack) or feed it straight into the data stream (an injection attack). Each needs its own defence, and the strongest checks add evidence a camera cannot supply. (2026-09-27, by Charles Archibong) - [Preparing customer due diligence for the EU AML Regulation](https://trust.myaza.co/blog/eu-aml-regulation-2027-readiness): The EU AML Regulation applies directly from 10 July 2027 and replaces national CDD rules with one text. Build to the Regulation now: its data list, verification routes, 25% ownership test and refresh ceilings are fixed, while AMLA's detailed CDD standards were still in draft as of September 2026. (2026-09-27, by Charles Archibong) - [Companies House ID verification: the November 2026 deadline and what KYB teams will see](https://trust.myaza.co/blog/companies-house-identity-verification-deadline): Since 18 November 2025, new UK directors and PSCs must verify their identity with Companies House, and existing ones within a 12-month transition to mid-November 2026. It adds register signals for KYB teams, but does not show that the person applying to you is that director. (2026-09-27, by Charles Archibong) - [CBN's 2026 BVN framework changes: what onboarding teams should note](https://trust.myaza.co/blog/cbn-bvn-framework-changes-2026): From 1 May 2026, under a CBN circular dated 12 March 2026, only adults can enrol for a BVN, the linked phone number can change only once, banks must hold BVNs tied to suspicious transactions on a 24-hour watchlist, and database access is limited to licensed institutions. (2026-09-27, by Charles Archibong) - [CBN's baseline standards for automated AML: preparing your stack](https://trust.myaza.co/blog/cbn-automated-aml-baseline-standards): The CBN's baseline standards, issued on 10 March 2026, set mandatory minimum requirements for automated systems that detect, analyse and report suspicious activity in real time. The CBN has said compliance is assessed at the level of the institution, so buying a tool is a start, not an answer. (2026-09-27, by Charles Archibong) - [goAML explained: how reports reach financial intelligence units](https://trust.myaza.co/blog/goaml-reporting-explained): goAML is software built by the UN Office on Drugs and Crime for financial intelligence units. Reporting entities register on their FIU's goAML portal and submit reports by web form or as XML files that follow the goAML schema, using the FIU's own codes. (2026-09-27, by Charles Archibong) - [Writing a suspicious activity report an FIU can act on](https://trust.myaza.co/blog/writing-a-suspicious-activity-report): A useful SAR or STR narrative answers who, what, when, where, why and how in one chronological account, explains why the activity is unusual for this customer, and gives facts an analyst can follow without opening your systems. (2026-09-27, by Charles Archibong) - [From alert to decision: running an investigation queue that keeps up](https://trust.myaza.co/blog/alert-triage-and-case-management): Keep two queues with two jobs. Triage alerts quickly and label every one; open an investigation only when the evidence needs an owner, group it by customer, prioritise by risk and deadline, and close it with a recorded outcome and rationale. (2026-09-27, by Charles Archibong) - [Backtest before you switch on: testing monitoring rule changes safely](https://trust.myaza.co/blog/backtesting-monitoring-rules): Replay recent scored events through the proposed configuration, then read each decision that would change as well as the count. A backtest shows what a change would have done to the past; your written expectation is what tells you whether that is the right result. (2026-09-27, by Charles Archibong) - [Transaction monitoring rules that find risk instead of noise](https://trust.myaza.co/blog/transaction-monitoring-rules): A monitoring rule earns its place when it maps to a risk you actually carry, fires on data you actually send, and produces alerts an analyst can close with a reason. Tune rules against labelled outcomes, never against alert counts alone. (2026-09-27, by Charles Archibong) --- # Introduction Myaza Trust combines identity verification with Risk Intelligence. Use identity verification to establish who a person or business is. Use Risk Intelligence to screen customers, decide on transactions and account activity, keep customers under continuous review, apply Travel Rule controls and manage investigations. Choose the shortest path for the job: - [User verification](https://trust.myaza.co/documentation/user-verification/markdown) for a hosted or embedded individual flow. - [Business verification](https://trust.myaza.co/documentation/business-verification-quickstart/markdown) for a KYB workflow and company link. - [Risk Intelligence quickstart](https://trust.myaza.co/documentation/risk-intelligence-quickstart/markdown) for a server-side transaction decision. - [On-Demand Wallet Screening](https://trust.myaza.co/documentation/wallet-screening/markdown) for an explicit wallet check. ## How the integration works Identity verification is **asynchronous**. You submit a verification request and receive a verification ID immediately; the result arrives later through a [webhook](https://trust.myaza.co/documentation/webhooks/markdown) or status read. You can drive a verification two ways: pass the configuration in code (country, ID types, steps), or build the flow once in the dashboard as a [workflow](https://trust.myaza.co/documentation/workflows/markdown) and reference it by id, adding extra checks and an approve / decline / review [decision](https://trust.myaza.co/documentation/decisioning/markdown) without changing your code. Every flow runs in one of our [SDKs](https://trust.myaza.co/documentation/sdks/markdown) or as a shareable hosted link. Risk activity and transaction decisions are **synchronous**. The response tells your backend whether to allow, review or block the action. Webhooks are optional notifications for those decisions and are the normal delivery path for ongoing monitoring changes. ## Base URL | Product | Sandbox | Production | |---|---|---| | Identity verification | `https://trust.myaza.app/api/kyc` | `https://trust.myaza.app/api/kyc` | | Risk Intelligence | `https://sandbox.trust.myaza.app/api/v1` | `https://trust.myaza.app/api/v1` | The official SDK chooses the correct host. API keys, webhook endpoints and resources stay isolated by environment. See [Environments](https://trust.myaza.co/documentation/environments/markdown). ## Conventions - All request and response bodies are JSON, except media upload which is `multipart/form-data`. - Timestamps are ISO 8601 strings in UTC (e.g. `2026-04-27T12:00:00.000Z`). - IDs are opaque strings; do not parse them. - Statuses in responses are lower-case (`pending`, `verified`, `failed`, `not_found`, `error`). ## Next steps - [User verification](https://trust.myaza.co/documentation/user-verification/markdown) or [Business verification](https://trust.myaza.co/documentation/business-verification-quickstart/markdown): launch a workflow and receive the result. - [Verification API quickstart](https://trust.myaza.co/documentation/quickstart/markdown): a full individual verification through HTTP. - [Risk Intelligence quickstart](https://trust.myaza.co/documentation/risk-intelligence-quickstart/markdown): a first transaction decision. - [Authentication](https://trust.myaza.co/documentation/authentication/markdown): create and use API keys. - [Create a verification](https://trust.myaza.co/documentation/api-create-verification/markdown): the core endpoint in detail. - [Workflows](https://trust.myaza.co/documentation/workflows/markdown): configure a flow visually and reuse it by id or hosted link. --- # Quickstart > Looking for transaction decisions, customer screening, continuous monitoring > or Travel Rule? Use the [Risk Intelligence quickstart](https://trust.myaza.co/documentation/risk-intelligence-quickstart/markdown). This guide takes you through a full verification using `curl`. By the end you will have created an API key, discovered your enabled ID types, uploaded a selfie, submitted a verification, and read the result. > **Building an app or website flow?** You usually don't need the raw API. Build a [workflow](https://trust.myaza.co/documentation/workflows/markdown) in the dashboard and mount it with an [SDK](https://trust.myaza.co/documentation/sdks/markdown) (`workflowId="wf_…"`), or share its hosted link with zero integration. This page walks the API underneath, for server-side integrations and for understanding what the SDKs do. We use the **sandbox** environment (`pk_test_…` keys against `https://trust.myaza.app/api/kyc`) so you can test freely without affecting production. Sandbox and production share the same base URL, and your key prefix decides which environment you hit. ## 1. Create a test API key In the dashboard, go to **Settings → Organization → Developers → API Keys**, switch the environment selector to **Sandbox**, and create a key. Copy the full key (`pk_test_…`); it is shown only once. Store it in your shell: ```bash export MYAZA_KEY="pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" export MYAZA_BASE="https://trust.myaza.app/api/kyc" ``` ## 2. Check your configuration `GET /config` returns the ID types enabled for your organisation and which features each supports. ```bash curl "$MYAZA_BASE/config" \ -H "Authorization: Bearer $MYAZA_KEY" ``` ```json { "environment": "SANDBOX", "idTypes": [ { "country": "NG", "idType": "bvn", "features": { "documentVerification": false, "livenessCheck": true, "govDbCheck": true } } ] } ``` ## 3. Upload media (optional) Number-only IDs (such as BVN) don't need media. For document or selfie/liveness flows, upload each file and keep the returned `mediaId`. ```bash curl "$MYAZA_BASE/upload" \ -H "Authorization: Bearer $MYAZA_KEY" \ -F "type=selfie" \ -F "file=@./selfie.jpg" ``` ```json { "mediaId": "media_01j9..." } ``` ## 4. Create the verification Submit the ID details. The `metadata.requestId` is **your** idempotency key: reuse it to safely retry without creating duplicates. ```bash curl "$MYAZA_BASE/verify" \ -H "Authorization: Bearer $MYAZA_KEY" \ -H "Content-Type: application/json" \ -d '{ "country": "NG", "idType": "bvn", "idNumber": "12345678901", "metadata": { "requestId": "order_1001", "userId": "user_42" } }' ``` The API responds immediately with `202 Accepted`: ```json { "verificationId": "ver_01j9...", "status": "processing" } ``` ## 5. Get the result Verification runs asynchronously. The publishable key polls **minimal status** (no PII): ```bash curl "$MYAZA_BASE/status/ver_01j9..." \ -H "Authorization: Bearer $MYAZA_KEY" ``` ```json { "verificationId": "ver_01j9...", "status": "approved", "checkStatus": "verified", "createdAt": "2026-04-27T12:00:00.000Z", "completedAt": "2026-04-27T12:00:05.000Z" } ``` To read the **full result** (biodata, ID number, facial match), call the [result endpoint](https://trust.myaza.co/documentation/api-verification-result/markdown) from your backend with a **secret (`sk_`) key**, never from client code: ```bash curl "$MYAZA_BASE/verifications/ver_01j9..." \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" ``` ```json { "verificationId": "ver_01j9...", "status": "approved", "checkStatus": "verified", "country": "NG", "idType": "bvn", "result": { "firstName": "JOHN", "lastName": "DOE", "dateOfBirth": "1990-01-01", "idNumberMasked": "1234•••901", "facialMatch": null }, "completedAt": "2026-04-27T12:00:05.000Z" } ``` Better still, receive the result on your backend via a [webhook](https://trust.myaza.co/documentation/webhooks/markdown). ## Next steps - Set up [webhooks](https://trust.myaza.co/documentation/webhooks/markdown) instead of polling. - Review the full [verification lifecycle](https://trust.myaza.co/documentation/verifications/markdown) and all status values. - Go live by reading about [environments](https://trust.myaza.co/documentation/environments/markdown) and production access. --- # Authentication Every request to the Myaza verification and Risk Intelligence APIs (except the public [health check](https://trust.myaza.co/documentation/errors/markdown#health-check)) must be authenticated with an API key. ## Two key types: publishable and secret Keys come in two **scopes**. The scope decides which endpoints a key may call: | Type | Prefix | Where it lives | Can call | |---|---|---|---| | **Publishable** | `pk_…` | Client-side, safe to ship in the SDK / your frontend | Start a verification (`/verify`), uploads (`/upload`), SDK config (`/config`), and **minimal** submission status (`/status`: state + reason, **no PII**) | | **Secret** | `sk_…` | **Backend only**, never in client code | Full verification results and every Risk Intelligence endpoint under `/api/v1` | Each scope also carries the environment dimension in its prefix: | Prefix | Type | Environment | |---|---|---| | `pk_test_` | Publishable | Sandbox | | `pk_live_` | Publishable | Production | | `sk_test_` | Secret | Sandbox | | `sk_live_` | Secret | Production | A key looks like `pk_test_aB3dE5fG7hJ9kL1mN3pQ5rS7tU9vW1xY`: the prefix plus 32 random alphanumeric characters. > **Never ship a secret (`sk_`) key in your SDK, mobile app, or any client code.** > The publishable key in your frontend can only *start* verifications and poll > minimal status; it can never read identity data. Fetch results from your > backend with a secret key. A publishable key hitting a secret-only endpoint > gets `403 secret_key_required`. ## Authenticating a request Pass the full key as a Bearer token in the `Authorization` header: ```bash curl "https://trust.myaza.app/api/kyc/config" \ -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` The key identifies your organisation and its environment. There is no separate organisation ID to send. Risk Intelligence uses `https://sandbox.trust.myaza.app/api/v1` with `sk_test_` and `https://trust.myaza.app/api/v1` with `sk_live_`. The official Node.js SDK selects the host from its `environment` option. ## Creating and managing keys API keys are managed in the dashboard under **Developers → API Keys**. - Pick the **key type** (Publishable or Secret) when creating a key. The **environment selector** in the dashboard header decides whether it's a `_test_` (sandbox) or `_live_` (production) key. - **Secret keys are shown only once**, at creation. Store the value immediately in a secrets manager; it cannot be retrieved again, only the prefix is displayed afterward. - **Publishable keys can be viewed anytime** in the dashboard: they're public by design (they ship in your frontend), so there's no secret to protect. - Creating or revoking a key requires the `api_keys:create` permission. Members without it see keys read-only. - **Revoke** a key the moment it may be compromised. Revocation is immediate: the next request with that key returns `401`. > Creating and revoking keys emit the `api_key.created` and `api_key.revoked` [webhook events](https://trust.myaza.co/documentation/webhooks/markdown) so you can wire them into your own security auditing. ## Errors | Status | Body | Cause | |---|---|---| | `401` | `{ "error": "Missing or invalid Authorization header" }` | No `Authorization: Bearer …` header. | | `401` | `{ "error": "Invalid API key" }` | Unknown or revoked key. | | `403` | `{ "error": "secret_key_required" }` | A publishable (`pk_`) key was used on a secret-only endpoint (full result or media). Use a `sk_` key from your backend. | | `403` | `{ "error": "environment_mismatch" }` | The key's environment doesn't match the resource or host (for example, a test key used for a production resource). Keep one environment throughout a flow and use the documented host for that product. | See [Errors](https://trust.myaza.co/documentation/errors/markdown) for the full list. ## Where each key is used - **Publishable (`pk_…`)**: the [client SDKs](https://trust.myaza.co/documentation/sdks/markdown), which take an `apiKey` and call the API from the user's device, plus any frontend code that starts verifications. Match the environment: `pk_test_` with the SDK's `sandbox` environment, `pk_live_` with `production`. - **Secret (`sk_…`)**: your **backend** reading results: `GET /verifications/:id` for the full result + PII and `GET /verifications/:id/media/:kind` for captured media, plus every Risk Intelligence request under `/api/v1`. Keep it server-side only. See the [Risk Intelligence quickstart](https://trust.myaza.co/documentation/risk-intelligence-quickstart/markdown). ## Best practices - **Never put a secret key in client code.** If a publishable key leaks it can only start verifications; a leaked secret key exposes identity data, so treat it like a password. - **Never commit a key to a public repository** or paste it where it could be scraped. Revoke immediately if a key leaks. - Use `_test_` keys while integrating and in CI; reserve `_live_` keys for production traffic. - Rotate keys periodically. - Scope keys per service or environment so you can revoke one without disrupting everything. --- # Environments Myaza Trust has isolated Sandbox and Production environments. The key prefix always identifies the environment. Risk Intelligence also uses a separate Sandbox host, while identity verification keeps one host and selects the environment from the key. | Product | Sandbox | Production | Key type | |---|---|---|---| | Identity verification | `https://trust.myaza.app/api/kyc` | `https://trust.myaza.app/api/kyc` | `pk_test_` / `pk_live_` in client SDKs; `sk_test_` / `sk_live_` for backend-only results | | Risk Intelligence | `https://sandbox.trust.myaza.app/api/v1` | `https://trust.myaza.app/api/v1` | `sk_test_` / `sk_live_` on your backend | Data does not cross environments: keys, verifications, webhook endpoints, and configuration are all scoped to one environment. ## Sandbox Use Sandbox to build and test your integration. It exercises the same public API surface and webhook delivery as Production. Explicit Risk Intelligence `sandboxScenario` fixtures are deterministic and non-billable. Ordinary Sandbox requests without a scenario use configured Sandbox pricing and can be billed. Use the Risk Intelligence SDK with `environment: 'sandbox'`; it selects the Sandbox host for you. Remove `sandboxScenario` in Production. ## Production Production runs real verifications against live government databases and **uses your credit balance**. ### Production access requires an approved business Before your `pk_live_` keys can run verifications, your organisation's business profile (KYB) must be **approved** by Myaza. Until then, production verify and upload requests are rejected: ```json { "error": "business_not_approved", "message": "Your business must be approved by Myaza before using production verification services." } ``` Complete and submit your business profile under **Onboarding** in the dashboard, then wait for approval. Sandbox is unaffected and remains available throughout. ### Billing Each completed production verification deducts from your organisation's credit balance at the price configured for that country and ID type. If the account has insufficient credit, the verification is recorded and marked `failed` with an insufficient-credits reason (it still fires a `verification.failed` webhook) rather than being silently dropped. Top up and set low-balance thresholds under **Billing**. ## Switching environments in the dashboard The dashboard header has an environment switcher (Sandbox / Production). It controls which environment's data you see (including which keys you create) without changing the dashboard URL. ## Checklist for going live 1. Your business profile is **approved** for production. 2. You created the required live keys: publishable for a verification client, secret for backend results or Risk Intelligence. 3. You registered **production** webhook endpoints and verified signatures. 4. Your credit balance is funded and a low-balance threshold is set. 5. You swapped test keys for live keys and, for Risk Intelligence, selected the Production environment/host. --- # Integrate with AI Give your agent the [documentation index](/llms.txt), then choose the product skill below. Each skill covers the customer journey, decisions, security, recovery and acceptance tests—not just API calls. ## Choose a skill Save the linked file as `.agents/skills/myaza-/SKILL.md` for Codex, or `.claude/skills/myaza-/SKILL.md` for Claude Code, inside your integration repository. Review it before enabling it. These are portable Markdown instructions; they do not install software, request credentials or run commands. | Product | Integration skill | | --- | --- | | Identity verification | [Download skill](/documentation/skills/verification/SKILL.md) | | Workflows and decisioning | [Download skill](/documentation/skills/workflows/SKILL.md) | | Business verification | [Download skill](/documentation/skills/business-verification/SKILL.md) | | Risk Intelligence | [Download skill](/documentation/skills/risk-intelligence/SKILL.md) | | Screening | [Download skill](/documentation/skills/screening/SKILL.md) | | Events and continuous monitoring | [Download skill](/documentation/skills/monitoring/SKILL.md) | | Transactions and fraud decisions | [Download skill](/documentation/skills/transactions/SKILL.md) | | Travel Rule | [Download skill](/documentation/skills/travel-rule/SKILL.md) | | Crypto operations | [Download skill](/documentation/skills/crypto-operations/SKILL.md) | | Address Intelligence | [Download skill](/documentation/skills/address-intelligence/SKILL.md) | | Investigations and reporting | [Download skill](/documentation/skills/investigations/SKILL.md) | | Webhooks and operations | [Download skill](/documentation/skills/webhooks/SKILL.md) | ## Give the agent a useful brief Describe your customer journey, target markets, review team and what your application should do when a result is approved, held or blocked. Share synthetic examples only. ```text Integrate Myaza Trust for our customer onboarding and crypto transfers. Start in Sandbox. Read the Myaza product skills and current documentation. Map our customer reference to externalUserId. Propose the decision and review flow before changing our policies. Keep credentials on our server. Verify and deduplicate signed webhooks. Add executable tests for retries, missing information and blocked outcomes. Return the implementation, test evidence and remaining Live prerequisites. ``` ## Find the exact contract | Resource | Purpose | | --- | --- | | [llms.txt](/llms.txt) | Concise discovery and product links | | [llm.txt](/llm.txt) | Alias for clients using the singular name | | [Machine index](/documentation/index.json) | Every guide, product skill and content digest | | [Complete Markdown](/llms-full.txt) | All published guides, without page navigation | | [Public v1 OpenAPI](https://trust.myaza.app/api/openapi.json) | Machine-readable public v1 operations and schemas | Every guide has a **Markdown** link and an `/markdown` URL. Fetch individual guides first; use the full bundle when your tool cannot follow links. The index's SHA-256 values identify the exact content reviewed. ### API families Do not replace one API prefix with another. They are separate contracts. | Family | Read first | Boundary | | --- | --- | --- | | `/api/kyc` | [Authentication](https://trust.myaza.co/documentation/authentication/markdown), [verification quickstart](https://trust.myaza.co/documentation/quickstart/markdown) | Capture, verification and SDK operations; some are publishable-key safe, others are server-only | | `/api/v1` | [Risk Intelligence quickstart](https://trust.myaza.co/documentation/risk-intelligence-quickstart/markdown), OpenAPI | Server-side risk, transactions and crypto operations | | `/api/identity` | [Events](https://trust.myaza.co/documentation/monitoring-events/markdown) | The documented Identity event contract, not a replacement transaction endpoint | | Dashboard and platform routes | Dashboard UI | Session/role protected administration; not a substitute for customer secret-key APIs | The v1 schema does not describe every KYC or Identity operation. Read the relevant guide as well as the schema. Follow each endpoint's authentication contract; never expose a secret key in a browser or mobile app. ## Build the whole customer flow 1. **Identify:** use stable customer and operation references, scoped to your organisation and environment. 2. **Collect:** request only the evidence needed for the selected journey. 3. **Decide:** distinguish capture completion, processing, review, policy outcome and execution authority. 4. **Enforce:** your application applies the current decision. Myaza does not operate your wallet or freeze customer accounts. 5. **Recover:** verify webhooks, deduplicate, reconcile uncertain requests and handle out-of-order events. 6. **Audit:** retain the reason, policy version, decision and your application's action without copying sensitive payloads into logs. For Travel Rule, complete information, a successful connection test and a low fraud score are not interchangeable with transfer clearance. See [wallet authorisation](https://trust.myaza.co/documentation/travel-rule-execution/markdown). ## Before Live Test permission denial, tenant/environment isolation, duplicate requests, timeouts, missing evidence, rejected decisions and webhook recovery. Use [Sandbox scenarios](https://trust.myaza.co/documentation/sandbox-testing/markdown) and the product testing guide. Ordinary Sandbox traffic may use configured pricing; do not assume all testing is free. Keep proposed policy changes separate from publication. Your authorised team approves risk appetite, country obligations and Live activation. Report local tests, Sandbox integration, real partner communication and Live acceptance separately. ## Read-only MCP The Core repository provides an optional `pnpm mcp:integration` process with documentation, v1 schema and example tools. It does not accept customer keys or execute transactions. This is a local integration aid, not a publicly hosted MCP endpoint. --- # Status and reliability Myaza Trust is monitored every minute from Johannesburg, London, Frankfurt, Amsterdam, Virginia and California by an independent monitoring service that runs outside our own infrastructure. ## Live status The [status page](https://status.myaza.co) shows the current state of each service, active incidents and scheduled maintenance. It stays up even if Myaza itself does not. You can subscribe there to be emailed when we post an incident or maintenance update. ## Availability record The [reliability record](/reliability) shows measured production availability over the last 7, 30, 60 and 90 days, with a daily timeline, each service's median response time, monthly history, incidents and the methodology. A day with failed checks and no declared incident is marked as such rather than hidden. For a procurement or security review, send the [shareable report](/reliability/report?period=90d), or download it as a PDF from that page. Any past period can be requested with `?from=YYYY-MM-DD&to=YYYY-MM-DD`. The same data is available as JSON: ```bash curl "https://trust.myaza.app/api/public/reliability?period=90d" curl -o report.pdf "https://trust.myaza.co/api/reliability/report?period=90d" ``` ## How availability is calculated Availability is the share of checks that succeeded, over complete UTC days. A check is one request from one region; a slow but successful answer counts as available and is reported separately. Checks that failed during scheduled maintenance are **not** excluded. Days before monitoring began are shown as having no data, never as available. These figures describe observed availability. They are not a service level agreement; contractual commitments, where agreed, are set out in your agreement. --- # Get configuration ``` GET /api/kyc/config ``` Returns the ID types enabled for your organisation in the current environment, along with the features available for each. Call this on startup to drive which ID options you present to users and which capture steps you run. **Authentication:** `Authorization: Bearer pk_…` (required). ## Request ```bash curl "https://trust.myaza.app/api/kyc/config" \ -H "Authorization: Bearer $MYAZA_KEY" ``` ## Response `200 OK` ```json { "environment": "PRODUCTION", "idTypes": [ { "country": "NG", "idType": "bvn", "features": { "documentVerification": false, "livenessCheck": true, "govDbCheck": true } }, { "country": "NG", "idType": "drivers-license", "features": { "documentVerification": true, "livenessCheck": true, "govDbCheck": true } } ], "branding": { "companyName": "Acme Inc.", "logo": "https://trust.myaza.app/api/kyc/branding/logo/ast_123", "primaryColor": "#5645F5" } } ``` ### Fields | Field | Type | Description | |---|---|---| | `environment` | string | `SANDBOX` or `PRODUCTION`, derived from your API key. | | `idTypes[]` | array | The ID types enabled for your org. Disabled ones are omitted entirely. | | `idTypes[].country` | string | ISO-3166 alpha-2 country code: a government-database market (`NG`, `GH`, `KE`, `ZA`, `CI`) or any country enabled for [global document verification](https://trust.myaza.co/documentation/id-types/markdown). | | `idTypes[].idType` | string | The ID type identifier to send to [`POST /verify`](https://trust.myaza.co/documentation/api-create-verification/markdown). | | `idTypes[].features.documentVerification` | boolean | Whether document image capture is enabled for this ID type. | | `idTypes[].features.livenessCheck` | boolean | Whether a liveness/selfie step is enabled. | | `idTypes[].features.govDbCheck` | boolean | Whether the ID is checked against the government database. | | `branding` | object | Your organisation's branding, set under **Settings → Organization**. May be omitted if nothing is configured. | | `branding.companyName` | string | Your organisation's display name. | | `branding.logo` | string | Absolute, public URL of your org logo. The [SDKs](https://trust.myaza.co/documentation/sdks/markdown) render this when `appearance.logo` is set to `'default'`. Omitted if no logo is uploaded. | | `branding.primaryColor` | string | Your org's brand colour (hex), if set. | Only enabled ID types are returned. If an ID type you expect is missing, enable it under **Settings → Organization → ID Types** (or ask Myaza to enable it for your account). See [Countries & ID types](https://trust.myaza.co/documentation/id-types/markdown) for the catalogue of supported values. --- # Upload media ``` POST /api/kyc/upload ``` Uploads a single capture file (document image, selfie, or video) and returns a `mediaId`. You then reference these IDs in [`POST /verify`](https://trust.myaza.co/documentation/api-create-verification/markdown). Number-only verifications (e.g. BVN) don't require any upload. **Authentication:** `Authorization: Bearer pk_…` (required). **Content type:** `multipart/form-data`. **Production note:** like verify, this endpoint requires an [approved business](https://trust.myaza.co/documentation/environments/markdown#production) for `pk_live_` keys. ## Form fields | Field | Required | Description | |---|---|---| | `file` | yes | The raw file bytes. | | `type` | yes | What the file is: one of the values below. | ### Allowed `type` values `document_front`, `document_back`, `selfie`, `document_front_video`, `document_back_video`, `liveness_video`, `proof_of_address`, `business_document`, `address_photo`, `auth_reference` `auth_reference` is the reference face photo for a biometric-authentication session: upload it here, then pass its `mediaId` as `faceReferenceMediaId` on [`POST /api/kyc/sessions`](https://trust.myaza.co/documentation/api-create-session/markdown). It is never sent by an applicant mid-flow. ### Allowed MIME types `image/jpeg`, `image/png`, `image/webp`, `video/webm`, `video/mp4`. Proof-of-address and business documents also accept `application/pdf`. ### Limits Maximum file size is **25 MB**. Uploaded files are held with a short TTL and are only retained permanently once referenced by a verification. Orphaned uploads are discarded automatically. ## Request ```bash curl "https://trust.myaza.app/api/kyc/upload" \ -H "Authorization: Bearer $MYAZA_KEY" \ -F "type=document_front" \ -F "file=@./id-front.jpg" ``` ## Response `200 OK` ```json { "mediaId": "media_01j9abc..." } ``` Pass the returned IDs to verify under `mediaIds`, keyed by capture slot: ```json { "mediaIds": { "documentFront": "media_01j9abc...", "selfie": "media_01j9def..." } } ``` ## Errors | Status | Body | Cause | |---|---|---| | `400` | `{ "error": "file is required" }` | No `file` field sent. | | `400` | `{ "error": "type must be one of …" }` | Missing or invalid `type`. | | `400` | `{ "error": "mimeType one of …" }` | Unsupported MIME type. | | `400` | `{ "error": "File too large (max 25MB)" }` | File exceeds the size cap. | | `401` | `{ "error": "Invalid API key" }` | Auth failed. | See [Errors](https://trust.myaza.co/documentation/errors/markdown) for the complete reference. --- # Create a verification ``` POST /api/kyc/verify ``` Submits an identity for verification. The request returns immediately with `202 Accepted` and a `verificationId`; the actual verification runs asynchronously. Receive the outcome via [webhooks](https://trust.myaza.co/documentation/webhooks/markdown) or by polling [`GET /status/:id`](https://trust.myaza.co/documentation/api-verification-status/markdown). This call is also what fires [`verification.started`](https://trust.myaza.co/documentation/webhooks/markdown#when-the-applicant-submits). That is the submission event: it fires here, before any check has run, and is how your backend learns that somebody finished the flow. **Authentication:** `Authorization: Bearer pk_…` (required). **Content type:** `application/json`. **Production note:** `pk_live_` keys require an [approved business](https://trust.myaza.co/documentation/environments/markdown#production). ## Request body | Field | Type | Required | Description | |---|---|---|---| | `country` | string | yes | ISO-3166 alpha-2 country code: a government-database market (`NG`, `GH`, `KE`, `ZA`, `CI`) or any country enabled for [global document verification](https://trust.myaza.co/documentation/id-types/markdown). | | `idType` | string | yes | ID type identifier, e.g. `bvn`. See [Countries & ID types](https://trust.myaza.co/documentation/id-types/markdown). | | `idNumber` | string | no* | The ID number. Required for number-based IDs; omit for pure document flows. | | `userData` | object | no | Applicant-provided details to cross-check: `firstName`, `lastName`, `dateOfBirth`. | | `mediaIds` | object | no | `mediaId`s from [`POST /upload`](https://trust.myaza.co/documentation/api-upload/markdown). Keys: `documentFront`, `documentBack`, `selfie`, `documentFrontVideo`, `documentBackVideo`, `livenessVideo`. | | `externalUserId` | string | no | **Your** stable reference for the person being verified (your user id). It is returned as `externalUserId`, links the resulting [entity](https://trust.myaza.co/documentation/identity-hub/markdown), and rides every [webhook](https://trust.myaza.co/documentation/webhooks/markdown#your-reference-on-every-event) about this verification. Strongly recommended. | | `metadata` | object | yes | Request metadata (see below). Any extra keys are **your customer metadata**: bounded (16 KiB, four levels, 100 keys), stored apart from Myaza's own fields, and echoed in responses and on every webhook about this verification. Use opaque correlation values (a loan id, an account id), never credentials or unnecessary personal data. | | `metadata.requestId` | string | yes | **Your** unique idempotency key for this request. | | `metadata.userId` | string | no | Legacy location for your user reference. Prefer the top-level `externalUserId`; when both are present the top-level field wins. | | `metadata.device` | object | no | Free-form device metadata; forwarded to the dashboard. | | `metadata.endUser` | object | no | Secret keys only. Your user's own connection, when you call this endpoint from your backend: `ipAddress`, `userAgent` and optionally `acceptLanguage`. See [Calling from your backend](#calling-from-your-backend). | \* Whether `idNumber` and which `mediaIds` are needed depends on the ID type. Check its `features` from [`GET /config`](https://trust.myaza.co/documentation/api-config/markdown). ### Optional header | Header | Description | |---|---| | `X-SDK-Version` | The SDK version making the call; recorded with the verification for support. | ## Request ```bash curl "https://trust.myaza.app/api/kyc/verify" \ -H "Authorization: Bearer $MYAZA_KEY" \ -H "Content-Type: application/json" \ -d '{ "country": "NG", "idType": "drivers-license", "idNumber": "ABC12345", "userData": { "firstName": "John", "lastName": "Doe", "dateOfBirth": "1990-01-01" }, "mediaIds": { "documentFront": "media_01j9abc...", "selfie": "media_01j9def..." }, "externalUserId": "user_42", "metadata": { "requestId": "order_1001", "loanId": "loan_20191" } }' ``` ## Response `202 Accepted` ```json { "verificationId": "ver_01j9...", "status": "processing", "externalUserId": "user_42", "metadata": { "loanId": "loan_20191" } } ``` `externalUserId` and `metadata` come back exactly as stored, so a retry of the same `requestId` returns the same values. The verification is now queued. Track it via its `verificationId`. `processing` is the same word [Get verification status](https://trust.myaza.co/documentation/api-verification-status/markdown) returns for this id a moment later. There is one status vocabulary across the whole API, so nothing changes meaning between the response that accepts a submission and the one that reports on it. ## Calling from your backend When you call this endpoint from your server with a secret key, the request reaches us from **your server**, not your user. We don't treat your server as your user's device: its address and user agent are kept for the record only, and no Device Intelligence signals are raised from them. To have your user's device and network analysed (country, network type, VPN and Tor checks, blocklists), pass their connection as you received it: ```json "metadata": { "requestId": "order_1001", "endUser": { "ipAddress": "102.89.34.10", "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) ...", "acceptLanguage": "en-NG,en;q=0.9" } } ``` - Send the address your own server saw for your user, not your server's address. - A value we can't read (for example an IP address that isn't valid) is ignored. It never makes the request fail. - `endUser` is ignored on a publishable key. Those calls come from your user's own device, so we already see their connection. - Without `endUser` or a device fingerprint, Device Intelligence has nothing to analyse, and it is not charged. ## Idempotency `metadata.requestId` is your idempotency key. If you submit a request with a `requestId` that already exists **for your organisation**, the API returns the existing verification instead of creating a new one: ```json { "verificationId": "ver_01j9...", "status": "approved" } ``` Always send a stable `requestId` per logical verification so retries (timeouts, network errors) never create duplicates or double-charge you. Reusing another organisation's `requestId` is not possible: a collision across orgs returns `403`. ## Errors | Status | Body | Cause | |---|---|---| | `400` | `{ "error": "Invalid request body", "message": "…" }` | Validation failed (bad country, missing `requestId`, etc.). | | `401` | `{ "error": "Invalid API key" }` | Auth failed. | | `403` | `{ "error": "Forbidden" }` | The `requestId` belongs to another organisation. | | `403` | `{ "error": "business_not_approved", … }` | Production key but business not approved. | | `429` | `{ "error": "Too many requests, please try again later." }` | [Rate limit](https://trust.myaza.co/documentation/rate-limits/markdown) exceeded. | > An accepted request (`202`) can still end in a `failed`, `not_found`, or `error` status later, for example insufficient credit, an ID not found in the government database, or a face mismatch. Those outcomes arrive via status/webhook, not as HTTP errors, and each carries a `reason` plus a stable `reasonCode`. See the [verification lifecycle](https://trust.myaza.co/documentation/verifications/markdown) and the [failure reason codes](https://trust.myaza.co/documentation/verifications/markdown#failure-reason-codes). --- # Get verification status ``` GET /api/kyc/status/:verificationId ``` Returns the **minimal** lifecycle state of a verification. `status` is the merged top-line (`not_started`, `in_progress`, `processing`, `in_review`, `awaiting_resubmission`, `approved`, `declined`, `abandoned`, `expired`, `error`) and `checkStatus` is what the automated checks found (`pending`, `verified`, `failed`, `not_found`, `error`). Act on `status`; keep `checkStatus` as the explanation. The two differ whenever your workflow or a reviewer decided something the checks alone would not have, for example `processing` with `checkStatus: "verified"` while your workflow waits on a screen. Before submission there is no `checkStatus`, because no checks have run. See [Status and checkStatus](https://trust.myaza.co/documentation/verifications/markdown#status-and-checkstatus) for every combination. This endpoint is **publishable-safe**: it can be called with the `pk_` key that ships in your SDK, so it deliberately returns **no PII, no match scores, and no result data**. To read the full result + extracted biodata, use [Get verification result](https://trust.myaza.co/documentation/api-verification-result/markdown) with a **secret** (`sk_`) key from your backend. [Webhooks](https://trust.myaza.co/documentation/webhooks/markdown) are the recommended way to learn about completion without polling. **Authentication:** `Authorization: Bearer pk_…` or `sk_…` (required). A verification is only visible to the organisation that created it. ## Request ```bash curl "https://trust.myaza.app/api/kyc/status/ver_01j9..." \ -H "Authorization: Bearer $MYAZA_PUBLISHABLE_KEY" ``` ## Responses Every response includes `verificationId`, `status`, `createdAt`, your `externalUserId` (so a publishable key can tell which user an id belongs to; your `metadata` stays on the [secret-key result](https://trust.myaza.co/documentation/api-verification-result/markdown)), and the workflow attribution pair `workflowId` / `workflowVersion` (both `null` when the SDK was configured with plain props instead of a [workflow](https://trust.myaza.co/documentation/workflows/markdown)), and `attempt` with its `submittedAt`. A verification you [send back or verify again](https://trust.myaza.co/documentation/api-verify-again/markdown) keeps its id: when the applicant resubmits, `attempt` counts up and `submittedAt` and `completedAt` describe their latest attempt. ### Why a verification is still `processing` `processing` means "no answer yet", and it covers both the eight seconds we spend reading a document and the eight days an [address presence watch](https://trust.myaza.co/documentation/address-intelligence/markdown#presence-verification) runs. Those read identically on a row, so **`waitingOn`** says which: | `waitingOn` | The decision is held on | |---|---| | `null` | Nothing. The checks are simply still running. | | `"screening"` | A sanctions, PEP or adverse-media screen. | | `"key_people"` | The directors and owners on a business application finishing their own verifications. | | `"address_presence"` | A presence watch confirming the person lives at the address. Days, not seconds. | It is only ever set while a decision run is genuinely parked, and it clears the moment the run continues. Values are add-only: treat one you do not recognise as a check you have not learned about yet, rather than mapping it to a known one. ### `processing` / `approved` ```json { "verificationId": "ver_01j9...", "status": "approved", "checkStatus": "verified", "externalUserId": "user_42", "workflowId": "wf_AbC123dEf456", "workflowVersion": 3, "attempt": 1, "createdAt": "2026-04-27T12:00:00.000Z", "submittedAt": "2026-04-27T12:00:00.000Z", "completedAt": "2026-04-27T12:00:05.000Z" } ``` > `workflowVersion` is the published version that **actually ran**. Publishing a > workflow overwrites its live configuration in place, so keep the pair if you need > to explain later exactly which rules a submission went through. The workflow's > human-readable name is on the [full result](https://trust.myaza.co/documentation/api-verification-result/markdown); this > endpoint stays minimal. > An `approved` status confirms the person was accepted, but carries no identity > data. Fetch the [full result](https://trust.myaza.co/documentation/api-verification-result/markdown) with a secret key to read > the biodata and facial-match score. > `approved` with `checkStatus: "failed"` is not a contradiction. It means the > person was accepted despite what the checks found, by a reviewer or by one of your > workflow rules, and `reason`/`reasonCode` below say what they were accepted > despite. Branch on `status`; store `checkStatus` alongside it. ### `declined` / `error` `declined` means the person was not accepted, either because the checks did not pass or because a reviewer declined them. `error` means a system problem occurred on our side and you were not charged. Both include a human-readable `reason` and a stable `reasonCode` you can branch on. See [failure reason codes](https://trust.myaza.co/documentation/verifications/markdown#failure-reason-codes). A `reason` is present whenever the checks did not pass, even on a verification a reviewer later approved. ```json { "verificationId": "ver_01j9...", "status": "declined", "checkStatus": "failed", "reason": "The selfie does not match the photo on the government record for this ID. Take it again in good light, looking straight at the camera. If it still does not match, check that the ID number entered is the right one.", "reasonCode": "selfie_mismatch", "createdAt": "2026-04-27T12:00:00.000Z", "completedAt": "2026-04-27T12:00:05.000Z" } ``` ### `declined` with `identity_not_found` An ID number the government database does not hold is a decline like any other; the `reasonCode` is what tells it apart from a failed face match. There is no separate `not_found` status. ```json { "verificationId": "ver_01j9...", "status": "declined", "checkStatus": "not_found", "reason": "The BVN number was not found in the government database.", "reasonCode": "identity_not_found", "createdAt": "2026-04-27T12:00:00.000Z", "completedAt": "2026-04-27T12:00:05.000Z" } ``` ## Errors | Status | Body | Cause | |---|---|---| | `404` | `{ "error": "Verification not found" }` | Unknown ID, or it belongs to another organisation. | | `401` | `{ "error": "Invalid API key" }` | Auth failed. | ## Polling guidance If you must poll, do so with backoff (e.g. every 2–3 seconds, widening over time) and stop once `status` is no longer `pending`. Prefer [webhooks](https://trust.myaza.co/documentation/webhooks/markdown) for production. Once complete, fetch the [full result](https://trust.myaza.co/documentation/api-verification-result/markdown) from your backend. --- # Get verification result ``` GET /api/kyc/verifications/:id ``` Returns the **full** result of a verification, including the extracted biodata, ID number, and facial-match score. This is the backend counterpart to the minimal [status endpoint](https://trust.myaza.co/documentation/api-verification-status/markdown). **Authentication:** `Authorization: Bearer sk_…`, a **secret key only**. Calling this with a publishable (`pk_`) key returns `403 secret_key_required`. The verification is scoped to the key's organisation **and** environment (a sandbox secret key cannot read a production verification). > **Never call this from client code.** Identity data must only be fetched from > your backend with a secret key. ## Request ```bash curl "https://trust.myaza.app/api/kyc/verifications/ver_01j9..." \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" ``` ## Responses Every response includes `verificationId`, `status`, `checkStatus`, `country`, `idType`, `createdAt`, and `workflow`, which [workflow](https://trust.myaza.co/documentation/workflows/markdown) was responsible for the submission. `status` is the outcome to act on; `checkStatus` is what the automated checks found. They differ whenever your workflow or a reviewer decided something the checks alone would not have. See [Status and checkStatus](https://trust.myaza.co/documentation/verifications/markdown#status-and-checkstatus). ### `approved` ```json { "verificationId": "ver_01j9...", "status": "approved", "checkStatus": "verified", "externalUserId": "user_42", "metadata": { "loanId": "loan_20191" }, "country": "NG", "idType": "bvn", "createdAt": "2026-04-27T12:00:00.000Z", "completedAt": "2026-04-27T12:00:05.000Z", "workflow": { "id": "wf_AbC123dEf456", "name": "Standard KYC", "version": 3 }, "result": { "idNumber": "12345678901", "idNumberMasked": "1234•••901", "firstName": "JOHN", "lastName": "DOE", "middleName": "A", "dateOfBirth": "1990-01-01", "gender": "Male", "dataMatch": true, "facialMatch": { "match": true, "confidence": 85 }, "facialMatchSource": "gov_record" } } ``` `facialMatch` is `null` when no liveness/selfie check was part of the flow. `facialMatchSource` says which photo the selfie was compared with: `gov_record`, `chip`, or `document` when the workflow [allows the photo printed on the document](https://trust.myaza.co/documentation/workflows/markdown#matching-the-selfie-to-the-photo-on-a-document) and no stronger photo existed. `workflow` is `null` when the SDK was configured with plain props instead of a workflow. Its `version` is the published version that **actually ran**: publishing overwrites a workflow's live configuration in place, so `id` alone stops describing what happened after your next publish. Inspect the exact configuration a version used under **Workflows → Version history**. ### Checks that did not pass (`checkStatus`: `failed`, `not_found` or `error`) Carries the human-readable `reason` and stable `reasonCode` (same values as the [status endpoint](https://trust.myaza.co/documentation/api-verification-status/markdown) and [webhooks](https://trust.myaza.co/documentation/webhooks/markdown)). ```json { "verificationId": "ver_01j9...", "status": "declined", "checkStatus": "failed", "externalUserId": "user_42", "metadata": { "loanId": "loan_20191" }, "country": "NG", "idType": "bvn", "createdAt": "2026-04-27T12:00:00.000Z", "completedAt": "2026-04-27T12:00:05.000Z", "reason": "The selfie does not match the photo on the government record for this ID. Take it again in good light, looking straight at the camera. If it still does not match, check that the ID number entered is the right one.", "reasonCode": "selfie_mismatch" } ``` ### After a decision When the verification has been decided, by a reviewer in your dashboard, by your backend through [`POST /verifications/:id/review`](https://trust.myaza.co/documentation/api-review-verification/markdown) or by your workflow, `review` holds the current decision a person made and `statusHistory` lists every change, newest first: ```json { "verificationId": "ver_01j9...", "status": "declined", "checkStatus": "verified", "review": { "decision": "DECLINED", "reviewedAt": "2026-04-28T09:14:00.000Z", "reviewedById": "api_key:ak_01j9..." }, "statusHistory": [ { "status": "declined", "previousStatus": "in_review", "source": "api", "changedBy": { "type": "api_key", "id": "ak_01j9..." }, "changedAt": "2026-04-28T09:14:00.000Z", "decision": "DECLINED", "outcome": null, "runId": null, "resubmitSteps": [] }, { "status": "in_review", "previousStatus": "processing", "source": "workflow", "changedBy": { "type": "workflow", "id": "wf_AbC123dEf456" }, "changedAt": "2026-04-27T12:00:07.000Z", "decision": null, "outcome": "review", "runId": "wfr_01j9...", "resubmitSteps": [] } ] } ``` Each entry has the same fields as one [`verification.status_updated`](https://trust.myaza.co/documentation/webhooks/markdown#status-changes) webhook, so what you store from the webhook and what you read here agree. The reviewer's internal note is never included. ### Attempts A verification you [send back or verify again](https://trust.myaza.co/documentation/api-verify-again/markdown) keeps its id. Once the applicant resubmits, the result describes their latest attempt, `attempt` counts up, and each earlier attempt is listed under `attempts`, oldest first: ```json { "verificationId": "ver_01j9...", "status": "approved", "attempt": 2, "submittedAt": "2026-04-29T10:02:00.000Z", "completedAt": "2026-04-29T10:02:06.000Z", "attempts": [ { "attempt": 1, "checkStatus": "failed", "reason": "The selfie does not match the photo on the government record for this ID. Take it again in good light, looking straight at the camera.", "reasonCode": "selfie_mismatch", "assuranceLevel": null, "decision": "RESUBMISSION", "resubmitSteps": ["liveness"], "submittedAt": "2026-04-27T12:00:00.000Z", "completedAt": "2026-04-27T12:00:05.000Z", "resubmittedAt": "2026-04-29T10:02:00.000Z" } ] } ``` An earlier attempt carries what it concluded and why, never its biodata or media. The current result is the one to act on, and `statusHistory` still lists every decision across all attempts. ## Result fields | Field | Type | Description | |---|---|---| | `externalUserId` | string \| null | Your reference for the subject, exactly as supplied: the top-level `externalUserId` on the submission (or the legacy `userId` / `metadata.userId`), or the session it was minted with. | | `metadata` | object \| null | Your customer metadata, echoed unchanged. | | `workflow` | object \| null | `{ id, name, version }`: the workflow that drove the submission, its current name, and the published version that ran. `null` for prop-configured SDK mounts. | | `review` | object \| null | The current decision a person or your backend made: `{ decision, reviewedAt, reviewedById }`. `reviewedById` is a user id, or `api_key:` for a decision made through the API. `null` when nobody has decided. | | `statusHistory` | array | Every change to `status`, newest first, whoever made it. Empty until something decides the verification. See [After a decision](#after-a-decision). | | `attempt` | number | Which attempt this result describes. `1` until the verification is sent back and the applicant resubmits. | | `submittedAt` | string | When this attempt was submitted. `completedAt` is timed from it. | | `attempts` | array | The earlier attempts, oldest first. Empty until the verification is redone. See [Attempts](#attempts). | | `result.idNumber` | string \| null | The plaintext ID number. | | `result.idNumberMasked` | string | The ID number, masked (e.g. `1234•••901`). | | `result.firstName` | string \| null | Given name from the source record. | | `result.lastName` | string \| null | Family name from the source record. | | `result.middleName` | string \| null | Middle name, if available. | | `result.dateOfBirth` | string \| null | Date of birth (`YYYY-MM-DD`). | | `result.gender` | string \| null | Gender from the source record. | | `result.dataMatch` | boolean \| null | Whether submitted biodata matched the source record. | | `result.facialMatch` | object \| null | `{ match: boolean, confidence: number }`, or `null` if no facial check ran. | | `result.facialMatchSource` | string \| null | Which photo the selfie was compared with: `gov_record`, `chip` or `document` (the photo printed on the ID, weaker evidence). `null` if no facial check ran. | | `result.facialMatchSkipped` | string \| null | Why no facial check ran when one was expected: `no_face_on_document` when the photo printed on the document was the only photo to compare with and no face could be read on it (your workflow kept the verification for review). `null` otherwise. | ## Captured media The images and videos captured during the flow are fetched per-kind from `GET /api/kyc/verifications/:id/media/:kind` (`selfie`, `document-front`, `document-back`, `liveness-video`, `document-front-video`, `document-back-video`, and `document-portrait`, the portrait cut from the document when the selfie was matched to it), also **secret key only**. These same URLs are delivered in the `media` object of verification [webhooks](https://trust.myaza.co/documentation/webhooks/markdown). On a run that verifies several IDs, each check in `multiId.checks` carries its own `recordPhoto` and `documentPortrait` as media URLs (`id-check--photo` and `id-check--portrait`), and each entry in `supportingDocuments` carries a `url` for its file. These links follow your workflow's photo switches and returned-data settings, the same as the webhook: a photo you have switched off comes back as `null`. ## Errors | Status | Body | Cause | |---|---|---| | `403` | `{ "error": "secret_key_required" }` | A publishable (`pk_`) key was used. Use a secret key. | | `404` | `{ "error": "Verification not found" }` | Unknown ID, wrong environment, or another organisation's verification. | | `401` | `{ "error": "Invalid API key" }` | Auth failed. | --- # Face match ``` POST /api/kyc/face/compare POST /api/kyc/face/upload GET /api/kyc/face/checks GET /api/kyc/face/checks/:id ``` Compare two photos of a face and learn, synchronously, whether they show the same person. This is the server-to-server counterpart of the dashboard's Face match tab: same engine, same verdicts, same history (API checks appear there labelled "API"). **Authentication:** `Authorization: Bearer sk_…` (secret keys only, for the whole surface). The inputs are biometric data and the verdict is a compliance record, so this never belongs in client-side code. **Production note:** `sk_live_` keys require an [approved business](https://trust.myaza.co/documentation/environments/markdown#production). ## Compare ``` POST /api/kyc/face/compare ``` One call: send both photos inline and get the verdict back. Each photo is **exactly one** of an image field (URL, data URI, or base64) or a `mediaId` from the optional upload endpoint below. | Field | Required | Description | |---|---|---| | `imageA` | one of | The first photo, inline: an `https://` URL we fetch, a `data:` URI, or base64-encoded bytes. JPEG, PNG or WebP; the format is detected from the bytes, never from a label. | | `imageB` | one of | The second photo, same forms. | | `mediaAId` | one of | Alternative to `imageA`: a `mediaId` from `POST /face/upload`. | | `mediaBId` | one of | Alternative to `imageB`. | | `consent` | yes | Must be `true`: you attest the people shown have consented to the comparison. | | `sandboxOutcome` | no | Sandbox only, see below. Ignored on live keys. | ```bash curl -X POST https://trust.myaza.app/api/kyc/face/compare \ -H "Authorization: Bearer sk_live_…" \ -H "Content-Type: application/json" \ -d '{ "imageA": "https://cdn.example.com/enrolment/selfie-42.jpg", "imageB": "data:image/jpeg;base64,/9j/4AAQ…", "consent": true }' ``` **Size limits.** URLs and uploads accept photos up to 25 MB. Base64 travels inside the JSON body, which is capped at 10 MB, so base64 photos top out around 7 MB of image; send anything bigger by URL or upload. **URL fetching.** Image URLs must be http(s) and resolve to a public address; anything that points into a private network is refused with `image_url_not_allowed`. Redirects are followed (up to 3), each hop checked the same way. The response: ```json { "faceCheck": { "id": "fc_…", "verdict": "match", "match": true, "confidence": 91, "threshold": 80, "reviewThreshold": 60, "chargedAmount": "0.0300", "createdAt": "2026-08-13T12:00:00.000Z" } } ``` ### The verdict `verdict` is three-way, judged against your organisation's thresholds (the pass mark and review floor, managed on the dashboard's Face match tab): | Verdict | Meaning | |---|---| | `match` | Similarity at or above your pass mark. | | `review` | Similarity between your review floor and pass mark. The score alone cannot decide; a human should compare the photos. | | `no_match` | Similarity below your review floor. | | `no_result` | The comparison could not run (for example, no detectable face). Returned with HTTP 502 and **nothing is charged**. | There is deliberately no per-request threshold: the lines that judge a verdict are your organisation's recorded policy, and every check records the lines that judged it. If part of your flow needs a stricter bar, branch on `confidence` in your own code; the recorded verdict stays consistent. ## Uploading first (optional) ``` POST /api/kyc/face/upload ``` When the bytes are already on your side and you would rather not inline them, or you want to reuse one photo across several checks, upload it once and pass the `mediaId`. Multipart form with a single `file` field (jpeg, png or webp, up to 25 MB); each call returns one `mediaId`. ```bash curl -X POST https://trust.myaza.app/api/kyc/face/upload \ -H "Authorization: Bearer sk_live_…" \ -F "file=@photo-a.jpg" # → { "mediaId": "med_…" } ``` ## Re-read a check ``` GET /api/kyc/face/checks/:id ``` Returns the same `faceCheck` object for any past check belonging to your organisation and environment. ## History ``` GET /api/kyc/face/checks ``` Paginated face-check history for your organisation and the key's environment, newest first. Query parameters: `verdict` (`match` / `review` / `no_match` / `no_result`), `from` / `to` (ISO dates), `page`, `pageSize` (max 100). Rows carry the same fields as the compare response plus `initiatedVia` (`api` or `dashboard`). ## Deleting a check ``` DELETE /api/kyc/face/checks/:id ``` Removes a check from your history. It is a soft delete, with the same body and response shape as the other record deletions; see [Delete, expire & cancel](https://trust.myaza.co/documentation/api-manage-records/markdown#delete-a-face-check). ## Sandbox behaviour Sandbox keys (`sk_test_`) always return **simulated** verdicts; the real comparison engine runs in production only. With no `sandboxOutcome` the verdict is a simulated `match`; pass `review`, `no_match` or `no_result` to see the others. Simulated scores are placed inside your organisation's own thresholds, so they behave exactly as a live score would. Sandbox checks are free, and sandbox photos are replaced with a placeholder after 3 days. ## Billing Production checks are billed per comparison at your facial comparison price (see [your pricing](https://trust.myaza.co/documentation/environments/markdown)). A `no_match` still charges, because the check ran and answered. A `no_result` never charges. ## Errors | Status | Code | Meaning | |---|---|---| | 400 | `same_photo` | Both sides are the same photo (same media id, or identical inline images). | | 400 | `media_unreadable` | One of the photos could not be read; send both again. | | 400 | `invalid_image` | An inline image is neither an https URL, a data URI, nor valid base64. | | 400 | `unsupported_media_type` | The bytes are not a JPEG, PNG or WebP photo. | | 400 | `image_too_large` | The photo exceeds 25 MB. | | 400 | `image_url_not_allowed` | The image URL is not http(s), or resolves to a non-public address. | | 400 | `image_fetch_failed` | The image URL could not be fetched (unreachable, error status, empty body, or too many redirects). | | 402 | `insufficient_credits` | Your balance cannot cover the check. | | 403 | `spot_checks_disabled` | Face checks are not enabled for your organisation. | | 403 | `secret_key_required` | A publishable key was used; this surface is secret-key only. | | 429 | `rate_limited` | Hourly face-check limit reached; retry later. | | 502 | `no_result` | The comparison could not run; nothing charged. | --- # Create a session ``` POST /api/kyc/sessions ``` Mints a hosted verification link for **one applicant** from one of your published [workflows](https://trust.myaza.co/documentation/workflows/markdown), entirely server to server. You get back a URL that is theirs alone: send it by email, SMS, WhatsApp, or render it as a QR, and the applicant completes the flow on our hosted page with no SDK integration on your side. The session **is** the link. It carries one verification, it is traceable to your own reference for the applicant, and once it is submitted it is spent. Forwarding it to a second person does not work. This is the API twin of the dashboard's "Create session" action; the two accept the same request and mint the same thing. **Authentication:** `Authorization: Bearer sk_…` (**secret key required**). The response contains a live verification credential and the request may carry applicant details, so this endpoint is backend-only. A publishable key receives `403 secret_key_required`. **Content type:** `application/json`. **Production note:** `sk_live_` keys require an [approved business](https://trust.myaza.co/documentation/environments/markdown#production). Looking for one URL many people can open, each visit starting its own verification? That is a workflow's [hosted link](https://trust.myaza.co/documentation/workflows/markdown#2-share-a-hosted-link). This endpoint is the opposite shape: you know the applicant, and you want a link that is theirs. ## Request body | Field | Type | Required | Description | |---|---|---|---| | `workflowId` | string | yes | The **published** workflow to run, e.g. `wf_AbC123dEf456`. Drafts and archived workflows are refused. | | `externalUserId` | string | no | **Your** reference for the subject: your user id on an individual flow, the business on a KYB one. It is what links the resulting verification and [entity](https://trust.myaza.co/documentation/identity-hub/markdown) back to your records, and what a returning applicant resumes by. Strongly recommended. | | `metadata` | object | no | Your own correlation fields for this applicant (a loan id, an account id). Bounded (16 KiB, four levels, 100 keys), stored on the session, inherited by the verification it produces and carried on every [webhook](https://trust.myaza.co/documentation/webhooks/markdown#your-reference-on-every-event) about it. | | `userData` | object | no | What you already know about the subject, to prefill the flow: `firstName`, `lastName`, `dateOfBirth`, and for KYB flows `businessName`. Prefill, never assertion: the applicant confirms or corrects every value, so none of it is treated as verified. `email` is the applicant's address: nothing in the flow asks for it, and it is where we write when you have the applicant [emailed about a decision](https://trust.myaza.co/documentation/api-review-verification/markdown#emailing-the-applicant). When you leave it out, the `email` you send the link to stands in for it. | | `business` | object | no | For KYB flows, the company where you already know it: `country` (ISO-2), `registrationNumber`, `registrationName`. The applicant lands on a picked-company card instead of a search box, and the register remains the source of truth. | | `email` | string | no | Email the link to the applicant on your behalf. Optional: deliver it your own way and omit this. | | `faceReferenceMediaId` | string | see note | Biometric-authentication workflows only: the mediaId of a reference face photo (uploaded via [`POST /api/kyc/upload`](https://trust.myaza.co/documentation/api-upload/markdown) with type `auth_reference`). The applicant's live selfie is matched against this photo instead of a stored enrolment, so you can authenticate somebody you never enrolled through KYC. | A **biometric-authentication** workflow must name its subject at mint time: pass `externalUserId` for a user with an active biometric enrolment, or `faceReferenceMediaId` with a reference photo. Neither present is refused with `422 user_reference_required`; a reference without an enrolment with `422 not_enrolled`. A **biometric-enrolment** workflow always requires `externalUserId` (the template binds a face to a named record). This is checked when the session is created, so a link that could only fail at submit is never minted. ## Request ```bash curl "https://trust.myaza.app/api/kyc/sessions" \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflowId": "wf_AbC123dEf456", "externalUserId": "user_42", "metadata": { "loanId": "loan_20191" }, "userData": { "firstName": "John", "lastName": "Doe" }, "email": "john@example.com" }' ``` ## Response `201 Created` ```json { "session": { "sessionId": "cmt1gfjce05g547grqbmjsiqf", "url": "https://trust.myaza.co/verify/pXk3...", "shortCode": "7K9F-2QHM", "expiresAt": "2026-08-21T12:00:00.000Z", "kind": "individual", "externalUserId": "user_42", "metadata": { "loanId": "loan_20191" }, "workflow": { "id": "wf_AbC123dEf456", "name": "Identity verification", "version": 3 } }, "delivery": { "sent": true } } ``` | Field | Description | |---|---| | `session.url` | The applicant's link. Treat it like a credential: whoever opens it verifies as this applicant. | | `session.shortCode` | A human-typable code for when a camera cannot scan a QR. Display only, never a credential. | | `session.expiresAt` | When the link stops accepting a **new** start: 24 hours for individual flows, 7 days for business (KYB) ones, unless the workflow sets its own session lifetime. A submitted verification is unaffected by expiry. | | `session.kind` | `individual` or `business`, following the workflow's subject type. | | `session.externalUserId`, `session.metadata` | Your reference and metadata, echoed exactly as stored. The verification this session produces, and every webhook about it, carry the same values. | | `delivery` | `null` when no `email` was given; `{ "sent": true }` or `{ "sent": false, "reason": "send_failed" }` otherwise. A failed send never fails the mint; the link in `session.url` works regardless. | The config the applicant walks is the workflow's published snapshot, frozen at mint time. Editing or re-publishing the workflow never changes a link that is already in someone's inbox. ## Tracking the session The `sessionId` doubles as the verification's id once the applicant submits, so the reference you hold from the start resolves from the start: - [`GET /status/:id`](https://trust.myaza.co/documentation/api-verification-status/markdown) reports the attempt live: `in_progress` with the current step and captured slots, then the full verification lifecycle after submission, and `abandoned` or `expired` if the applicant never finishes. - [Session webhooks](https://trust.myaza.co/documentation/webhooks/markdown) (`session.started`, `session.resumed`, `session.abandoned`, `session.expired`) push the same lifecycle to your backend, and the verification events follow once they submit. - Opening the `url` in a WebView or an iframe? The page tells its host what the applicant is doing, step by step, with nothing to install: see [Hosted link events](https://trust.myaza.co/documentation/hosted-events/markdown). - Sending the `url` yourself over WhatsApp, SMS or email? It previews as your request: your organisation's name and logo, the workflow's primary colour and "Verify your identity with {Your organisation}" in the title, the same way a [hosted link](https://trust.myaza.co/documentation/workflows/markdown) does. A spent or expired session previews as a neutral Myaza Trust card. ## Ending a session early A session you no longer want opened can be expired with `POST /api/kyc/sessions/:sessionId/expire`: the link stops working at once and you receive `session.expired`. To stop one reversibly instead, cancel it with `POST /api/kyc/sessions/:id/cancel` and uncancel it later. See [Delete, expire & cancel](https://trust.myaza.co/documentation/api-manage-records/markdown). ## Errors | Status | `error` | Meaning | |---|---|---| | `400` | `invalid_input` | The body failed validation; `message` names the first problem. | | `403` | `secret_key_required` | A publishable key was used. Call this from your backend with a secret key. | | `403` | `business_not_approved` | `sk_live_` before [KYB approval](https://trust.myaza.co/documentation/environments/markdown#production). Sandbox keys work from day one. | | `422` | `workflow_not_available` | The workflow is not published (or is suspended), so it cannot start a verification. | | `429` | `rate_limited` | Too many sessions minted this hour. Retry later, or contact support to raise your limit. | --- # Errors The API uses conventional HTTP status codes and returns a JSON body with an `error` field (and sometimes a `message`) on failure. ```json { "error": "business_not_approved", "message": "Your business must be approved …" } ``` ## Status codes | Status | Meaning | Typical causes | |---|---|---| | `200` | OK | Successful `GET`/`upload`. | | `202` | Accepted | Verification queued (see [create verification](https://trust.myaza.co/documentation/api-create-verification/markdown)). | | `400` | Bad request | Invalid body, bad/missing field, unsupported file type, file too large. | | `401` | Unauthorized | Missing `Authorization` header, or invalid/revoked API key. | | `403` | Forbidden | Production key without an approved business, a publishable key on a secret-only endpoint (`secret_key_required`), or a `requestId` owned by another org. | | `404` | Not found | Verification ID does not exist (or isn't yours). | | `429` | Too many requests | [Rate limit](https://trust.myaza.co/documentation/rate-limits/markdown) exceeded. | | `500` | Server error | E.g. pricing not configured for the requested country/ID type. | ## Common error bodies | `error` | Status | Meaning | |---|---|---| | `Missing or invalid Authorization header` | 401 | No Bearer token sent. | | `Invalid API key` | 401 | Key is unknown or revoked. | | `Invalid request body` | 400 | Verify payload failed validation; see `message` for details. | | `Forbidden` | 403 | The `requestId` belongs to a different organisation. | | `business_not_approved` | 403 | Production access requires an [approved business](https://trust.myaza.co/documentation/environments/markdown#production). | | `secret_key_required` | 403 | A publishable (`pk_`) key was used on a secret-only endpoint (full result or media). Use a secret (`sk_`) key from your backend. See [authentication](https://trust.myaza.co/documentation/authentication/markdown). | | `environment_mismatch` | 403 | The key's environment doesn't match the server it was sent to (e.g. a `pk_live_` key against the sandbox host). Use the base URL for the key's [environment](https://trust.myaza.co/documentation/environments/markdown). | | `Verification not found` | 404 | Unknown verification ID. | | `File too large (max 25MB)` | 400 | Upload exceeded the size cap. | | `Too many requests, please try again later.` | 429 | Slow down; see [rate limits](https://trust.myaza.co/documentation/rate-limits/markdown). | | `pricing_not_configured` | 500 | No price set for that country/ID type. Contact Myaza. | ## Asynchronous failures A `202 Accepted` from [create verification](https://trust.myaza.co/documentation/api-create-verification/markdown) only means the request was queued. The verification can still finish as `failed`, `not_found`, or `error`. Those are **not** HTTP errors: read them from [`GET /status/:id`](https://trust.myaza.co/documentation/api-verification-status/markdown) or the [`verification.*` webhooks](https://trust.myaza.co/documentation/webhooks/markdown). Each non-success outcome carries a human-readable `reason` plus a stable `reasonCode` you can branch on (e.g. `document_expired`, `selfie_mismatch`, `identity_not_found`). The full catalogue is in [failure reason codes](https://trust.myaza.co/documentation/verifications/markdown#failure-reason-codes). ## Health check ``` GET /api/kyc/health ``` A public, unauthenticated endpoint for uptime monitoring. ```json { "status": "ok" } ``` ## Handling errors well - Retry `429`, `502`, `503`, `504` and connection failures with backoff and the same idempotency key. Do not blindly retry every `500`: preserve its request ID and contact support if it persists. - Treat `4xx` (except `429`) as **non-retryable**: fix the request; retrying unchanged will fail again. - Always read the `error`/`message` fields; don't rely on status code alone. ## Server SDK errors (package preview) The server SDK is not yet available on the public npm registry. For current integrations, handle the HTTP status and error code as described above. When the package is published, its typed errors will let your backend branch without parsing messages: ```js import { AuthenticationError, IdempotencyConflictError, InsufficientCreditError, MyazaError, RateLimitError, ValidationError, } from '@myazahq/trust-sdk'; try { await myaza.transactions.retrieve('activity_123'); } catch (error) { if (error instanceof RateLimitError) { console.log('Retry after seconds:', error.retryAfterSeconds); } else if (error instanceof ValidationError || error instanceof IdempotencyConflictError) { console.error(error.code, error.details, error.requestId); } else if (error instanceof InsufficientCreditError) { console.log({ pending: error.pending, statusUrl: error.statusUrl }); } else if (error instanceof AuthenticationError) { console.error('Check the key type and environment.'); } else if (error instanceof MyazaError) { console.error(error.status, error.code, error.requestId); } } ``` Transaction and activity decisions fail closed when credit is unavailable; a late record is not an authorisation response. A paused customer risk assessment sets `InsufficientCreditError.pending` or returns `pending_credit`, keeps the same resource and resumes after funding. Do not submit a duplicate. --- # Rate limits The API is rate limited to protect platform stability. When you exceed the limit you receive `429 Too Many Requests`: ```json { "error": "Too many requests, please try again later." } ``` ## The limit | Scope | Limit | Window | |---|---|---| | Per client | 100 requests | 15 minutes | Responses include standard [`RateLimit`](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/) headers (draft-7), so you can read your remaining quota and reset time programmatically rather than guessing: ``` RateLimit-Limit: 100 RateLimit-Remaining: 84 RateLimit-Reset: 540 ``` ## Handling 429 - **Back off and retry.** Wait until the window resets (use the `RateLimit-Reset` seconds value), then retry. - **Use exponential backoff with jitter** if you don't read the headers. - **Reuse the same `requestId`** when retrying a verification so the retry is idempotent and never double-charges. - **Batch and pace** bulk work instead of bursting. > Limits may differ between sandbox and production and can be adjusted for your account. Contact Myaza if your production volume needs a higher ceiling. --- # Verify again ``` POST /api/kyc/verifications/:id/rerun ``` Asks the applicant to redo a **finished** verification: the same workflow, the same person reference, and a link to send them. Use it when a document has expired, a check failed on something fixable, or your own policy asks for periodic re-verification. This is the API twin of the dashboard's "Verify again" button, and the same operation as sending a verification back with [Decide a verification](https://trust.myaza.co/documentation/api-review-verification/markdown#sending-it-back). All of them run one core. **The verification keeps its id.** When the applicant resubmits, what they send becomes the next **attempt** of this same verification, not a second verification. What the earlier attempt found is kept rather than overwritten: it is listed under `attempts` on the [result](https://trust.myaza.co/documentation/api-verification-result/markdown#attempts) and shown on the verification's timeline in your dashboard. You hold one id per applicant per check, and your webhook handler keeps updating the record it already has. Calling it sets the verification's status to `awaiting_resubmission` and sends [`verification.status_updated`](https://trust.myaza.co/documentation/webhooks/markdown#status-changes). Any link sent earlier for the same verification stops working, so the applicant can only ever use the latest one. **Authentication:** `Authorization: Bearer sk_…` (**secret key required**). The response contains a live verification credential. A publishable key receives `403 secret_key_required`. **Content type:** `application/json`. **Production note:** `sk_live_` keys require an [approved business](https://trust.myaza.co/documentation/environments/markdown#production). ## Redo only what failed By default the applicant walks the whole flow again. Pass `steps` to narrow the redo to specific sections instead: a selfie that did not match does not require the applicant to re-photograph a perfectly good document. ```json { "steps": ["liveness"], "message": "Your selfie was too dark to match. Please retake it in good light." } ``` The applicant then walks **only** the ticked steps, with your message shown when they open the link. Everything they are not asked to redo is kept from the earlier attempt. Valid step names: | Step | What the applicant redoes | |---|---| | `id-type` | ID type selection | | `id-input` | Typing their ID number | | `document-capture` | Document photos | | `liveness` | Selfie and liveness | | `proof-of-address` | Proof of address | | `nfc` | Chip scan | | `email-verification` | Email confirmation | | `phone-verification` | Phone confirmation | | `business-details` | Business details (KYB) | | `business-key-people` | Directors and owners (KYB) | | `business-documents` | Business documents (KYB) | | `questionnaire` | Questionnaire | An unknown step name is refused with `400 invalid_steps` rather than ignored. A silently dropped typo would turn a targeted redo into a full one, which is the one failure you could not see happening. ## Email the link Pass `email` and we send the link to the applicant for you, in your organisation's name. The link is still returned in the response, and `delivery` tells you whether the email went out. Leave `email` out to deliver the link yourself: we hold no contact details for your users. ```json { "steps": ["liveness"], "email": "ada@example.com" } ``` ## Request body | Field | Type | Required | Description | |---|---|---|---| | `steps` | string[] | no | Sections to redo, from the table above. Absent or empty means the whole flow. | | `message` | string | no | Shown to the applicant on a narrowed flow, up to 500 characters. Write it for them, not for your logs. | | `policy` | string | no | `original` runs the workflow version the applicant walked the first time; `latest` runs today's published version. Absent follows the workflow's own setting, which is `original` unless you changed it. | | `email` | string | no | Send the link to this address as well as returning it. | ## Request ```bash curl "https://trust.myaza.app/api/kyc/verifications/ver_abc123/rerun" \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "steps": ["liveness"], "email": "ada@example.com" }' ``` ## Response `201 Created` ```json { "session": { "sessionId": "cmt1gfjce05g547grqbmjsiqf", "url": "https://trust.myaza.co/verify/pXk3...", "shortCode": "7K9F-2QHM", "expiresAt": "2026-08-22T12:00:00.000Z", "kind": "individual", "workflow": { "id": "wf_AbC123dEf456", "name": "Identity verification", "version": 3 } }, "retryOf": "ver_abc123", "attempt": 1, "steps": ["liveness"], "full": false, "delivery": { "emailed": true, "reason": null } } ``` | Field | Description | |---|---| | `session` | The link to send the applicant, the same shape [creating a session](https://trust.myaza.co/documentation/api-create-session/markdown) returns. `session.url` is a credential: whoever opens it verifies as this applicant. | | `retryOf` | The verification being redone. The applicant's resubmission is recorded against this id. | | `attempt` | The attempt the verification holds now. The applicant's resubmission becomes attempt `attempt + 1`. | | `steps` | The plan the link was minted with, in flow order. | | `full` | `true` when the whole flow will run, which is what absent or empty `steps` produces. | | `delivery` | When you passed `email`: `{ emailed, reason }`. `emailed: false` with `reason: "send_failed"` means the email could not be sent. The link still works, so send it yourself. Null when you passed no `email`. | ## When the applicant resubmits Their resubmission is checked like any submission and completes the **same** verification. `verification.completed` (or `verification.failed`) arrives with the same `verificationId` and `attempt` counted up, and [Get verification status](https://trust.myaza.co/documentation/api-verification-status/markdown) reports the new result. You are charged for the steps the applicant redoes, not for the ones they keep. A link stops working once it has been used, or once something newer replaces it. The applicant sees why when they submit: | Code | When | |---|---| | `verification_in_progress` | A resubmission for this verification is still being checked. | | `resubmission_superseded` | This verification has already been resubmitted. | | `resubmission_withdrawn` | The verification was decided, or sent back again, after this link was sent. Only the newest link works. | ## Errors | Status | Code | Meaning | |---|---|---| | 400 | `invalid_input` | The body is malformed, for example `email` is not a valid address. | | 400 | `invalid_steps` | A step name is not in the table above. The message lists the valid names. | | 404 | `verification_not_found` | No verification with that id in your organisation and environment. | | 409 | `verification_in_progress` | The verification has not finished. A pending check may still come back clean. | | 422 | `no_workflow` | The verification did not run a workflow, so there is no flow to repeat. Start the person again from your own integration. | | 422 | `workflow_not_available` | The workflow behind the verification is no longer published, so it cannot be redone. | | 429 | `rate_limited` | Too many sessions started this hour. Try again shortly. | Verifying again is allowed from **any** finished status, including a passed verification. Re-verifying someone whose document is about to expire is the same operation as retrying a failure. --- # Decide a verification ``` POST /api/kyc/verifications/:id/review ``` Approves, declines or sends back a **finished** verification, or changes a decision made earlier. This is the API twin of the review controls in your dashboard. The two run the same core, so a decision made here does exactly what one made there does: - the person's record moves to approved, rejected or under review - declining pauses their ongoing screening, and approving resumes it - the change is added to the verification's timeline in your dashboard, and to `statusHistory` on the [result endpoint](https://trust.myaza.co/documentation/api-verification-result/markdown#after-a-decision) - your webhook endpoints receive [`verification.status_updated`](https://trust.myaza.co/documentation/webhooks/markdown#status-changes) with `source: "api"` - the applicant is emailed about it when you ask, or when the workflow does (see [Emailing the applicant](#emailing-the-applicant)) The verification itself is never rewritten. `checkStatus` keeps saying what the checks found, so an approval granted despite a failed check stays visible. **Authentication:** `Authorization: Bearer sk_…` (**secret key required**). A publishable key ships in a browser, and a browser must never be able to approve its own applicant, so it receives `403 secret_key_required`. **Content type:** `application/json`. **Production note:** `sk_live_` keys require an [approved business](https://trust.myaza.co/documentation/environments/markdown#production). ## Request body | Field | Type | Required | Description | |---|---|---|---| | `decision` | string | yes | `APPROVED`, `DECLINED` or `RESUBMISSION`. | | `note` | string | no | Why, up to 2,000 characters. Kept in your audit log and on the verification's timeline in your dashboard. It is never sent to your webhook endpoints or returned by the API. | | `steps` | string[] | no | `RESUBMISSION` only. The steps to redo, named as in [Verify again](https://trust.myaza.co/documentation/api-verify-again/markdown#redo-only-what-failed). Absent or empty means the whole flow. | | `message` | string | no | `RESUBMISSION` only. Shown to the applicant when they open the link, up to 500 characters. Write it for them, not for your logs. | | `policy` | string | no | `RESUBMISSION` only. `original` runs the workflow version the applicant walked; `latest` runs today's. Absent follows the workflow's own setting. | | `notifyApplicant` | boolean | no | `APPROVED` or `DECLINED` only. `true` emails the applicant about the decision and `false` does not. Absent follows the workflow: an approval is emailed when its **Email the applicant when approved** setting is on, and a decline never is. | | `email` | string | no | On a `RESUBMISSION`, send the link to the applicant at this address as well as returning it. On an approval or a decline sent with `notifyApplicant: true`, the address to email instead of the one the verification holds. | A field sent with a decision it does not apply to is refused with `400 invalid_input` rather than ignored, because a caller who sent it has misunderstood something: `steps`, `message` or `policy` with an approval or a decline, `email` with one that does not also send `notifyApplicant: true`, or `notifyApplicant` with a `RESUBMISSION`. ## Request ```bash curl "https://trust.myaza.app/api/kyc/verifications/ver_abc123/review" \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "decision": "DECLINED", "note": "Duplicate of an existing customer" }' ``` ## Response `200 OK` ```json { "review": { "verificationId": "ver_abc123", "decision": "DECLINED", "status": "declined", "previousStatus": "approved", "checkStatus": "verified", "reviewedAt": "2026-09-13T09:00:00.000Z", "entityId": "ent_01j9def789", "attempt": 1, "delivery": null, "applicantEmail": null, "resubmission": null } } ``` | Field | Description | |---|---| | `status` | The verification's top-line status now, the same value [Get verification status](https://trust.myaza.co/documentation/api-verification-status/markdown) returns. | | `previousStatus` | The top-line status immediately before this decision. | | `checkStatus` | What the checks found. No decision changes it. | | `entityId` | The person's record the decision was applied to. Null when the checks never created one, for example on a failed check. | | `attempt` | The attempt the verification holds now. A verification you send back keeps its id, and the applicant's resubmission becomes the next attempt. | | `delivery` | On a `RESUBMISSION` with `email`: `{ emailed, reason }`. `emailed: false` with `reason: "send_failed"` means the email could not be sent, and the link still works. Null otherwise. | | `applicantEmail` | On an approval or a decline where emailing the applicant was asked for, by `notifyApplicant` or by the workflow: `{ emailed, reason, recipient }`. `recipient` is `{ masked, source }`: the address masked (`a•••@example.com`) and where it came from, `email_verification`, `sdk`, `entity` or `provided`. When `emailed` is `false`, `reason` says why: `no_recipient` (there was no address to send to) or `send_failed`. The decision stands either way. Null when nobody asked. | | `resubmission` | On a `RESUBMISSION`, the link to send the applicant: `sessionId`, `url`, `shortCode`, `expiresAt`, `steps` and `full`. `url` is a credential: whoever opens it verifies as this applicant. Null otherwise. | ## Emailing the applicant We can tell the applicant about an approval or a decline, in your organisation's name: ```json { "decision": "APPROVED", "notifyApplicant": true } ``` The email says what was decided and who to contact. It never gives a reason: that is yours to give, in your own words. We only write to an address the verification already holds, using the first of these that has one: 1. the email the applicant proved they own with a code, when the workflow runs email verification; 2. an email your integration passed, as `userData.email` on the SDK or on a [session you created](https://trust.myaza.co/documentation/api-create-session/markdown) (the address you emailed the session link to counts too); 3. the email on the person's entity record. To use another address, send it as `email` beside `notifyApplicant: true`. When there is no address, nothing is sent and `applicantEmail.reason` is `no_recipient`. Leave `notifyApplicant` out and the workflow decides: when it has **Email the applicant when approved** switched on, an approval is emailed however it was made. A decline is only ever emailed when you ask. Every email is added to the verification's timeline in your dashboard. ## Sending it back ```json { "decision": "RESUBMISSION", "steps": ["document-capture"], "message": "Your document photo was too blurry to read. Please retake it in good light." } ``` The applicant gets a link on the same workflow and walks only the steps you name. Their resubmission becomes the next attempt of **this** verification, under the same id, so you never have a second verification to reconcile. This is the same operation as [Verify again](https://trust.myaza.co/documentation/api-verify-again/markdown). Add `"email": "ada@example.com"` and we send the link for you. Otherwise delivering it is yours to do. Either way it is in this response and in the webhook, and any link sent earlier for the same verification stops working. ## Changing a decision Call it again with a different `decision`. The latest decision is the verification's status, each earlier one stays on record, and every change sends its own `verification.status_updated`. Read the whole trail back with [Get verification result](https://trust.myaza.co/documentation/api-verification-result/markdown#after-a-decision), whose `statusHistory` lists every change; in your dashboard it is on the verification's timeline. A decision can be made on any finished verification: one your workflow sent for review, one it approved or declined automatically, or one a person has already decided. ## Errors | Status | Code | Meaning | |---|---|---| | 400 | `invalid_input` | The body is malformed, `decision` is not one of the three values, or a field came with a decision it does not apply to. | | 400 | `invalid_steps` | A step name is not a valid step. The message lists the valid names. | | 403 | `secret_key_required` | A publishable key was used. | | 404 | `verification_not_found` | No verification with that id in your organisation and environment. | | 409 | `verification_in_progress` | The checks have not finished. Wait for the result before deciding it. | | 422 | `no_workflow` | A `RESUBMISSION` on a verification that did not run a workflow, so there is no flow to send the applicant back into. Approve or decline it instead. | | 429 | `rate_limited` | Too many sessions started this hour. A send-back starts one. Try again shortly. | --- # List verifications ``` GET /api/kyc/verifications ``` Your organisation's verification history, paginated and filterable — reconcile your records without keeping your own ledger of ids. Scoped to the key's environment, so a sandbox key never lists production traffic. Records you have [deleted](https://trust.myaza.co/documentation/api-manage-records/markdown) are not listed and are not counted in `total`. **Authentication:** `Authorization: Bearer sk_…` (**secret key required**), like the [per-id result read](https://trust.myaza.co/documentation/api-verification-result/markdown) beside it. ## Query parameters | Parameter | Description | |---|---| | `status` | One value from the [status vocabulary](https://trust.myaza.co/documentation/api-verification-status/markdown) — the same words `/status/:id` and webhooks use, so the filter and the labels can never disagree. An unknown word is a `400 invalid_status` naming the vocabulary. | | `channel` | `sdk` (capture-flow verifications) or `dashboard` (spot checks). | | `subjectType` | `individual` or `business`. | | `country` | ISO-2 country code. | | `idType` | ID type (or the business product, which rides the same field). | | `workflowId` | The `wf_…` public id — every submission a workflow drove. | | `externalUserId` | Your reference for the subject; matches sessions, entities and webhook payloads. | | `from` / `to` | ISO dates bounding `createdAt`. | | `page` / `pageSize` | Pagination; `pageSize` max 100, default 25. | ## Response ```json { "verifications": [ { "verificationId": "cmt…", "requestId": "order_1001", "externalUserId": "user_42", "metadata": { "loanId": "loan_20191" }, "channel": "sdk", "subjectType": "individual", "status": "approved", "checkStatus": "verified", "reasonCode": null, "country": "NG", "idType": "bvn", "workflowId": "wf_AbC123dEf456", "workflowVersion": 3, "createdAt": "2026-08-20T12:00:00.000Z", "completedAt": "2026-08-20T12:00:04.512Z" } ], "total": 812, "page": 1, "pageSize": 25 } ``` Rows are deliberately light: identifiers and states, no biodata. `metadata` is your own customer metadata echoed unchanged, not something Myaza derived. `status` is the outcome to act on (what the checks, your workflow or a reviewer concluded); `checkStatus` is what the automated checks themselves found, so approving a failed check (an exception) stays distinguishable from approving a passing one. See [Status and checkStatus](https://trust.myaza.co/documentation/verifications/markdown#status-and-checkstatus). The full result, PII included, lives on [`GET /verifications/:id`](https://trust.myaza.co/documentation/api-verification-result/markdown). Business rows additionally carry `businessName`. --- # Spot checks For manual crypto investigations, open **Spot Checks → Wallet** in the dashboard. Enter a wallet address for an on-demand risk result and case actions. Backend integrations can use the [On-Demand Wallet Screening API](https://trust.myaza.co/documentation/api-wallet-screenings/markdown). [Transaction Monitoring](https://trust.myaza.co/documentation/monitoring-events/markdown) uses the same engine to screen crypto counterparties automatically. ``` POST /api/kyc/checks/individual POST /api/kyc/checks/business GET /api/kyc/checks/individual GET /api/kyc/checks/business GET /api/kyc/checks/individual/:id GET /api/kyc/checks/business/:id ``` Run a one-off check without the SDK capture flow: verify a person's ID number against the government database, or a business registration number against the registry. This is the server-to-server counterpart of the dashboard's Spot Checks tabs — the same gates, pricing and sandbox behaviour, and the same history (API checks appear there labelled "API"). **Authentication:** `Authorization: Bearer sk_…` (**secret keys only**, for the whole surface). Results carry PII, so this never belongs in client-side code. **Production note:** `sk_live_` keys require an [approved business](https://trust.myaza.co/documentation/environments/markdown#production). Sandbox and development checks are free and serve [test data](https://trust.myaza.co/documentation/sandbox-testing/markdown). Checks run **asynchronously**: the submit returns `202` with a `verificationId` within milliseconds, and the result is ready seconds later on the detail endpoint. ## Individual checks ``` POST /api/kyc/checks/individual ``` | Field | Required | Description | |---|---|---| | `country` | yes | `NG`, `GH`, `KE`, `ZA` or `CI` (the government-database markets). | | `idType` | yes | The ID type, e.g. `bvn`, `nin`, `passport`. See [Countries & ID types](https://trust.myaza.co/documentation/id-types/markdown). | | `idNumber` | no* | The ID number. Required for number-based IDs; a document ID may instead send the document front for extraction. | | `userData` | no | Details to validate against the government record: `firstName`, `lastName`, `dateOfBirth`. | | `mediaIds` | no | Pre-uploaded photos from [`POST /upload`](https://trust.myaza.co/documentation/api-upload/markdown): `documentFront`, `documentBack`, `selfie`. A selfie adds the facial comparison against the record photo. | | `images` | no | The same three slots **inline** instead of pre-uploaded: each an https URL, a data URI, or base64 — the same forms the [face match](https://trust.myaza.co/documentation/api-face-match/markdown) endpoint takes. Send each slot as `mediaIds` **or** `images`, never both. | | `consent` | yes | Must be `true`: you attest the person consented to the check. | | `requestId` | no | Your idempotency key. A retry with the same value returns the same check instead of running (and billing) a second one. Recommended. | | `externalUserId` | no | Your own reference for the person. A verified check is saved on the customer record with that reference. | | `createEntity` | no | Defaults to `true`: a verified check is saved as a customer record. Without `externalUserId`, it is saved on the record that already holds this ID number, or as a new record. Send `false` to keep the result in spot-check history only. If you later send the same ID number with your own reference (a verification, a check or the entity API), the record created here is renamed to that reference rather than duplicated. | ```bash curl -X POST https://trust.myaza.app/api/kyc/checks/individual \ -H "Authorization: Bearer sk_live_…" \ -H "Content-Type: application/json" \ -d '{ "country": "NG", "idType": "bvn", "idNumber": "12345678901", "userData": { "firstName": "John", "lastName": "Doe" }, "consent": true, "requestId": "check-user-42-2026-08-20" }' # → 202 { "verificationId": "cmt…", "status": "pending" } ``` Then read the result: ``` GET /api/kyc/checks/individual/:id ``` ```json { "check": { "id": "cmt…", "status": "verified", "country": "NG", "idType": "bvn", "idNumber": "12345678901", "result": { "firstName": "JOHN", "lastName": "DOE", "dateOfBirth": "1990-01-01", "dataMatch": true, "facialMatch": { "match": true, "confidence": 85 } }, "govRecord": { "…": "customer-safe record extract" }, "media": { "selfie": "https://trust.myaza.app/api/kyc/verifications/…/media/selfie" }, "chargedAmount": "0.0500", "createdAt": "…", "completedAt": "…" } } ``` `status` is `pending` until processing finishes, then `verified`, `failed`, `not_found` or `error` with `reason` and a stable [`reasonCode`](https://trust.myaza.co/documentation/errors/markdown). `result` is present on `verified` only. ## Business checks ``` POST /api/kyc/checks/business ``` | Field | Required | Description | |---|---|---| | `country` | yes | ISO-2 country code (about 48 registry countries are supported). | | `registrationNumber` | yes | The registration number (e.g. `RC123456`). | | `registrationName` | no | The registered name, cross-checked against the registry when given. | | `subdivisionCode` | maybe | ISO 3166-2 region (e.g. `US-DE`) — **required** for the countries whose registry is split by region (US, IN, CA, AE) and rejected elsewhere. | | `product` | no | The check product; defaults to the standard business check. Nigeria adds tax-related products. | | `consent` | yes | Must be `true`. | | `requestId` | no | Your idempotency key, as above. | | `externalUserId` | no | Your own reference for the business. A verified check is saved on the customer record with that reference. | | `createEntity` | no | Defaults to `true`: a verified check is saved as a customer record. Without `externalUserId`, it is saved on the record that already holds this registration number, or as a new record. Send `false` to keep the result in spot-check history only. If you later send the same ID number with your own reference (a verification, a check or the entity API), the record created here is renamed to that reference rather than duplicated. | | `sandboxOutcome` | no | `verified` or `not_found` — pins the canned result outside production. Silently ignored on live keys. | The detail (`GET /api/kyc/checks/business/:id`) returns the registry's answer as `businessRecord`: company particulars, status, share capital and key personnel in a customer-safe extract, plus `businessName`, the `product`, and the region the registry was asked about. ## History ``` GET /api/kyc/checks/individual GET /api/kyc/checks/business ``` Paginated, newest first, scoped to your organisation and the key's environment. Query parameters: `status` (`pending` / `verified` / `failed` / `not_found` / `error`), `country`, `idType` (individual) or `product` (business), `from` / `to` (ISO dates), `page`, `pageSize` (max 100). Rows are light — identifiers, status, cost, timestamps and `initiatedVia` (`api` or `dashboard`) — with the full result on the detail endpoints. A check you no longer want in your history can be removed with `DELETE /api/kyc/checks/individual/:id` or `DELETE /api/kyc/checks/business/:id`. See [Delete records](https://trust.myaza.co/documentation/api-manage-records/markdown): the delete is soft, and a check that is still running is refused with `409 check_in_progress`. ## Errors | Status | Code | Meaning | |---|---|---| | 400 | `invalid_input` | The body failed validation, or a slot was sent both as `mediaIds` and `images`. | | 400 | `product_unsupported` | That product is not offered for the country. | | 400 | `subdivision_required` / `subdivision_unsupported` / `subdivision_unknown` | The registry-region rule above. | | 400 | `invalid_image` / `unsupported_media_type` / `image_too_large` / `image_url_not_allowed` / `image_fetch_failed` | An inline image problem — same rules as [face match](https://trust.myaza.co/documentation/api-face-match/markdown). | | 402 | `insufficient_credits` | Your balance cannot cover the check. | | 403 | `spot_checks_disabled` / `business_checks_disabled` | The module is not enabled for your organisation. | | 403 | `id_type_not_enabled` | The ID type is not granted to your organisation, or the required feature is disabled. | | 403 | `spot_check_unavailable` | The ID type is temporarily switched off for spot checks. | | 403 | `secret_key_required` | A publishable key was used; this surface is secret-key only. | | 403 | `business_not_approved` | A live key before KYB approval. | | 422 | `only_test_ids_allowed` | A sandbox key sent a real ID number; use the published [test IDs](https://trust.myaza.co/documentation/sandbox-testing/markdown). | | 429 | `rate_limited` | The hourly verification cap was reached; retry later. | --- # Delete, expire & cancel Four server-to-server endpoints for tidying your own records: delete a verification or an unfinished attempt, delete a spot check, delete a face check, and end a live verification session before its clock runs out. **Authentication:** `Authorization: Bearer sk_…` (**secret key required**). A publishable key receives `403 secret_key_required`. Removing records is a backend act, and a publishable key ships in a browser. **Content type:** `application/json`. ## What a delete does Deleting is **soft**. The record leaves every surface your organisation can read: the dashboard, the [list](https://trust.myaza.co/documentation/api-list-verifications/markdown), the [result](https://trust.myaza.co/documentation/api-verification-result/markdown) and [status](https://trust.myaza.co/documentation/api-verification-status/markdown) endpoints, the media URLs in earlier webhooks, and your exports. Your analytics stop counting it. Nothing is refunded, because the work it describes was done. The record is not destroyed. Myaza keeps it, marked as deleted, so a compliance history cannot be silently rewritten. **A deleted record cannot be restored through the API.** If something was deleted by mistake, contact support; a restore arrives as a `verification.restored` or `entity.restored` [webhook](https://trust.myaza.co/documentation/webhooks/markdown). A verification that is still `processing` cannot be deleted. Its checks may still come back, and a system error may still refund the charge. Wait for the result. ## Delete a verification or attempt ``` DELETE /api/kyc/verifications/:id ``` `:id` is the id you already hold. A verification adopts its session's id, so this works from the moment a session is created: a finished verification is deleted, and a session nobody has submitted is deleted as an attempt. ```bash curl -X DELETE "https://trust.myaza.app/api/kyc/verifications/ver_abc123" \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "reason": "Test data from the integration rehearsal" }' ``` | Field | Type | Required | Description | |---|---|---|---| | `reason` | string | no | Up to 500 characters, kept in your organisation's audit log. It is never sent to your webhook. | ### Response `200 OK` ```json { "verificationId": "ver_abc123", "kind": "verification", "deleted": true, "deletedAt": "2026-09-05T10:12:00.000Z" } ``` `kind` is `verification` for a submitted record and `attempt` for a session that never submitted. Your endpoints receive `verification.deleted` with the same two fields. ## Delete a spot check ``` DELETE /api/kyc/checks/individual/:id DELETE /api/kyc/checks/business/:id ``` The same body and the same soft-delete semantics, for the [spot checks](https://trust.myaza.co/documentation/api-spot-checks/markdown) run server to server or from the dashboard. Spot checks never fire webhooks, so nothing is sent when one is deleted. ```json { "id": "chk_9f1…", "deleted": true, "deletedAt": "2026-09-05T10:12:00.000Z" } ``` ## Delete a face check ``` DELETE /api/kyc/face/checks/:id ``` `:id` is the `faceCheck.id` returned by [`POST /api/kyc/face/compare`](https://trust.myaza.co/documentation/api-face-match/markdown) or listed by `GET /api/kyc/face/checks`. A face check is synchronous, so there is no processing state to wait for: any check you can read can be deleted. The photos stay under their existing retention policy, and nothing is refunded. ```bash curl -X DELETE "https://trust.myaza.app/api/kyc/face/checks/fc_abc123" \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "reason": "Uploaded the wrong photo pair" }' ``` | Field | Type | Required | Description | |---|---|---|---| | `reason` | string | no | Up to 500 characters, kept in your organisation's audit log. | ### Response `200 OK` ```json { "id": "fc_abc123", "deleted": true, "deletedAt": "2026-09-05T10:12:00.000Z" } ``` Face checks never send webhooks, so no event follows. A deleted check disappears from `GET /api/kyc/face/checks`, cannot be re-read at `/checks/:id`, and is no longer offered as a rerun source in the dashboard. ## Expire a session ``` POST /api/kyc/sessions/:sessionId/expire ``` Ends a live session early. The link stops working at once, the applicant cannot continue, and anything they had entered is discarded. Your endpoints receive `session.expired` with the next `sequence` number, exactly as when a session times out on its own, so nothing about your webhook handling changes. Use it when a link went to the wrong person, or when you have replaced it with a new one. ```bash curl -X POST "https://trust.myaza.app/api/kyc/sessions/cmt1gfjce05g547grqbmjsiqf/expire" \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" ``` ### Response `200 OK` ```json { "sessionId": "cmt1gfjce05g547grqbmjsiqf", "status": "expired", "changed": true, "expiresAt": "2026-09-05T10:12:00.000Z" } ``` `changed` is `false` when the session had already ended on its own; the call is safe to repeat. A session the applicant has submitted cannot be expired, because it has a verification now. ## Cancel a session or a processing verification ``` POST /api/kyc/sessions/:id/cancel POST /api/kyc/sessions/:id/uncancel ``` Stops a verification midway, and takes that back. The `:id` is either a session's or a verification's (a verification keeps its session's id), and either can be cancelled: - **A session the applicant has not submitted.** They can no longer continue: the hosted link, the SDK and `/session/start` answer `409 session_cancelled` until you uncancel it. What they had entered is kept, so after an uncancel they carry on where they left off. Your endpoints receive `session.cancelled`, then `session.uncancelled`. - **A verification that is still processing.** That is one that is queued, waiting for credit, or holding its decision on screening, key people or an address check. Nothing further runs on it until you uncancel it, and then it carries on from where it stopped. Your endpoints receive `verification.status_updated` with `status: "cancelled"`, and again when it is uncancelled. While a person's latest session for a workflow is cancelled, the SDK does not start them over either. Uncancel it, or create a new session for them, to let them try again. **Money.** Once any check has run (a registry lookup, a document read, a government lookup, the device analysis), cancelling refunds nothing. If nothing has run yet, the amount reserved for it is refunded. Uncancelling never charges twice. Optional body for a cancel: `{ "reason": "Customer asked us to stop" }` (up to 500 characters). It goes to your audit log and is never sent to a webhook. ```bash curl -X POST "https://trust.myaza.app/api/kyc/sessions/cmt1gfjce05g547grqbmjsiqf/cancel" \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "reason": "Customer asked us to stop" }' ``` ### Response `200 OK` ```json { "id": "cmt1gfjce05g547grqbmjsiqf", "kind": "verification", "status": "cancelled", "previousStatus": "processing", "changed": true, "refunded": false } ``` `kind` is `attempt` for a session that had not submitted and `verification` for one that had. `changed` is `false` when it was already cancelled; the call is safe to repeat. An uncancel answers the same shape, with the status it went back to. ## Errors | Status | Code | Meaning | |---|---|---| | 400 | `invalid_input` | The reason is longer than 500 characters. | | 404 | `verification_not_found` / `not_found` / `session_not_found` | Nothing with that id in your organisation and environment. A record that is already deleted answers the same way. | | 409 | `verification_in_progress` / `check_in_progress` | Still processing. Wait for the result before deleting it. | | 409 | `session_already_submitted` | The applicant finished; there is nothing left to expire. | | 409 | `verification_running` | Cancel only: its checks are running right now. They finish within a minute or two; try again then. | | 409 | `not_processing` | Cancel only: it has finished, or the session has already ended. | | 409 | `not_cancelled` | Uncancel only: it is not cancelled. | | 409 | `session_expired` | Uncancel only: the session ran out of time while it was cancelled. Create a new session instead. | | 403 | `secret_key_required` | A publishable key was used. | --- # Email & SMS verification ``` POST /api/kyc/contact/verifications POST /api/kyc/contact/verifications/:id/check GET /api/kyc/contact/verifications/:id ``` Prove that someone controls an email address or a phone number. Send a one-time code, then check the code they typed. Both calls answer synchronously, so there is nothing to poll and no webhook to wait for. This is the server-to-server form of the contact step the SDK runs inside a verification flow. Use it when you want the possession check on its own: at sign-up, before a payout, when a customer changes their number. **Authentication:** `Authorization: Bearer sk_…` (secret keys only). The response describes a real person's contact details and the call spends your credits on a message, so it never belongs in client-side code. **Production note:** `sk_live_` keys require an [approved business](https://trust.myaza.co/documentation/environments/markdown#production). ## Send a code ``` POST /api/kyc/contact/verifications ``` | Field | Required | Description | |---|---|---| | `channel` | yes | `email` or `phone`. | | `destination` | yes | The address or number to verify. A phone number in national format resolves against `country`; one written with a dial code resolves on its own. | | `country` | no | ISO-2. Resolves national phone formats, and selects the price row for markets you have priced separately. It is stored on the challenge and reused when the check is charged, so the amount you are quoted here is the amount you are billed. | | `via` | no | `sms` (default) or `whatsapp`. WhatsApp is accepted and currently delivers by SMS; the response says which channel actually carried the code. | | `codeLength` | no | Digits in the code, 4 to 8. Default 6. | | `maxAttempts` | no | Wrong codes allowed before the challenge dies, 1 to 5. Default 3. | | `externalUserId` | no | Your own reference for this person. When you already hold a compliance entity under that reference, a verified contact is recorded against it as proved evidence. | ```bash curl -X POST https://trust.myaza.app/api/kyc/contact/verifications \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "channel": "phone", "destination": "08031234567", "country": "NG", "externalUserId": "user_8f21" }' ``` ```json { "contactVerification": { "id": "clx8s0a4p0001qw9k2m7f3xyz", "status": "pending", "channel": "phone", "destination": "+2348031234567", "deliveryChannel": "sms", "country": "NG", "signals": { "country": "NG", "valid": true, "callingCode": "234", "lineType": "mobile", "virtual": false }, "externalUserId": "user_8f21", "attemptsUsed": 0, "chargedAmount": null, "verifiedAt": null, "expiresAt": "2026-09-12T10:05:00.000Z", "createdAt": "2026-09-12T10:00:00.000Z" } } ``` `destination` comes back normalised: an email is lowercased, a phone number is E.164. Store that form, because it is what the check and the entity record use. Codes are valid for five minutes. ## Check the code ``` POST /api/kyc/contact/verifications/:id/check ``` | Field | Required | Description | |---|---|---| | `code` | yes | The code the person typed. | ```bash curl -X POST https://trust.myaza.app/api/kyc/contact/verifications/clx8s0a4p0001qw9k2m7f3xyz/check \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "code": "418305" }' ``` ```json { "contactVerification": { "id": "clx8s0a4p0001qw9k2m7f3xyz", "status": "verified", "channel": "phone", "destination": "+2348031234567", "deliveryChannel": "sms", "country": "NG", "signals": { "country": "NG", "valid": true, "callingCode": "234", "lineType": "mobile", "virtual": false }, "externalUserId": "user_8f21", "attemptsUsed": 1, "chargedAmount": "0.0500", "verifiedAt": "2026-09-12T10:01:12.000Z", "expiresAt": "2026-09-12T10:05:00.000Z", "createdAt": "2026-09-12T10:00:00.000Z" } } ``` A wrong code answers `400 invalid_code` with `attemptsRemaining`, so you can tell the person how many tries are left. When the budget runs out the challenge is dead and you send a new code. Re-checking a verified challenge is safe. It returns the same record and does not charge again. ## Read a challenge ``` GET /api/kyc/contact/verifications/:id ``` The same record, at any point in its life. `status` is `pending`, `verified` or `expired`. Useful for reconciling your own state after a dropped response. ## What the signals tell you Every challenge carries the intelligence computed when the code was sent, at no extra cost. Branch on it however your risk policy needs. **Email** | Field | Meaning | |---|---| | `domain` | The mail domain. | | `disposable` | The domain is a known throwaway mailbox service. | | `freeProvider` | A consumer email service such as Gmail or Outlook. | | `mxFound` | The domain publishes mail servers. `null` means the lookup could not answer, which is not the same as no. | **Phone** | Field | Meaning | |---|---| | `country` | ISO-2 the number belongs to, from its dial code. | | `callingCode` | Country calling code, digits only. | | `valid` | The number is a possible, valid number for that country. | | `lineType` | `mobile`, `fixed_line`, `voip`, `toll_free` and so on. `null` when the range cannot be classified. | | `virtual` | `true` only for a known VOIP range, `false` for any other known type, `null` when the type is unknown. Fire rules on `true`, never on a gap. | ## Pricing A check bills the same component your workflows already bill for the same work: `email_verification` or `phone_verification` on the `sdk_verification` module. One rate card governs both doors, and Billing in your dashboard shows the rate. Three rules follow from that: - **A code that is never checked costs nothing.** Billing happens when the check completes and returns an answer, not when the message goes out. - **We refuse before we send.** If your balance cannot cover the check the send answers `402 insufficient_credits` with the required amount, rather than spending a message you cannot pay for. - **Sandbox and development are free**, and `chargedAmount` is `null` there. `chargedAmount` on a completed check is what it cost, in USD, as a decimal string. ## Sandbox On `sk_test_` and `sk_dev_` keys nothing is sent to anyone. The code is all zeros at the length you asked for, so `000000` by default, `0000` at `codeLength: 4`. `deliveryChannel` comes back as `test`. Everything else behaves exactly as it does live, so you can build and test the whole flow at no cost. ## Rate limits Two caps protect your account and ours, both counted per rolling hour: - **Five codes per destination.** Hammering one number is stopped before it starts. - **A per-organisation ceiling across all destinations**, 500 by default. This is what bounds a leaked key rotating through numbers. Contact support if your volume needs more. Over either cap, the send answers `429 send_rate_limited`. ## Using a verified contact in a KYC flow A contact proved here is deliberately **not** carried into a `POST /verify` submission. That submission prices its own contact steps, so honouring a proof from this API would bill the same check twice. If you want the contact verified as part of a verification, use the SDK's contact step, which is configured per workflow. What this API does record, when you send an `externalUserId` and an entity already exists under it, is the proved contact on that entity's [compliance record](https://trust.myaza.co/documentation/identity-hub/markdown) as `otp_verified` evidence, which is the strongest source there is. It never creates an entity that does not exist. ## Errors | Status | Code | Meaning | |---|---|---| | 400 | `invalid_request` | The body failed validation. `message` says what. | | 400 | `invalid_destination` | The phone number could not be parsed. Send `country` for national formats. | | 400 | `invalid_code` | Wrong code. `attemptsRemaining` says how many tries are left. | | 402 | `insufficient_credits` | Balance too low for the check. `required` and `balance` are decimal strings. | | 403 | `secret_key_required` | A publishable key was used. This surface is secret keys only. | | 403 | `business_not_approved` | Production access needs an approved business. | | 404 | `challenge_not_found` | Unknown id, or one that belongs to another organisation, another environment, or the SDK's contact step rather than this API. | | 410 | `challenge_expired` | The five-minute window closed. Send a new code. | | 429 | `send_rate_limited` | A send cap was reached. See Rate limits. | | 429 | `too_many_attempts` | The attempt budget is spent. Send a new code. | | 502 | `send_failed` | The message could not be handed to its delivery channel. Nothing was charged. | --- # API explorer Search the [interactive API explorer](/documentation/api-explorer) to find v1 endpoints and inspect their fields. It reads Myaza's public v1 OpenAPI contract and does not send requests or ask for your API key. For `/api/kyc` identity integrations, start with [User Verification](https://trust.myaza.co/documentation/user-verification/markdown) or [Business Verification](https://trust.myaza.co/documentation/business-verification-quickstart/markdown). For v1, see [On-Demand Wallet Screening](https://trust.myaza.co/documentation/api-wallet-screenings/markdown) for a copyable first request. --- # On-Demand Wallet Screening API `POST /api/v1/wallet-screenings` screens one address. Use a secret `sk_test_` or `sk_live_` key on your backend. The key selects the environment. Use a new `Idempotency-Key` for each intended screening and keep that key for exact retries. A replay returns the same screening without another screening call or charge. ```bash curl -X POST "https://sandbox.trust.myaza.app/api/v1/wallet-screenings" \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: wallet-customer-42-20260930" \ -d '{"network":"ethereum","address":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","asset":"USDT","sandboxScenario":"wallet.sanctions.direct"}' ``` `network` is the chain, such as `ethereum` or `TRX`; `asset` is separate, so USDT and USDC are never network values. Network identifiers may contain letters, numbers, periods, underscores, colons and hyphens, up to 40 characters. Myaza translates known aliases and forwards other well-formed identifiers as supplied, without a fixed allowlist. The configured provider determines network and address acceptance. `sandboxScenario` works only with a test key. Optionally add `subject: {"externalUserId":"customer_42"}` to link an existing customer and enable linked alerts and cases. With the Sandbox scenario above and the default direct-sanctions rule, these selected response fields show what to inspect first. IDs and timestamps vary: ```json { "screening": { "id": "wsc_test_example", "environment": "sandbox", "kind": "address", "status": "screened", "wallet": { "address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "blockchain": "ETH", "asset": "USDT" }, "risk": { "score": 92, "level": "critical" }, "decision": "block", "exposures": [ { "category": "SANCTIONS", "direction": "source", "type": "direct", "hops": 0, "share": 65 }, { "category": "EXCHANGE", "direction": "source", "type": "direct", "hops": 0, "share": 35 } ], "fundFlow": { "kind": "exposure_map", "totalExposures": 2, "truncated": false, "nodes": [ { "id": "exposure-0", "exposurePosition": 0, "category": "SANCTIONS", "direction": "source", "type": "direct", "hops": 0, "share": 65, "entityName": "OFAC SDN wallet", "isHighRisk": true }, { "id": "exposure-1", "exposurePosition": 1, "category": "EXCHANGE", "direction": "source", "type": "direct", "hops": 0, "share": 35, "entityName": "Sandbox Exchange", "isHighRisk": false } ] }, "rulesTriggered": [ { "name": "Direct sanctions exposure", "version": 1, "action": "BLOCK" } ] } } ``` The response also contains the full permitted profile, exposure details, rule conditions, case links and timestamps. `block` is Myaza Trust's API value for a decline decision. The `rulesTriggered` array records the matched rule version. ```ts const response = await fetch(`${process.env.MYAZA_API_BASE_URL}/wallet-screenings`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.MYAZA_SECRET_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'wallet-customer-42-20260930', }, body: JSON.stringify({ network: 'ethereum', address: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' }), }); const { screening } = await response.json(); console.log(screening.id, screening.decision, screening.rulesTriggered); ``` Responses use the Myaza Trust contract: `screening.id`, `environment`, `wallet`, `risk`, `decision`, `exposures`, `rulesTriggered`, `provider`, `screenedAt` and `createdAt`. The provider section contains only identity and references, never raw screening data. `decision` is `allow`, `review` or `block`. A new result returns `201`, an idempotent replay `200` and an in-progress request `202`. ## Wallet verification & screening The address overview uses the saved screening evidence. These fields are also returned for each counterparty wallet in a [transaction assessment](https://trust.myaza.co/documentation/monitoring-events/markdown#transaction-assessment-response). | Address overview | Response field | |---|---| | Screening target | `kind` (`address` for a wallet address) | | Wallet address | `wallet.address` | | Provider | `provider.upstreamProvider` when returned, such as `merklescience` (Merkle Science); `provider.name` identifies the integration, such as `didit` | | Score | `risk.score`, out of 100 | | Risk level | `risk.level`, the Myaza severity band | | Blockchain | `wallet.blockchain` | | Owner | `profile.owner` | | Owner type | `profile.ownerType` and optional `profile.ownerSubtype` | | User | `profile.user` | | User type | `profile.userType` and optional `profile.userSubtype` | | First transaction time | `profile.firstTransactionAt` | | Latest transaction time | `profile.latestTransactionAt` | | Date added | `profile.providerDateAdded` | Owner and user labels are separate provider attributions, not proof of wallet control. Missing fields are `null`; Myaza does not copy owner into user or substitute the screening date for a missing transaction date. Existing `profile.entityLabel` and `profile.entityType` fields remain available. `provider.riskLevel` preserves the provider's severity separately from Myaza's risk band. An empty profile does not mean the wallet is clean. Use `GET /api/v1/wallet-screenings/{id}` to read one result and `GET /api/v1/wallet-screenings?limit=50&cursor=...` for scoped history. A different organisation or environment cannot read the screening. `GET /api/v1/wallet-screenings/{id}/report` downloads a Myaza PDF of the saved decision, exposures and triggered rule versions. It does not rerun screening and is not cryptographically signed. For a completed Didit address result, `GET /api/v1/wallet-screenings/{id}/signed-report` renders the vendor-signed PDF from the retained exact provider response. It returns `404` when no signed report exists and `502` when the vendor cannot render it. Neither route exposes the raw provider JSON. Both require the same scoped secret key as the result read. `fundFlow` is a bounded exposure map: the first 40 normalised exposure rows, with `totalExposures` and `truncated` indicating whether more were retained in `exposures`. It does not claim a verified transfer path or expose the provider's raw graph. | Result | Next step | |---|---| | `201` created | Save `screening.id` and act on `decision`. | | `200` replay | Reuse the original result; no second screening or charge was created. | | `202` in progress | Read the returned screening or assessment status before acting. | | `422` invalid wallet or malformed network | Correct the input (`invalid_wallet` or `invalid_network`). No provider call or billable usage was made. | | `409` idempotency conflict | Use the original payload with that key, or choose a new key for a new screening. | If the provider rejects a network after receiving the request, Myaza records the failed attempt. This is separate from pre-call input validation; do not blindly retry it or treat it as a clean result. Operational uncertainty becomes a reviewable result, never a clean wallet. To receive updates, subscribe to [Wallet Intelligence webhooks](https://trust.myaza.co/documentation/webhook-wallet-intelligence/markdown). --- # Supported countries Coverage depends on what you're verifying: a **person** (KYC) or a **business** (KYB). > Your organisation's *actual* availability is gated by the grants Myaza enables for you. For KYC, [`GET /config`](https://trust.myaza.co/documentation/api-config/markdown) returns your enabled countries and ID types at runtime; for KYB, the [business verification](https://trust.myaza.co/documentation/business-verification/markdown) flow exposes the registry countries you can use. ## Individual verification (KYC) Two tiers, with full detail (ID types, capture, assurance) in [Countries & ID types](https://trust.myaza.co/documentation/id-types/markdown): - **Government-database markets**: validated against the source government database, the strongest assurance. These support **number-only IDs** (BVN, NIN, …) as well as document types: 🇳🇬 Nigeria (`NG`) · 🇬🇭 Ghana (`GH`) · 🇰🇪 Kenya (`KE`) · 🇿🇦 South Africa (`ZA`) · 🇨🇮 Côte d'Ivoire (`CI`) - **Global documents**: **any other ISO-3166 country** verifies by document capture (`passport`, `drivers-license`, `national-id`). Passports can reach the stronger `chip` assurance tier via NFC. No government-database check runs outside the five markets above. ## Business verification (KYB) Businesses are verified against the official company registry by registration number, available in **48 countries** (fetched live from registry sources; the list below is current coverage). See [Business verification](https://trust.myaza.co/documentation/business-verification/markdown) for how to run one. **Africa**: 🇨🇮 Côte d'Ivoire · 🇰🇪 Kenya · 🇾🇹 Mayotte · 🇲🇦 Morocco · 🇳🇬 Nigeria · 🇿🇦 South Africa · 🇹🇿 Tanzania · 🇹🇳 Tunisia **Europe**: 🇧🇾 Belarus · 🇧🇪 Belgium · 🇧🇬 Bulgaria · 🇨🇾 Cyprus · 🇨🇿 Czechia · 🇩🇰 Denmark · 🇪🇪 Estonia · 🇫🇮 Finland · 🇫🇷 France · 🇩🇪 Germany · 🇬🇷 Greece · 🇬🇬 Guernsey · 🇮🇪 Ireland · 🇱🇻 Latvia · 🇲🇹 Malta · 🇲🇩 Moldova · 🇳🇱 Netherlands · 🇳🇴 Norway · 🇵🇱 Poland · 🇷🇴 Romania · 🇸🇰 Slovakia · 🇸🇮 Slovenia · 🇨🇭 Switzerland · 🇺🇦 Ukraine · 🇬🇧 United Kingdom **Asia & Middle East**: 🇮🇳 India · 🇮🇱 Israel · 🇯🇵 Japan · 🇰🇿 Kazakhstan · 🇳🇵 Nepal · 🇸🇦 Saudi Arabia · 🇹🇷 Turkey · 🇦🇪 United Arab Emirates · 🇺🇿 Uzbekistan **Americas**: 🇦🇷 Argentina · 🇨🇦 Canada · 🇵🇷 Puerto Rico · 🇺🇸 United States **Oceania**: 🇦🇺 Australia · 🇳🇿 New Zealand > Nigeria additionally supports Tax-ID and TIN lookups; every listed country supports the standard registration-number lookup. See [Business verification → Products](https://trust.myaza.co/documentation/business-verification/markdown#products). --- # Address Intelligence Address strings in our markets are free prose: "3rd house after the yellow kiosk, off Adeola Odeku" is a real address and no string comparison will ever match it against a utility bill. Address Intelligence sidesteps the string entirely. The applicant places a **map pin** on where they live, optionally adds **directions** and an **entrance photo**, and the platform corroborates the pin against the evidence it already holds: the government record's address, the [Proof of Address](https://trust.myaza.co/documentation/workflows/markdown#capture-add-ons) document, and the submission IP's country. The result is a **soft sub-result**. It never changes the verification's own pass/fail status; it feeds [decisioning](https://trust.myaza.co/documentation/decisioning/markdown), the webhook, and the dashboard's verification detail, where the pin renders on a map beside the tier badge that says how strongly it is corroborated. It is always the applicant's own claim, never presented as a verified fact. On a **business (KYB) flow** the pin is the **business premises**, checked against the registry's registered address, and the entrance-photo slot is not offered. ## Turning it on Address collection is a workflow step: toggle **Address Intelligence** in the builder (it sits after Proof of Address on KYC flows, and right after the business-details step on KYB flows). The config block: ```json { "addressCollection": { "enabled": true, "requirePin": false, "photo": "optional", "directions": "optional", "attestPresence": true } } ``` | Field | Meaning | |---|---| | `enabled` | Adds the step to the flow. | | `requirePin` | When `true`, the step cannot be skipped, and a submission without a pin is refused with `422 address_collection_required`. Default `false`: the applicant may skip. | | `photo` | The entrance-photo slot: `off`, `optional` (default) or `required`. Ignored on KYB flows, which never offer the slot. | | `directions` | The free-text directions field, same three modes. | | `propertyFields` | The default for every typed address detail (number, street, unit, building name, neighbourhood, city, state, area code): `off`, `optional` (default) or `required`. `required` here means the house or flat **number** must be given — the addressing fact — while the building name stays optional. | | `fields` | Per-field overrides on the same three modes, keyed by `propertyName`, `propertyNumber`, `street`, `unit`, `neighbourhood`, `city`, `state`, `postcode`. A field set here wins over `propertyFields`, so you can require a city without requiring a plot number. | | `attestPresence` | Take a one-shot GPS fix from the device when the applicant confirms. This is the evidence behind the `attested` tier. Best effort: a denied permission or a slow fix costs the tier, never the flow. | | `presence` | Phase 2: multi-day presence verification of the pin. `{ "enabled": true, "windowDays": 10, "minNights": 3 }` mints a **watch** when the verification passes; see below. With it on, the step shows a plain-language consent notice (how it works, the person's control, data protection) and the success screen sets expectations: keep location on, periodic at-address checks, a notification when confirmed. | The workflow is authoritative. If the step is off, a submitted address block is dropped (never stored, never billed); if `requirePin` is set, the server enforces it whatever the client did. A field set to `off` is stripped at submit rather than stored, and a `required` field left blank is refused with **`422 missing_address_fields`**, which names the offending keys in `missing`. Require carefully. Someone in an unnumbered compound genuinely cannot answer a required house number, and a required field they cannot fill is a dead end rather than better data. The applicant is the only honest source of a plot number; the map can supply the street and the area, which is why those prefill. ## Assurance tiers Every collected pin resolves to a tier, strongest first: | Tier | Meaning | |---|---| | `attested` | The person provably stood at the pin: the device's own GPS fix at confirmation was within a tolerance of the pin (250 m, widened to the fix's reported accuracy, capped at 1 km). | | `corroborated` | An independent record names the pin's locality: the government record's address, or the Proof of Address document. | | `null` | A collected claim with nothing yet backing it. Still useful for delivery, field visits, and future re-checks. | Corroboration is deliberately **locality-level** (city/area, not street): street-level comparison of free-prose addresses manufactures `no_match` verdicts out of formatting. IP evidence stays **country-level**, because mobile carrier networks route thousands of subscribers through a handful of gateways and the city an IP resolves to is the gateway's, not the person's. ## What comes back The verification result and the completion webhook carry `data.addressIntel` (null when the step did not run): ```json { "addressIntel": { "status": "collected", "tier": "attested", "pin": { "lat": 6.4281, "lng": 3.4219, "accuracy": 12 }, "plusCode": "6FR5CCHC+6Q", "property": { "name": "Sunrise Villa", "number": "11", "unit": "Flat 4" }, "claimed": { "neighbourhood": "Oniru", "city": "Lagos", "state": "Lagos", "postcode": "106104" }, "composedAddress": "Flat 4, Sunrise Villa, 11 Adeola Odeku Street, Oniru, Lagos, Lagos, 106104", "capturedAtAddress": true, "distanceMeters": 38, "corroboration": { "govRecord": "match", "poaDocument": "unknown" }, "geocode": { "city": "Lagos", "state": "Lagos", "country": "NG" }, "derivedAddress": "Adeola Odeku Street, Victoria Island, Lagos", "directions": "black gate opposite the kiosk", "entitiesAtSamePin": 0, "entitiesAtSamePin30d": 0, "heldMatch": true, "knownAddresses": 4, "corroborationSources": { "govRecord": "held" }, "checkedAt": "2026-08-25T12:00:00.000Z" } } ``` - `status` and `tier` are add-only vocabularies; Phase 2's presence verification will extend both. - `plusCode` is the pin's 10-digit [Plus Code](https://plus.codes), the open, offline-computable address handle for places without street addresses. Derived from the pin, never an input, and typeable into any maps app. - `derivedAddress` is a street-level display line reverse-geocoded from the pin, the way OkHi builds its formatted addresses. It deliberately **never carries a house number**: map data holds street names, not plot numbers, and inventing one would dress a guess as a fact. Null where the map knows too little to say anything. - `property` and `claimed` are what the applicant typed in the edit-details form: `property` carries the building name, number and unit; `claimed` carries their own corrections to the area fields (neighbourhood, city, state, area code). They are claims, recorded verbatim and clearly labelled as such — they never feed the corroboration verdicts, which stay on the map's own answer. `composedAddress` merges the claims with the derived street into the full line, unit first. Two provenances in one string, which is why it is labelled composed and never presented as an official record. Null when the applicant typed nothing. - `capturedAtAddress` / `distanceMeters` report the attest fix against the pin (`null` when no fix was taken). - Each corroboration source answers `match`, `no_match` or `unknown` (nothing to compare against). On a KYB flow `govRecord` is the **premises verdict**: whether the pin's locality agrees with the registry's registered address. - `entitiesAtSamePin` counts **other** subjects of yours whose pin lands on the same map cell, and `entitiesAtSamePin30d` the same count over the trailing 30 days. Shared households are ordinary, so a small all-time number means little; a large **recent** one is the multi-accounting signal worth routing to review. It is a review flag, **never** an identity link and never a block. - When a submission carries nothing to corroborate against, the verdicts fall back to the address evidence the subject's entity already holds from earlier verifications. `corroborationSources` says where each verdict's evidence came from (`submission` or `held`), `heldMatch` answers whether the pin agrees with anything already held (null when nothing is held), and `knownAddresses` counts the entity's address-evidence records. - The entrance photo arrives as `media.addressPhoto`, fetched like every other media URL with a secret key. ## Decisioning The `address.*` fields join the condition namespace, so a graph can route on them: | Field | Type | |---|---| | `address.collected` | boolean (null when the step did not run) | | `address.tier` | `attested` / `corroborated` / null | | `address.capturedAtAddress` | boolean | | `address.distanceMeters` | number | | `address.govRecordMatch` | `match` / `no_match` / `unknown` | | `address.poaMatch` | `match` / `no_match` / `unknown` | | `address.entitiesAtSamePin` | number | | `address.entitiesAtSamePin30d` | number | | `address.heldMatch` | boolean (null when the entity holds nothing to compare) | | `address.knownAddresses` | number | A typical pattern: fast-track when `address.tier exists`, and send `address.entitiesAtSamePin >= 3` or `address.govRecordMatch equals no_match` to review. Conditions on null values fail closed, so a flow without the step never trips an address rule. ## Which map renders where There is nothing to configure and no map vendor account to bring. Google Maps is bundled with the platform: | Surface | Map | |---|---| | **Hosted links** (including key-person invites, step-up and re-run links) | **Google Maps**, automatically. The hosted pages run on our origin, so the platform key stays locked to it. | | **Embedded web SDK** on your own site | **Google Maps**, automatically, through a small frame served from our origin (the map itself always runs on our pages, which is what keeps the key safe on any domain). No setup on your side. | | **Mobile SDKs** (React Native, Flutter) | **Google Maps**, automatically, inside a small in-app web view served from our origin, on the same footing as an embedded web mount. React Native needs the optional `react-native-webview` peer; Flutter bundles `webview_flutter`. Without the web view, or when the page does not load, the built-in picker renders instead. | If the map vendor ever fails to load, or the frame cannot verify the page embedding it, the step falls back to the built-in OpenStreetMap picker rather than going blank — the flow never depends on Google to finish. The interaction is identical everywhere: the map moves under a fixed centre pin, so the applicant drags the world, not the marker. Google surfaces render **satellite imagery with labels**, because "find your roof" is answerable in a dense compound where "find your street on a road map" often is not. ## SDKs and permissions All three SDKs render the step, including hosted links: a draggable map (fixed centre pin), an **address search box** (type "11 Bassey Street", pick a candidate, and the pin lands there with the house number prefilled; search runs on explicit submit and is available whenever the platform's geocoder is on), a "Use my current location" shortcut, an **edit-details sheet** where every part of the address is correctable — number, street, unit, building name, directions, neighbourhood, city, state and area code, prefilled from the map's own answer, each optional or required as the workflow sets it, with the flow's country shown read-only — plus the entrance photo. The pin always works by dragging alone, so **location permission is never required to finish the flow**; it powers only the shortcut and the attest fix. On every SDK the step is a short guided flow rather than one long screen: find the address (search, with as-you-type suggestions where the platform has Google Places configured, ranked so streets near the applicant come first), confirm the pin (the edit-details sheet opens over the map), show the entrance, then review and confirm. A returning applicant with a confirmed pin lands straight on the review screen. The React Native and Flutter SDKs walk the same four steps as the web SDK, with the map and the entrance framing sized so Continue stays on screen on a small phone. A field the workflow marks **required** is enforced in the app on all three SDKs: Continue on the pin step opens the details sheet naming what is still needed, required fields carry an asterisk, and the review step's confirm stays disabled until they are filled. A required field the applicant left on its map prefill counts as filled. ### Street View entrance framing Where a workflow enables it (Address step, "Street View entrance"), an applicant can frame their entrance in Google Street View instead of, or as well as, taking a photo: they pan the street imagery until the entrance sits inside a frame, and the platform fetches that exact view server-side. It is stored as its own capture, separate from the applicant's photo, and both the dashboard and the webhook `media` object label which is which (`media.addressPhoto` is the applicant's own photo; `media.addressStreetView` is the framed street imagery). It runs on hosted links, on embedded mounts and in the mobile SDKs alike: an embedded or mobile SDK never receives a maps key, so it frames the imagery inside a page served from our own origin, which the platform hands it at mount (React Native needs the optional `react-native-webview` peer for this; without it the entrance photo is the route). `required` removes the skip affordance while imagery exists, but it is a client-side nudge, not a gate. Street View coverage is not guaranteed anywhere, so where the pin has no coverage the card explains and the photo remains the route; the server never refuses a submission over a missing frame. - **Web**: the browser's standard geolocation prompt. Nothing to configure. - **React Native**: the SDK's Expo config plugin adds the iOS usage string and Android foreground location permissions by default (`location: false` in the plugin options opts out). - **Flutter**: add the platform permission strings to your host app; see the [Flutter SDK](https://trust.myaza.co/documentation/sdk-flutter/markdown) setup. iOS crashes on the permission request if `NSLocationWhenInUseUsageDescription` is missing. ## Sandbox and billing The step bills through the `address_collection` component when a submission carries an address block the workflow accepts; sandbox and development runs are free. On a [test ID](https://trust.myaza.co/documentation/sandbox-testing/markdown) submission the result is canned: `attested` when a device fix rode along, else `corroborated`, so both tiers are exercisable at no cost. **Selecting the outcome.** Outside production, `metadata.sandboxOutcome` accepts the address tokens `address_attested`, `address_corroborated`, `address_collected` and `address_mismatch` — the same contract as the business lookup's `verified`/`not_found` pin. The token decides the judged fields (tier, captured-at-address, corroboration verdicts) while the geocoded lines stay real; `address_mismatch` is the one your review-routing rules exist for. Production ignores the token silently. On hosted dev and sandbox flows the review step also shows a **Test result** control that sends the token for you. ## Presence verification The strongest tier goes beyond the moment of capture: with `addressCollection.presence.enabled`, a passing verification mints a **presence watch** that runs over the following days (the `windowDays`, default 10) and resolves to one of three verdicts: | Verdict | Meaning | Billed? | |---|---|---| | `verified` | Enough distinct nights (default 3) or qualifying days of at-pin evidence. The `address.presence_verified` trust signal fires. | Yes | | `failed` | An integrity contradiction was caught: repeated mock-location evidence at the fence. Absence is never a failure; only caught spoofing is. | Yes | | `inconclusive` | The window lapsed without enough evidence. Not a finding against anyone. | **No** | > **Presence verification needs setup in your app.** Switching it on in the workflow starts the check, but only a phone can collect the evidence. The SDK does two things by itself when the flow is submitted: it sends the first report, and it asks the person to allow location all the time so the phone can report with your app closed. That background part only works if your app has the background location setup below. Every other report has to come from your app calling the presence reporter when it opens. If your app has neither, every watch collects one day, never reaches what it needs, and ends `inconclusive`. ### What your app has to do 1. **Run the flow in the React Native or Flutter SDK.** A hosted link or the web SDK runs in a browser, which cannot report presence, so no watch starts there. 2. **Pass your user reference as `userId`** when you mount the flow. The SDK stores the pinned address on the phone under that value, and your app reports with the same value as `externalUserId`. Without one, no watch starts. 3. **Add the background location setup to your app** ([React Native](https://trust.myaza.co/documentation/sdk-react-native/markdown#background-monitoring-os-geofencing), [Flutter](https://trust.myaza.co/documentation/sdk-flutter/markdown#background-monitoring-native-geofencing)). Background monitoring is **on by default**: after the flow is submitted the SDK asks for the "always" location permission and arms the geofence itself (the Flutter SDK does this only in versions after 3.5.0), and from then on the phone reports whenever the person is at the address, even with your app closed. It can only do that if your app declares background location (and, on React Native, calls `registerBackgroundPresence()` at startup). Without that setup the SDK skips the step and nothing is asked. To turn it off for a workflow, switch off **Background monitoring** on the Address step's Presence tab. 4. **Call the presence reporter every time your app opens or comes back to the foreground**, for as long as the watch runs. This covers people who refuse the "always" permission, and phones whose battery saver stops background work. The code is under [Reporting presence from your app](#reporting-presence-from-your-app). From React Native SDK 3.5.0 and Flutter SDK 3.6.0 a report also switches background monitoring on for a person whose check is running without it (someone who verified on an older version): they see the system's "allow all the time" prompt once. Pass `autoBackground: false` to ask at a moment you choose. 5. **Subscribe to the evening reminder and turn it into a push notification**, if people do not open your app every day. See [If your app is not opened often](#if-your-app-is-not-opened-often). 6. **Check it is working.** After a day or two, [`GET /api/kyc/address/presence/:externalUserId`](#knowing-a-check-is-running) or the Presence monitoring page in the dashboard should show days going up. Days stuck at 0 mean no report is arriving, and the reporter's `reason` tells you why. See [If days stay at 0](#if-days-stay-at-0). **How much evidence a watch needs.** With the default policy a watch verifies on 3 nights or 5 days at the address within 10 days. Background monitoring counts a full night or day for every stay of 30 minutes or more, with nothing asked of the person. A report from your app counts a little over half of one (0.6), because it shows a moment rather than a stay, so with the reporter alone the person has to open your app at home on **5 different evenings** (between 20:00 and 06:00, phone time) or on **9 different days** within the window. Evidence arrives two ways, weighted strongest first: OS geofence dwell (background monitoring, on by default, weight 1.0) and **foreground one-shot checks** (your app calls the SDK's presence reporter when it opens, no background permission needed, weight 0.6). Both are evaluated on the device. There is deliberately no permissionless network tier: a country-level IP cannot evidence an address, so someone who declines location entirely is unverifiable by presence, and Proof of Address is their honest route. **Where a watch can start.** Evidence only ever comes from a phone, so a watch is started only when one could report against it: the flow ran in the **React Native or Flutter SDK**, you passed your user reference as `userId`, and the subject is a person rather than a business. A flow that runs in a browser (a hosted link or the web SDK), carries no user reference, or verifies a business starts no watch, and the result's `addressIntel.presence` says so with `arranged: false` and an `unavailableReason` (`browser_flow`, `unknown_client`, `no_user_reference` or `business_subject`). The browser flow also does not tell the applicant a check is running. **Privacy is structural**: the phone evaluates the geofence on-device against the pin it stored at capture, and only the derived record (calendar day + dwell + a night flag) is ever transmitted. Raw location traces never leave the device, and the server stores per-day aggregates only. ### Knowing a check is running A presence watch takes days to resolve, so you are told when it starts as well as when it ends. **`entity.address_presence_started`** fires once, the moment the watch is minted, whether that was a passing verification, your own `POST /api/identity/entities/:externalUserId/address/verify` call, the dashboard, a re-attestation, or the next cycle of an always-on chain (`startedVia` says which). It carries the `watchId`, your `externalUserId`, the `policy` the watch has to clear (`windowDays`, `minNights`, `minDays`) and the `deadlineAt` it must decide by, so you can show a real progress bar rather than a bare "in progress". Like every presence payload it carries no pin and no coordinate. Re-minting for a customer who is already being monitored returns the live watch and fires nothing, because no new monitoring began. **There is deliberately no per-night progress webhook.** Observations are per-day aggregates by design, and pushing each one to your endpoint as it lands would turn a compliance check into a location feed about your customer. Progress is a pull instead, at whatever cadence you want it: - `GET /api/identity/entities/:externalUserId/address` (secret key, your backend) returns the live `presence` block with `status`, `nights` and `days`. - `GET /api/kyc/address/presence/:externalUserId` (publishable key, your app) returns the same state plus a 0..1 `progress.score`, for showing the person where their own check stands. So: two pushes per watch, one when it starts and one when it decides, and a pull for everything between them. While a watch is open, [`GET /api/kyc/status/:id`](https://trust.myaza.co/documentation/api-verification-status/markdown) also reports `waitingOn: "address_presence"` on the verification it belongs to, so a row that is still `processing` says why rather than looking stuck. ### Holding approval until the address is confirmed By default a verification is decided on the instant capture and can be approved while the watch that was meant to confirm the address is still running. **Hold approval until the address is confirmed**, on the Presence tab of the Address Intelligence step, stops that: the decision waits for the watch, approves a confirmed address, declines caught spoofing, and sends an unresolved one to review. The switch writes the Presence wait step into your decision graph, so it is the same thing as adding that step on the canvas and you can do either. Set how long the decision waits (up to 30 days); make it at least as long as the watch's own window, or the decision gives up before the evidence is in and everything routes to review. If your graph already routes on the presence result in its own way the switch leaves it alone and says so, because rewriting it could change decisions you wrote deliberately. ### Reporting presence from your app The SDK sends the first report itself the moment the flow is submitted (when location is allowed), so the watch gets its first day, and its first night when the person submits at night. **Everything after that comes from this call.** Make it when your app starts and every time it returns to the foreground, for the signed-in user: ```tsx // React Native: call this from your root component. import { useEffect } from 'react'; import { AppState } from 'react-native'; import { reportAddressPresence } from '@myazahq/kyc-sdk-react-native'; export function usePresenceReporting(userId: string | null) { useEffect(() => { if (!userId) return; const report = () => reportAddressPresence({ apiKey: 'pk_live_…', externalUserId: userId }); report(); // the app has just opened const sub = AppState.addEventListener('change', (state) => { if (state === 'active') report(); // back in the foreground }); return () => sub.remove(); }, [userId]); } ``` ```dart // Flutter: mix this into your root widget's State. class _AppState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _reportPresence(); // the app has just opened } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) _reportPresence(); // back in the foreground } Future _reportPresence() async { final userId = currentUserId; // your signed-in user's reference if (userId == null) return; await MyazaAddressPresence.report(apiKey: 'pk_live_…', externalUserId: userId); } } ``` Use the **same value** you passed as `userId` when the flow ran, and your **publishable** key. It is safe to call on every open for every user: it never throws and never blocks, it returns straight away (without touching location) for a user this phone holds no address for, and a phone only holds one for users who ran a presence flow on it. Several reports on the same day count once. Calling it again yourself when the flow submits is harmless too: the watch is created a few seconds after a submission is accepted, and a report from a pin captured in the last 15 minutes waits for it (up to 90 seconds) rather than arriving first. Every call returns a `reason`. Log it: it is the quickest way to see why a watch is not filling. | React Native | Flutter | What it means | What to do | |---|---|---|---| | `reported` | `reported` | The person was at the address; the day (and night, if it is between 20:00 and 06:00) was counted. | Nothing. | | `outside_fence` | `outsideFence` | The person is somewhere else right now. Nothing is sent: being away is never held against anyone. | Nothing. Normal on most opens. | | `no_pin` | `noPin` | This phone holds no address for this user. The flow ran on another device or in a browser, ran without this `userId`, or the stored address expired (45 days after capture, unless the watch is always-on). | Check the flow and this call use the same `userId`. | | `no_watch` | `noWatch` | No check is running for this user: it has finished, or none was started. | Nothing. | | `stopped` | `stopped` | Your organisation stopped monitoring this user. The SDK switched background location off and forgot the stored address. | Nothing. | | `no_fix` | `noFix` | Location permission was refused, or the phone could not work out where it is. | Explain why you need location, then send the person to settings (`openLocationSettings()`). | | `services_off` | `servicesOff` | The phone's location switch is off. | `openLocationSettings('services')`. | | `network_error` | `networkError` | Myaza could not be reached. | Nothing. The next open tries again. | #### If days stay at 0 Check these in order. The first one that applies is usually the cause. 1. **No `userId` on the flow.** No address is stored on the phone and no watch starts. The result's `addressIntel.presence.unavailableReason` reads `no_user_reference`. 2. **The flow ran in a browser** (a hosted link or the web SDK). No watch starts; the reason reads `browser_flow`. 3. **Your app does not declare background location**, or on React Native does not call `registerBackgroundPresence()` at startup. Only app-open reports can count. 4. **The person refused location, or location is switched off.** The reporter answers `no_fix` or `services_off`. 5. **Flutter SDK 3.5.0 or older.** On iPhone and on Android 11 and later, those versions never asked the person to allow location all the time, so background monitoring stayed off unless they chose it in Settings. Update the SDK; see [Flutter background monitoring](https://trust.myaza.co/documentation/sdk-flutter/markdown#background-monitoring-native-geofencing). From 3.6.0 the presence reporter asks people who verified on an older version, the next time your app opens. 6. **The stay has not been reported yet.** With background monitoring on, a stay is sent when the person leaves or at a check-in, the first one 35 minutes after they arrive. See [When background evidence is sent](#when-background-evidence-is-sent). Opening the app sends an app-open report at once. 7. **React Native: `expo-background-task` was installed without a rebuild.** The periodic check-in needs the package's native code in the app build. SDK 3.4.0 and older closed the app when background monitoring was switched on in that state; later versions skip the periodic check-in instead. Rebuild the app after installing it. 8. **No watch is running for that user.** The reporter answers `no_watch`, and the status endpoint answers `not_started`. That is also the answer for a user reference the server does not know, so check the reference matches the one the flow used. Reports the server does not accept are dropped without an error, so the status endpoint and the reporter's `reason` are the two places to look. The check runs on the phone: only the day and a night flag are sent, never a coordinate. A report made with a faked location is sent **flagged**, because evidence of spoofing is worth more to the watch than silence. Call `clearPresencePin(externalUserId)` when the person signs out. ### If your app is not opened often With the reporter alone, a check only fills when the person opens your app at home, and a night only counts between 20:00 and 06:00 on their phone. Background monitoring removes that need, but some people refuse the "always" permission. For them, the way to get the app opened at the right moment is a push notification from you, and Myaza tells you when to send one. **`entity.address_presence_reminder`** is sent each evening, between 20:00 and 22:00 at the person's own local time, for every running check that has no evidence for that night yet. It is sent at most once per check per evening. It is not sent when tonight is already counted, when the check has finished, or when background monitoring is already reporting for that phone. Myaza never contacts the person itself: we hold no push token, and a message from us would arrive from a name they do not know. How to integrate it: 1. **Subscribe a webhook endpoint** to `entity.address_presence_reminder` (Developers, then Webhooks, in the dashboard, or the webhooks API). 2. **When it arrives, find your user by `externalUserId` and send them a push notification straight away.** The evening is when a report earns a night, so do not queue it for later. Honour the person's notification settings; if they have turned your notifications off, skip it. For example: - Title: "Confirm your address" - Body: "Open the app while you're at home tonight to finish checking your address." 3. **Nothing else is needed on the device.** Tapping the notification opens your app, the reporter you call on app open (step 4 above) runs, and if the person is at home that night is counted. You do not need to pass anything from the notification into the SDK. If you deep link somewhere, make sure the reporter still runs on that path. 4. **Test it in sandbox.** Outside production a reminder is sent once, a few minutes after a check starts, whatever the time of day, so you can see it arrive and build against it. On your webhook's page in the dashboard, **Send test event** delivers a sample. The payload carries ids, progress and the deadline, never a location: ```json { "event": "entity.address_presence_reminder", "data": { "watchId": "aw_…", "entityId": "ent_…", "externalUserId": "user_42", "localDay": "2026-10-01", "reason": "no_evidence_tonight", "progress": { "nights": 1, "days": 2 }, "policy": { "windowDays": 10, "minNights": 3, "minDays": 5 }, "deadlineAt": "2026-10-08T09:14:02.000Z", "alwaysOn": false, "environment": "PRODUCTION" } } ``` `localDay` is the person's own calendar day the reminder is for. `progress` counts the nights and days already observed, against the `policy` the check has to meet by `deadlineAt`. The person's local time comes from their phone at the time of the flow, or from the address when the phone did not say. Two more levers, both on the Address step's Presence tab: a **longer window** (up to 30 days) gives more chances to catch the occasional evening, and **fewer nights required** lowers the bar, at the cost of weaker evidence. For someone who will not share location at all, [Proof of Address](https://trust.myaza.co/documentation/workflows/markdown#capture-add-ons) is the honest route. ### Holding the decision on it Add the **Presence** wait step in the decision graph (or start from the `presence-verified` template): the run parks until the watch resolves, then `address.presenceStatus`, `address.presenceNights` and `address.presenceDays` route the outcome. The wait skips cleanly when no watch was minted, so it is safe on mixed traffic. You can also drive it server-to-server: `POST /api/identity/entities/:externalUserId/address/verify` mints a watch for an existing entity, `…/address/revoke` withdraws it, and the `entity.address_verified` / `entity.address_presence_failed` webhooks carry the resolution. Both resolution webhooks say which evidence produced the verdict: ```json "evidence": { "tier": "background", "background": { "days": 4, "nights": 3 }, "foreground": { "days": 1, "nights": 0 }, "flaggedDays": 0, "backgroundMeetsPolicy": true } ``` Each day and night is credited to the strongest evidence seen that day, so the two sources never count the same day twice. `tier` is the strongest evidence that credited any day (`background`, `foreground`, or `null` when nothing qualified). `flaggedDays` counts days reported with a faked location, which is what a `failed` verdict rests on. `backgroundMeetsPolicy` answers the question the counts alone cannot: whether the background evidence on its own met the check's bar. If your risk policy only accepts addresses confirmed with the app closed, require it to be `true`; if app-open evidence is enough for you, ignore it. Sandbox fast-forwards the whole lifecycle: outside production the window runs in **minutes**, and the persona tokens `presence_verified`, `presence_fail` and `presence_inconclusive` on `externalUserId` produce canned streams through the real scorer. A plain user gets no canned stream, so you can drive your own reporter calls end to end, free. ## Book triage Before anything is deployed in your app, you can grade your **existing book**: upload a CSV (an `address` column, plus optional `customer_id`, `name`, `country`) at Compliance → Address triage in the dashboard, and each row comes back as `corroborated` (the evidence already held agrees on a locality — nothing to do), `confirmable_in_app` (a customer relationship exists, so a short re-capture flow confirms it: send a step-up link, or let the address step run on their next verification; there is no capture to reuse, so it is a short flow rather than one tap), or `needs_presence` (presence verification is the route to evidence). The distribution chart is the remediation plan; the CSV export carries every row. Metered per row via the `address_triage` component; runs of up to 5,000 rows. ## Stability, re-attestation and the certificate Once presence data flows, three more surfaces build on it: - **Stability attestation** — `GET /api/identity/entities/:externalUserId/address` answers the tier, months of tenure at the current address, a `strong`/`moderate`/`weak` stability grade, and the move count over 24 months. An origination-time pricing input. - **Re-attestation** — `POST …/address/reattest` runs a short-window watch that answers "still at this address: yes / no / unknown". Address-status answers only, under the consent taken at onboarding; never live whereabouts. Billed on resolution like any watch. - **Navigation packet** — `GET …/address/packet` returns the pin, the entrance-photo URL and the directions your flow collected: your own data, packaged for a delivery or recovery team. - **Verifiable certificate** — the dashboard's entity Address card issues a PDF certificate whose QR resolves to a public, tokenised status page. The page answers the tier and confirmation dates **only** — never the address, the name, or a coordinate — and the underlying events sit on a tamper-evident, hash-chained audit log. One privacy rule worth knowing: the pin is **never** used to link or merge identities, and same-pin density only ever flags for review. Coordinates stay your customer's collected data; they ride your webhook and your dashboard, and nothing else. ### Address-only workflows (standalone, no identity step) Address Intelligence also runs **without any identity verification**, for books whose KYC already happened elsewhere (imports, a previous partner). Set `scope: "address"` on a workflow (or start from the **Address-only verification** template) and the flow becomes: consent, optional contact checks, the address evidence you asked for, done. No ID step, no biometrics; publish rejects every identity-evidence field on such a workflow, and the consent screen stops claiming captures that will never happen. **You choose how the address is verified.** There are two honest ways and the workflow picks either or both: | On the rail | What the applicant does | What you get | |---|---|---| | Address step | Drops a pin on their home, optionally a door photo and directions | The pin, its tier, corroboration against the evidence you hold, and presence monitoring if you want it | | Proof of address | Uploads a utility bill, bank statement or similar | The document read, its name match and recency, and the address extracted from it | Turning one off is a switch on the rail, and the builder locks whichever is the last one left on: a flow that verifies neither would verify nothing, so that is the only combination publish refuses. A document-only flow never opens a map, and a pin-only flow never asks for a document. Bill accordingly: each step is priced on its own, so you are never charged for the one you did not turn on. Because there is no ID to name a country, the applicant declares their own, and the flow corrects itself as evidence arrives. It opens on the country the visitor's IP suggests, which is a guess and treated as one: the moment the pin resolves to a country, that replaces the guess, and picking a search result replaces even an explicit choice, because it is the newest and most specific thing the applicant has said. An accepted-country list on the Proof of Address step bounds all of it, so the flow never defaults to a market you do not take. Country search is **biased**, never restricted: a wrong guess must not make the right address unfindable. Submissions carry `idType: "address"` (the workflow is required, exactly as KYB flows require theirs) and bill only the address-side components: `address_collection`, plus proof of address, contact OTPs, the questionnaire and device intelligence where enabled. The row completes with the corroboration verdicts and tier on `addressIntel` (also returned by `GET /api/kyc/verifications/:id`), and never fails on a corroboration gap; the decision layer is where you branch, including holding on the presence watch. The linking contract is the important part for an existing book: mint a session per customer (`POST /api/kyc/sessions` with their `externalUserId`, or share the hosted link) and the capture lands on **the entity you already imported**, writing the pin and nothing else. An `EXTERNAL_VERIFIED` provenance, the identity link and the profile your partner's KYC established all survive untouched; a customer with no existing record gets a plain `UNVERIFIED` entity, which is exactly what it is. Presence verification, always-on monitoring, certificates and triage then work exactly as below. When capture happens on a hosted web link but presence should run in your own app, hand the SDK the pin yourself: `savePresencePin(externalUserId, { lat, lng })` (React Native) / `savePresencePin(externalUserId, lat, lng)` (Flutter) is the org-side handoff that arms the foreground and background tiers. ### Always-on monitoring Set `presence.alwaysOn: true` on the workflow's address step and presence verification stops being a one-off: each resolved check automatically starts the next cycle, so the address keeps being confirmed month after month. Coverage is billed **per entity per year**, priced by cadence: the first presence check bills on its own resolution as usual, and every renewal cycle after it is covered by the year charge. `presence.cadenceDays` picks from a closed menu — monthly (`30`), every two months (`60`), quarterly (`90`, the default), every six months (`180`) or yearly (`365`) — each with its own rate-card row (`address_monitoring_30` through `address_monitoring_365`), so a monthly book pays more per year than a yearly one. There is deliberately no back-to-back mode. In year one a monitored entity costs the capture plus the first check plus the year of coverage (quarterly example: ₦48 + ₦240 + ₦800, about ₦1,088 all-in); every year after is the coverage fee alone, and only the FIRST cycle ever bills the per-check price, so presence and monitoring never charge twice for one piece of work. Per-day observation rows are retained for 365 days after a watch resolves and then pruned; the watch's counters, verdict and score remain the durable record. Results arrive through the same `entity.address_verified` and `entity.address_presence_failed` webhooks, so "this person is no longer staying at the address" arrives as an ordinary resolution with `verdict: "inconclusive"`. A short balance pauses the chain at its next renewal and it resumes automatically after a top-up. Monitoring stops when you stop it (see [Stopping monitoring](#stopping-monitoring)), when a check fails on tampered evidence, or when the entity leaves your book. On mobile, pair it with the background tier and the OS confirms residency with no further user action. React Native: install `expo-task-manager`, set the config plugin's `location: "always"`, call `registerBackgroundPresence()` at your app root and `enableBackgroundPresence()` after capture. Flutter: declare the background-location entries in your own manifest and Info.plist, then call `MyazaBackgroundPresence.enable()`; the plugin's native side survives app death, and on Android a reboot too. Declaring background location changes your app's store review posture, which is why it is opt-in at every layer on both platforms; the [Background Location Declarations](https://trust.myaza.co/documentation/background-location-declarations/markdown) page carries the ready-to-paste Play Console and App Review texts. #### When background evidence is sent The background tier also records a stay **while the person is still there**, not only when they leave. The first time is 35 minutes after the person arrives, and after that once every three hours (SDK versions before this change waited three hours for the first one too). At those points any confirmed reading inside the circle records the stay so far and starts a new one: an app open with `reportAddressPresence()` / `MyazaAddressPresence.report()`, each reading of the Android foreground service, a background job on Android (Flutter, built in: about 40 minutes after an arrival, then every couple of hours; React Native, with the optional `expo-background-task`: about every 40 minutes), each iOS launch, and iOS background refresh when you opt in (see each SDK's README). The phone's operating system decides when a background job actually runs, so these are "not before" times. **Nothing is sent while a stay is only running.** Background evidence reaches the server when the person leaves the circle or at one of those check-ins, never in between. A person who verified ten minutes ago and has not moved shows 0 days, correctly. To tell them so, read the open stay on the phone with `presenceStay(externalUserId)` (React Native) or `presenceStay()` (Flutter): it returns when they arrived and the earliest time the next report can go out. Someone who hardly leaves home is therefore still credited day by day, and one exit the phone failed to report can no longer cost more than the hours since the last check-in. None of this needs a permission beyond the background location the tier already holds. ### Knowing which tier is running ### Stopping monitoring Your team can stop monitoring a customer at any time; the customer cannot stop it themselves. In the dashboard, open **Risk Intelligence → Presence checks**, tick the customers (or open one) and choose **Stop monitoring**. From your backend, call `POST /api/identity/entities/:externalUserId/address/revoke` with an optional `{ "reason": "…" }`. A stop does two things: the check that is running ends without a result (its status becomes `revoked` and it is not billed), and always-on monitoring stops renewing, including when it was waiting between checks. Results already recorded stay as they are, and a year of always-on cover already paid for is not refunded. There is no resume: start a new check from the customer's Address card or with `…/address/verify`. You receive `entity.address_presence_stopped`, and the phone learns the next time your app opens: the status endpoint answers `stopped: true`, and the presence reporter then switches off background location for that customer, forgets the stored pin and returns the reason `stopped` instead of reporting. Myaza support can also stop monitoring on your behalf; that appears in your audit log with their reason. A person can revoke the location permission in Settings and nothing tells your app. Both SDKs expose `presenceStatus(externalUserId)`, which answers with the tier that is ACTUALLY running (`background`, `foreground` or `none`), the stored-pin state, the phone's location-services toggle (`locationServicesEnabled`: off, nothing can run whatever the permissions say, and the reporter answers `services_off`), both permission states, whether the geofence is armed and whether the Android foreground service is running; `openLocationSettings()` is the recovery path, since neither OS allows re-prompting in-app after a denial, and on Android `openLocationSettings('services')` opens the toggle itself. The dashboard's presence table shows the same fact from the server's side: each watch carries an evidence-tier chip derived from the sources its observations actually arrived on, so a watch quietly running on app-open moments alone, with no geofence reporting, is visible at a glance. ### Showing the person where the check stands Somebody kept from a feature until their address is verified should be able to see the progress in your app, and a webhook relayed through your backend is the long way round. `GET /api/kyc/address/presence/:externalUserId` (publishable key, handoff-capable) answers: ```json { "status": "in_progress", "watchId": "aw_…", "startedAt": "2026-09-03T09:12:00.000Z", "deadlineAt": "2026-09-13T09:12:00.000Z", "decidedAt": null, "progress": { "score": 0.6, "nightsObserved": 3, "daysObserved": 4 }, "tier": "foreground", "alwaysOn": false, "nextCycleAt": null } ``` `status` is one of `not_started`, `in_progress`, `verified`, `failed`, `inconclusive`, `expired` or `revoked` (add-only). `stopped` is `true` when your team stopped monitoring and nothing has started since (see [Stopping monitoring](#stopping-monitoring)). `progress.score` is the completion of the watch's bar on **weighted** evidence: five foreground nights and three geofence nights both read `1`, so a foreground-only phone never looks further along than the scorer will credit it; the raw counts ride beside it for context. `tier` is what is actually feeding the watch. The endpoint is enumeration-safe (an unknown user, a business entity and a user with no watch all answer the identical `not_started` shape) and PII-light by construction: never the pin, the address or a coordinate. Poll it on app open, and send a notification of your own when it turns `verified` to bring the person back. ### The Android foreground service A geofence alone is not reliable on Android once a manufacturer's battery manager decides an app is idle: transitions are dropped, nothing says so, and the watch quietly lapses to inconclusive. The phones on that list (Tecno, Infinix, itel, Xiaomi, Oppo, Vivo) are the ones the Nigerian and Kenyan markets carry. A foreground service, with its persistent notification, is the one thing those managers leave alone, and it is what OkHi's own integration guidance recommends for the same markets. Both mobile SDKs offer it as a third opt-in beside the geofence: React Native `enableForegroundService({ …, notification: { title, body } })` (the `location: "always"` plugin setting declares the two permissions it needs) and Flutter `MyazaPresenceService.enable(…, notification: PresenceNotification(…))` (you declare the service and its permissions in your own manifest; the [Background Location Declarations](https://trust.myaza.co/documentation/background-location-declarations/markdown) page has the block). While it runs, a low-power fix every ten minutes is turned into the same enter/exit spans the geofence folds, on the same stored state, so the two never double-count a stay; the queue flushes while the process is alive; a fence the OS dropped is re-armed; and on Android a reboot restarts it. Android only: iOS region monitoring is reliable on its own, and continuous background updates there would be the heavier posture for nothing. The notification is yours to word, and it is on screen for days, so word it honestly. --- # Background Location Declarations Turning on the background presence tier means your app declares background location, and both app stores review that declaration. This page is the kit: the framing, the text to paste, the video to record and what to do if a reviewer pushes back. You only need any of it if you call `enableBackgroundPresence()` (React Native) or `MyazaBackgroundPresence.enable()` (Flutter). The SDK never adds background location to your app on its own. Without it, presence verification still runs on app-open readings (see [Address Intelligence](https://trust.myaza.co/documentation/address-intelligence/markdown#presence-verification)), and nothing on this page applies. ## Frame it around your feature, not around verification Reviewers approve background location when it powers something the **user** gets from **your app**, and when the app would not work properly without it. "We use a verification SDK" is not a reason. "You cannot open a full account with us until your home address is confirmed, and confirming it takes several days" is. So before you write anything, fill in three blanks. Every text below uses them: | Placeholder | What to write | Example (a digital bank) | |---|---|---| | `{{App}}` | Your app's name as it appears in the store | Acme Money | | `{{Feature}}` | The feature the user unlocks or keeps by confirming their address | Full account (higher limits) | | `{{Why}}` | Why your business must confirm the address before giving them that feature | Our banking licence requires a verified residential address before we can lift account limits | Pick the row closest to your business: | Your business | `{{Feature}}` | `{{Why}}` | |---|---|---| | Bank, wallet or neobank | Full account, higher transaction limits, without an agent visiting your home | A verified residential address is a condition of our licence for full accounts (in Nigeria, the CBN's Tier 3) | | Savings, investment or crypto exchange | Withdrawals above the basic limit | Our regulator requires a verified address before higher withdrawals | | Insurance | Policy issue | The insured address must be confirmed before cover starts | | Merchant or agent onboarding | Merchant account activation | We confirm a merchant's premises before they can take payments | **Personal-loan apps cannot use this on Google Play.** Play's Financial Services policy forbids personal-loan, earned-wage-access and loan lead-generation apps from requesting precise location (`ACCESS_FINE_LOCATION`), and geofencing needs it. If your Play listing is a personal-loan app, use [Proof of Address](https://trust.myaza.co/documentation/workflows/markdown#capture-add-ons) instead. The same limit affects the address step's own location fix, so talk to us before enabling address capture in such an app. Say what is actually true for your business. If a verified address is a regulatory requirement for you, name it: reviewers treat a legal obligation that the user benefits from (a working account) very differently from a nice-to-have. ### The questions the reviewer is answering Google tells its reviewers to ask these. Your texts and video should answer each one without the reviewer having to guess: 1. **Is background location important to the app's core functionality?** Yes: without it the customer cannot get {{Feature}} in the app. 2. **Does it deliver clear value to the user?** They get {{Feature}}, confirmed from their phone instead of by a field agent or a paper bill. 3. **Would users expect it?** They started the address confirmation themselves, after a screen explaining it. 4. **Could the app do the same in the foreground?** No. Living somewhere can only be shown over several nights, when the app is closed; a single check while the app is open proves only that the person visited once. ## What the SDK actually does Everything you declare must match this. Reviewers test apps, and a declaration that under-describes what the app does is the fastest way to have it removed. - With the user's consent, the app registers **one geofence** around the address the user placed on a map. - The operating system wakes the app when the phone enters or leaves that area. - From time to time (no more often than about every 40 minutes), and whenever the user opens the app, the SDK takes **one low-power location reading** to confirm whether the phone is still at the address. It is used only for that check. - On the device, the SDK works out which days the user was at the address and whether they were there at night. Only those daily summaries are sent (a date, minutes present, night yes or no). Coordinates are never sent after the address is captured, and no location history is kept anywhere. - Checking stops when the address is confirmed (or, for ongoing monitoring, when your organisation stops it), when the user withdraws consent, or when they turn location off. Fill every form truthfully. A declaration that does not match what the app does can get the app removed, and repeated violations can get a developer account banned. ## Give people a switch Both stores look for a way to stop background location from inside your app, and saying you have one makes the declaration stronger. Put a switch where the user manages their account (for example "Address confirmation: On") and call `disableBackgroundPresence()` (React Native) or `MyazaBackgroundPresence.disable()` (Flutter) when they turn it off. Turning it back on calls the matching enable function. The texts below mention the switch; remove that sentence if you do not add one. ## Google Play ### 1. The in-app disclosure (required before the permission prompt) Play requires a disclosure inside your app, shown **before** the system asks for "Allow all the time", in normal use (not only in your privacy policy). It must say that the app collects location, that this happens in the background, and what for. The SDK's address screens already carry a disclosure ("How it works": *your phone confirms it is at this address over the coming days, even when the app is closed. Only day-level summaries ever leave your phone, never your movements*). Keep those screens. Because the disclosure must be **your app's** statement, also show this sentence, in your app, on the screen where the user chooses to confirm their address: > {{App}} collects location data to confirm your home address for {{Feature}}, even when the app is closed or not in use. Only daily summaries are used, never your movements, and checking stops once your address is confirmed. Give it one button that continues ("Continue") and one that does not ("Not now"). "Not now" must leave the user in a working app, ideally with another way to confirm their address. Google's rules for this screen: it must be in the app itself (not the store listing or your website), it must not look like a system dialog, it must be readable by a teenager, and the accept button should not be a vague "Got it" or "OK". ### 2. Location permissions declaration Play Console: **App content → Sensitive app permissions → Location permissions**. The form asks for three things. **App purpose** (one or two sentences about your app as a whole): > {{App}} is [what your app is, for example "a digital bank for everyday payments and savings"]. Customers confirm their home address in the app to get {{Feature}}. **One feature that needs location in the background** (describe one feature only): > Address confirmation for {{Feature}}. {{Why}}. With the customer's explicit in-app consent, the app registers a single geofence around the address the customer placed on a map. Based on when the phone enters and leaves that area over several days, and an occasional low-power location reading (no more often than about every 40 minutes, and when the app is opened) that only checks whether the phone is still at that one address, the app confirms that the customer lives there. Location is evaluated on the device: the app sends only daily summaries (date, minutes present, whether night hours were included). No location history is collected, coordinates are not transmitted after the address is captured, and checking stops when the address is confirmed. Customers can turn address confirmation off at any time with a switch in the app. Background access is required because living at an address can only be shown over several days and nights, most of which the app is not open for. **Video:** the link from step 3. If the form asks whether this is core functionality, answer yes: without it the customer cannot get {{Feature}} through the app. ### 3. The video (30 seconds) Record on a real Android phone, portrait, with no editing beyond trimming. Keep it to 30 seconds or less. Upload it where the reviewer can open it without signing in (an unlisted YouTube link is preferred; a shared Google Drive MP4 is also accepted) and paste the link into the form. | Time | What the screen shows | What to do | |---|---|---| | 0 to 4 s | Your app's home screen, then the {{Feature}} entry point | Tap into {{Feature}} ("Upgrade account", "Apply for a loan") | | 4 to 9 s | Your disclosure sentence from step 1 | Pause so it can be read, then tap Continue | | 9 to 13 s | The SDK's "Let's confirm your address" screen and its "How it works" panel | Open "How it works" so the text is visible | | 13 to 18 s | The map with the pin | Place the pin, confirm the address | | 18 to 23 s | The system prompt, then "Allow all the time" in the permission screen | Choose "Allow all the time" | | 23 to 27 s | The success screen: the address check is running | Hold on it | | 27 to 30 s | Your "Address confirmation" switch | Show that it can be turned off | The three things the reviewer is looking for, in this order: the disclosure, the permission prompt, and the feature that uses it. ### 4. Data safety form - **Location, precise:** collected; not shared (Myaza processes it only on your behalf, which Google Play does not count as sharing). - **Purposes:** App functionality; Fraud prevention, security and compliance. - **Optional:** Yes, if a user who declines can still use the rest of your app. - **Encrypted in transit:** Yes. ### 5. The Android foreground service (only if you turned it on) If you also call `enableForegroundService()` (React Native) or `MyazaPresenceService.enable()` (Flutter), your app runs a location foreground service with a persistent notification, and from Android 14 Play asks for a second declaration (**App content → Foreground service permissions**, type Location). When the form asks for the use case, choose user-initiated location sharing: the customer started the address confirmation themselves. Most apps should not turn this on. It exists for phones whose battery managers stop background work, and the periodic check-in above now covers much of that without it. Google has also announced that **geofencing stops being an accepted use of a location foreground service from 27 January 2027**, so expect this tier to be retired before then. If you do use it: ```xml ``` > {{App}} runs a location foreground service, with a visible notification, only while an address confirmation the customer consented to is in progress for {{Feature}}. It takes a low-power location reading every few minutes, checks on the device whether the phone is at the one address the customer confirmed, and sends daily summaries only (date, minutes present, whether night hours were included). It stops when the address is confirmed, when the customer withdraws consent, or when the check ends. ## Apple App Store Apple has no declaration form. Review reads your purpose strings, your App Review notes and your privacy label. **Purpose strings** (state the purpose, not the mechanism): ```xml NSLocationWhenInUseUsageDescription Used to confirm your home address for {{Feature}}. NSLocationAlwaysAndWhenInUseUsageDescription Lets {{App}} confirm your home address over the next few days, even when the app is closed. Only daily summaries are used, never your movements. ``` If you opt into the periodic iOS check-in, also add `co.myazahq.kyc.presence.checkin` under `BGTaskSchedulerPermittedIdentifiers` and `fetch` under `UIBackgroundModes`. Do not add the `location` background mode: region monitoring does not need it, and it invites questions. Apple asks for no capability beyond these for region monitoring. Some vendors' guides ask for the "Location updates" background mode as well; this SDK does not use continuous location updates, so leave it off. **App Store description** (optional, one line near the end of your description): > {{App}} asks for location access "Always" only to confirm your home address for {{Feature}}: it checks whether your phone is at that one address over a few days, and you can turn it off at any time. **App Review notes:** > {{App}} confirms a customer's residential address before giving them {{Feature}}. {{Why}}. The "Always" location permission is used for one purpose: with the customer's consent, one region is monitored around the address they placed on a map, and the app occasionally confirms whether the phone is inside it. Only daily summaries (date, minutes present, night yes or no) leave the device. There is no location history and no tracking of where the customer goes, and the customer can turn it off with a switch in the app. To test: sign in with [demo account], open [path to {{Feature}}], and the consent screen appears before any permission prompt. Apple also asks apps to offer an alternative to people who decline. Keep another route open for them: [Proof of Address](https://trust.myaza.co/documentation/workflows/markdown#capture-add-ons) in the same workflow, or the app-open check-ins, which need only while-in-use location. **Privacy label:** Location, Precise; used for App Functionality (and Fraud Prevention if you list it); linked to the user; not used for tracking. ## If you are rejected | What the reviewer says | What it usually means | What to do | |---|---|---| | "Not core functionality" | The declaration reads as a vendor feature, not yours | Rewrite around `{{Feature}}` and `{{Why}}`; show in the video that the user cannot get {{Feature}} without it | | "Prominent disclosure missing" | Your disclosure appears after the prompt, or only in the privacy policy | Show your disclosure sentence before the prompt, in the video, with Continue and Not now | | "Video does not show the feature" | The video stops at the permission prompt | End on the success screen showing the check running | | "Declaration does not match app behaviour" | Something runs that you did not mention (often the foreground service) | Declare it, or turn it off | | "Foreground location would be enough" | The texts do not explain why several days matter | Add question 4's answer above to the feature text and say it in the video | | Rejected although you removed the feature | An older build on another track (internal, closed, open testing) still has the permission | Remove the permission from every track, or declare it for all of them | Reply to the rejection with the corrected text and a new video link rather than resubmitting silently. If background location is still refused, you lose nothing essential: presence verification carries on from app-open readings, it just takes more opens to finish, and [Proof of Address](https://trust.myaza.co/documentation/workflows/markdown#capture-add-ons) remains available. ## Checklist - [ ] `{{App}}`, `{{Feature}}` and `{{Why}}` filled in and true for your business - [ ] Your disclosure sentence shows before the permission prompt, with a working "Not now" - [ ] An in-app switch turns address confirmation off (and the texts mention it) - [ ] Android manifest has `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION` and `ACCESS_BACKGROUND_LOCATION` (React Native: `{ "location": "always" }` in the config plugin adds them) - [ ] Play: location declaration text, 30-second video link, data safety form - [ ] Your privacy policy is linked both in Play Console and inside the app, and mentions location for address confirmation - [ ] Your Play listing is not a personal-loan app (see the note under the business table) - [ ] Foreground service: off, or declared with its own form - [ ] iOS: both purpose strings, App Review notes with a demo path, privacy label --- # Hosted link events A hosted verification page (`https://trust.myaza.co/verify/…`) tells whatever is hosting it what the applicant is doing: when the flow is ready, which step they are on, when they submit, and when something goes wrong. Nothing to install. Open the link in a WebView (or an iframe) and listen. The events are deliberately light. They carry ids, step names and error codes, never anything the applicant typed or captured. The result itself still arrives by [webhook](https://trust.myaza.co/documentation/webhooks/markdown) and [`GET /status/:id`](https://trust.myaza.co/documentation/api-verification-status/markdown), exactly as before. ## Which link to open Mint a **per-applicant session** from your backend with [`POST /api/kyc/sessions`](https://trust.myaza.co/documentation/api-create-session/markdown) and open its `url`. That link belongs to one person and carries your `externalUserId`, so every event you receive is already about a user you know. The `sessionId` it returns is also the verification's id once the applicant submits. A shared hosted link (`/verify/workflow/`) emits the same events, but a shared link is one URL for many people, so the events cannot tell you who is verifying. ## The events Every message is a JSON object with `source: "myaza-kyc"` and a `type`: | `type` | When | Fields | |---|---|---| | `ready` | The page loaded the session and is about to show the first step. | `sessionId`, `environment`, `subjectType`, `scope` | | `started` | The flow mounted. | `sessionId` | | `step` | The applicant reached a step, including the one the flow opened on. | `sessionId`, `step` | | `submitted` | The applicant submitted. Processing continues on the server. | `sessionId`, `verificationId`, `status` (`pending`) | | `completed` | A returning applicant opened a link whose verification was already submitted. | `sessionId` | | `error` | A technical error (network, camera permission, expired link). | `sessionId`, `code`, `message` | | `closed` | An embedded flow's Done or close was pressed. | `sessionId` | `step` values are the SDK's step names: `consent`, `email-verification`, `phone-verification`, `country-select`, `id-type`, `id-input`, `document-capture`, `liveness`, `proof-of-address`, `address-search`, `address-collection`, `address-entrance`, `address-review`, `questionnaire`, `business-details`, `business-key-people`, `business-documents`, `applicant-role`, `submitted`. `error` codes are the SDK's `KYCError` codes (`network_error`, `camera_permission_denied`, `upload_failed`, `feature_disabled`, `insufficient_credits`, `invalid_api_key`, `unknown`). New event types may be added; existing ones are never renamed. Ignore types you do not recognise. ## React Native (`react-native-webview`) The page posts to the WebView's own channel, so there is nothing to configure on the page side: ```tsx import { WebView } from 'react-native-webview'; { const message = JSON.parse(event.nativeEvent.data); if (message.source !== 'myaza-kyc') return; if (message.type === 'submitted') { // message.verificationId is the id to poll or match against your webhook. navigation.replace('VerificationPending', { id: message.verificationId }); } if (message.type === 'error') console.warn('KYC error', message.code); }} /> ``` The camera works inside `react-native-webview` on iOS 14.3+ and on Android once your app grants the WebView's camera permission request (`onPermissionRequest`). NFC chip reading, background presence checks and native liveness are not available inside a WebView; if your workflow needs them, use the [React Native SDK](https://trust.myaza.co/documentation/sdk-react-native/markdown). ## Flutter (`webview_flutter`) Register a JavaScript channel named `MyazaKYC`; the page posts to it when it exists: ```dart final controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..addJavaScriptChannel('MyazaKYC', onMessageReceived: (message) { final event = jsonDecode(message.message) as Map; if (event['source'] != 'myaza-kyc') return; if (event['type'] == 'submitted') { // event['verificationId'] } }) ..loadRequest(Uri.parse(session.url)); ``` ## In an iframe Append your page's origin to the link as `origin`, and the page posts to that window only when the browser confirms it really is the embedder: ```html ``` ```js window.addEventListener('message', (event) => { if (event.origin !== 'https://trust.myaza.co') return; if (event.data?.source !== 'myaza-kyc') return; // event.data.type, event.data.step, event.data.verificationId … }); ``` An `origin` that does not match the page's actual parent is ignored: no events are posted at all, rather than to a window that should not read them. ## What the events are not They are a courtesy to the host's UI, not the record. Treat `submitted` as "the applicant is done with the flow"; the verdict still comes from your [webhook](https://trust.myaza.co/documentation/webhooks/markdown) or from polling [`GET /status/:id`](https://trust.myaza.co/documentation/api-verification-status/markdown) with the `sessionId` you minted. --- # Webhooks Webhooks let you receive verification outcomes (and other events) in real time instead of polling. Myaza sends a signed HTTP `POST` to each endpoint you register whenever a subscribed event occurs. The runtime catalogue is the source of truth, and the dashboard derives the current canonical event count from it rather than copying a number into this guide. Register and manage endpoints under **Settings → Organization → Developers → Webhooks**. Endpoints are environment-scoped; test your integration with sandbox endpoints before enabling production. ## Request format Each delivery is a `POST` with a JSON body and these headers: | Header | Example | Purpose | |---|---|---| | `Content-Type` | `application/json` | — | | `X-Myaza-Signature` | `sha256=` | HMAC-SHA256 of the raw body. **Verify this.** | | `X-Myaza-Event` | `verification.completed` | The event type. | | `X-Myaza-Delivery` | `` | Unique per delivery; use it to deduplicate. | | `User-Agent` | `Myaza-Webhooks/1.0` | Identifies the sender. | ## Complete public catalogue | Event | When it fires | |---|---| | `verification.started` | The applicant pressed submit. Fires before any check has run. See [When the applicant submits](#when-the-applicant-submits). | | `verification.completed` | Verification succeeded: identity confirmed. | | `verification.failed` | Completed but validations did not pass (e.g. face mismatch, insufficient credit). | | `verification.not_found` | The ID number was not found in the government database. | | `verification.error` | A system error occurred during verification. | | `verification.status_updated` | A verification's top-line status changed: a reviewer decided it in the dashboard, your backend decided it [through the API](https://trust.myaza.co/documentation/api-review-verification/markdown), or your workflow reached its verdict. See [Status changes](#status-changes). | | `session.started` | An applicant opened a resumable verification session. | | `session.resumed` | An applicant returned to an existing verification session. | | `session.abandoned` | An opened verification session passed its completion window. | | `session.expired` | An unopened verification session passed its completion window. | | `session.cancelled` | You (or Myaza support) cancelled a session before it was submitted. The applicant cannot reopen it until it is uncancelled. | | `session.uncancelled` | A cancelled session was uncancelled, so the applicant can carry on. | | `verification.deleted` | You deleted a verification or an unfinished attempt (`kind` says which). See [Delete records](https://trust.myaza.co/documentation/api-manage-records/markdown). | | `verification.restored` | Myaza restored a verification you had deleted, at your request. | | `session.started` | An applicant opened a verification session for the first time. | | `session.resumed` | An applicant returned to a verification session they had left. | | `session.abandoned` | An applicant opened a verification session and left without submitting it. | | `session.expired` | A verification session expired before the applicant ever opened it. | | `api_key.created` | An API key was created (security audit). | | `api_key.revoked` | An API key was revoked (security audit). | | `credits.deducted` | Credit was deducted from the wallet for a verification. | | `credits.low` | Wallet balance dropped below the configured threshold. | | `credits.topped_up` | The wallet was topped up. | | `workflow.run.completed` | A [workflow](https://trust.myaza.co/documentation/workflows/markdown) decision run finished; carries the `outcome` (`approve`/`decline`/`review`). See [Decisioning](https://trust.myaza.co/documentation/decisioning/markdown#the-workflowruncompleted-webhook). | | `workflow.action` | A `webhook` action node fired mid-graph. | | `workflow.run.failed` | A decision run could not finish (engine fault, **not** a decline). | | `entity.created` | A completed verification created a monitored [entity](https://trust.myaza.co/documentation/identity-hub/markdown). | | `entity.imported` | An entity was created via the direct-create or bulk-import API. | | `identity.resolved` | An entity was linked to a global identity. | | `entity.updated` | You edited an entity; carries the changed field names, never values. | | `entity.deleted` | You deleted an entity. Its screening pauses. | | `entity.restored` | Myaza restored an entity you had deleted, at your request. | | `event.flagged` | A monitored [event](https://trust.myaza.co/documentation/monitoring-events/markdown) scored to a non-`ALLOW` decision. | | `screening.match` | A sanctions / PEP / adverse-media / crypto-wallet [screen](https://trust.myaza.co/documentation/screening/markdown) returned a non-clear result (the payload's `type` field says which; `WALLET` for wallet hits). | | `alert.created` | Event monitoring opened an [alert](https://trust.myaza.co/documentation/investigations/markdown). | | `alert.updated` | A repeat firing rolled up into an existing open alert. | | `entity.reverification_due` | An entity's perpetual-KYC renewal is due. | | `entity.address_presence_started` | Presence monitoring began for a customer. Fires once, when the watch is minted, and carries the policy it has to clear plus the deadline. The verdict follows later. See [Knowing a check is running](https://trust.myaza.co/documentation/address-intelligence/markdown#knowing-a-check-is-running). | | `entity.address_presence_stopped` | Somebody stopped presence monitoring for a customer: your team (dashboard or API) or Myaza support, never the customer. Carries `wasMonitoring` (a running check was ended; `false` means always-on monitoring between checks was ended and its last result stands), `alwaysOn`, `stoppedBy` (`user`, `api_key` or `platform`), `stoppedAt` and the progress so far. The reason is never included. | | `entity.address_presence_reminder` | An evening reminder, sent between 20:00 and 22:00 at the person's local time, for a running presence check with no evidence for tonight yet. At most once per check per evening. Turn it into a push notification so the person opens your app at home. See [If your app is not opened often](https://trust.myaza.co/documentation/address-intelligence/markdown#if-your-app-is-not-opened-often). | | `entity.address_verified` | A presence watch confirmed the person lives at their claimed address. See [Address Intelligence](https://trust.myaza.co/documentation/address-intelligence/markdown#presence-verification). | | `entity.address_presence_failed` | A presence watch resolved without confirming: `verdict` is `failed` (an integrity contradiction was caught) or `inconclusive` (the window lapsed, unbilled). | | `case.overdue` | An investigation [case](https://trust.myaza.co/documentation/investigations/markdown) blew past its SLA. | | `key_person.completed` | A KYB [key person](https://trust.myaza.co/documentation/key-people/markdown) resolved (their KYC finished, or a reviewer attested them). Carries their headline `role` and the full `roles` set. | The `data` payloads for the compliance events (`entity.created`, `identity.resolved`, `event.flagged`, `screening.match`, `alert.*`) are documented with each feature. See [Identity Hub API](https://trust.myaza.co/documentation/identity-hub/markdown#webhooks) and [Event monitoring](https://trust.myaza.co/documentation/monitoring-events/markdown#the-eventflagged-webhook). The `workflow.*` events are documented under [Decisioning](https://trust.myaza.co/documentation/decisioning/markdown#the-workflowruncompleted-webhook). ## When the applicant submits `verification.started` is the submission event. It fires the moment the applicant presses submit, before any check has run, and it is the only event that marks that moment. The name describes what happens next on our side (processing begins), but the trigger is the applicant finishing the flow. This catches people out because the event family changes at exactly that point: | Moment | Event | |---|---| | The applicant opens your link, or the SDK is mounted | `session.started` | | They return to a flow they had left | `session.resumed` | | **They press submit** | **`verification.started`** | | The checks finish | one of `verification.completed`, `verification.failed`, `verification.not_found`, `verification.error` | An attempt that never reaches submission ends in the session family instead: `session.abandoned` if they opened it and walked away, `session.expired` if they never opened it at all. A session and the verification it becomes share one id, so `verificationId` is the same value on every event in that table. Correlate the whole attempt on it. If what you want is the event that tells you somebody completed one of your [workflows](https://trust.myaza.co/documentation/workflows/markdown), this is the one. The payload carries `workflowId` and `workflowVersion`, so you know which flow and which published version they walked. ## Payload structure Events share the same envelope, and the `data` object varies by event type. The one exception is [`verification.status_updated`](#status-changes), which is sent flat. ```json { "id": "evt_01j9abc123", "event": "verification.completed", "createdAt": "2026-04-27T12:00:00.000Z", "data": { "verificationId": "ver_01j9xyz456", "requestId": "order_1001", "externalId": "prov_rec_01j9", "externalUserId": "user_42", "metadata": { "loanId": "loan_20191" }, "workflowId": "wf_AbC123dEf456", "workflowVersion": 3, "attempt": 1, "submittedAt": "2026-04-27T12:00:00.000Z", "status": "approved", "checkStatus": "verified", "reason": null, "reasonCode": null, "idType": "bvn", "country": "NG", "idNumber": "12345678901", "userData": { "firstName": "JOHN", "lastName": "DOE", "dateOfBirth": "1990-01-01" }, "facialMatch": { "match": true, "confidence": 85 }, "facialMatchSource": "gov_record", "media": { "selfie": "https://trust.myaza.app/api/kyc/verifications/ver_01j9xyz456/media/selfie", "livenessVideo": "https://trust.myaza.app/api/kyc/verifications/ver_01j9xyz456/media/liveness-video" }, "environment": "PRODUCTION", "createdAt": "2026-04-27T12:00:00.000Z" } } ``` On a non-success event (`verification.failed`, `.not_found`, `.error`) the `data` carries a human-readable `reason` and a stable `reasonCode` you can branch on; both `null` above: ```json { "status": "declined", "checkStatus": "failed", "reason": "The document expired on 2020-01-01. A current, non-expired document is required.", "reasonCode": "document_expired" } ``` See the full [failure reason codes](https://trust.myaza.co/documentation/verifications/markdown#failure-reason-codes) catalogue. `facialMatchSource` says which photo the selfie was compared with: `gov_record` (the government record), `chip` (the verified photo on the document's chip) or `document` (the photo printed on the document, used only when the workflow allows it and neither stronger photo exists). It is `null` when no facial comparison ran. A printed photo is weaker evidence, so you may want to treat a `document` match with more care, for example by sending it to review. `facialMatchSkipped` says why no facial comparison ran when one was expected. It is `no_face_on_document` when the photo printed on the document was the only photo the selfie could be compared with and no face could be read on it, and `null` otherwise. Your workflow either declines that verification (`reasonCode: document_photo_no_face`) or keeps it for review. `workflowId` is the [workflow](https://trust.myaza.co/documentation/workflows/markdown) that drove the verification (`null` for SDK mounts configured with plain props). When the workflow has a [decision graph](https://trust.myaza.co/documentation/decisioning/markdown), the pass/fail result above arrives first, and the approve / decline / review **decision** follows in a separate `workflow.run.completed` event. `workflowVersion` is the **published version that actually ran**. Publishing a workflow overwrites its live config in place and bumps the version, so the id alone stops describing what happened as soon as you publish again; store the pair. Look up the exact configuration a version used in the dashboard under **Workflows → Version history**. It is `null` when the verification had no workflow, predates this field, or attributed to a workflow that was never published. `attempt` says which attempt of the verification the event describes. A verification you [send back or verify again](https://trust.myaza.co/documentation/api-verify-again/markdown) keeps its id, so when the applicant resubmits you receive another `verification.completed` (or `verification.failed`) with the same `verificationId`, `attempt` counted up, and `submittedAt` set to when they resubmitted. Update the record you already hold rather than creating a new one. An `api_key.*` event's `data` instead looks like: ```json { "id": "evt_01j9abc789", "event": "api_key.created", "createdAt": "2026-04-27T12:00:00.000Z", "data": { "apiKeyId": "key_01j9abc000", "name": "Mobile app production key", "environment": "PRODUCTION", "createdBy": "user@example.com" } } ``` ## Your reference on every event You never need a mapping table to know which of your users an event is about. Every event that originates from an entity, a session or a verification carries two fields beside Myaza's own ids: | Field | Meaning | |---|---| | `externalUserId` | **Your** stable reference for the person or business: what you passed as `externalUserId` when you [created the session](https://trust.myaza.co/documentation/api-create-session/markdown), [submitted the verification](https://trust.myaza.co/documentation/api-create-verification/markdown) (or its legacy `userId` / `metadata.userId`) or [registered the entity](https://trust.myaza.co/documentation/identity-hub/markdown). | | `metadata` | Your own correlation fields (a loan id, an account id), echoed exactly as you sent them. Bounded at 16 KiB, four levels and 100 keys; Myaza's request controls (`requestId`, `userId`, `device`, sandbox options) are never part of it. | Myaza's ids (`verificationId`, `entityId`, `sessionId`, `alertId`, ...) stay exactly where they are; the two fields are added, never substituted. Both are resolved once, when the event is created, and persisted with it, so a retry or a manual redelivery replays the same values even if you later changed the entity. `externalId` on verification events is the verification source's own record id, kept for compatibility; correlate on `externalUserId`. Events about your organisation rather than one of your users carry neither field: `api_key.*`, `credits.low`, `credits.topped_up`, `credits.granted`, `credits.promo_expired` and `screening.adjudication_overdue`. A KYB key person's own verification events resolve to the business's reference (the applicant you know), while events about that person's entity keep the `kp_` id you received in `keyPeopleInvites`. ## Status and checkStatus Every verification payload carries two status fields, and they answer different questions. The full explanation, with every combination, is in [Status and checkStatus](https://trust.myaza.co/documentation/verifications/markdown#status-and-checkstatus). In short: **`status`** is the outcome: should this person be onboarded, rejected, or are we still waiting? Update your own record from it. It uses the same vocabulary as the dashboard and `GET /api/kyc/status/:id`. | `status` | Meaning | |---|---| | `not_started` | The link exists but nobody has opened it. | | `in_progress` | The applicant is part-way through. | | `processing` | Submitted. Checks are running, or a decision has not landed yet. | | `in_review` | Your workflow asked a person to decide this one. | | `awaiting_resubmission` | A reviewer sent it back for the applicant to redo some steps. | | `approved` | Accepted, automatically or by a person. | | `declined` | Rejected, automatically or by a person. | | `abandoned` | Opened, then left unfinished past its deadline. | | `expired` | Timed out without ever being opened. Send a new link. | | `error` | A fault on our side. You were not charged. | **`checkStatus`** is what the automated checks found: `pending`, `verified`, `failed`, `not_found` or `error`. Your workflow's decision rules and your reviewers never change it. It only resets to `pending` when the applicant resubmits a verification that was sent back. The two agree when the checks alone decided the outcome. They differ whenever your workflow or a reviewer decided something else, for example `processing` with `checkStatus: "verified"` while your workflow waits on a watchlist screen, or `declined` with `checkStatus: "verified"` when a workflow rule rejected someone whose checks passed. ### Why both Take an applicant whose selfie scored just under the pass mark. The checks fail. Your workflow routes it to review rather than declining outright, a compliance officer looks at the photos, recognises a lighting problem rather than a different face, and approves. That verification is now: ```json { "status": "approved", "checkStatus": "failed", "reasonCode": "selfie_mismatch", "decision": "APPROVED", "reviewedById": "usr_01j9abc123", "reviewedAt": "2026-04-28T09:14:00.000Z" } ``` Both are true. You onboarded them, and you onboarded them despite a failed face match. If your record only stored `approved`, you could not answer why that was allowed, which is the question your own regulator asks about every exception. `reason` and `reasonCode` stay populated for the same purpose: they describe what was overridden. Branch on `status`. Store `checkStatus` alongside it. ### The status on lifecycle events `verification.started`, `verification.completed`, `verification.failed`, `verification.not_found` and `verification.error` are sent the moment the checks finish (or start), **before your workflow has decided anything**. Their `status` therefore reflects the checks alone: a `verification.completed` says `approved` because the checks passed. If your workflow has decision rules, that is not the final answer. The workflow's verdict follows as `verification.status_updated` with `source: "workflow"`, carrying the `status` to act on (`approved`, `declined` or `in_review`). Always take the latest `status` from `verification.status_updated`, not from the first event you received. Without decision rules, the checks are the decision, and the `status` on `verification.completed` stands unless a reviewer later changes it. ### Status changes `verification.status_updated` fires every time a verification's top-line status changes because somebody, or something, decided it. `source` says which: | `source` | What happened | |---|---| | `dashboard` | A reviewer approved, declined or sent it back from your dashboard, or changed an earlier decision. | | `api` | Your backend did the same through [`POST /verifications/:id/review`](https://trust.myaza.co/documentation/api-review-verification/markdown). | | `workflow` | Your [workflow](https://trust.myaza.co/documentation/decisioning/markdown) reached its verdict (`approve`, `decline` or `review`), and that moved the status. | | `platform` | Myaza support approved or declined it on your behalf, usually at your request. | Unlike the other events, this one is sent **flat**: the request body is the object itself, not wrapped in `{ id, event, createdAt, data }`. The event name is in the `X-Myaza-Event` header, as for every event, and in the body as `event` beside `createdAt`, so a body you store describes itself. When the verification has them, `externalUserId` and `metadata` are included as well. A decision made by a person: ```json { "event": "verification.status_updated", "createdAt": "2026-04-28T09:14:00.000Z", "verificationId": "ver_01j9xyz456", "subjectType": "individual", "attempt": 1, "status": "awaiting_resubmission", "previousStatus": "in_review", "checkStatus": "failed", "summary": { "reason": "The document photo is too blurry to read. Take it again in good light.", "reasonCode": "document_blurry", "assuranceLevel": null, "facialMatch": null, "dataMatch": null }, "source": "dashboard", "changedBy": { "type": "user", "id": "usr_01j9abc123" }, "changedAt": "2026-04-28T09:14:00.000Z", "decision": "RESUBMISSION", "outcome": null, "reviewedById": "usr_01j9abc123", "reviewedAt": "2026-04-28T09:14:00.000Z", "entityId": "ent_01j9def789", "workflowId": "wf_AbC123dEf456", "workflowVersion": 3, "runId": null, "resubmission": { "url": "https://trust.myaza.co/verify/hs_live_abc123", "steps": ["document-capture"], "full": false, "expiresAt": "2026-05-05T09:14:00.000Z" }, "environment": "PRODUCTION" } ``` A workflow's verdict: ```json { "event": "verification.status_updated", "createdAt": "2026-04-28T08:02:11.000Z", "verificationId": "ver_01j9xyz456", "attempt": 1, "status": "in_review", "previousStatus": "processing", "checkStatus": "failed", "summary": { "reason": "The selfie does not match the photo on the government record for this ID. Take it again in good light, looking straight at the camera.", "reasonCode": "selfie_mismatch", "assuranceLevel": null, "facialMatch": false, "dataMatch": true }, "source": "workflow", "changedBy": { "type": "workflow", "id": "wf_AbC123dEf456" }, "changedAt": "2026-04-28T08:02:11.000Z", "decision": null, "outcome": "review", "reviewedById": null, "reviewedAt": null, "runId": "wfr_01j9xyz789", "resubmission": null } ``` | Field | Description | |---|---| | `event`, `createdAt` | The event name and when the change happened, carried in the body too. `createdAt` equals `changedAt`. | | `attempt` | Which attempt of the verification the change is about. A verification you send back keeps its id, and the applicant's resubmission is the next attempt. | | `summary` | What the checks concluded, so you can act on the change without a second request: `reason`, `reasonCode`, `assuranceLevel`, `facialMatch` and `dataMatch`. Conclusions only: no names, dates of birth or ID numbers, which stay on the [secret-key result](https://trust.myaza.co/documentation/api-verification-result/markdown). | | `status` | The top-line status after the change. | | `previousStatus` | The top-line status immediately before it. Null on the occasional older change whose prior status was never recorded. | | `source` | `dashboard`, `api`, `workflow` or `platform`. More sources may be added, so treat an unfamiliar value as a status change like any other. | | `changedBy` | Who made the change. `type` is `user` (a member of your team; `id` is their user id), `api_key` (`id` is the key's id), `workflow` (`id` is the workflow's id) or `platform` (Myaza support; `id` is null). | | `decision` | What a person decided: `APPROVED`, `DECLINED` or `RESUBMISSION`. Null when the workflow made the change. | | `outcome` | What the workflow decided: `approve`, `decline` or `review`. Null when a person made the change. | | `reviewedById`, `reviewedAt` | The person or API key that decided, and when. `reviewedById` is null when the workflow or Myaza support made the change. Kept for integrations built before `changedBy`. | | `runId` | The [workflow run](https://trust.myaza.co/documentation/decisioning/markdown) that decided, when the workflow made the change. | A workflow's verdict fires this event only when it moves the status. If a person already decided while the workflow was still waiting (on key people, for example), their decision stands, the verdict changes nothing, and you receive `workflow.run.completed` alone. On a resubmission, `resubmission.url` is the link to send the applicant, unless the reviewer had us email it. They redo only the steps in `steps`; `full: true` means the reviewer asked for the whole flow. Their resubmission completes the **same** verification: the next `verification.completed` or `verification.failed` arrives with the same `verificationId` and `attempt` counted up. See [Verify again](https://trust.myaza.co/documentation/api-verify-again/markdown). The reviewer's internal note is deliberately **not** included. It is free text somebody wrote about your applicant, so it stays in your dashboard: in the audit log and on the verification's timeline. The short message written *for* the applicant travels on the resubmission link instead. Decisions can change. A reviewer or your backend may reverse an earlier decision, and each change arrives as its own `verification.status_updated`. Deliveries retry independently and can arrive out of order, so treat the latest `changedAt` as current. Every change also appears on the verification's **Timeline** in your dashboard, with who made it and why. An endpoint only receives `verification.status_updated` when it is subscribed to it. Endpoints created before the event existed are not, so open the endpoint under **Developers → Webhooks** and choose **Edit events** to add it. The endpoint keeps its URL and signing secret. ## Captured media Verification events carry a `media` object: a map of the images and videos captured during the flow. Each value is an **absolute URL** (not the bytes themselves). Fetch each one from your backend with a **secret (`sk_`) key** as a Bearer token (media is sensitive, so a publishable key returns `403 secret_key_required`): ```bash curl -H "Authorization: Bearer sk_live_..." \ https://trust.myaza.app/api/kyc/verifications/ver_01j9xyz456/media/selfie \ --output selfie.jpg ``` The URLs are scoped to the secret key's **organisation and environment** (a sandbox key cannot read production media) and do not expire. The object only contains the kinds that were actually captured (a number-only-ID flow has just `selfie` + `livenessVideo`), and is `null` when no media is associated with the event. | Key | Description | |---|---| | `selfie` | Liveness selfie still. | | `documentFront` | Front of the ID document. | | `documentBack` | Back of the ID document, when captured. | | `livenessVideo` | Recording of the liveness challenge. | | `documentFrontVideo` | Recording captured while scanning the document front. | | `documentBackVideo` | Recording captured while scanning the document back. | | `documentPortrait` | The portrait cut from the document, present when the selfie was matched to the photo printed on it. | | `silentCapture1` to `silentCapture3` | Silent capture: up to three unposed photos of the applicant, taken while the camera was already on (a laptop's webcam during document capture, and while they positioned for the selfie). Present unless the workflow switches silent capture off, and withheld when the workflow's Presence Intelligence returned data leaves out silent capture photos. | | `recordPhoto` | The government ID photo: the photo on the government record, or on the ID's chip, that the selfie was compared with. On a biometric re-authentication it is the reference photo the live selfie was matched against. Present when there is one. | | `chipPortrait` | The photo read from the ID's NFC chip, present on single-ID verifications when the chip passed authentication. On a multi-ID run, a chip-only ID's photo arrives as its `idCheckPhoto`. | | `supportingDocument` | The nth supporting document the workflow asked for (1-based, in the order of `supportingDocuments`). | | `businessDocument` | The nth company document on a business verification (1-based, in the order of `business.documents`). | Each entry in `supportingDocuments` and `business.documents` also carries `url`, the same link as its `supportingDocument` or `businessDocument` key, so you can fetch a document straight from its result. The entry's `mediaId` is an internal reference and cannot be used to fetch the file. Leaving out the Supporting documents step's **media** returned data withholds those files and sets each `url` to `null`; leaving out the business **documents** withholds the company documents and their files. ### Multi-ID verifications When a workflow verifies [more than one ID in one run](https://trust.myaza.co/documentation/workflows/markdown), each ID is a check in `data.multiId.checks`, and each check's files are listed in `media` under its own number. The number starts at 1 and follows the order of `multiId.checks`, so the second check's files are the `idCheck2…` keys. | Key | Description | |---|---| | `idCheckPhoto` | The photo on the government record for that ID. The selfie was compared with it. | | `idCheckPortrait` | The portrait cut from that ID's document, present when the selfie was matched to the photo printed on it. | | `idCheckFront` | Front of that ID's document. | | `idCheckBack` | Back of that ID's document, when captured. | | `idCheckFrontVideo` | Recording captured while scanning that document's front. | | `idCheckBackVideo` | Recording captured while scanning that document's back. | Each check also carries its own photos as URLs, so you can read them straight from the check. `multiId.checks` starts at 0 and the keys start at 1, so the first check's `recordPhoto` and `documentPortrait` hold the same URLs as `idCheck1Photo` and `idCheck1Portrait`. Either is `null` when the check has no such photo. A number-only ID (BVN, NIN) has no document, so when its record returned a photo it has only `idCheckPhoto`. `media.recordPhoto` is also present: it is the photo of the first ID that passed (or the first ID, when none passed). ```json "media": { "selfie": "https://trust.myaza.app/api/kyc/verifications/ver_01j9xyz456/media/selfie", "livenessVideo": "https://trust.myaza.app/api/kyc/verifications/ver_01j9xyz456/media/liveness-video", "recordPhoto": "https://trust.myaza.app/api/kyc/verifications/ver_01j9xyz456/media/record-photo", "idCheck1Photo": "https://trust.myaza.app/api/kyc/verifications/ver_01j9xyz456/media/id-check-1-photo", "idCheck2Photo": "https://trust.myaza.app/api/kyc/verifications/ver_01j9xyz456/media/id-check-2-photo" } ``` Your workflow's returned data (on the ID Verification step) controls these links. The photos (**Government ID photo**, **Chip photo** and **Photo from the ID card**) are sent by default and each can be unticked: unticking **Government ID photo** withholds `recordPhoto` and every `idCheckPhoto`, and so does leaving out the facial match. Where the government ID photo came from the chip (a verification, or one ID of a multi-ID run, completed from the chip alone), it is the same picture as the chip photo, so unticking either one, or leaving out the facial match, withholds both. Leaving out **Document images** withholds each check's documents and portrait. Leaving out **Multiple IDs breakdown** withholds every `idCheck` link along with `multiId`. The `verification.started` event carries none of the per-ID, company document or record photo links; they arrive on the result events. The same settings apply to the per-ID photo and supporting document links in the [verification result](https://trust.myaza.co/documentation/api-verification-result/markdown) (`GET /api/kyc/verifications/:id`). They control what we send: a media URL you already hold keeps working with your secret key. ## Verifying the signature Compute `HMAC-SHA256(rawBody, endpointSecret)` and compare it to the `X-Myaza-Signature` header using a **constant-time** comparison. Always use the **raw request body**, not a re-serialised JSON object, or the signature won't match. Your endpoint secret is shown when you create the endpoint in the dashboard. ```js // Node.js / Express const crypto = require('crypto'); app.post('/webhooks/myaza', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['x-myaza-signature']; const expected = 'sha256=' + crypto .createHmac('sha256', process.env.MYAZA_WEBHOOK_SECRET) .update(req.body) .digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(req.body); // process event.event / event.data … res.status(200).send('OK'); }); ``` ```python # Python / Flask import hmac, hashlib from flask import request, abort @app.route('/webhooks/myaza', methods=['POST']) def webhook(): sig = request.headers.get('X-Myaza-Signature', '') expected = 'sha256=' + hmac.new( WEBHOOK_SECRET.encode(), request.get_data(), hashlib.sha256 ).hexdigest() if not hmac.compare_digest(sig, expected): abort(401) event = request.get_json() return 'OK', 200 ``` ## Retries A delivery that doesn't receive a `2xx` response (or times out) is retried with exponential backoff. After 5 failed attempts the delivery is marked `FAILED` and can be retried manually from the dashboard. | Attempt | Delay after previous attempt | |---|---| | 1 | 30 seconds | | 2 | 5 minutes | | 3 | 30 minutes | | 4 | 2 hours | | 5 | 24 hours | ## Best practices - **Verify the signature before processing** the payload. - **Respond `2xx` immediately**, then do heavy work asynchronously to avoid timeouts and retries. - **Be idempotent.** The same delivery may arrive more than once; deduplicate on `X-Myaza-Delivery` (or `verificationId`). - **Store the secret** in an environment variable, never in source. - **Test on sandbox** before enabling production endpoints. --- # Sandbox & test IDs ## Wallet scenarios Wallet screening in a customer sandbox uses Myaza Trust's deterministic scenarios. It never performs a live blockchain lookup and never creates billable wallet usage. In **Spot Checks → Wallet**, choose a scenario before screening, or send `sandboxScenario` to the [On-Demand Wallet Screening API](https://trust.myaza.co/documentation/api-wallet-screenings/markdown). The same published sandbox rules run against the simulated exposures, and sandbox cases, alerts and webhook deliveries use the sandbox environment. Examples include `wallet.clean`, `wallet.low_risk`, `wallet.medium_risk`, `wallet.high_risk`, `wallet.critical`, `wallet.unknown`, `wallet.sanctions.direct`, `wallet.sanctions.1_hop`, `wallet.sanctions.2_hops`, `wallet.mixer.direct`, `wallet.mixer.1_hop`, `wallet.mixer.2_hops`, `wallet.stolen_funds`, `wallet.ransomware`, `wallet.darknet`, `wallet.scam`, `wallet.terrorist_financing`, `wallet.high_risk_exchange`, `wallet.high_risk_jurisdiction`, `wallet.p2p_exchange`, `provider.timeout` and `provider.unavailable`. `wallet.invalid_address` and `wallet.unsupported_network` produce `422` validation errors before a screening or charge. `wallet.unknown` models a pending screening result and produces a review result. Use a format-valid address and hash with the chosen network; a scenario does not bypass input validation. Production rejects the scenario field with `422`. Test your Myaza KYC integration end to end: webhooks, polling, the full result payload (biodata, facial match, OCR data, captured-media URLs), and every failure branch, all **without calling any government database or consuming paid verification credits**. | Environment | Key prefix | Real IDs? | Test IDs? | Charged? | |-------------|-----------|-----------|-----------|----------| | **Sandbox** | `pk_test_` / `sk_test_` | rejected | canned result | never | | Production | `pk_live_` / `sk_live_` | yes | treated as real | yes | **Sandbox** accepts **only** published test IDs. A real (non-test) `idNumber` is rejected immediately at `POST /verify` with `422 only_test_ids_allowed`. All sandbox results are **fully fake**: fake biodata, fake government photos, fake OCR, and fake gov-DB data. No external API is ever called. ## How test IDs work Pass a test ID in the `idNumber` field of `POST /verify`. The **last one or two digits encode the scenario**; everything before is zero-padding so the value stays format-valid for its ID type. Example for NG BVN: ``` 00000000001 → verified 00000000002 → not_found 00000000004 → selfie_mismatch 00000000007 → document_expired (document IDs only) 00000000012 → provider_error ``` ### Scenario reference Scenarios 5 and 6 follow your workflow's identity details policy. When the workflow records every field the scenario disagrees on (5: first and last name; 6: first name and date of birth) instead of failing on it, the test ID verifies with `dataMatch: false`, exactly as a real mismatch would. | Idx | Scenario | Status | `reasonCode` | Applies to | |----:|----------|--------|--------------|-----------| | 1 | verified | verified | — | all | | 2 | not_found | not_found | `identity_not_found` | all | | 3 | gov_validation_failed | failed | `gov_validation_failed` | all | | 4 | selfie_mismatch | failed | `selfie_mismatch` | all | | 5 | gov_data_mismatch | failed | `gov_data_mismatch` | all | | 6 | document_data_mismatch | failed | `document_data_mismatch` | document | | 7 | document_expired | failed | `document_expired` | document | | 8 | document_type_mismatch | failed | `document_type_mismatch` | document | | 9 | document_unreadable | failed | `document_unreadable` | document | | 10 | document_blurry | failed | `document_blurry` | document | | 11 | id_number_not_found | failed | `id_number_not_found` | document | | 12 | provider_error | error | `provider_error` | all | | 13 | unsupported_id_type | error | `unsupported_id_type` | all | | 14 | media_not_found | error | `media_not_found` | all | | 15 | insufficient_credits | error | `insufficient_credits` | all | | 16 | system_error | error | `system_error` | all | | 17 | id_type_not_enabled | error | `id_type_not_enabled` | all | | 18 | document_verification_disabled | error | `document_verification_disabled` | document | | 19 | gov_db_check_disabled | error | `gov_db_check_disabled` | all | | 20 | sandbox_not_found | not_found | `sandbox_not_found` | all | | 21 | sandbox_failed | failed | `sandbox_failed` | all | | 22 | age_restricted | failed | `age_restricted` | all | | 23 | age_unverified | failed | `age_unverified` | all | | 24 | document_photo_no_face | failed | `document_photo_no_face` | document | | 25 | identity_suspended | failed | `identity_suspended` | all | Scenarios 22 and 23 follow your workflow's age restriction. Scenario 22 returns a date of birth just outside your own limits (a year under the minimum, or a year over the maximum) and scenario 23 returns none, so both fail exactly as a real holder would. On a workflow with no age restriction, both verify. If your workflow sends that case to review instead of declining it, scenario 22 verifies with the out-of-range date of birth and your decision rule sends it to review; scenario 23 always fails. Scenario 24 follows your workflow's settings for the photo on the document. It stands for a document ID whose selfie could only be compared with the photo printed on the document, where no face could be read on that photo. If your workflow compares that ID with the photo on the document and declines the case, it fails with `document_photo_no_face`. If your workflow sends that case to review, it verifies with `facialMatchSkipped: "no_face_on_document"`, and your decision rule on `verification.documentPhotoNoFace` sends it to review. If your workflow does not compare that ID with the photo on the document, it verifies. Scenario 25 stands for a government record the issuer has suspended, such as a suspended NIN. It fails with `identity_suspended` and returns no biodata and no face result, as a live suspended record does. > Document-only scenarios (6–11, 18) are not published for number-only IDs (BVN/NIN/vNIN). ### The `verified` test ID per ID type Change the trailing digits per the table above to pick a different scenario. | Country | ID type | Format | `verified` test ID | |---------|---------|--------|--------------------| | NG | bvn / nin | 11 digits | `00000000001` | | NG | vnin | 16 chars | `0000000000000001` | | NG | passport | A + 8 digits | `A00000001` | | NG | drivers-license | TST + 8 digits | `TST00000001` | | NG | pvc | 19 digits | `0000000000000000001` | | GH | ghana-card | GHA-NNNNNNNNN-N | `GHA-000000001-0` | | GH | voters | 10 digits | `0000000001` | | GH | ssnit | C + 12 digits | `C000000000001` | | GH | passport | G + 8 digits | `G00000001` | | KE | national-id | 8 digits | `00000001` | | KE | passport | K + 8 digits | `K00000001` | | ZA | national-id | 13 digits | `0000000000001` | | ZA | passport | Z + 8 digits | `Z00000001` | | CI | cni | CI + 9 digits | `CI000000001` | | CI | residence-card | CR + 9 digits | `CR000000001` | ## Example: number-only ID ```bash curl -X POST https://your-server/api/kyc/verify \ -H "Authorization: Bearer pk_test_xxx" \ -H "Content-Type: application/json" \ -d '{ "country": "NG", "idType": "bvn", "idNumber": "00000000001", "userData": { "firstName": "Chidi", "lastName": "Okafor", "dateOfBirth": "1990-04-12" }, "metadata": { "requestId": "test-001" } }' ``` ## Document IDs In **sandbox**, OCR is **never run**; a fake OCR result is returned instead. Either let the SDK document flow default to the **verified** fixture (no ID number collected), or pick a scenario by passing the document test ID in `idNumber` (e.g. `A00000007` for an expired passport) or via the `sandboxOutcome` override below. ## Business (KYB) Business verification never calls a real registry outside production: a registry lookup incurs an external check cost in whichever environment requests it, so sandbox and development serve canned data instead. - **Any registration number is accepted** and resolves to a verified fixture that echoes the name you searched for. The catalogue values still pick their scenario: `RC0000001` is verified, `RC0000002` is not found, `RC0000003` is found with no company status (as CAC returns some new companies) and `RC0000004` is found but inactive. The last two are for trying a workflow's company-status rules. A business picked from the real company search (which does work in every environment) can therefore be checked on sandbox against fixture data. - **`sandboxOutcome`** on the submit (`"verified"`, `"not_found"`, `"status_unknown"` or `"status_inactive"`) pins the result for any number, and is ignored silently in production. - **The sandbox company carries a full key-people set** for exercising [due diligence](https://trust.myaza.co/documentation/key-people/markdown): a 60% beneficial owner, directors, a signatory, a persona named "Pep" that the sandbox screening stub flags (set `SCREENING_PROVIDER=stub`), and a **corporate shareholder**, `Sandbox Holdings Ltd` (RC0000900), whose own register resolves to a person majority-owner and a second company above it (RC0000901), so ownership-chain look-through, effective percentages, and the depth limit are all testable at no cost. ## Address presence Presence verification has its own personas and a compressed clock so a multi-day watch is testable in minutes; they are documented with the feature on the [Address Intelligence](https://trust.myaza.co/documentation/address-intelligence/markdown#sandbox-and-billing) page. In short: outside production the watch window runs in **minutes**, and an `externalUserId` containing `presence_verified`, `presence_fail` or `presence_inconclusive` seeds a canned observation stream through the real scorer. A plain id gets no seed, so you can drive the ingest endpoint yourself. ## Overrides Both go in `metadata` and are ignored in production. - **`sandboxOutcome`**: force any scenario regardless of the test ID, e.g. `"sandboxOutcome": "selfie_mismatch"`. - **`sandboxDelayMs`**: how long `pending → completed` takes (0–60000 ms). Defaults to ~1200 ms; set `0` for an instant result. ## Captured media Media uploaded under a test ID is kept for **3 days**, then the bytes are replaced with a placeholder (an image for stills, a sample clip for videos). The `data.media` URLs in webhooks keep working; they just serve the placeholder after the sweep. ## Webhook simulator From a webhook endpoint's detail page (or `POST /api/dashboard/webhooks/:id/simulate` with `{ "event": "verification.completed" }`), fire a realistic, correctly-signed sample payload of any event at your endpoint to test signature verification and your handler, without running a verification. It uses the real delivery path (HMAC signing + retry). ## Notes - Sandbox verifications are **never charged**, even for the `insufficient_credits` scenario. - Test IDs bypass the ID-type allowlist, so a new organisation can test immediately. Use the `id_type_not_enabled` scenario to simulate a revoked allowlist. - Branch on the stable `reasonCode`, not the human-readable `reason`. --- # Workflows A **workflow** is a verification flow you build once in the dashboard (the capture steps, the countries and ID types, the branding and copy, any extra checks, and an optional [decisioning](https://trust.myaza.co/documentation/decisioning/markdown) graph), and then reuse without touching your code. Instead of passing a dozen props to the [SDK](https://trust.myaza.co/documentation/sdks/markdown), you reference a workflow by its id (`wf_…`) and the server hands the SDK the whole configuration. Because the configuration lives on the server, you can change the flow (add a step, tighten a rule, tweak the welcome copy) by **re-publishing in the dashboard**. Live integrations pick up the new version on their next verification. No SDK upgrade, no redeploy. Build workflows under **Dashboard → Workflows**. Each is scoped to one [environment](https://trust.myaza.co/documentation/environments/markdown) (a sandbox workflow doesn't exist in production) and has a draft you edit and a published snapshot your integrations read. ## Three ways to run a workflow ### 1. Embed it in the SDK (`workflowId`) Mount the [Web](https://trust.myaza.co/documentation/sdk-react/markdown), [React Native](https://trust.myaza.co/documentation/sdk-react-native/markdown), or [Flutter](https://trust.myaza.co/documentation/sdk-flutter/markdown) SDK with a `workflowId` instead of spelling out `country`, `idTypes`, and the step toggles: > **Minimum SDK versions for `workflowId`.** Web `@myazahq/kyc-sdk-react` **≥ 2.2.0** · React Native `@myazahq/kyc-sdk-react-native` **≥ 2.1.0** · Flutter `kyc_sdk_flutter` **≥ 2.2.0**. Earlier versions can't resolve a workflow ID: the web and React Native SDKs silently ignore it (the flow runs on props alone), and Flutter below 2.2.0 won't compile a workflow mount without a placeholder `country`. If a workflow embed seems to "do nothing", check the installed SDK version first. ```tsx console.log(s.verificationId)} /> ``` The SDK resolves the workflow on mount and uses it as the source of truth: **the workflow's configuration wins over any overlapping props** (country, ID types, step toggles, appearance, copy). Runtime data (`userId`, `userData`, `metadata`, and your callbacks) always comes from your code. With a `workflowId`, `country` becomes optional (the workflow carries it). An unknown, unpublished, or wrong-environment workflow surfaces as a blocking error via `onError`, never a silently different flow. ### 2. Share a hosted link Every published workflow can also be turned into a **hosted link**: a public URL you send directly to a person (WhatsApp, email, SMS) with zero integration: ``` https://trust.myaza.co/verify/workflow/ ``` The person opens the link, completes the flow in their browser, and you receive the result by [webhook](https://trust.myaza.co/documentation/webhooks/markdown) exactly as you would from an embedded SDK. Desktop visitors are offered a "continue on your phone" QR hand-off for the camera steps. Enable, disable, or regenerate the link from the workflow's page. Regenerating immediately kills the old URL. A link opened inside a WebView or an iframe reports the applicant's progress to its host as [hosted link events](https://trust.myaza.co/documentation/hosted-events/markdown). **How the link previews when shared.** Pasted into WhatsApp, iMessage, Slack or an email client, the link unfurls as your request, not as Myaza's website: the title reads "Verify your identity with {Your organisation}" (or "Verify your business with", or the scope's own title for an address, contact or face check), the description is the Consent step's description when you have written one, and the preview card carries your logo (or your initial when none is uploaded), your organisation name and the workflow's primary colour from its Appearance settings, with a small "Secured by Myaza Trust" footer. A link that is no longer available previews as a neutral Myaza Trust card, so a preview never reveals whether a token exists. The pages stay out of search results regardless. ### 3. Mint a per-applicant session from your backend When you know exactly who should verify, [`POST /api/kyc/sessions`](https://trust.myaza.co/documentation/api-create-session/markdown) (secret key) mints a hosted link that belongs to **one applicant**: single use, traceable to your `externalUserId`, optionally prefilled with what you already know, and optionally emailed on your behalf. A hosted link is one URL for many people; this is one URL for one person. The dashboard's "Create session" action mints the same thing by hand. ## How the SDK resolves a workflow When mounted with a `workflowId`, the SDK calls one endpoint (instead of [`GET /config`](https://trust.myaza.co/documentation/api-config/markdown)): ``` GET /api/kyc/workflows/:workflowId Authorization: Bearer pk_… ``` ```json { "workflow": { "id": "wf_AbC123dEf456", "name": "Standard KYC", "version": 3 }, "config": { "country": "NG", "idTypes": ["bvn", "passport"], "enableLiveness": true }, "environment": "PRODUCTION", "idTypes": [ { "country": "NG", "idType": "bvn", "features": { "govDbCheck": true } } ], "branding": { "companyName": "Acme Inc.", "logo": "https://…", "primaryColor": "#5645F5" } } ``` The workflow must belong to the API key's organisation **and** environment and be **published**: anything else is an indistinguishable `404 workflow_not_found`, so ids can't be probed across organisations. The `config` is org-authored template data and contains no PII, so a publishable (`pk_`) key is enough. You normally never call this yourself; the SDK does. ## Capture add-ons A workflow can turn on extra capture steps and checks beyond the core document + selfie + liveness flow. Each one is a step you toggle in the builder; most feed a soft signal into [decisioning](https://trust.myaza.co/documentation/decisioning/markdown) without ever changing the verification's own pass/fail status. | Add-on | What it does | |---|---| | **Consent screen** | The welcome screen every flow opens on, where the person agrees to the verification. Its copy is editable, and the screen can be switched off on the Consent step (or by default for new workflows under Workflow Settings) when your own app has already asked: the flow then opens on its first real step. Consent remains your organisation's attestation to Myaza whichever way the switch is set. | | **Contact verification** | An email and/or phone OTP right after consent: a cheap possession check that filters fake contacts before any gov-DB or liveness spend. Branch on `email.disposable`, `phone.countryMismatch`, and the like. | | **Supporting documents** | Extra documents you need on file, each with the title and guidance you write. A document can be uploaded, signed on screen, or either: choose under **How the applicant provides it**. A signature drawn on screen is kept as an image with the verification. Needs web SDK 3.6.0, React Native SDK 3.5.0 or Flutter SDK 3.6.0 for signing on screen. | | **Proof of Address** | Collects a utility bill / bank statement / tenancy agreement / government-issued document, then checks recency and, by the workflow's name rule, whether the applicant's name appears on it. The rule (Required / Optional / Not needed) is set per workflow with per-country, per-document exceptions, for markets where a bill names the meter or the landlord rather than the tenant. Branch on `verification.poaStatus` and `verification.poaNameMatch`. | | **[Address Intelligence](https://trust.myaza.co/documentation/address-intelligence/markdown)** | A map pin (+ optional entrance photo and directions) corroborated against the government record, the PoA document and the IP country. On KYB flows the pin is the business premises. Branch on `address.tier` and the other `address.*` fields. | | **NFC chip read** | Reads the eMRTD chip in a passport (mobile only) and runs server-side passive authentication, the strongest assurance tier. Branch on `verification.nfcStatus` / `verification.assuranceLevel`. | | **Questionnaire** | Extra compliance declarations (source of funds, expected volume, …) asked before submission. Answers ride the webhook and can be branched on as `questionnaire.`. A question can map onto canonical profile fields via `mapsTo`, including `walletAddress`, which appends the answer to the entity's wallet set and enrols [WALLET screening](https://trust.myaza.co/documentation/screening/markdown#types). | | **Device Intelligence** | Device + IP fraud analysis, **on by default**: shared and recovered devices, emulators, rooted phones, app integrity (App Attest / Play Integrity), virtual cameras, VPN, hosting and Tor networks, time zone and expected-IP checks, and repeated attempts. Its Countries tab is an IP geofence and its Rules tab sets approve, review or decline per finding. Branch on `device.*` and `ip.*`. Register your apps under Settings, Mobile apps for app integrity. | | **Liveness mode** | Choose gesture challenges, the screen-flash sequence, or **both** for the strongest presence check. | | **Multi-region** | List several countries; the flow opens with a country-select step and applies that country's ID types and validation rules. | | **Multiple IDs** | Verify two or three IDs in one run (e.g. NIN **and** BVN): the applicant picks each from what you allow for their country, one selfie covers them all, and it lands as **one verification** whose result follows your pass policy (all must pass, at least two, or any one). Works across regions, each country choosing which IDs its verifications offer. Branch on the `multiId.*` fields for finer routing. Each ID is charged for the checks run on it (its lookup, document read, chip read and face match), while the selfie, liveness and other shared checks are charged once. | The web SDK accepts these as props too, but a workflow is the recommended path: you compose the checks once in the dashboard, and every embed and hosted link inherits them, and you change them by re-publishing, with no redeploy. ## Emailing the applicant Switch on **Success → Applicant email → Email the applicant when approved** and, once a verification is approved, whether by the workflow or by a reviewer, we send the applicant a short email in your organisation's name. Declines are never emailed automatically; a reviewer can choose to email one as they decide it. No address is set on the workflow. We only write to one the verification already holds, using the first of these that has one: 1. the email the applicant verified with a code, when the workflow runs email verification; 2. an email your integration passes, as `userData.email` on the SDK or on a [session you create](https://trust.myaza.co/documentation/api-create-session/markdown); 3. the email on the person's entity record. When none of these holds an address, nothing is sent. Every email is added to the verification's timeline. The setting is not offered on biometric re-authentication workflows, which run every time somebody signs in. ## Identity details that do not match Pass the name and date of birth you hold (`userData`), and every verification compares them with the ID: the government record for a number-only ID such as a BVN, and the document itself for a passport or licence. By default a mismatch fails the verification. Under **ID Verification → Advanced → Identity details** you can set names and the date of birth, independently, to **record** instead. The comparison still runs and still lands on the result (`dataMatch: false`, with the field marked `enforced: false` in `dataValidationDetails`), but the verification can pass. Route those results with a decision rule on `verification.nameMatch` or `verification.dateOfBirthMatch`, for example sending them to review, or handle them in your webhook. Keep face matching on when you record names. The face is then the only thing tying the person to the ID number they entered, so publishing warns you when face matching is off for any ID. ## Matching the selfie to the photo on a document The selfie is compared with the strongest photo a verification holds: the photo on the government record, or the verified photo on the document's chip when the [NFC step](https://trust.myaza.co/documentation/nfc-chip/markdown) reads one. When neither is available, a document ID can use the photo printed on the document. New workflows enable this for every supported document ID, including IDs added later. Manage the workflow-wide setting under **ID Verification → Advanced**. To exclude one ID, open its country settings under **ID Verification → Countries** and switch off **Match the selfie to the photo on the document**. Where an ID also shows **Facial match**, that setting must stay on. Over the API, set `documentPhotoMatch: true` on the workflow config for every document ID, or on `countries[].idOptions[idType]` for one ID. The most specific value wins, so `false` on an ID leaves it out. - It runs only when the verification has no government record photo and no verified chip photo. - A mismatch fails the verification with `reasonCode: selfie_mismatch`, and the reason names the photo on the document. - A printed photo is weaker evidence than a government or chip photo, so it never raises the [assurance level](https://trust.myaza.co/documentation/id-types/markdown). - It is billed as a facial comparison. - If no face can be read on the photo (glare, a thumb over it, a photo too small to see), nothing is compared. Under **ID Verification → Rules** choose what happens. **Decline**, the default, fails the verification with `reasonCode: document_photo_no_face` and asks the person to photograph the document again. **Review** keeps the result the checks produced, and a decision rule on `verification.documentPhotoNoFace` sends it to a person to approve or decline. The tab writes that rule for you, and publishing warns you if no rule reads the field. Either way the result carries `facialMatchSkipped: "no_face_on_document"`. The result says which photo was used: `facialMatchSource` is `gov_record`, `chip` or `document`, and `null` when no comparison ran. It rides the result and the [webhook](https://trust.myaza.co/documentation/webhooks/markdown), and a decision rule on `verification.facialMatchSource` can, for example, send document-photo matches to review. ## Selfie review After the liveness capture the SDKs used to show the selfie with Retake and Continue. From web SDK 3.6.0, React Native SDK 3.5.0 and Flutter SDK 3.6.0 they hand straight on to the next step, because the capture already waits for a face that is in the frame and still. To bring the review back, switch on **Presence Intelligence → Show the selfie for review**. A selfie that fails to upload still shows the review, which is where the retry is. ## How the screen-flash check is re-checked The 3D Flash Check shows a short, random sequence of colours on the screen and looks for each one reflected on the face. The phone reports what it saw, and we then read the recording ourselves on our servers, colour by colour, at the moments the phone says each colour was on. For the best result ask people to hold the phone close, at about arm's length or nearer, and to step away from bright light such as a window or direct sun. In a bright room the screen's light can be too weak to show on the face. When that happens the phone asks the person to move and tries once more, and if our own reading of the recording is too faint to judge we record it as inconclusive. An inconclusive reading is never held against the person. ## When the liveness recording does not arrive Every liveness check records a short video, which we re-check on our servers. If that recording never reaches us, or reaches us unreadable, the liveness check cannot be re-checked. That happens with some older phones and browsers, and it is also what a replayed video on a second phone tends to produce. Under **Presence Intelligence → Rules** choose what happens: - **Review**, the default, keeps the result the checks produced, and a decision rule on `verification.livenessVideoMissing` sends it to a person to approve or decline. The tab offers to add that rule, and publishing warns you if no rule reads the field. - **Decline** fails the verification with `reasonCode: liveness_video_missing` and asks the person to take the selfie again, usually on another phone or browser. Over the API, set `livenessVideoMissing: "decline"` on the workflow config. The verification page shows why the recording is missing when the SDK could tell. ## Age restrictions Under **ID Verification → Age** you can limit a workflow to people of a certain age in each country. Once it is on, every verification compares the holder's age with the range for their country, using the date of birth the check established: the government record, the document, or its chip. A date of birth the applicant typed is never used for this. - **Minimum age** follows each country's legal age of majority by default: 18 in most countries, between 16 and 21 elsewhere. The tab shows it beside every country, with a note where the age differs inside a country. You can set one minimum for every country instead. - **Maximum age** is off by default, because no country sets a legal maximum. Set one if your policy needs it. - **A country's own limits** override the defaults for that country alone. Choose what happens to someone outside the range, separately for **under the minimum** and **over the maximum**: - **Decline**, the default: the verification fails with `reasonCode: age_restricted`. - **Review**: the verification keeps the result its checks produced, and a decision rule on `verification.ageLimit` (`under_minimum` or `over_maximum`) sends it to a person to approve or decline. The tab writes that rule for you, and publishing warns you if no rule reads the field. A verification whose date of birth could not be read always fails with `age_unverified`, so an unreadable document is never waved through. Neither reason names an age. Test scenarios 22 and 23 exercise both against your own limits (see [Sandbox testing](https://trust.myaza.co/documentation/sandbox-testing/markdown)). ## Templates New workflows start from a **template**: a pre-built flow you can use as-is or customise. Templates are country-agnostic (you pick the country and ID types in the builder) and ship with a sensible decision graph. The catalogue: **Onboarding**: everyday signup shapes. - **Standard KYC**: full capture, auto-approve on pass. - **Low-friction signup**: number-only IDs (BVN/NIN) + selfie; failures go to review, not decline. - **Hosted link collection**: tuned for sharing; can't be dismissed mid-way, desktop→phone hand-off. - **Re-verification**: a light refresh for customers due for renewal. - **High-assurance biometric**: camera-only, gesture + flash liveness, NFC chip read; chip-verified identities auto-approve, softer matches go to review. - **Contact-verified onboarding**: email + phone OTP before capture; throwaway emails routed to review. **Compliance**: screening, strict regimes, human-in-the-loop. - **KYC + AML screening**: screen every verified user; watchlist hits open a case. - **Strict compliance**: auto-decline confirmed matches, escalate the rest. - **Manual review everything**: no auto-approvals; a human signs off each customer. - **Geographic risk routing**: extra scrutiny for high-risk nationalities or IP-country mismatches. - **Risk-tiered onboarding**: let the monitoring risk tier set the friction. - **Crypto exchange onboarding**: full KYC plus AML screening that covers the customer's crypto wallet addresses; watchlist or wallet hits open a case, network-flagged identities are declined. **Specialised**: targeted checks. - **Age-gated service (18+)**: verify identity and age together. - **Expired-document guard**: decline expired IDs, flag soon-to-expire ones. - **Network trust fast-track**: trust identities already verified elsewhere on the Myaza network. - **Strict data & face match**: both checks must explicitly pass. - **Failure triage & retry**: give recoverable capture failures a second chance. - **Device & IP fraud guard**: route multi-accounting, emulator, and bot signals to review. - **Address-verified onboarding**: identity plus a proof-of-address check. - **Presence-verified address**: smart-address capture plus a multi-day presence watch the decision holds on. - **Address-only verification**: no identity step at all; the address capture is the whole flow, for books verified elsewhere. See [Address Intelligence](https://trust.myaza.co/documentation/address-intelligence/markdown#address-only-workflows-standalone-no-identity-step). - **Biometric enrolment** and **Face re-authentication**: a liveness selfie into, then against, the customer's biometric template; the workflow-shaped face check with contact-code companions and a decision on the outcome. - **Declarations refresh**: a questionnaire alone, for periodic attestations and source-of-funds updates. - **Contact re-verification**: one-time codes to the email and phone on file, nothing else. These five are **scoped workflows**: `scope` on the config names the one thing the flow verifies (`address`, `biometric-authentication`, `biometric-enrollment`, `questionnaire`, `contact`), publish rejects any stronger evidence, submissions carry the scope's marker `idType` and require the published workflow, and the capture lands on your existing customer record by `externalUserId` without ever touching its KYC status. A workflow's scope is chosen with the **Scope** control at the top of the builder rail and is **editable until the first publish, then locked**: verifications, hosted links and embeds already reference what that workflow ID verifies, so a mid-life change would rewrite their meaning (the API returns `scope_locked`). To get a different scope from a live workflow, use **Scoped copy** on the workflows list. It creates a fresh draft under a new workflow ID that carries the parts that mean the same thing under any scope (branding, country, contact steps, session policy) and none of the identity evidence, seeded so the copy opens ready to publish. **Business (KYB)**: registry lookups with due diligence. - **Standard KYB**: registry lookup with a customer due-diligence gate for key people. - **KYB + AML screening**: screen every verified business before approval. - **Enhanced KYB with declarations**: registry check plus a compliance questionnaire. ## Business (KYB) workflows A workflow can verify a **business** instead of a person. It looks the company up in the official registry, optionally collects company documents and declarations, and runs **customer due diligence on the people who own or control it**, screening directors, beneficial owners (UBOs), and the applicant, and optionally requiring each to complete their own KYC via a per-person invite link. Live business submissions **require** a published KYB workflow; the server rejects a business verification with no workflow. See **[Business verification (KYB)](https://trust.myaza.co/documentation/business-verification/markdown)** for the full guide (products, the application layers, the result shape, and decisioning), [Key people & ownership](https://trust.myaza.co/documentation/key-people/markdown) for the due-diligence layer (roles, thresholds, corporate shareholders, ownership chains), and [Supported countries](https://trust.myaza.co/documentation/countries/markdown#business-verification-kyb) for registry coverage. ## Versioning Editing a published workflow only changes its **draft**: live integrations keep seeing the last published snapshot until you publish again. Every publish bumps the version and appends an immutable history entry you can inspect or restore from. Each verification records the exact `workflowId` and version that drove it, and the [`data.workflowId`](https://trust.myaza.co/documentation/webhooks/markdown) field appears on every verification webhook so you can attribute results back to a flow. ## Next steps - [Decisioning](https://trust.myaza.co/documentation/decisioning/markdown): the rules engine that turns verification data into approve / decline / review. - [Webhooks](https://trust.myaza.co/documentation/webhooks/markdown): [`verification.started`](https://trust.myaza.co/documentation/webhooks/markdown#when-the-applicant-submits) when an applicant submits your flow, the `workflow.run.completed` event, and workflow attribution on results. - [Web SDK](https://trust.myaza.co/documentation/sdk-react/markdown): mounting with a `workflowId`. --- # Decisioning Every [workflow](https://trust.myaza.co/documentation/workflows/markdown) can carry a **decision graph**: server-side logic that runs *after* a verification reaches a terminal state, branches on the verification, entity, identity, screening, and fraud data, and lands on a decision: **approve**, **decline**, or **review**. It's the difference between "the ID checked out" and "this customer is onboarded." You build the graph in the workflow builder (a simple ordered rule list, or an advanced canvas). Decisioning is optional: a workflow with no graph simply completes and reports the raw verification result. ## How a run works When a verification finishes, the engine starts a **run**: 1. It gathers the run's data: the verification result, the resolved [entity](https://trust.myaza.co/documentation/identity-hub/markdown) and global identity, any [screening](https://trust.myaza.co/documentation/screening/markdown) results, and the [Device Intelligence](https://trust.myaza.co/documentation/workflows/markdown#capture-add-ons) signals. 2. It walks the graph from the start node, evaluating each **branch** and executing each **action**. 3. It lands on a terminal **status** node, which sets the outcome. 4. It records the full path it took (the trace) and fires the [`workflow.run.completed`](#the-workflowruncompleted-webhook) webhook. Runs are one-per-verification and idempotent, so a retried or re-processed verification never decides twice. ## Node types | Node | What it does | |---|---| | **Branch** | Evaluates a condition over the [field namespace](#field-reference) and takes the `true` or `false` edge. | | **Action** | A side effect: **tag** the run, fire a `workflow.action` **webhook**, or **open a case** for an investigator. Actions never stop the flow. | | **Screening / key-people** | Waits for AML screening (or KYB key-people verification) to resolve before continuing, up to a timeout. | | **Status** | Terminal. Sets the outcome: `approve`, `decline`, or `review`. | ## Outcomes A status node's outcome writes the **entity's disposition** (the compliance decision on the person or business), but never overrides the verification's own result: | Outcome | Entity disposition | Meaning | |---|---|---| | `approve` | `APPROVED` | Onboard the customer. | | `decline` | `REJECTED` | Reject the customer. | | `review` | `UNDER_REVIEW` | A human should look before you act. | The outcome arrives on the [`workflow.run.completed`](#the-workflowruncompleted-webhook) webhook. What you *do* with it (grant access, hold the account, notify your ops team) is up to your backend. Completion and its notification intent are committed together. Delivery retries reuse the event ID and do not reapply an old disposition over a later decision. Use `externalUserId` to correlate the customer, deduplicate the signed event and read current state before lifting a restriction. Face-reuse findings arrive after verification and can start a separate signal policy; see [verification device and face findings](/documentation/webhook-risk-intelligence#verification-device-and-face-findings). ## Field reference Branch conditions read a **closed namespace** of fields. Unknown paths are rejected when you publish (so a typo surfaces immediately rather than silently never matching), and a null value fails the comparison closed. Use the `exists` operator to test presence. Operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `exists`. Conditions combine with `all` (AND), `any` (OR), and `not`. **`verification.*`**: the check result | Field | Example values | |---|---| | `verification.status` | `VERIFIED`, `FAILED`, `NOT_FOUND`, `ERROR` | | `verification.reasonCode` | `selfie_mismatch`, `document_expired`, … (see [Errors](https://trust.myaza.co/documentation/errors/markdown)) | | `verification.assuranceLevel` | `chip`, `gov_db`, `document` (how strongly it passed) | | `verification.country` / `.idType` / `.nationality` / `.gender` | — | | `verification.dataMatch` / `.facialMatch` | `true` / `false` | | `verification.nameMatch` / `.dateOfBirthMatch` | `true` / `false`, null when not compared. Read these when your workflow records identity mismatches instead of failing on them | | `verification.facialConfidence` | `0`–`100` | | `verification.facialMatchSource` | `gov_record`, `chip`, `document`: which photo the selfie was compared with, null when no comparison ran. A `document` match used the photo printed on the ID, which is weaker evidence | | `verification.documentPhotoNoFace` | `true` when the photo printed on the document was the only photo to compare the selfie with, no face could be read on it, and your workflow sends that case to review. Null unless the checks passed | | `verification.livenessVideoMissing` | `true` when the flow ran a liveness check, its recording did not arrive or could not be read, and your workflow sends that case to review. Null unless the checks passed | | `verification.age` | integer, when the date of birth is known | | `verification.documentExpired` / `.documentExpiresInDays` | — | | `verification.cameraSuspect` | `true` when capture-integrity signals flag injection | | `verification.poaStatus` / `.poaNameMatch` / `.poaDocumentAgeDays` | Proof-of-Address result | | `verification.nfcStatus` / `.nfcAuthentic` / `.nfcDataMatch` | NFC chip result | **`entity.*` / `identity.*`**: the person across your org and the network `entity.exists`, `entity.riskTier` (`LOW`/`MEDIUM`/`HIGH`), `entity.riskScore`, `entity.disposition`, `entity.status`, `entity.kycProvenance`, `identity.exists`, `identity.trustState` (`VERIFIED`/`FLAGGED`/`UNVERIFIED`, the cross-org network signal). **`screening.*`**: sanctions / PEP / adverse media / crypto wallets `screening.active`, `screening.pending`, `screening.anyMatch`, `screening.confirmed`, `screening.sanctions.status`, `screening.pep.status`, `screening.adverseMedia.status`, `screening.wallet.status` (null when no wallets are attached), `screening.maxMatchScore`. **`device.*` / `ip.*`**: Device Intelligence `device.matchedEntities` (other ID numbers seen on the device), `device.matchedPeople` (other people seen on the device), `device.emulator`, `device.attempts24h` (attempts from this device in the last 24 hours), `device.recovered` (the same physical device back under a new install), `device.linkedDeclined` (other people declined for fraud on this device), `device.sessionDevices` (devices behind this session's photos and submission), `device.rooted`, `device.virtualCamera`, `device.attestation` (`verified`, `failed` or `unavailable`; null when the app did not attest), `ip.country`, `ip.type`, `ip.datacenter`, `ip.tor`, `ip.countryMismatch`, `ip.timezoneMismatch` and `ip.expectedMatch` (`exact`, `network`, `operator`, `country` or `different`; null when the session was created without an expected IP address). What the workflow's own Device Intelligence settings decided, all null unless the verification passed: `device.review` (a finding the workflow sends to review, the flag to route on), `device.ruleResult` (the finding that decided: `clear`, `country_not_allowed`, `app_integrity_failed`, `declined_device`, `virtual_camera`, `emulator`, `rooted_device`, `tor_exit`, `multiple_devices`, `expected_ip_mismatch`, `privacy_network`, `timezone_mismatch`, `recovered_device`, `shared_device`, `repeated_attempts` or `ip_country_mismatch`) and `ip.countryAllowed` (whether the geofence allows the IP's country; null when geofencing is off or the IP has no known country). Setting a finding to Review in the builder adds the `device.review` rule for you. **`email.*` / `phone.*`**: contact verification `email.verified`, `email.disposable`, `email.freeProvider`, `email.domain`, `phone.verified`, `phone.country`, `phone.countryMismatch`, `phone.lineType` (`mobile`, `fixed_line`, `voip`, `toll_free`, and similar; null when the number range is unknown), `phone.virtual` (true only for a known VoIP range), `phone.matchedVerifications` (how many other verifications in your organisation verified the same number, a soft reuse signal) and `phone.attempts` (codes entered before the one that verified). **`business.*` / `keyPeople.*`**: KYB `business.companyStatus`, `business.nameMatch`, `business.addressMatch`, `business.ageYears`, `keyPeople.anyFlagged`, `keyPeople.allCleared`, `keyPeople.cddComplete`, `keyPeople.kycComplete`, `keyPeople.kycFailed`, `keyPeople.corporate`, `keyPeople.ownershipUnresolved`, and more, for [business workflows](https://trust.myaza.co/documentation/workflows/markdown#business-kyb-workflows). The key-people roll-up is listed in full in [Key people & ownership](https://trust.myaza.co/documentation/key-people/markdown#what-you-receive). **`questionnaire.`**: the answers to any [questionnaire](https://trust.myaza.co/documentation/workflows/markdown#capture-add-ons) the flow asks, keyed by question. **`list.`**: whether the verification is on one of your [custom lists](https://trust.myaza.co/documentation/lists/markdown#custom-lists): `true`, `false`, or null when the verification has nothing of the list's type. **`country.*`**: your [High-risk countries](https://trust.myaza.co/documentation/lists/markdown#high-risk-countries) list `country.documentHighRisk`, `country.ipHighRisk`, `country.phoneHighRisk`, `country.anyHighRisk`. Null when the country is not known. ## The `workflow.run.completed` webhook When a run reaches a terminal outcome, you receive: ```json { "event": "workflow.run.completed", "data": { "runId": "wfr_01j9xyz…", "workflowId": "wf_AbC123dEf456", "workflowVersion": 3, "verificationId": "ver_01j9xyz456", "entityId": "ent_01j9…", "outcome": "approve", "tags": ["network-verified"], "environment": "PRODUCTION", "completedAt": "2026-07-12T12:00:00.000Z" } } ``` Related events: - **`workflow.action`**: fired by a `webhook` action node mid-graph (`{ runId, workflowId, verificationId, entityId, nodeId, note }`). - **`workflow.run.failed`**: the engine could not finish a run (a broken graph or crash loop). This is an engine fault, **not** a decline, so never treat it as one. - **`verification.status_updated`**: fires alongside `workflow.run.completed`, with `source: "workflow"`, whenever the verdict moves the verification's top-line status (for example from `processing` to `in_review`). The same event covers a decision made by a reviewer or by your backend, so one handler can keep your record current whoever decided. See [Status changes](https://trust.myaza.co/documentation/webhooks/markdown#status-changes). See [Webhooks](https://trust.myaza.co/documentation/webhooks/markdown) for signing, retries, and the full event list. ## Notes - The graph is **immutable per run**: a run is pinned to the workflow version and graph snapshot it started with, so re-publishing never changes an in-flight decision. - A **screening** node waits only for the *first* resolution; later re-screens are handled by ongoing [monitoring](https://trust.myaza.co/documentation/monitoring-rules/markdown), not re-decisioning. - For KYB, the key-people gate holds the **decision**, never the business verification itself. The registry result is terminal the moment it's done; the onboarding decision waits for the people. --- # Lists Lists are sets of values you keep in the dashboard under **Configure → Lists**. Some decline a verification automatically. Others do nothing until one of your [decision rules](https://trust.myaza.co/documentation/decisioning/markdown) asks about them. | List type | Who creates it | What it does | |---|---|---| | Blocklist | Built in, one per entry type | A verification that matches an entry is declined | | Allowlist | Built in (faces) | A matching face skips the duplicate-face rules | | High-risk countries | Built in, yours to edit | Nothing by itself. Rules read it through the `country.*` fields | | Custom | You | Nothing by itself. Rules read it through `list.` | Lists are separate for each environment. A list you fill in Sandbox does not exist in Production. Lists are managed in the dashboard. There is no public API for them yet. ## Custom lists A custom list is a list you name and fill yourself. It holds **one type of entry**, and your workflow rules can ask whether a verification is on it. Typical uses: - A list of staff email addresses, so internal test sign-ups go to review instead of approval. - A list of user references you want checked by a person every time. - A list of countries you serve, so anything else is sent to review. - A list of office IP addresses, so sign-ups from your own network are easy to tell apart. A custom list never declines or approves anything on its own. Until a rule reads it, it has no effect. ### Create a list 1. Open **Configure → Lists** and select **Create list**. 2. Give it a name and, if you like, a description. 3. Choose what it holds. This cannot be changed afterwards. You need the `identity:manage` permission to create or change a list, and `identity:read` to see one. You can keep up to 50 custom lists per environment, each with up to 10,000 entries. ### Entry types | Entry type | What you add | What a rule compares it with | |---|---|---| | Email address | `name@example.com` | The email address the applicant confirmed with a code | | Phone number | `+2348012345678`, with the country code | The phone number the applicant confirmed with a code | | User | Your own user reference, such as `user_12345` | The `externalUserId` you send with the verification | | Country | A country | The country of the ID, the IP address, or the confirmed phone number | | IP address | A single IPv4 or IPv6 address | The IP address the verification was submitted from | | Device | A device fingerprint | The device the verification was submitted from | | ID number | An ID number, for one ID type | The number of any ID the verification used | | Business | A registration number, for one country | The registration number of the business being verified | A few details worth knowing: - **Email addresses and phone numbers** are stored so they can be matched but not read back. The list shows each one partly hidden. To find one, type it in full. - **Email and phone lists** only match a contact the applicant proved with a code. A typed, unconfirmed address never matches. Your workflow needs the email or phone verification step for these lists to be useful. - **User references** are compared exactly as you send them, including upper and lower case. - **ID numbers** ignore spaces, hyphens, dots and case. The same number on a different ID type is a different document. - **IP addresses** are single addresses. Ranges are not supported. - **Device fingerprints** are the ones shown on a verification's Device Intelligence card. - When your backend submits verifications with a secret key, pass the applicant's connection as described in [Create a verification](https://trust.myaza.co/documentation/api-create-verification/markdown), or IP address and device lists will have nothing to compare. ### Add entries Open the list and select **Add entries**. Countries are picked from a list. Everything else is typed or pasted, one value per line or separated by commas, up to 500 at a time. - A value already on the list is left as it is. - A value that is not valid for the list's type is not added. It stays in the box so you can correct it, and the rest are added. - For ID numbers, choose the ID type first. For businesses, choose the country of the register first. ### Use a list in a rule Every custom list has a **key**, made from its name when you create it: "VIP customers" becomes `vip_customers`. A rule reads the list through the field `list.`. The list's page shows the field and lets you copy it. In the workflow builder, open **Decisioning**, add a rule, and choose the list from the **Lists** group. The field has three possible values: | Value | Meaning | |---|---| | `true` | The verification is on the list | | `false` | The verification has a value of the list's type, and it is not on the list | | unknown | The verification has nothing of the list's type, for example no confirmed email. A rule on the field does not match | For example, to send anyone on a list called "Manual review" to review: ```json { "field": "list.manual_review", "op": "eq", "value": true } ``` Because unknown never matches, a rule such as `list.served_countries` `eq` `false` only fires when a country is known and is not on the list. To also catch verifications with no known value, add a second condition using the `exists` operator. ### Rename or delete a list You can rename a list or change its description at any time. The key does not change, so rules that read the list keep working. A list cannot be deleted while a workflow rule reads it. Remove the rule from the workflow first, including from its unpublished draft, then delete the list. Deleting a list deletes its entries for good. Publishing a workflow is refused if one of its rules reads a list that does not exist in that environment. Create the list there, or change the rule. ## High-risk countries One custom list is built in: **High-risk countries**. It starts with the countries on the FATF call-for-action and increased-monitoring lists, and you can add or remove countries. Rules read it through four fields: | Field | True when | |---|---| | `country.documentHighRisk` | The verification's own country (the ID's, or the business register's) is on the list | | `country.ipHighRisk` | The IP address is in a country on the list | | `country.phoneHighRisk` | The confirmed phone number's country is on the list | | `country.anyHighRisk` | Any of the three is on the list | A country that is not known gives an unknown value, which never matches. ## Blocklists Blocklists are built in, one for each entry type: face, ID number, IP address, user, phone number, email address, device and business. A verification that would otherwise pass and matches an entry is declined. The applicant is told only that the verification could not be completed. The reason is never shown to them, because it would tell a fraudster what to change. Your webhook and the verification result carry a `reasonCode` naming the list that matched: `face_blocklisted`, `document_blocklisted`, `ip_blocklisted`, `user_blocklisted`, `phone_blocklisted`, `email_blocklisted`, `device_blocklisted` or `business_blocklisted`. You can add an entry from **Configure → Lists**, or straight from a verification with **Add to blocklist**. --- # SDKs ## Server SDK for Risk Intelligence The `@myazahq/trust-sdk` server package is in development and is not yet available on the public npm registry. For an integration you can run now, use the [Risk Intelligence HTTP quickstart](https://trust.myaza.co/documentation/risk-intelligence-quickstart/markdown) with a secret key on your backend. It covers customer registration and a transaction decision. For an explicit check, use the [On-Demand Wallet Screening API](https://trust.myaza.co/documentation/api-wallet-screenings/markdown). The unreleased server SDK source includes `myaza.riskIntelligence.walletScreenings.create()`, `retrieve()` and `list()`, plus saved PDF downloads. Its legacy transaction-reference methods screen a counterparty address at the Wallet Screening rate, not the transaction hash. The SDK will forward requests to Myaza; risk scoring and rules run on the server. ## Client SDKs for identity verification Myaza provides drop-in client SDKs that render the full verification UI (ID selection, document scan, and active liveness) and call the verification API for you. They are thin UI layers: they capture the user's data and media, [upload](https://trust.myaza.co/documentation/api-upload/markdown) it, and [create the verification](https://trust.myaza.co/documentation/api-create-verification/markdown). The SDK uses a **publishable (`pk_`) key**, safe to ship in client code. Results arrive **asynchronously** on your backend via [webhooks](https://trust.myaza.co/documentation/webhooks/markdown), or fetch the [full result](https://trust.myaza.co/documentation/api-verification-result/markdown) with a **secret (`sk_`) key** from your backend (the publishable key can only poll [minimal status](https://trust.myaza.co/documentation/api-verification-status/markdown)). | SDK | Package | Platform | |---|---|---| | Web | `@myazahq/kyc-sdk-react` | React (≥ 18) | | React Native | `@myazahq/kyc-sdk-react-native` | React Native / Expo (SDK ≥ 56) | | Flutter | `myaza_kyc_sdk_flutter` | Flutter / Dart | All three SDKs share the same **core flow** and call the same endpoints. The SDK detects the environment **automatically from the API key prefix** and resolves the base URL itself: `pk_test_*` → sandbox, `pk_live_*` → production. There is no `environment` option. Some newer capabilities are **web SDK only** for now; see [Feature availability](#feature-availability). ## Configure with a workflow first There are two ways to tell an SDK what to run, and they are not equal: 1. **Mount a workflow (the recommended path).** Build the flow in the dashboard as a [workflow](https://trust.myaza.co/documentation/workflows/markdown), covering countries and ID types, capture steps, add-ons (contact OTP, proof of address, NFC, questionnaire), branding, copy, and server-side [decisioning](https://trust.myaza.co/documentation/decisioning/markdown), then mount it by id: ```tsx ``` The SDK resolves the whole configuration from the server on mount. Changing the flow is a **re-publish, not a redeploy**, and on mobile not an app-store release. Every published workflow can also be shared as a [hosted link](https://trust.myaza.co/documentation/workflows/markdown#2-share-a-hosted-link) with zero integration at all. 2. **Configure via props (secondary).** Spell the flow out in code (`country`, `idTypes`, step toggles, `appearance`, copy). Fine for a quick experiment or a single fixed flow, but every change means shipping code, and the add-on steps are far easier composed in the builder. When both are present, **the workflow's configuration wins over any overlapping props**. Runtime data (`userId`, `userData`, `metadata`, and your callbacks) always stays in code: a workflow is a shared template and cannot carry per-user values. Everything below that describes a prop also exists as a builder setting on the workflow; treat the props as the code-side equivalents. ## Platform guides Each SDK has its own integration guide. Pick your platform: - **[Web SDK (React)](https://trust.myaza.co/documentation/sdk-react/markdown)**: `` button + modal flow for React (≥ 18). - **[React Native SDK](https://trust.myaza.co/documentation/sdk-react-native/markdown)**: on-device native liveness; needs a custom dev build (**not Expo Go**). - **[Flutter SDK](https://trust.myaza.co/documentation/sdk-flutter/markdown)**: `MyazaKYC.show()` modal sheet for Flutter / Dart. The rest of this page covers what's **shared across all three**: supported countries, branding, consent/success copy, handling results, errors, and resilience. > The React Native SDK ships native code (on-device liveness via Apple Vision + Google ML Kit), so it needs a custom dev/native build. **It does not run in Expo Go.** See the [React Native SDK](https://trust.myaza.co/documentation/sdk-react-native/markdown) guide. ## Feature availability All three SDKs are at **full feature parity**: the core flow (document capture, selfie, active liveness, consent/success copy, branding, callbacks) and every capability on top of it: | Capability | Web (`2.8.x`) | React Native (`2.1.x`) | Flutter (`2.2.x`) | |---|---|---|---| | Core flow (docs · selfie · liveness · branding · copy) | ✅ | ✅ | ✅ | | [Workflows](https://trust.myaza.co/documentation/workflows/markdown): `workflowId` embed | ✅ | ✅ | ✅ | | Capture add-ons: contact OTP, proof of address, questionnaire | ✅ | ✅ | ✅ | | Business (KYB) verification | ✅ | ✅ | ✅ | | Global & multi-region countries | ✅ | ✅ | ✅ | | NFC chip read (eMRTD) | preview only | ✅ | ✅ | The **NFC chip read** is an on-device capability: browsers can't talk to a passport chip, so the web SDK renders the chip screen only for the dashboard builder preview, while the native SDKs read chips for real. The read is best-effort by design: a phone without NFC, or a chip that won't read, continues the flow (see [ID types](https://trust.myaza.co/documentation/id-types/markdown)). [Hosted workflow links](https://trust.myaza.co/documentation/workflows/markdown#2-share-a-hosted-link) remain the zero-integration option on any platform. ## Supported countries & ID types Mounting a [workflow](https://trust.myaza.co/documentation/workflows/markdown)? Pick the countries and ID types in the builder, with nothing to pass in code. For prop-configured mounts, pass a subset via `idTypes` to limit what the user can pick, or omit it to offer everything enabled for your organisation in that country. Only types valid for the selected country are accepted. All three SDKs use the **same** kebab-case `idTypes` keys (Flutter takes them as plain strings; there is no enum): | Country | `idTypes` keys (all SDKs) | |---|---| | `NG` Nigeria | `bvn`, `nin`, `vnin`, `passport`, `drivers-license`, `pvc` | | `GH` Ghana | `ghana-card`, `voters`, `drivers-license`, `ssnit`, `passport` | | `KE` Kenya | `national-id`, `passport` | | `ZA` South Africa | `national-id` | | `CI` Côte d'Ivoire | `cni`, `residence-card` | See [ID types](https://trust.myaza.co/documentation/id-types/markdown) for the document requirements (capture vs. number-only, scan sides) of each. > The five countries above are the **government-database-backed** markets. All three SDKs additionally verify **any ISO country** via document capture ([Global Documents](https://trust.myaza.co/documentation/id-types/markdown)): pass any two-letter country code, and the ID types your organisation is granted for it are offered automatically (grants are enforced server-side). --- ## Branding & theming In a [workflow](https://trust.myaza.co/documentation/workflows/markdown), set all of this in the builder's **Appearance** panel. Every embed and hosted link inherits it, and a re-publish updates it everywhere with no redeploy. The `appearance` object below is the code-side equivalent for prop-configured mounts (a workflow's values win on overlap). All three SDKs accept an `appearance` object to match the flow to your brand. The same fields exist on each platform: React and React Native take CSS-style colour strings (e.g. `"#5645F5"`); Flutter takes `Color` values. On the web, the colours are injected as CSS variables **scoped to the SDK** (they never leak into your page); setting one colour cascades to all of its shades. React Native uses the identical field names and string values. | Field | React type | Flutter type | Description | |---|---|---|---| | `primaryColor` | `string` | `Color?` | Brand colour: buttons, selected states, focus rings, the shield hero. | | `primaryTextColor` | `string` | `Color?` | Text/icons rendered on top of `primaryColor` (e.g. button labels). | | `accentColor` | `string` | `Color?` | Subtle hover/active surfaces. | | `backgroundColor` | `string` | `Color?` | Modal/sheet background. | | `surfaceColor` | `string` | `Color?` | Cards & panels. | | `borderColor` | `string` | `Color?` | Borders and input outlines. | | `textColor` | `string` | `Color?` | Primary text colour. | | `companyName` | `string` | `String` | Shown on the trigger button and the persistent header. | | `logo` | `string` | `String?` | Image URL, or `'default'` for your dashboard logo (see below). | | `theme` | `'light' \| 'dark'` | `MyazaThemeMode` | Initial light/dark mode. When `showThemeToggle` is enabled, users can flip it; otherwise the flow stays on this value. | Flutter also accepts `logoAsset` (a bundled `Image.asset` path) as a fallback when `logo` is not set. ### Logo The logo renders as a small circular avatar in the header (top-left), persistent on every step, next to `companyName`. - **An image URL** (`"https://…/logo.png"`) is used directly. - **`'default'`** uses the logo configured for your organisation under **Settings → Organization** in the dashboard. The SDK fetches it from [`GET /config`](https://trust.myaza.co/documentation/api-config/markdown) (`branding.logo`) on mount. If your org has no logo set, or the image fails to load, it falls back to the built-in shield icon. - **Omitted** shows no header logo. ```tsx // React — brand with your dashboard logo and a custom palette appearance={{ primaryColor: "#0F7B6C", primaryTextColor: "#FFFFFF", surfaceColor: "#F4F7F6", logo: "default", theme: "light", }} ``` ## Consent screen copy The first screen of the flow (the welcome/consent step) shows a heading and a short description. Both default to Myaza's built-in copy. In a [workflow](https://trust.myaza.co/documentation/workflows/markdown), override them in the builder's **Consent** panel; the `consent` object below is the code-side equivalent, with identical fields on every SDK. | Field | React type | Flutter type | Description | |---|---|---|---| | `title` | `string` | `String?` | Heading. Defaults to `Welcome, {firstName}` when a first name is known, otherwise `Identity Verification`. | | `description` | `string` | `String?` | Sub-text under the heading. Defaults to the built-in regulatory copy. | Both fields support `{firstName}` and `{lastName}` tokens, which are replaced with the values you pass in `userData` (an empty string when absent), so a custom title can still greet the user by name. ```tsx // React consent={{ title: "Welcome, {firstName}", description: "We just need to confirm it's really you. This takes about a minute.", }} ``` ```dart // Flutter consent: const KYCConsentContent( title: 'Welcome, {firstName}', description: "We just need to confirm it's really you. This takes about a minute.", ), ``` Omit `consent` (or either field) to keep the defaults. ## Success screen copy The final screen, shown after the user submits, displays a confirmation heading and a short description. Both default to Myaza's built-in copy. In a [workflow](https://trust.myaza.co/documentation/workflows/markdown), override them in the builder's **Success** panel; the `success` object below is the code-side equivalent, with identical fields on every SDK. | Field | React type | Flutter type | Description | |---|---|---|---| | `title` | `string` | `String?` | Heading. Defaults to `Verification Submitted!`. | | `description` | `string` | `String?` | Sub-text under the heading. Defaults to the built-in "submitted for review" copy. | Both fields support the same `{firstName}` / `{lastName}` tokens as `consent`, which are replaced with the values you pass in `userData` (an empty string when absent). ```tsx // React success={{ title: "You're all set, {firstName}!", description: "We'll email you once your verification is reviewed.", }} ``` ```dart // Flutter success: const KYCSuccessContent( title: "You're all set, {firstName}!", description: "We'll email you once your verification is reviewed.", ), ``` Omit `success` (or either field) to keep the defaults. This screen is purely cosmetic; the verification outcome still arrives asynchronously (see [Handling results](#handling-results)). ## Handling results The SDK finishing only means the verification was **created**, never that it passed. Treat the outcome as eventual: 1. The SDK uploads media and creates the verification, then invokes `onSubmit` with a `KYCSubmission`. 2. Your **backend** receives the terminal [`verification.*` webhook](https://trust.myaza.co/documentation/webhooks/markdown) (or fetches the [full result](https://trust.myaza.co/documentation/api-verification-result/markdown) with a secret key; the SDK's publishable key can only poll [minimal status](https://trust.myaza.co/documentation/api-verification-status/markdown)). 3. You update the user's state from that authoritative result. The `onSubmit` payload (`KYCSubmission`) carries: ```jsonc { "verificationId": "ver_…", // use this to correlate the webhook / status poll "status": "processing", // always 'processing' at this point "metadata": { "requestId": "order_1001" }, "submittedAt": "2026-04-27T12:00:00.000Z" } ``` > Always pass a stable `requestId` in `metadata` so retries are [idempotent](https://trust.myaza.co/documentation/verifications/markdown#idempotency). ## Errors `onError` fires for **technical** failures only, never for a failed verification, which arrives asynchronously via webhook. The error carries a stable `code` (the same set across all three SDKs): | Code | Meaning | |---|---| | `network_error` | Connection failure or timeout, **after automatic retries are exhausted**. | | `invalid_api_key` | Server returned `401`; check the key (its prefix selects the environment automatically). | | `insufficient_credits` | Server returned `402`. `details` includes `{ required, balance, currency }` for your own logging; the message shown to the end user deliberately names no figure and no cause, since your balance is not something they can act on. Verifications normally pause rather than reaching this. | | `upload_failed` | Media [upload](https://trust.myaza.co/documentation/api-upload/markdown) failed, **after automatic retries are exhausted**. | | `camera_permission_denied` | The user denied (or the OS/browser blocks) camera access. | | `feature_disabled` | Server returned `403`; the ID type or a verification feature isn't enabled for your organisation. | | `unknown` | Anything else. | All SDKs deliver the same typed error: in React and React Native `onError` receives a `KYCError`, a real `Error` (so existing `(error: Error)` handlers keep working) that also carries `code` and optional `details`; in Flutter it receives a `KYCError` with `code`, `message`, and optional `details`. See [Errors](https://trust.myaza.co/documentation/errors/markdown) for the API-level error model. > Voice guidance is text-to-speech **output**; it never records audio, so there is **no microphone permission** and no microphone error code. ## Resilience & capture quality All three SDKs handle real-world conditions so the flow doesn't crash, hang, or capture unusable media: - **Network retries.** Media uploads and the verify submission retry transient failures (network / timeout / `5xx`) with exponential backoff + jitter; terminal `4xx` surface immediately. The UI shows "Reconnecting… / retrying (n/3)…", and `onError` fires only **after** retries are exhausted. - **Camera permission.** A denied camera shows a clear "camera access needed" screen (with how to re-enable it: an *Open Settings* action on Flutter & React Native) and reports `camera_permission_denied`. Document capture keeps a gallery-upload fallback on that screen as an escape hatch. - **Multiple faces.** During liveness, if more than one face is in frame the challenge pauses ("Make sure only your face is visible") and resumes automatically when only one remains, guarding capture quality and a class of spoofing. - **Lighting.** Too-dark *and* too-bright (glare) conditions are detected live and surfaced as guidance ("Move to a brighter area" / "Too bright, reduce glare"); auto-capture is discouraged until lighting is acceptable. - **Liveness is on-device & native** on mobile: Apple Vision (iOS) + Google ML Kit (Android) on both React Native and Flutter; the web SDK uses MediaPipe in the browser. The selfie is **auto-captured** once the challenges pass (you can't trigger it on a static image), and a short liveness video is recorded for server-side review. For the full prop reference and styling options, see each package's README. --- # Web SDK (React) The Web SDK is a drop-in React component that renders the full verification UI (ID selection, document scan, and active liveness) and calls the verification API for you. It uses a **publishable (`pk_`) key** and detects the environment automatically from the key prefix (`pk_test_*` → sandbox, `pk_live_*` → production). For shared concepts (supported countries, branding, results, and errors), see [Client SDKs](https://trust.myaza.co/documentation/sdks/markdown). ## Install ```bash pnpm add @myazahq/kyc-sdk-react ``` ```bash yarn add @myazahq/kyc-sdk-react ``` ```bash npm install @myazahq/kyc-sdk-react ``` ## Usage `` renders a "Verify Identity" button plus the full modal flow. The trigger is a real `; } export default () => ( ); ``` The flow advances through `KYCStep` values, reported via `onStepChange` / `currentStep`. The core path is `consent` → `id-type` → (`id-input` for number-only IDs, or `document-capture`) → `liveness` → `submitted`. Steps enabled by a [workflow](https://trust.myaza.co/documentation/workflows/markdown) slot in automatically: `email-verification` / `phone-verification` (right after consent), `country-select` (multi-region), `proof-of-address`, `questionnaire`, and the KYB steps (`business-details`, `business-key-people`, `business-documents`, `applicant-role`). --- # React Native SDK The React Native SDK mirrors the [web SDK](https://trust.myaza.co/documentation/sdk-react/markdown)'s **core** API (same props, same callbacks) but runs **on-device, native liveness**: Apple Vision on iOS and Google ML Kit on Android, via a [react-native-vision-camera](https://react-native-vision-camera.com) v5 (Nitro) frame processor. Because it ships native code, it needs a custom native build and **does not run in Expo Go**. For shared concepts (supported countries, branding, results, and errors), see [Client SDKs](https://trust.myaza.co/documentation/sdks/markdown). > **Feature availability.** This SDK is at **full parity** with the web and Flutter SDKs: [workflow](https://trust.myaza.co/documentation/workflows/markdown) embedding (`workflowId`), the capture add-ons (contact OTP, proof of address, questionnaire), business (KYB) verification, any [Global Documents](https://trust.myaza.co/documentation/id-types/markdown) country, plus **[NFC chip reading](https://trust.myaza.co/documentation/nfc-chip/markdown)**, which the web SDK can't do at all (browsers can't talk to a passport chip). See [Feature availability](https://trust.myaza.co/documentation/sdks/markdown#feature-availability). ## Requirements | Requirement | Minimum | |---|---| | **iOS** deployment target | **15.1** | | **Android** `minSdkVersion` | **24** (Android 7.0) · `compileSdk` 34 · NDK 27.1 | | **Expo SDK** | **56** (React 19, React Native 0.85) | | **React Native** | **0.83+**, with the **New Architecture enabled** (VisionCamera v5 / Nitro requires it; Expo SDK 56 enables it by default) | | **Build toolchain** | Xcode + CocoaPods (iOS) · **JDK 17** for Android Gradle builds | | **Runtime** | A **dev client** or bare build, **not Expo Go** | Peer dependencies to install in your app: `expo` (≥56), `react` (≥19), `react-native` (≥0.83), `react-native-vision-camera` (v5), `react-native-vision-camera-worklets` (≥5), `react-native-worklets` (≥0.8), `react-native-nitro-modules` (≥0.35), `react-native-nitro-image` (≥0.15), `react-native-safe-area-context` (≥4), `react-native-svg` (≥15). ## Install Pick the path that matches your project. ### Expo app (managed / prebuild, recommended) ```bash npx expo install @myazahq/kyc-sdk-react-native \ react-native-vision-camera react-native-vision-camera-worklets \ react-native-worklets react-native-nitro-modules react-native-nitro-image \ react-native-safe-area-context react-native-svg ``` Add the config plugin to `app.json` (it adds the iOS camera-usage string and Android `CAMERA` / `INTERNET` permissions), then build a dev client: ```jsonc // app.json { "expo": { "plugins": [ "@myazahq/kyc-sdk-react-native" ] } } ``` VisionCamera v5 ships **no config plugin** (v4 did), so it takes no `plugins` entry. Listing it makes `expo prebuild` fail with `Cannot find module '.../lib/VisionCamera'`, because Expo loads the package's main entry as a plugin. The camera permission and usage strings come from the Myaza plugin; pass `cameraPermission` to change the iOS wording. ```bash npx expo prebuild npx expo run:ios # or: npx expo run:android ``` The SDK plugin accepts an optional custom camera prompt: `["@myazahq/kyc-sdk-react-native", { "cameraPermission": "Your message…" }]`. It also adds the **location** permission strings by default (the [Address Intelligence](https://trust.myaza.co/documentation/address-intelligence/markdown) step's "Use my current location" shortcut and attest fix; foreground only, and never required to finish the flow) — pass `{ "location": false }` to opt out if none of your workflows collect an address, or `{ "locationPermission": "Your message…" }` to customise the iOS prompt. ### Optional modules Four `expo-*` modules are **optional peers**. The SDK loads each one lazily and carries on without it, so nothing crashes if you skip them, but each one is a capability rather than a detail: | Module | What installing it buys | Without it | |--------|------------------------|-----------| | `expo-device` | Make, model, manufacturer and physical-vs-simulator in the device metadata | Those fields are omitted and the device class is guessed from the platform, so [Device Intelligence](https://trust.myaza.co/documentation/workflows/markdown#capture-add-ons) has a weaker fingerprint and shared-device detection suffers | | `expo-application` | Your app's id, version and build number on the submission | The `app` block is omitted entirely, so a result cannot be traced to the build that produced it | | `expo-localization` | The device's region, explicitly | Country defaults and the reported locale fall back to the JS runtime's locale, which often carries no region at all (`en` rather than `en-NG`) | | `expo-document-picker` | "Choose a file" on the proof-of-address and KYB document steps | Those steps accept a camera capture only, so a PDF bank statement cannot be submitted at all | ```bash npx expo install expo-device expo-application expo-localization expo-document-picker ``` `react-native-webview` is an **optional** peer. With it installed, the address step renders the Google map and the Street View entrance framer inside your app; without it the built-in map and the entrance photo remain, and nothing else changes. Address fields the workflow marks required are enforced in the app (Continue opens the details sheet naming what is still needed), so a submission is never refused for a missing field at the end of the flow. ### Bare React Native app (no Expo prebuild) The SDK uses a few `expo-*` modules, so add the Expo module runtime (you don't need the managed workflow), then install the SDK and its peers: ```bash # 1. One-time: add Expo modules to a bare RN app npx install-expo-modules@latest # 2. Install the SDK + peer dependencies npm install @myazahq/kyc-sdk-react-native \ react-native-vision-camera react-native-vision-camera-worklets \ react-native-worklets react-native-nitro-modules react-native-nitro-image \ react-native-safe-area-context react-native-svg \ expo expo-image-manipulator expo-image-picker expo-speech expo-font \ expo-glass-effect expo-crypto expo-location # 3. iOS pods cd ios && pod install && cd .. ``` Then add the native permissions manually (the config plugin only runs under prebuild): ```xml NSCameraUsageDescription We use the camera to photograph your ID and capture a live selfie. ``` ```xml ``` If your workflows use the [Address Intelligence](https://trust.myaza.co/documentation/address-intelligence/markdown) step, also add the location strings (iOS crashes on the permission request without the usage string): ```xml NSLocationWhenInUseUsageDescription Your location helps place the map pin on your address. ``` ```xml ``` The native face-detector module (a VisionCamera v5 **Nitro** HybridObject plus its Android lib loader) is **autolinked**, so no manual linking is needed. Ensure the **New Architecture** is enabled and the worklets/frame-processor build is set up per VisionCamera's [setup docs](https://react-native-vision-camera.com/docs/guides) (it relies on `react-native-worklets` / `react-native-vision-camera-worklets`, installed above). > **Rebuild your app after updating the SDK.** The SDK ships its own native views (the blurred surround of the full-screen liveness camera, and on iOS the guard that keeps a finger on the signature pad from scrolling the screen or dragging the sheet down). They are autolinked, but a JavaScript-only update cannot add them: run `npx expo prebuild` (or `cd ios && pod install`) and build the app again whenever you move to a new SDK version. An app that only reloads its bundle keeps working, with a plain tint in place of the blur. ### Presence reporting (Address Intelligence): required if you use presence verification > **Presence verification needs setup in your app.** When the flow is submitted the SDK sends the first report and turns on background monitoring, which only works once your app has the background location setup below. Every other report has to come from your app calling the reporter when it opens. If people do not open your app daily, also turn the [evening reminder webhook](https://trust.myaza.co/documentation/address-intelligence/markdown#if-your-app-is-not-opened-often) into a push notification. If your app has none of these, every presence check collects one day and ends `inconclusive`. The [full checklist](https://trust.myaza.co/documentation/address-intelligence/markdown#what-your-app-has-to-do) is on the Address Intelligence page. When a workflow enables [presence verification](https://trust.myaza.co/documentation/address-intelligence/markdown#presence-verification), the SDK stores the confirmed pin on the phone under the `userId` you pass (so pass one) and sends the first report when the flow is submitted. After that, call the reporter when your app starts and every time it comes back to the foreground, for the signed-in user: ```tsx // Call this from your root component. import { useEffect } from 'react'; import { AppState } from 'react-native'; import { reportAddressPresence } from '@myazahq/kyc-sdk-react-native'; export function usePresenceReporting(userId: string | null) { useEffect(() => { if (!userId) return; const report = () => reportAddressPresence({ apiKey: 'pk_live_…', externalUserId: userId, // the same userId the KYC flow ran with }); report(); // the app has just opened const sub = AppState.addEventListener('change', (state) => { if (state === 'active') report(); // back in the foreground }); return () => sub.remove(); }, [userId]); } // Each call resolves to { reported, inside, reason }, reason being // 'reported' | 'no_pin' | 'services_off' | 'no_fix' | 'outside_fence' | 'no_watch' | 'stopped' | 'network_error' ``` It is safe to call on every open for every user: for a user this phone holds no address for it returns `no_pin` straight away without touching location, and several reports on one day count once. What each `reason` means, and what to do about it, is in the [reason table](https://trust.myaza.co/documentation/address-intelligence/markdown#reporting-presence-from-your-app). With the reporter alone a person needs to open your app at home on 5 different evenings, or on 9 different days, within the default 10-day window; background monitoring below needs 3 nights or 5 days and happens without them doing anything. It never throws and never blocks startup. The geofence is evaluated **on-device**: only the derived day + night flag is transmitted, never a coordinate. A fix outside the fence sends nothing; a mock-location fix is reported flagged. `clearPresencePin(externalUserId)` drops the stored pin (sign-out, or after the watch resolves). Stored pins self-expire after 45 days for bounded checks; a pin captured under [always-on monitoring](https://trust.myaza.co/documentation/address-intelligence/markdown#always-on-monitoring) never expires until revoked. ### Background monitoring (OS geofencing) **People already being checked.** The SDK switches background monitoring on when a flow is submitted. People who verified on an older version, and people on always-on monitoring, never pass that step again. From 3.5.0 the reporter does it for them: when their check is still running and background monitoring is not on, a report switches it on. The person sees the system's "allow all the time" prompt once, and the SDK never asks again from a report. If they already allowed it in Settings there is no prompt. Your app still needs the background location setup below. To show your own explanation first, pass `autoBackground: false` and call `enableBackgroundPresence()` when you are ready. The stronger tier: the OS wakes the SDK on fence crossings around the stored pin, app closed or not. Entries stamp a timestamp; exits fold the dwell into per-day aggregates on the phone and flush them, the same privacy floor as the foreground tier. **It is on by default.** When a presence flow is submitted, the SDK asks the person to allow location all the time and arms the geofence itself; you do not call anything for that. What it cannot do is set up your app, so add these three once: 1. Install the optional peer: `npx expo install expo-task-manager`. 2. Declare background location via the config plugin: `["@myazahq/kyc-sdk-react-native", { "location": "always" }]`. This changes your app's store review posture; the [Background Location Declarations](https://trust.myaza.co/documentation/background-location-declarations/markdown) page carries the ready-to-paste Play Console and App Review texts. 3. Register the task at your app's **root module**, before the component tree: ```tsx // index.js import { registerBackgroundPresence } from '@myazahq/kyc-sdk-react-native'; registerBackgroundPresence(); ``` Without all three the SDK skips background monitoring, asks for nothing, and your app's reporter is the only source of evidence. To turn it off for a workflow, switch off **Background monitoring** on the Address step's Presence tab. You can still arm or disarm it yourself, for example to offer it again later to someone who refused: ```tsx const result = await enableBackgroundPresence({ apiKey: 'pk_live_…', externalUserId: 'user_42' }); // result.reason: 'enabled' | 'module_missing' | 'not_registered' | 'no_pin' | 'foreground_denied' | 'background_denied' | 'start_failed' ``` **When evidence is sent.** A stay is reported when the person leaves the area, or at a "still here" check-in: the first 35 minutes after they arrive, then every three hours. Nothing is sent in between, so the day count does not move while someone simply stays at home. A check-in happens when the app is opened and, if you install the optional `expo-background-task` (`npx expo install expo-background-task`, plus its config plugin), about every 40 minutes in the background. Android and iOS decide when a background task really runs. `presenceStay('user_42')` returns the stay the phone has open, `{ since, nextReportAt }` in epoch milliseconds, or `null`. Use it to tell the person a report is on its way. > **Rebuild after installing an optional Expo package.** `expo-task-manager` and `expo-background-task` contain native code. If the JavaScript is installed but the native module is not in the app build (a package added without a rebuild, or one hoisted by a monorepo), SDK 3.4.0 and older closed the app in a release build when background monitoring was switched on. Later versions check for the native module first and skip the feature. Either way the feature only works after a rebuild. `not_registered` means step 3 is missing. `disableBackgroundPresence()` disarms the fence. A refusal at any step leaves the foreground tier working exactly as before: the tiers degrade, never break. ### The Android foreground service On phones whose battery managers drop geofence transitions (Tecno, Infinix, Xiaomi and friends), a persistent notification is what keeps the check alive. Opt-in, Android only, on the same `location: "always"` plugin setting, which also declares the `FOREGROUND_SERVICE` and `FOREGROUND_SERVICE_LOCATION` permissions. `registerBackgroundPresence()` at the root already defines its task: ```tsx const result = await enableForegroundService({ apiKey: 'pk_live_…', externalUserId: 'user_42', notification: { title: 'Address verification in progress', body: 'Open the app to see your progress' }, }); // result.reason: 'enabled' | 'unsupported_platform' | 'module_missing' | 'no_pin' | 'foreground_denied' | 'background_denied' | 'start_failed' ``` A low-power fix every ten minutes becomes the same enter/exit spans the geofence folds, on the same stored state; the queue flushes while the process is alive; a dropped fence is re-armed. `disableForegroundService()` stops it. See [Address Intelligence](https://trust.myaza.co/documentation/address-intelligence/markdown#the-android-foreground-service) for the reasoning. ### Which tier is running? Permissions get revoked in Settings and nothing tells the app. Ask, and offer the only honest road back: ```tsx import { presenceStatus, openLocationSettings } from '@myazahq/kyc-sdk-react-native'; const status = await presenceStatus('user_42'); // status.tier: 'background' | 'foreground' | 'none', plus pinStored, alwaysOn, // locationServicesEnabled, both permission states, geofenceArmed, foregroundServiceRunning if (!status.locationServicesEnabled) { await openLocationSettings('services'); // the phone's location toggle is off } else if (status.tier === 'none' && status.pinStored) { await openLocationSettings(); // no OS allows re-prompting in-app after a denial } ``` To show the person where the check stands, poll `GET /api/kyc/address/presence/:externalUserId` with the publishable key; see [Address Intelligence](https://trust.myaza.co/documentation/address-intelligence/markdown#showing-the-person-where-the-check-stands). > Voice guidance is text-to-speech **output**. The SDK never records audio, so **no microphone permission** is requested or required (`enableMicrophonePermission: false`). ## Usage `` renders a "Verify Identity" trigger plus the full-screen flow. The API is identical to the web SDK, with the same props and callbacks. Unlike the web SDK there is **no stylesheet to import**: styling is built in. ### Recommended: mount a workflow Build the flow once in the dashboard as a [workflow](https://trust.myaza.co/documentation/workflows/markdown), then mount it by id. The country, ID types, capture steps, branding and copy all come from the workflow, so changing the flow is a re-publish rather than an app release, which matters even more on mobile, where a redeploy means an app-store round trip. Requires **≥ 2.1.0** (the `2.0.x` line silently ignores the id). ```tsx import { MyazaKYC } from "@myazahq/kyc-sdk-react-native"; export default function VerifyScreen() { return ( console.log("submitted", submission.verificationId)} onError={(err) => console.error(err.code, err.message)} onClose={() => console.log("closed")} > Verify Identity ); } ``` **`userData` is worth passing.** It is the name you believe the user has, and it is compared against the name read off their document, and that comparison is what produces `dataMatch` on the verification. Leave it out and the check simply never runs: there is nothing to compare the document against, and `dataMatch` comes back `null`. It cannot live on the workflow. `userId`, `userData` and `metadata` are per-user runtime values, and a workflow is a template shared by every visitor, so these stay in code even when everything else moves to the dashboard. ### Or configure everything in code Skip the workflow and pass the flow's shape as props. Useful for a quick start or a single fixed flow; anything you'd change later means an app release. ```tsx import { MyazaKYC } from "@myazahq/kyc-sdk-react-native"; export default function VerifyScreen() { return ( { // The verification was created; status is always 'pending'. // Reconcile the final result on your backend via webhook or a secret-key // GET /verifications/:id call (never from the client). console.log("submitted", submission.verificationId); }} onError={(err) => console.error(err.code, err.message)} onClose={() => console.log("closed")} > Verify Identity ); } ``` The SDK accepts the **same props** as the [web SDK's props table](https://trust.myaza.co/documentation/sdk-react/markdown#props), including `workflowId`, `livenessMode`, `deviceIntelligence`, `consentStep` (skip the welcome screen when your app has already asked for consent), and the capture add-ons (contact OTP, proof of address, questionnaire, NFC). The one exception is `className` (React Native has no class names); style the trigger by passing `style`, or render your own trigger with the hook. > `disableClose` blocks user dismissal on native too: the iOS swipe-down and the Android back button. Because the built-in `` trigger has no external close handle, pair `disableClose` with the [`useMyazaKYC()` hook](#programmatic-control) and call its `close()` to dismiss the flow yourself. ### Biometric re-authentication A workflow on the **biometric-authentication** scope confirms it is really your user, matching a live selfie against the face you enrolled. Three options on the workflow, plus your own words for its screens, shape how the React Native flow behaves (the web and Flutter SDKs honour the same three). All three live on the workflow's **Presence Intelligence** panel in the builder, under **Flow**, so they need no code change; a prop-configured mount can pass the same `biometric` block. - **Where the verdict lands**: **In the app and on your webhook** (the default) keeps the person on one loading screen from the moment the selfie is taken until the check settles, polling the status endpoint for up to a minute, then shows whether it was them; your app hears the verdict in `onResult` and your webhook receives it as on every other flow. **In the app only** is the same wait, but the server sends no webhook for that check at all (`verification.started` and the terminal events included), so the verdict is your app's alone. **Webhook only** shows the ordinary submitted screen and leaves the verdict to your webhook. Enrolment workflows record the reference and have no verdict to deliver, so they never wait. - **Selfie review**: whether the captured selfie is shown back with Retake and Continue before it is sent. **Off by default** on the biometric scopes: a face check is a few seconds long, and a review screen is a stop in the middle of it. Turn it on when you want the person to approve the photo first. - **Done button**: whether the final screen carries a Done button. **On by default.** Turn it off when your app closes the flow itself from `onResult` (or from `onSubmit` on a webhook delivery); the screen then stays until your app dismisses the SDK. - **Screen copy**: your own title and description for the screens the person sees, on the workflow's Presence Intelligence panel under **Face check screens**: the loading screen while the check runs ("Checking it's you") and, on a verdict delivered in the app, the verified screen ("You're verified") and the not-verified screen ("We couldn't confirm it's you"). Leave a field empty to keep the default; `{firstName}` and `{lastName}` fill from the `userData` you pass, as on the welcome and success screens. A not-verified description replaces the reason the server sends. Enrolment offers the loading screen only. In code the same block is `biometric.copy` (`waiting`, `verified`, `declined`, each with `title` and `description`). When the flow waits, the SDK fires a callback with the verdict: ```tsx console.log("submitted", s.verificationId)} onResult={(r) => { // r.status is the same vocabulary GET /api/kyc/status/:id serves: // 'approved' | 'declined' | 'in_review' | 'error' | … if (r.status === "approved") unlockSensitiveAction(); }} > Confirm it is you ``` `onSubmit` still fires the moment the check is submitted. `onResult` fires once when the wait settles, never when it times out, and carries the state and reason pair only; scores and other result data stay behind your secret key. Treat it as a courtesy to your UI: the webhook (where the flow sends one) and `GET /api/kyc/verifications/:id` remain the record, and a decision your app enforces should rest on those, not on a value a client reported. ## App size The SDK adds native machine learning to your app, and that is where the weight sits. The defaults are already the small ones, but two of the four levers below are yours to pull and they are worth more than everything the SDK does on its own. **On-device models are fetched, not bundled.** Face detection and text recognition both run on Google ML Kit on Android, and the SDK depends on the Play Services variants, which download their models on first use. Measured on a real integrator's release APK, the bundled pair cost **18.5 MB per device** (arm64: text 10.55 MB, face 7.95 MB) plus `.tflite` files in `assets/`, which ship to every device because assets are not split by architecture. Fetched, that is about 0.4 MB of shims. The SDK starts both downloads the moment the flow opens, so they overlap the consent and ID-type screens. If a model has not arrived by the time it is needed, the step says so rather than failing quietly: liveness waits and explains, and the MRZ scanner tells the person the printed code cannot be read and lets them continue without the chip. The trade is real. The Play Services variants need Google Play Services, so they do not work on Huawei or bare AOSP devices. If you ship to those, put this in your root `build.gradle` and you get fully-offline models back, at 18.5 MB per device: ```gradle ext { myazaKycBundledMlKit = true } ``` **Ship an App Bundle, or filter your architectures.** Native libraries dominate the rest of the download, and a universal APK carries every architecture at once. An `.aab` lets Play deliver only the one a device needs. If you must ship an APK, name the architectures your users actually have: ```gradle android { defaultConfig { ndk { abiFilters 'arm64-v8a', 'armeabi-v7a' } } } ``` **Turn on R8 and resource shrinking.** The SDK ships its own consumer rules, so you do not need to work out which of its classes are constructed by name: ```gradle android { buildTypes { release { minifyEnabled true shrinkResources true } } } ``` **Use Expo SDK 54 or newer.** Its default template builds smaller than earlier ones, and the SDK's peer range assumes it. ## Programmatic control For a custom trigger, drive the flow with the `useMyazaKYC()` hook: ```tsx import { useMyazaKYC } from "@myazahq/kyc-sdk-react-native"; import { Pressable, Text } from "react-native"; function Trigger() { const { open, close, isOpen, currentStep } = useMyazaKYC({ apiKey: "pk_test_…", country: "NG", onSubmit: (s) => console.log(s.verificationId), }); return ( Verify ({currentStep ?? "idle"}) ); } ``` The flow advances through the same `KYCStep` values as the web SDK: `consent` → `id-type` → `id-input` → `document-capture` → `liveness` → `submitted`. --- # Flutter SDK The Flutter SDK opens the full verification flow as a modal sheet via `MyazaKYC.show()`, with **on-device native liveness** (Apple Vision on iOS, Google ML Kit on Android). It uses a **publishable (`pk_`) key** and detects the environment automatically from the key prefix (`pk_test_*` → sandbox, `pk_live_*` → production). For shared concepts (supported countries, branding, results, and errors), see [Client SDKs](https://trust.myaza.co/documentation/sdks/markdown). > **Feature availability.** This SDK is at **full parity** with the web and React Native SDKs: [workflow](https://trust.myaza.co/documentation/workflows/markdown) embedding (`workflowId`, **≥ 2.2.0**; earlier versions need a placeholder `country` to compile a workflow mount), the capture add-ons (contact OTP, proof of address, questionnaire), business (KYB) verification, any [Global Documents](https://trust.myaza.co/documentation/id-types/markdown) country, plus **NFC chip reading**, which the web SDK can't do at all (browsers can't talk to a passport chip). See [Feature availability](https://trust.myaza.co/documentation/sdks/markdown#feature-availability). ## Install Add the dependency to your `pubspec.yaml`, then run `flutter pub get`: ```yaml dependencies: myaza_kyc_sdk_flutter: ^2.2.0 ``` ## Requirements | Requirement | Minimum | |---|---| | **Flutter** | **3.27** (Dart **3.6**) | | **iOS** deployment target | **13.0** | | **Android** `minSdkVersion` | **21** (Android 5.0) · `compileSdk` 34 | Face detection runs **on-device** (Apple Vision on iOS, Google ML Kit on Android: an Android-only Gradle dependency, so there's no cross-platform ML Kit iOS pod and the SDK still builds on Apple-Silicon iOS simulators). Add the **camera** permission on both platforms (there is **no** microphone permission: voice guidance is text-to-speech output only): ```xml NSCameraUsageDescription We use the camera to photograph your ID and capture a live selfie. ``` ```xml ``` If your workflows use the [Address Intelligence](https://trust.myaza.co/documentation/address-intelligence/markdown) step, also add the location strings. Both are best-effort (they power the "Use my current location" shortcut and the attest fix; the pin always works by dragging alone), but iOS **crashes** on the permission request if the usage string is missing: ```xml NSLocationWhenInUseUsageDescription Your location helps place the map pin on your address. ``` ```xml ``` The SDK bundles `webview_flutter`, which renders the Google map and the Street View entrance framer on the address step inside your app; when that page cannot load, the built-in map and the entrance photo remain. Address fields the workflow marks required are enforced in the app (Continue opens the details sheet naming what is still needed), so a submission is never refused for a missing field at the end of the flow. ### Presence reporting (Address Intelligence): required if you use presence verification > **Presence verification needs setup in your app.** When the flow is submitted the SDK sends the first report and turns on background monitoring, which only works once your app has the background location setup below. Every other report has to come from your app calling the reporter when it opens. If people do not open your app daily, also turn the [evening reminder webhook](https://trust.myaza.co/documentation/address-intelligence/markdown#if-your-app-is-not-opened-often) into a push notification. If your app has none of these, every presence check collects one day and ends `inconclusive`. The [full checklist](https://trust.myaza.co/documentation/address-intelligence/markdown#what-your-app-has-to-do) is on the Address Intelligence page. When a workflow enables [presence verification](https://trust.myaza.co/documentation/address-intelligence/markdown#presence-verification), the SDK stores the confirmed pin on the phone under the `userId` you pass (so pass one) and sends the first report when the flow is submitted. After that, call the reporter when your app starts and every time it comes back to the foreground, for the signed-in user: ```dart // Mix this into your root widget's State. class _AppState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _reportPresence(); // the app has just opened } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) _reportPresence(); // back in the foreground } Future _reportPresence() async { final userId = currentUserId; // the same userId the KYC flow ran with if (userId == null) return; final result = await MyazaAddressPresence.report(apiKey: 'pk_live_…', externalUserId: userId); // result.reason: reported | noPin | servicesOff | noFix | outsideFence | noWatch | stopped | networkError } } ``` It is safe to call on every open for every user: for a user this phone holds no address for it returns `noPin` straight away without touching location, and several reports on one day count once. What each `reason` means, and what to do about it, is in the [reason table](https://trust.myaza.co/documentation/address-intelligence/markdown#reporting-presence-from-your-app). With the reporter alone a person needs to open your app at home on 5 different evenings, or on 9 different days, within the default 10-day window; background monitoring below needs 3 nights or 5 days and happens without them doing anything. It never throws and never blocks startup. The geofence is evaluated **on-device**: only the derived day + night flag is transmitted, never a coordinate. A fix outside the fence sends nothing; a mock-location fix is reported flagged. `clearPresencePin(externalUserId)` drops the stored pin. Stored pins self-expire after 45 days for bounded checks; a pin captured under [always-on monitoring](https://trust.myaza.co/documentation/address-intelligence/markdown#always-on-monitoring) never expires until revoked. ### Background monitoring (native geofencing) The stronger tier: the OS wakes the plugin's native side on fence crossings, app closed or not. On Android the fence survives reboots, and iOS relaunches the app for crossings by itself. Entries stamp; exits fold the dwell into per-day aggregates natively and flush them. **It is on by default.** When a presence flow is submitted, the SDK asks the person to allow location all the time and arms the geofence itself; you do not call anything for that. What it cannot do is declare background location for your app, so add the background-location entries to **your own** Android manifest and iOS Info.plist once. The plugin never adds them for you, because the declaration changes your store review posture; the [Background Location Declarations](https://trust.myaza.co/documentation/background-location-declarations/markdown) page carries the ready-to-paste texts. Without them the SDK's request comes back refused and your app's reporter is the only source of evidence. To turn it off for a workflow, switch off **Background monitoring** on the Address step's Presence tab. You can still arm or disarm it yourself, for example to offer it again later to someone who refused: ```dart final result = await MyazaBackgroundPresence.enable( apiKey: 'pk_live_…', externalUserId: 'user_42', ); // result.reason: started | noPin | permissionDenied | backgroundDenied | unavailable ``` `enable()` asks for location in two steps (while in use, then all the time); `MyazaBackgroundPresence.disable()` disarms and forgets. A refusal leaves the foreground tier working exactly as before. **Update past 3.5.0.** In versions up to and including 3.5.0 the second step never reached the person, on iPhone or on Android 11 and later. `enable()` answered `backgroundDenied` unless they had already chosen "Always" (iPhone) or "Allow all the time" (Android) in Settings, so background monitoring did not start. Later versions ask. The fix is native on both platforms, so rebuild your app after updating (and run `pod install` for iOS). Three things follow from how the two systems work: - **iPhone shows the "Change to Always Allow" prompt once per install.** If the person declines, `enable()` answers `backgroundDenied` from then on without a prompt. - **Android 11 and later opens your app's location settings page** for the second step, where the person picks "Allow all the time". It is a settings page, not a pop-up. After two refusals Android shows nothing and `enable()` answers `backgroundDenied`. - **After a refusal on either platform**, explain why you need it, then call `openLocationSettings()`. - **People who verified before you updated were never asked**, and the reporter now asks for them. See below. **People already being checked.** The SDK switches background monitoring on when a flow is submitted. People who verified on an older version, and people on always-on monitoring, never pass that step again. From 3.6.0 the reporter does it for them: when their check is still running and background monitoring is not on, a report switches it on. The person sees the system's "allow all the time" prompt once, and the SDK never asks again from a report. If they already allowed it in Settings there is no prompt. Your app still needs the background location setup below. To show your own explanation first, pass `autoBackground: false` and call `MyazaBackgroundPresence.enable()` when you are ready. ### The Android foreground service On phones whose battery managers drop geofence transitions (Tecno, Infinix, Xiaomi and friends), a persistent notification is what keeps the check alive. Opt-in and Android only. The plugin ships the service class; you declare it, with its two permissions, in your own manifest (the block is on the [Background Location Declarations](https://trust.myaza.co/documentation/background-location-declarations/markdown#the-android-foreground-service) page), then: ```dart final result = await MyazaPresenceService.enable( apiKey: 'pk_live_…', externalUserId: 'user_42', notification: const PresenceNotification( title: 'Address verification in progress', body: 'Open the app to see your progress', ), ); // result.reason: started | unsupportedPlatform | noPin | permissionDenied | backgroundDenied | notDeclared | unavailable ``` `enable()` walks the same permission escalation and arms the fence too. A low-power fix every ten minutes becomes the same enter/exit spans the geofence folds, natively, on the same stored state; the queue flushes while the process is alive; a dropped fence is re-armed; a reboot restarts it. `MyazaPresenceService.disable()` stops it. See [Address Intelligence](https://trust.myaza.co/documentation/address-intelligence/markdown#the-android-foreground-service) for the reasoning. ### Which tier is running? ```dart final status = await presenceStatus('user_42'); // status.tier: PresenceTier.background | foreground | none, plus pinStored, alwaysOn, // locationServicesEnabled, both permission states, geofenceArmed, foregroundServiceRunning if (!status.locationServicesEnabled) { await openLocationSettings(target: PresenceSettingsTarget.services); // the toggle is off } else if (status.tier == PresenceTier.none && status.pinStored) { await openLocationSettings(); // the only road back after a denial } ``` **When evidence is sent.** A stay is reported when the person leaves the area, or at a "still here" check-in: the first 35 minutes after they arrive, then every three hours (versions after 3.5.0; older ones waited three hours for the first). Nothing is sent in between, so the day count does not move while someone simply stays at home. `presenceStay()` returns the stay the phone has open (`since`, `nextReportAt`) or `null`, so your app can tell the person a report is on its way. To show the person where the check stands on the server, call `fetchPresenceWatchStatus` (versions after 3.5.0; it reads `GET /api/kyc/address/presence/:externalUserId` with the publishable key): ```dart final check = await fetchPresenceWatchStatus( apiKey: 'pk_live_…', externalUserId: 'user_42', ); // null: the server could not be reached. // check.state: PresenceWatchState.notStarted | inProgress | verified | failed | // inconclusive | expired | revoked | unknown // check.progress (0..1), nightsObserved, daysObserved, deadlineAt, stopped ``` It never throws. See [Address Intelligence](https://trust.myaza.co/documentation/address-intelligence/markdown#showing-the-person-where-the-check-stands) for what each status means. ## Usage `MyazaKYC.show()` opens the full flow as a modal bottom sheet (a full-screen page on Android). Note that `context` is a **named** parameter, and the callbacks are passed to `show()` alongside `config`, not inside it. ### Recommended: mount a workflow Build the flow once in the dashboard as a [workflow](https://trust.myaza.co/documentation/workflows/markdown), then mount it by id. The country, ID types, capture steps, branding and copy all come from the workflow, so changing the flow is a re-publish rather than an app release, which matters more on mobile, where a redeploy means an app-store round trip. ```dart import 'package:flutter/material.dart'; import 'package:myaza_kyc_sdk_flutter/myaza_kyc_sdk_flutter.dart'; void startKYC(BuildContext context) { MyazaKYC.show( context: context, config: const MyazaKYCConfig( apiKey: 'pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', workflowId: 'wf_AbC123dEf456', // Runtime data — a workflow is a shared template and cannot carry any of it. userId: 'user_42', userData: UserData(firstName: 'Jane', lastName: 'Doe'), metadata: {'requestId': 'order_1001'}, ), onSubmit: (submission) => debugPrint('Submitted: ${submission.verificationId}'), onError: (error) => debugPrint('Error: ${error.code} — ${error.message}'), onClose: () => debugPrint('KYC closed'), ); } ``` **`userData` is worth passing.** It is the name you believe the user has, and it is compared against the name read off their document, and that comparison is what produces `dataMatch` on the verification. Leave it out and the check simply never runs: there is nothing to compare the document against, and `dataMatch` comes back `null`. It cannot live on the workflow. `userId`, `userData` and `metadata` are per-user runtime values, and a workflow is a template shared by every visitor, so these stay in code even when everything else moves to the dashboard. > `country` is optional here: the resolved workflow carries it, exactly as in the React SDK. Pass one only when you are not using a workflow. ### Or configure everything in code Skip the workflow and pass the flow's shape in `MyazaKYCConfig`. Useful for a quick start or a single fixed flow; anything you'd change later means an app release. ```dart import 'package:flutter/material.dart'; import 'package:myaza_kyc_sdk_flutter/myaza_kyc_sdk_flutter.dart'; void startKYC(BuildContext context) { MyazaKYC.show( context: context, config: MyazaKYCConfig( apiKey: 'pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', country: 'NG', idTypes: const ['bvn', 'drivers-license', 'passport'], userData: const UserData(firstName: 'Jane', lastName: 'Doe'), enableSelfie: true, enableDocumentCapture: true, enableLiveness: true, appearance: const MyazaKYCAppearance( primaryColor: Color(0xFF5645F5), companyName: 'Myaza', logo: 'default', theme: MyazaThemeMode.light, ), consent: const KYCConsentContent( title: 'Welcome, {firstName}', description: "A quick check to confirm it's really you.", ), success: const KYCSuccessContent( title: "You're all set, {firstName}!", description: "We'll email you once your verification is reviewed.", ), metadata: const {'requestId': 'order_1001', 'userId': 'user_42'}, ), onSubmit: (submission) { // The verification was created; submission.status is always 'pending'. // The final result arrives via webhook to your backend (or fetch it with a // secret-key GET /verifications/:id call, never from the client). debugPrint('Submitted: ${submission.verificationId}'); }, onError: (error) { // Technical errors only (network / 401 / 402 / upload). debugPrint('Error: ${error.code} — ${error.message}'); }, onClose: () => debugPrint('KYC closed'), ); } ``` ## Config (`MyazaKYCConfig`) | Field | Type | Default | Description | |---|---|---|---| | `apiKey` | `String` | — | **Required.** Sent as `Authorization: Bearer`. The **environment is derived from the key prefix** (`pk_test_*` → sandbox, `pk_live_*` → production); an unrecognised prefix throws. | | `workflowId` | `String?` | — | Run a published [workflow](https://trust.myaza.co/documentation/workflows/markdown) (`wf_…`) built in the dashboard. The SDK resolves its configuration on launch and uses it as the source of truth: **workflow config wins over overlapping fields**. Makes `country` optional. Requires **≥ 2.2.0**. | | `country` | `String?` | — | **Required unless `workflowId` is set.** ISO-2 country whose ID types are offered (`'NG'`, `'GH'`, …). Any ISO country works: the org's grants are enforced server-side. | | `countries` | `List?` | — | **Multi-region.** More than one entry inserts a country-select step; the picked entry's `idTypes` win. Usually set by a [workflow](https://trust.myaza.co/documentation/workflows/markdown). | | `idTypes` | `List?` | all for country | Subset of [ID type](https://trust.myaza.co/documentation/id-types/markdown) keys to offer (`['bvn', 'passport']`, the same kebab-case keys as the React SDKs); `null` shows everything enabled for the country. | | `userId` | `String?` | — | Your stable reference for the person being verified: repeat checks of the same `userId` collapse onto one entity, and it's how you correlate results back to your record. | | `userData` | `UserData?` | — | Pre-fills the user's details. | | `enableSelfie` | `bool` | `true` | Capture a selfie during liveness. | | `enableDocumentCapture` | `bool` | `true` | Enable the document-scan step for document IDs. | | `allowDocumentUpload` | `bool` | `true` | Allow picking a document photo from the device gallery instead of the camera. `false` hides the "upload instead" option, except on the camera-permission-denied screen, where it stays as an escape hatch. | | `allowDocumentScan` | `bool` | `true` | Allow scanning the document with the live camera. `false` never opens the camera for documents: the applicant picks a photo of each side from the gallery instead. Keep at least one of `allowDocumentScan` and `allowDocumentUpload` on; with both `false` the camera stays on. Usually set by a [workflow](https://trust.myaza.co/documentation/workflows/markdown). | | `enableLiveness` | `bool` | `true` | Run the liveness challenge step. The server can still disable it per ID type. | | `livenessMode` | `String` | `'gestures'` | How liveness proves presence: `'gestures'` (3D Active Motion, always including a head turn), `'flash'` (3D Flash Check, screen reflection), `'both'` (Dual Check: 3D Active Motion and 3D Flash Check in one run), or `'passive'` (Passive Liveness: the person holds still and a liveness model checks the capture). Usually set by a [workflow](https://trust.myaza.co/documentation/workflows/markdown). | | `deviceIntelligence` | `bool` | `true` | Collect device + IP fraud signals ([Device Intelligence](https://trust.myaza.co/documentation/workflows/markdown#capture-add-ons)). | | `consentStep` | `bool` | `true` | Show the consent (welcome) screen as the first step. `false` when your app has already asked for consent: the flow opens on its first real step. Usually set by a [workflow](https://trust.myaza.co/documentation/workflows/markdown). | | `biometric` | `BiometricFlowConfig?` | see [Biometric re-authentication](#biometric-re-authentication) | The biometric scopes' flow options: `selfieReview`, `resultDelivery` (`'both'` \| `'app'` \| `'webhook'`), `doneButton` and `copy` (your own words on the face check screens, see below). Usually set by a [workflow](https://trust.myaza.co/documentation/workflows/markdown); ignored off the biometric scopes. | | `voiceGuidance` | `VoiceGuidanceConfig` | enabled (`en-US`) | Spoken liveness instructions (accessibility, TTS **output**, no microphone). `VoiceGuidanceConfig.off` mutes it; `VoiceGuidanceConfig(language: 'fr-FR')` sets the voice language. | | `selfieReview` | `bool?` | `false` | Show the selfie with Retake and Continue after the liveness capture. Off by default from 3.6.0: the capture hands straight on to the next step. | | `showThemeToggle` | `bool` | `true` | Show a light/dark toggle in the header. Set `false` to hide it. The flow then stays on `appearance.theme` and the user can't switch it. | | `disableClose` | `bool` | `false` | Hide the close (X) button and block **all** user dismissal (swipe-down drag, Android back, barrier tap). The flow can then only be closed programmatically by popping the route `MyazaKYC.show()` returns (its `Future` completes on close). The terminal "Submitted" step is non-dismissible regardless. | | `appearance` | `MyazaKYCAppearance?` | brand defaults | Brand & theme the flow: colours, logo, light/dark. See [Branding & theming](https://trust.myaza.co/documentation/sdks/markdown#branding-theming). | | `consent` | `KYCConsentContent?` | built-in copy | Override the consent/welcome screen `title` and `description`. See [Consent screen copy](https://trust.myaza.co/documentation/sdks/markdown#consent-screen-copy). | | `success` | `KYCSuccessContent?` | built-in copy | Override the success/submitted screen `title` and `description`. See [Success screen copy](https://trust.myaza.co/documentation/sdks/markdown#success-screen-copy). | | `metadata` | `Map?` | — | Forwarded with the verify request (include your `requestId`). | | `livenessConfig` | `LivenessConfig?` | 2 challenges, 8s each | Tune the liveness challenge sequence (see below). | `UserData` accepts `firstName`, `lastName`, `dateOfBirth`, `gender`, `address`, `phoneNumber` and `email` (all optional). `email` is never asked for in the flow: it is the address your organisation can have the applicant [emailed at about a decision](https://trust.myaza.co/documentation/api-review-verification/markdown#emailing-the-applicant). ## Callbacks Passed to `MyazaKYC.show()` alongside `config`: | Callback | Type | Description | |---|---|---| | `onSubmit` | `void Function(KYCSubmission)` | Called when the server accepts the verification. `status` is always `'pending'`. | | `onResult` | `void Function(KYCResult)` | Called once with the verdict on a flow that **waits** for it in the app (a biometric re-authentication on the default delivery). Never on a timeout. See [Biometric re-authentication](#biometric-re-authentication). | | `onError` | `void Function(KYCError)` | Called for **technical** errors only: receives a typed `KYCError` (`code`, `message`, optional `details`). Verification outcomes don't come through here. See [Errors](https://trust.myaza.co/documentation/sdks/markdown#errors). | | `onClose` | `void Function()` | Called when the user closes the flow. | ## Biometric re-authentication A workflow on the **biometric-authentication** scope confirms it is really your user, matching a live selfie against the face you enrolled. Three options on the workflow, plus your own words for its screens, shape how the Flutter flow behaves, exactly as on the web and React Native SDKs. All three live on the workflow's **Presence Intelligence** panel in the builder, under **Flow**, so they need no code change; a config can pass the same `biometric` block. - **Where the verdict lands**: **In the app and on your webhook** (the default) keeps the person on one loading screen from the moment the selfie is taken until the check settles, polling the status endpoint for up to a minute, then shows whether it was them; your app hears the verdict in `onResult` and your webhook receives it as on every other flow. **In the app only** is the same wait, but the server sends no webhook for that check at all, so the verdict is your app's alone. **Webhook only** shows the ordinary submitted screen and leaves the verdict to your webhook. Enrolment workflows never wait. - **Selfie review**: whether the captured selfie is shown back with Retake and Continue before it is sent. **Off by default** on the biometric scopes. - **Done button**: whether the final screen carries a Done button. **On by default.** Turn it off when your app pops the sheet itself from `onResult`; the screen then stays until your app dismisses the SDK. - **Screen copy**: your own title and description for the screens the person sees, on the workflow's Presence Intelligence panel under **Face check screens**: the loading screen while the check runs ("Checking it's you") and, on a verdict delivered in the app, the verified screen ("You're verified") and the not-verified screen ("We couldn't confirm it's you"). Leave a field empty to keep the default; `{firstName}` and `{lastName}` fill from the `userData` you pass, as on the welcome and success screens. A not-verified description replaces the reason the server sends. Enrolment offers the loading screen only. In code the same block is `biometric.copy` (`waiting`, `verified`, `declined`, each with `title` and `description`). ```dart await MyazaKYC.show( context: context, config: const MyazaKYCConfig( apiKey: 'pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', workflowId: 'wf_AbC123dEf456', // a biometric-authentication workflow userId: 'user_42', // required: the enrolled user being re-authenticated ), onSubmit: (s) => debugPrint('submitted ${s.verificationId}'), onResult: (r) { // r.status is the same vocabulary GET /api/kyc/status/:id serves. if (r.status == 'approved') unlockSensitiveAction(); }, ); ``` `onSubmit` still fires the moment the check is submitted. `onResult` fires once when the wait settles, never when it times out, and carries the state and reason pair only (`verificationId`, `status`, `reason`, `reasonCode`); scores and other result data stay behind your secret key. The webhook (where the flow sends one) and `GET /api/kyc/verifications/:id` remain the record. ## Countries & ID types are plain strings `country` takes any ISO-2 code (`'NG'`, `'GH'`, `'FR'`, …) and `idTypes` takes the **same kebab-case keys as the React SDKs** (`'bvn'`, `'drivers-license'`, `'ghana-card'`, …). See the [ID types](https://trust.myaza.co/documentation/id-types/markdown) catalogue. There is no `Country`/`IdType` enum to import. The one enum you'll meet is `MyazaThemeMode` (`light` / `dark`) on `appearance.theme`. ## Liveness configuration `LivenessConfig` tunes the active-liveness step. Defaults match the web SDK. | Field | Type | Default | Description | |---|---|---|---| | `challengeCount` | `int` | `2` | Number of gesture challenges drawn from the pool. | | `challengePool` | `List?` | `kDefaultChallengePool` | The set of challenges to draw from. | | `timeoutPerChallenge` | `int` | `8` | Seconds allowed per challenge before it fails. | | `enableAvatar` | `bool` | `true` | Show the animated avatar that demonstrates each gesture. | The default pool covers four `LivenessChallenge` gestures: `nod`, `turn`, `blink`, and `smile`. --- # User Verification Verify a person with a published workflow. Start in Sandbox, send the person a link, and receive the result on your backend. You can embed the same workflow in an [SDK](https://trust.myaza.co/documentation/sdks/markdown) when you are ready. ## 1. Publish a workflow In **Workflows**, create an **Individual** workflow. Choose the countries, ID types and checks you need, then publish it in **Sandbox**. Copy its `workflowId`. A draft cannot start a verification. ## 2. Start a verification For one person, create a [session](https://trust.myaza.co/documentation/api-create-session/markdown) from your backend with a Sandbox secret key (`sk_test_…`): ```bash curl "https://trust.myaza.app/api/kyc/sessions" \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"workflowId":"wf_AbC123dEf456","externalUserId":"user_42"}' ``` The response includes `session.url` and `session.kind: "individual"`. Send the URL to that person. Treat it as a credential. If you want one link that many people can open, use the workflow's [hosted link](https://trust.myaza.co/documentation/workflows/markdown#2-share-a-hosted-link) instead. To keep verification inside your app, mount the published workflow with the [Web SDK](https://trust.myaza.co/documentation/sdk-react/markdown), [React Native SDK](https://trust.myaza.co/documentation/sdk-react-native/markdown) or [Flutter SDK](https://trust.myaza.co/documentation/sdk-flutter/markdown) and a `pk_test_…` key. ## 3. Receive the result Configure a [webhook](https://trust.myaza.co/documentation/webhooks/markdown) for verification updates. The checks and your workflow's final decision may arrive as separate events; follow the [latest decision guidance](https://trust.myaza.co/documentation/webhooks/markdown) before acting. You can also read [status](https://trust.myaza.co/documentation/api-verification-status/markdown) by session ID. Read the [full result](https://trust.myaza.co/documentation/api-verification-result/markdown) only on your backend with a secret key. Next: [verification lifecycle](https://trust.myaza.co/documentation/verifications/markdown), [supported ID types](https://trust.myaza.co/documentation/id-types/markdown), and [production setup](https://trust.myaza.co/documentation/environments/markdown). --- # Verification lifecycle A verification is **asynchronous**. Creating one returns instantly with `processing`; the platform then runs the checks, a [decision graph](https://trust.myaza.co/documentation/decisioning/markdown) may route it to a person, and it settles. You learn the outcome via [webhooks](https://trust.myaza.co/documentation/webhooks/markdown) or [polling](https://trust.myaza.co/documentation/api-verification-status/markdown). ## States Every surface reports state with the same vocabulary, and a submitted verification carries two fields: `status` (the outcome, which you act on) and `checkStatus` (what the automated checks found, which explains it). They often disagree, and that is expected. See [Status and checkStatus](#status-and-checkstatus) below for how to read the two together. ``` not_started ──► in_progress ──► processing ──┬──► approved │ ├──► declined │ ├──► in_review ──┬──► approved │ ├──► declined │ └──► awaiting_resubmission ──► processing │ └──► error not_started ──► expired (nobody ever opened the link) in_progress ──► abandoned (opened, then left unfinished) ``` | Status | Meaning | Settled? | |---|---|---| | `not_started` | The link exists but nobody has opened it. | No | | `in_progress` | The applicant is part-way through. | No | | `processing` | Submitted. Checks are running, or a decision has not landed. | No | | `in_review` | Your workflow asked a person to decide this one. | No | | `awaiting_resubmission` | A reviewer sent it back to redo some steps. | No | | `approved` | Accepted, automatically or by a person. | Yes | | `declined` | Rejected, automatically or by a person. | Yes | | `abandoned` | Opened, then left unfinished past its deadline. | Yes | | `expired` | Timed out without ever being opened. Send a new link. | Yes | | `error` | A fault on our side. You were not charged. | Yes | ## Status and checkStatus A submitted verification answers two different questions, and each has its own field. | Field | Question it answers | Values | |---|---|---| | `status` | **What is the outcome?** Should this person be onboarded, rejected, or are we still waiting? | The ten values in the table above. | | `checkStatus` | **What did the automated checks find?** The document read, the government database lookup, the face match, the chip read. | `pending`, `verified`, `failed`, `not_found`, `error` | **Act on `status`. Keep `checkStatus` as the explanation.** ### What each checkStatus value means `checkStatus` covers the checks and nothing else. Your workflow's decision rules, and any reviewer's decision, never change it. | `checkStatus` | Meaning | |---|---| | `pending` | The checks are still running. | | `verified` | Every check passed. | | `failed` | A check ran and did not pass: the face did not match, the details did not match the record, the document had expired, and so on. `reasonCode` says which. | | `not_found` | The ID number is not in the government database. | | `error` | The checks could not run because of a fault on our side. You were not charged. | ### How status is decided `status` is worked out from up to three things, in this order. The first one that applies wins. 1. **A reviewer's decision.** A person on your team approved it, declined it, or sent it back. This gives `approved`, `declined` or `awaiting_resubmission`. 2. **Your workflow's decision.** When your [workflow](https://trust.myaza.co/documentation/decisioning/markdown) has decision rules, its verdict gives `approved`, `declined` or `in_review`. While the workflow is still deciding (for example, waiting on a watchlist screen or on a business's directors), `status` is `processing`, whatever the checks found. 3. **The checks alone**, when no workflow decision rules are involved: | `checkStatus` | `status` | |---|---| | `pending` | `processing` | | `verified` | `approved` | | `failed` | `declined` | | `not_found` | `declined` | | `error` | `error` | So the two fields agree only when nothing but the checks decided the outcome. Once a workflow or a person is involved, they can say different things, and both are true. ### Common combinations | `status` | `checkStatus` | What happened | |---|---|---| | `processing` | `pending` | The checks are running. | | `processing` | `verified` or `failed` | The checks have finished, but your workflow has not decided yet. [`waitingOn`](https://trust.myaza.co/documentation/api-verification-status/markdown) says what it is waiting for. | | `approved` | `verified` | The checks passed and the person was accepted. | | `approved` | `failed` or `not_found` | The person was accepted **despite** a failed check. A reviewer made an exception, or one of your workflow rules approved it. `reason` and `reasonCode` say what the exception was for. | | `declined` | `failed` or `not_found` | The checks failed and the person was rejected. | | `declined` | `verified` | The checks passed, but the person was rejected anyway: a rule in your workflow declined them (a watchlist match, for example), or a reviewer did. | | `in_review` | any finished value | Your workflow asked a person to decide. Wait for them. | | `awaiting_resubmission` | any finished value | A reviewer sent it back so the applicant can redo some steps. | | `error` | `error` | A fault on our side. You were not charged. Retry or contact support. | | `not_started`, `in_progress`, `abandoned`, `expired` | *(absent)* | Nothing has been submitted, so no checks have run and there is no `checkStatus`. | An `approved` status with `checkStatus: "failed"` is not a contradiction. It records that the person was accepted, and that they were accepted despite a failed check. If you stored only `approved`, you could not later explain why that exception was allowed, which is exactly what an auditor or regulator asks about. ### When the two change - **`checkStatus` is set once per attempt.** It moves from `pending` to its result when the checks finish, then stays put. The one exception is a **resubmission**: when the applicant redoes a verification that was sent back, the same verification id starts a new attempt (`attempt` goes up by one), `checkStatus` goes back to `pending`, and the checks run again. - **`status` can change after the checks finish.** A workflow can reach its verdict minutes or days later, and a reviewer can decide, or change an earlier decision, at any time. Each of those changes is announced by the [`verification.status_updated`](https://trust.myaza.co/documentation/webhooks/markdown#status-changes) webhook, with `previousStatus`, the new `status`, and a `source` saying who or what made the change. ### Where you see them | Where | `status` | `checkStatus` | |---|---|---| | [`GET /status/:id`](https://trust.myaza.co/documentation/api-verification-status/markdown) (publishable or secret key) | Current | Current, once submitted | | [`GET /verifications/:id`](https://trust.myaza.co/documentation/api-verification-result/markdown) (secret key) | Current | Current | | [`GET /verifications`](https://trust.myaza.co/documentation/api-list-verifications/markdown) (secret key) | Current, per row | Current, per row | | [`verification.status_updated`](https://trust.myaza.co/documentation/webhooks/markdown#status-changes) webhook | The new status, with `previousStatus` | Current | | `verification.started`, `.completed`, `.failed`, `.not_found`, `.error` webhooks | **When the checks finished**, before any workflow decision. See below. | The checks' result | > **If your workflow has decision rules, do not treat `verification.completed` as the final answer.** Those events are sent the moment the checks finish, before your workflow has decided, so their `status` reflects the checks alone. The workflow's verdict arrives afterwards as `verification.status_updated` (with `source: "workflow"`), and that event carries the final `status`. Without decision rules, the checks are the decision, and the `status` on `verification.completed` is final unless a reviewer later changes it. ### Handling both in your code - Update your own record from `status`: `approved` → onboard; `declined` → reject or ask them to try again; `in_review` and `awaiting_resubmission` → keep waiting, a person is involved; `processing` → keep waiting; `error` → retry or contact support. - Store `checkStatus`, `reason` and `reasonCode` next to it. They explain the outcome, and they are the only record of an exception. - Use `reasonCode` (read alongside `checkStatus`) to decide what to tell the applicant. See [failure reason codes](#failure-reason-codes). - Take the latest `status` from `verification.status_updated` or from polling, not from the first webhook you received. A verification whose checks did not pass always carries a human-readable `reason` (a full sentence, safe to show a user) **and** a stable `reasonCode` you can branch on. Both stay populated even if a reviewer later approves it, because they describe what was overridden. See [failure reason codes](#failure-reason-codes) below. > **An ID the government database does not hold is a `declined`**, not a status of its own. `reasonCode: "identity_not_found"` is what distinguishes it from a failed face match, which is the level of detail worth branching on. ## Failure reason codes `reasonCode` is a stable machine token paired with `reason` on every non-success outcome. Branch on the code to drive tailored messaging or retry logic. It appears in [status](https://trust.myaza.co/documentation/api-verification-status/markdown) responses and [webhook](https://trust.myaza.co/documentation/webhooks/markdown) payloads (`data.reasonCode`). **`reason` is written for the applicant, on every `failed` and `not_found` outcome.** It is plain language you can forward to the person as it stands: it names what did not match and what to do next, and it deliberately carries no score, no threshold and nothing read back out of the government record. The numbers are not lost, they are structured fields on the result (`facialConfidence`, `dataValidationDetails`) where your support team reads them. **`reason` on an `error` outcome is written for YOU, not for them.** Those say things like "top up your balance" or "contact Myaza", because nothing the applicant does will fix a verification service outage or an empty credit balance. Show the person a generic "something went wrong, please try again" and route the `reason` to your own team. The `checkStatus` column below tells the two apart: `error` is ours, `failed` and `not_found` are theirs. The middle column is `checkStatus`, not `status`: a reason code describes what the CHECKS found, which is why it survives a reviewer later approving the verification. New codes may be added over time, so **treat an unrecognised code as a generic failure**. | `reasonCode` | `checkStatus` | Meaning | Retry? | |---|---|---|---| | `document_unreadable` | `failed` | No text could be read from the document photo. | New photo | | `document_blurry` | `failed` | The document photo was too blurry to read. | New photo | | `document_type_mismatch` | `failed` | The document doesn't match the selected ID type. | Fix input | | `id_number_not_found` | `failed` | No ID number could be read from the document. | New photo | | `document_data_mismatch` | `failed` | Submitted name/DOB don't match the document. | Fix input | | `document_expired` | `failed` | The document has expired. | New document | | `selfie_mismatch` | `failed` | The selfie doesn't match the photo it was compared with: the government record, the chip, or the photo printed on the document (`facialMatchSource` says which). | New selfie | | `document_photo_no_face` | `failed` | The photo printed on the document was the only photo the selfie could be compared with, and no face could be seen on it. Your workflow declines that case rather than sending it to review. | New photo | | `liveness_video_missing` | `failed` | The recording of the liveness check did not arrive or could not be read, so liveness could not be re-checked. Your workflow declines that case rather than sending it to review. | New selfie | | `gov_data_mismatch` | `failed` | Submitted details don't match the government record. | Fix input | | `gov_validation_failed` | `failed` | The government database flagged the ID as failed. | No | | `identity_not_found` | `not_found` | The ID number wasn't found in the government database. | Fix input | | `identity_suspended` | `failed` | The government record exists, but the issuer has suspended it (a suspended NIN is the usual case). No biodata or face result is returned, since nothing on the record can be verified against. The holder has to have it restored (for a NIN, at a NIMC enrolment centre). | After restoring | | `age_restricted` | `failed` | The holder's age is outside the workflow's age limits for their country, and the workflow declines that case rather than sending it to review. | No | | `age_unverified` | `failed` | The workflow restricts by age, but no date of birth could be read from the ID. | New photo or ID | | `unsupported_id_type` | `error` | This ID type isn't supported for the country. | No | | `provider_error` | `error` | A temporary verification service error occurred; not charged. | Yes | | `media_not_found` | `error` | An uploaded file expired before processing; not charged. | Yes | | `insufficient_credits` | `error` | Your credit balance was too low; not charged. Only reached when credit pause is not active for your organisation, otherwise the verification pauses instead. | After top-up | | `credit_hold_expired` | `error` | A paused verification reached the end of its retention window before funding arrived; the captured documents were deleted. | New capture | | `system_error` | `error` | An unexpected server error occurred; not charged. | Yes | | `id_type_not_enabled` | `error` | This ID type isn't enabled for your organisation. | No | | `document_verification_disabled` | `error` | Document verification is disabled for your organisation. | No | | `gov_db_check_disabled` | `error` | Government database checks are disabled. | No | | `sandbox_not_found` | `not_found` | Sandbox test ID returned the canned `not_found` outcome. | — | | `sandbox_failed` | `failed` | Sandbox test ID returned the canned `failed` outcome. | — | > When more than one check fails (e.g. selfie **and** data), `reasonCode` reflects the primary failure (the selfie, before the data) while `reason` names every leg that failed in one sentence. Retries must reuse the same [`requestId`](#idempotency). ## End-to-end flow 1. **Configure**: `GET /config` to learn enabled ID types and their features. 2. **Capture & upload**: for document/liveness flows, `POST /upload` each file → `mediaId`. 3. **Create**: `POST /verify` with `country`, `idType`, optional `idNumber`, `userData`, `mediaIds`, and a unique `metadata.requestId`. Returns `202` + `verificationId`, status `pending`. 4. **Settle**: the platform fires `verification.started`, then exactly one terminal event (`verification.completed`, `.failed`, `.not_found`, or `.error`). 5. **Reconcile**: update your records from the webhook (or a `GET /status/:id` poll). ## Idempotency `metadata.requestId` is your idempotency key. Submitting the same `requestId` again returns the existing verification rather than creating a new one (and never double-charges). Always: - Generate one stable `requestId` per logical verification. - Reuse it verbatim on any retry (timeout, `5xx`, `429`). See [create verification](https://trust.myaza.co/documentation/api-create-verification/markdown#idempotency). ## Designing your handler - **Treat results as eventual.** Never block a user flow on a synchronous result; show a "verifying…" state and resolve on webhook/poll. - **Be idempotent on your side too.** The same terminal event may be delivered more than once; key your processing on `verificationId`. - **Branch on `status`, store `checkStatus` alongside it.** `approved` → pass; `declined` → reject or request a retry; `error` → retry or escalate; `in_review` and `awaiting_resubmission` → keep waiting, a person is involved. Storing `checkStatus` too is what lets you answer why an exception was allowed. See [Status and checkStatus](#status-and-checkstatus). - **Branch on `reasonCode`** to tailor the user's next step (e.g. "your ID has expired" on `document_expired`), or show the `reason` as it stands on a `failed` or `not_found` outcome. On an `error` outcome the `reason` is addressed to you, so show the person a generic message instead. See [failure reason codes](#failure-reason-codes). ## Data & privacy notes - ID numbers are stored as plaintext (`idNumber`) so they appear unmasked in your dashboard and in [webhook](https://trust.myaza.co/documentation/webhooks/markdown) payloads (`data.idNumber`). The only place they're masked is the [`GET /status/:id`](https://trust.myaza.co/documentation/api-verification-status/markdown) response, which returns `result.idNumberMasked` (e.g. `1234•••901`); that endpoint is designed for safe polling from frontends, so it never echoes the full number back to whoever holds the API key. - If an ID type or feature is not enabled for your org, the attempt is recorded as `error` **without** persisting the submitted PII, and any uploaded media is discarded immediately. ## Location evidence and the verification map When device or IP-location evidence is available, the verification detail page shows the masked IP, approximate place and a map when valid coordinates are available. A map is intentionally not shown when the result contains only a country, city or IP address without usable coordinates; the page explains the missing evidence instead of placing a misleading marker. The same observation is connected to the verified person or business under **Entities → Connected records → Locations & IPs**. This gives investigators one history across identity verification and later monitored activity. The entity view groups repeated observations, keeps source and enrichment provenance, labels test evidence, and highlights useful signals such as new IP, rapid location change, shared IP or datacentre use. Locations are approximate risk evidence, not proof of where a person was physically present. IP addresses stay masked by default; revealing a full address requires sensitive-case permission and creates an audit record. --- # Countries & ID types When creating a verification you pass a `country` and an `idType`. Coverage comes in two tiers: - **Government-database markets**: `NG`, `GH`, `KE`, `ZA`, `CI`. Full catalogue, **including number-only IDs** (BVN, NIN, …), validated against the source government database. Strongest assurance. - **Global documents**: **any other ISO-3166 country** verifies via document capture (Document Intelligence): `passport`, `drivers-license`, and `national-id`. No government database sits behind these, so they reach the `document` assurance tier (a passport can still reach the stronger `chip` tier via its NFC chip). For the full geographic list, including the 48 [business (KYB)](https://trust.myaza.co/documentation/business-verification/markdown) registry countries, see [Supported countries](https://trust.myaza.co/documentation/countries/markdown). > The **authoritative** list for your organisation comes from [`GET /config`](https://trust.myaza.co/documentation/api-config/markdown); it returns only the countries and ID types **enabled for your account** and their feature flags. New countries and ID types reach your organisation only when Myaza grants them. Use the catalogue below for reference; use `/config` at runtime. > **SDK availability.** Global-document countries are available via the API and **all three SDKs** (web, React Native, Flutter). See [Feature availability](https://trust.myaza.co/documentation/sdks/markdown#feature-availability). ## Assurance levels Every passing verification carries an **`assuranceLevel`** telling you *how strongly* the identity was proven, surfaced on the [status](https://trust.myaza.co/documentation/api-verification-status/markdown) response, the [webhook](https://trust.myaza.co/documentation/webhooks/markdown) payload, and workflow [decisioning](https://trust.myaza.co/documentation/decisioning/markdown): | `assuranceLevel` | How it passed | |---|---| | `chip` | The document's [NFC chip](https://trust.myaza.co/documentation/nfc-chip/markdown) (eMRTD) was read, passively authenticated against the issuing country's certificate, and shown not to be a copy. Strongest. | | `gov_db` | Matched against the source government database (the five markets below). | | `document` | Verified from the document alone (Document Intelligence): the global-document tier. | Branch on it to, say, auto-approve `chip` and route `document`-only to review. ## Government-database markets | Country | `idType` values | |---|---| | `NG` Nigeria | `bvn`, `bvn-premium`, `nin`, `vnin`, `drivers-license`, `passport`, `pvc` | | `GH` Ghana | `ghana-card`, `voters`, `drivers-license`, `ssnit`, `passport` | | `KE` Kenya | `national-id`, `passport` | | `ZA` South Africa | `national-id`, `passport` | | `CI` Côte d'Ivoire | `cni`, `residence-card` | ID type identifiers are lowercase and hyphenated (e.g. `drivers-license`, not `drivers_license`). ## Global documents (any other country) For any ISO-3166 country outside the five above, pass its two-letter code with one of the generic document types: | `idType` | Capture | Chip (NFC) | |---|---|---| | `passport` | Front only (MRZ) | ✅ eMRTD, reaches `chip` assurance | | `drivers-license` | Front **and** back | — | | `national-id` | Front **and** back | — | > A country whose national ID is already covered by a database-backed key uses **that** instead of the generic `national-id`: `NG` → `nin`, `GH` → `ghana-card`, `CI` → `cni`. Kenya and South Africa's `national-id` *is* the database-backed key. ## Capture requirements ID types fall into two groups, which determines what you must capture and upload. ### Number-only IDs Verified from the ID number alone; no document scan required. You may still run a liveness/selfie step if enabled. | Country | Number-only IDs | |---|---| | `NG` | `bvn`, `bvn-premium`, `nin`, `vnin` | For these, send `idNumber` and (optionally) a `selfie` / `livenessVideo` media ID. ### Document IDs Every other ID type requires a document scan. Some need both sides: | Capture | IDs | |---|---| | Front only | `passport` (every country) · all document IDs not listed below | | Front **and** back | Global `drivers-license`, `national-id` · `NG`: `drivers-license`, `pvc` · `GH`: `ghana-card`, `voters`, `drivers-license` · `KE`/`ZA`: `national-id` · `CI`: `cni`, `residence-card` | For document IDs, [upload](https://trust.myaza.co/documentation/api-upload/markdown) the relevant files (`documentFront`, `documentBack`, `selfie`, …) and reference their `mediaId`s in the verify request. ## Feature flags Each enabled ID type from [`GET /config`](https://trust.myaza.co/documentation/api-config/markdown) carries three feature flags: | Feature | Meaning | |---|---| | `documentVerification` | A document image is captured and checked. | | `livenessCheck` | A selfie/liveness step runs and is face-matched to the document. | | `govDbCheck` | The ID is validated against the government database. **Always `false` for global-document countries** (no live government source is available there), so those verify by document capture alone. | Branch your capture flow on these flags so you never ask the user for something the org hasn't enabled. Manage which ID types are enabled under **Settings → Organization → ID Types**. --- # NFC chip verification Modern passports (every country) and some national ID cards carry an **eMRTD chip** following the ICAO 9303 standard. On NFC-capable phones, the mobile SDKs read the chip over its secure channel and submit the data with the verification. The server then runs **passive authentication**: it re-computes each data group's hash against the chip's signed security object, verifies the Document Signer signature, and checks that the signer chains to a trusted issuing state's certificate (CSCA). The client never decides authenticity; a chip that positively fails authentication is proven tampered and the verification fails with `chip_not_authentic`. A chip is **authentic only when its signer chains to a trusted issuer**. The signing certificate travels inside the chip data the applicant submits, so a valid signature alone proves only that some key signed it. A chip whose issuing country is not on Myaza's trusted issuer list comes back `inconclusive`: the verification falls back to the document and government database checks, with no `chip` assurance level, no chip photo and no face match against the chip photo. A chip-verified document earns the strongest [assurance level](https://trust.myaza.co/documentation/id-types/markdown#assurance-levels), `chip`, when a copied chip is also ruled out: the chip proves it is the original (Active Authentication, below), or it has no such feature and the live selfie matches the photo stored on the chip. Genuine chip data without that proof gets the next level (`gov_db` or `document`). Where the ID is also checked against a government database (a Nigerian passport, for example), the selfie is compared with the government's photo rather than the chip's, so on that path a chip reaches `chip` only through Active Authentication. > **Native SDKs only.** Reading a chip requires low-level NFC access (ISO-DEP) that only native apps get, so chip verification is available in the **[React Native](https://trust.myaza.co/documentation/sdk-react-native/markdown)** and **[Flutter](https://trust.myaza.co/documentation/sdk-flutter/markdown)** SDKs and is **not supported in the web SDK**: no browser API can talk to a passport chip. Web integrations that want `chip` assurance should route users through one of the mobile SDKs. Phones without an NFC radio skip the step automatically, and skipping never fails a verification. ## Supported documents Two things decide whether the chip step appears for an ID: - **The document carries a chip.** A passport carries one in every country, while national identity cards depend on whether that country issues an ICAO-compliant chip card. Driver's licences never carry a readable chip. - **Myaza holds the issuing country's certificate (CSCA).** A chip can only be confirmed genuine against the certificate of the country that issued it, so the step is offered only for countries on Myaza's certificate list. The list is built from the certificate lists the German, Dutch and Italian governments publish, and covers 138 countries, including Nigeria, Ghana, Kenya, Egypt, Ethiopia and Senegal. Documents from a country not on it skip the chip step and are checked by their document and, where available, the government database. South Africa does not issue chipped passports yet. The authoritative list for your account is [`GET /config`](https://trust.myaza.co/documentation/api-config/markdown), whose ID types each carry `supportsNfc`: true only when both conditions hold. A chip is also checked against the country its own data names as the issuer, so a chip signed under one country's certificate that claims to be another country's document is never treated as genuine. The table below shows which documents carry a readable chip. The chip step runs only where Myaza also holds the issuing country's certificate, as described above, so a tick for a country not on that list does not mean the chip step is offered there yet. ```nfc-countries ``` Some well-known ID cards carry chips that are **not** ICAO eMRTDs and cannot be read this way, by any vendor: the South African smart ID card, the Emirates ID, Malaysia's MyKad, and the smart cards of Singapore, Hong Kong, Japan and China all use proprietary chip applications. Those documents still verify through Document Intelligence; they simply cannot reach `chip` assurance. Three things to know: - The chip's secure channel is unlocked with a key derived from the document's **MRZ** (BAC or PACE), so the chip step always runs after the document scan that reads it. - **Chip capability follows each document's current issuance generation.** Older chipless generations still in circulation pass through unaffected: chip presence is a bonus signal, and a chipless card simply completes on the document checks at the assurance tier it can reach. - Which chip-capable IDs actually offer the step, whether users may skip it manually, and whether the chip portrait is face-matched are all configured per workflow in the builder's NFC panel. ## Data extraction The system reads and verifies the following data groups: | Data group | Contents | What we do | |---|---|---| | `SOD` | Document Security Object | Read in full and verified server-side: the signed hash of every data group plus the Document Signer signature, which must chain to a trusted issuing state's CSCA certificate for the chip to count as authentic. This is what makes everything else trustworthy. | | `DG1` | Personal data (MRZ) | Name, document number, nationality, birth date, expiry date. Hash-verified against the SOD, every ICAO check digit validated, and cross-checked against the details submitted with the verification. | | `DG2` | Facial image | The government-issued portrait stored on the chip. Used only from an authentic chip (above): hash-verified against the SOD and, on document-only verifications, face-matched against the live selfie. | | `DG7` | Signature | The holder's displayed signature image. Read when the chip carries one and hash-verified against the SOD. | | `DG11` | Additional personal details | Full name, personal number, full date of birth, place of birth, address, telephone and profession, where the issuer populated them. Hash-verified against the SOD. | | `DG12` | Additional document details | Issuing authority and date of issue. Hash-verified against the SOD. | Two honesty notes, because chip capabilities are widely overstated: - **DG3 (fingerprints) and DG4 (iris) cannot be read by any commercial system.** They sit behind Extended Access Control: only government inspection systems holding terminal-authentication certificates issued by each state can decrypt them. A vendor claiming to extract fingerprints or iris scans from a passport chip is describing something the standard does not permit them to do. - **DG7, DG11 and DG12 are optional by standard.** Many issuers do not write them at all, so their absence is normal and never a risk signal. The SDK checks the chip's own table of contents (EF.COM) and reads only the groups the document declares. ## Where the results surface The full result lands on the verification record and the [webhook](https://trust.myaza.co/documentation/webhooks/markdown) payload as `data.nfc`: - `status` (`verified` / `not_authentic` / `data_mismatch` / `inconclusive` / `unreadable`), `chipAuthentic`, `dataMatch`, `checkDigitsValid` and the parsed `mrz`. - `sod`, `dg2`, `dg7`, `dg11`, `dg12` sub-objects, each carrying `hashValid`: whether that group's bytes match the signed hash in the SOD. Parsed DG11/DG12 fields are included alongside, with `hashValid` telling you how much to trust them. - On document-only verifications, `dg2.faceMatch` and `dg2.faceMatchConfidence` report the live selfie against the authenticated chip portrait. - `cloneCheck`: how a copied chip was ruled out: `active_auth` (the chip answered our one-time challenge with its own private key), `face_bound` (the chip has no such key and the selfie matched its photo) or `none`. - `sod.signerRevoked`: `true` when the issuing country has cancelled the certificate that signed the chip (such a chip is never treated as genuine), `false` when its cancellation list was checked, `null` when we hold no current list for that country. A `null` chip is still accepted as genuine; see "Passport signer's cancellation status unknown" below if you would rather review or decline it. In workflow [decisioning](https://trust.myaza.co/documentation/decisioning/markdown) you can branch on `verification.nfcStatus`, `verification.nfcAuthentic`, `verification.nfcDataMatch`, `verification.nfcFacialMatch`, `verification.nfcActiveAuth`, `verification.nfcCloneCheck`, `verification.nfcSignerCancellationUnknown` and `verification.assuranceLevel`. ### Passport signer's cancellation status unknown Countries publish lists of the signing certificates they have cancelled, for example after a key leak. We check them, but not every country publishes one, and a list can be unreachable or out of date. When that happens for a chip that is otherwise genuine, `verification.nfcSignerCancellationUnknown` is `true`. These chips are **accepted by default**. To review or decline them instead, open the NFC step's **Rules** tab and add the suggested rule "Passport signer's cancellation status unknown". The field is `false` once the list was checked and empty (`null`) when the chip is not from a trusted issuer at all. ## Behaviour and availability - A missing, unreadable or inconclusive chip **never fails a verification**; it simply falls back to the document checks. Only a proven-tampered chip (`not_authentic`) hard-fails, and that includes a chip that fails Active Authentication: a copy of a genuine chip cannot answer the challenge with the original's private key. A chip without Active Authentication is not a failure, and nor is a chip whose answer never reached us, a submission that leaves out the chip's key, or an answer we could not read: none of those proves a copy, so the chip simply does not count towards `chip`. `activeAuth.reason` says which it was. So `status: verified` (and the `verification.nfcAuthentic` decision field) says the chip's data is genuine, not that the chip is the original: route on `verification.nfcCloneCheck` for that. - The optional detail groups (DG7/DG11/DG12) are read by the React Native SDK today, with Flutter following; the core groups (DG1, DG2, SOD) are read by both mobile SDKs. - On [sandbox keys](https://trust.myaza.co/documentation/sandbox-testing/markdown), a chip payload on a test ID returns a canned authentic result, so you can exercise the full contract at no cost. --- # Business verification quickstart Verify a company through a **Business** workflow. The registry lookup can be combined with documents, declarations and checks on key people. Business verification must be enabled for your organisation. ## 1. Publish a Business workflow In **Workflows**, create a **Business** workflow in **Sandbox**. Select a supported country and the checks you need, then publish it. Copy its `workflowId`. See [Business verification](https://trust.myaza.co/documentation/business-verification/markdown) for products and availability. ## 2. Send a link Use the workflow's [hosted link](https://trust.myaza.co/documentation/workflows/markdown#2-share-a-hosted-link) to invite many applicants. For one known business, call [`POST /api/kyc/sessions`](https://trust.myaza.co/documentation/api-create-session/markdown) from your backend with a Sandbox secret key (`sk_test_…`): ```bash curl "https://trust.myaza.app/api/kyc/sessions" \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflowId": "wf_AbC123dEf456", "externalUserId": "business_42", "business": { "country": "NG", "registrationNumber": "RC123456" } }' ``` The `201 Created` response includes: ```json { "session": { "sessionId": "cmt1gfjce05g547grqbmjsiqf", "url": "https://trust.myaza.co/verify/pXk3...", "kind": "business" } } ``` Send `session.url` to the applicant. The registry remains the source of truth for any prefilled company details. ## 3. Follow the result Receive [verification webhooks](https://trust.myaza.co/documentation/webhooks/markdown) or read the [session status](https://trust.myaza.co/documentation/api-verification-status/markdown). A company registry result and the workflow's onboarding decision can finish at different times, especially when [key people](https://trust.myaza.co/documentation/key-people/markdown) must complete due diligence. Use the final [workflow decision](https://trust.myaza.co/documentation/decisioning/markdown) for your onboarding action. Next: [Business verification details](https://trust.myaza.co/documentation/business-verification/markdown), [webhook payloads](https://trust.myaza.co/documentation/webhook-business-verification/markdown), and [production setup](https://trust.myaza.co/documentation/environments/markdown). --- # Business verification (KYB) KYB (Know Your Business) verifies a **company** instead of a person. It looks the business up in the official company registry by registration number and, optionally, collects company documents, compliance declarations, and runs **customer due diligence (CDD)** on the people who own or control it (directors, beneficial owners, the applicant). Available in 48 countries. See [Supported countries](https://trust.myaza.co/documentation/countries/markdown#business-verification-kyb). > KYB is enabled per organisation. If business verification isn't switched on for your account, ask Myaza to enable it. ## How to run a KYB verification A live business verification **requires a published KYB [workflow](https://trust.myaza.co/documentation/workflows/markdown)**: the workflow defines what you verify (country, product, application steps, and the decision graph). You run it exactly like individual KYC, two ways: 1. **Embed**: mount an [SDK](https://trust.myaza.co/documentation/sdks/markdown) with the KYB workflow's `workflowId`. The SDK collects the registration number (and any application steps you configured) and submits. 2. **Hosted link**: send the workflow's [hosted link](https://trust.myaza.co/documentation/workflows/markdown#2-share-a-hosted-link); the business completes it in the browser, no integration. Build the workflow under **Dashboard → Workflows** (choose **Business** when creating it) and start from a [template](https://trust.myaza.co/documentation/workflows/markdown#templates): **Standard KYB**, **KYB + AML screening**, or **Enhanced KYB with declarations**. ## Products Each verification runs a **product** (what gets looked up): | Product | Availability | Input | |---|---|---| | `business` | Every supported country | Registration number | | `business-tax` | Nigeria | Registration number (returns the business + its tax ID) | | `business-taxid` | Nigeria | Registration number | | `business-tin` | Nigeria | TIN | The default is `business`: a registry lookup by registration number. ## The application (optional layers) A registry lookup proves a business *exists*. It doesn't prove the submitter controls it. A KYB workflow can therefore collect a full application; each layer is a toggle in the builder: - **Company profile**: registered address, business email, phone, website. - **Company documents**: certificate of incorporation, MEMART, proof of address, etc., OCR-read and cross-checked against the registry record. - **Key people (CDD)**: the directors and beneficial owners (UBOs) discovered from the registry are **screened** (sanctions / PEP / adverse media) and can be required to complete their **own KYC** via a per-person invite link. Corporate shareholders are recognised and their ownership chains can be followed to the people behind them. See [Key people & ownership](https://trust.myaza.co/documentation/key-people/markdown). - **Applicant verification**: the person filling in the form verifies their own identity in-flow and declares their role. - **Declarations**: a compliance [questionnaire](https://trust.myaza.co/documentation/workflows/markdown#capture-add-ons) (source of funds, expected volume, …). The decision graph can wait for the key people's due diligence before approving. See [Decisioning](https://trust.myaza.co/documentation/decisioning/markdown). ## The result The business result arrives on your backend by [webhook](https://trust.myaza.co/documentation/webhooks/markdown), the same way KYC results do. A business event's `data` carries a `subjectType` of `"business"`, a `business` object, and a `null` `userData`: ```json { "subjectType": "business", "status": "approved", "checkStatus": "verified", "country": "NG", "business": { "product": "business", "registrationNumber": "RC0000000", "businessName": "ACME LIMITED", "record": { "companyStatus": "active", "registrationDate": "2018-04-01", "…": "…" } }, "userData": null } ``` - `status` is `verified` when the registry returns a match, or `not_found` (reason code `business_not_found`) when the registration number isn't in the registry. - `record` is an organisation-safe extract of the registry data (company particulars, share ownership, key personnel), never the raw internal response. - The **onboarding decision** (approve / decline / review), including any key-people gate, arrives separately in a [`workflow.run.completed`](https://trust.myaza.co/documentation/decisioning/markdown#the-workflowruncompleted-webhook) event. The business verification's own status is terminal the moment the registry lookup finishes; the decision can still be pending on the people. ## Decisioning fields Business runs expose `business.*` fields to [decisioning](https://trust.myaza.co/documentation/decisioning/markdown): `business.companyStatus`, `business.nameMatch`, `business.addressMatch`, `business.ageYears`, `business.keyPersonnelCount`, `business.hasForeignPersonnel`, plus the key-people roll-up (`keyPeople.anyFlagged`, `keyPeople.cddComplete`, `keyPeople.kycComplete`, `keyPeople.corporate`, `keyPeople.ownershipUnresolved`, …). The full roll-up is listed in [Key people & ownership](https://trust.myaza.co/documentation/key-people/markdown#what-you-receive). ## Next steps - [Workflows](https://trust.myaza.co/documentation/workflows/markdown): build and publish a KYB workflow. - [Supported countries](https://trust.myaza.co/documentation/countries/markdown#business-verification-kyb): where KYB is available. - [Key people & ownership](https://trust.myaza.co/documentation/key-people/markdown): due diligence on the owners, corporate shareholders, ownership chains. - [Decisioning](https://trust.myaza.co/documentation/decisioning/markdown): gate approval on registry status and key-people due diligence. --- # Key people & ownership A registry lookup proves a business exists. Regulators (FATF, the EU AML directives, FinCEN's CDD rule) additionally require identifying, and in many cases verifying, the natural persons who **own or control** it: directors, beneficial owners (UBOs), and signatories. A KYB [workflow](https://trust.myaza.co/documentation/workflows/markdown) does this for you, and this page explains who ends up on the list, how deeply each person is checked, and what happens when a shareholder turns out to be another company. Everything here is configured per workflow under **Dashboard → Workflows → [your KYB workflow] → Key People**. ## Where the list comes from Two sources, reconciled against each other: 1. **The registry.** The lookup's own record of officers and shareholders. One person often appears several times in a register (as a director, again as a shareholder, again as a person with significant control); we merge those into one party carrying every role they hold. 2. **The applicant.** When *Collect from the applicant* is on, the form asks the business to list their own directors and owners, with contact emails. After the lookup, the two lists are compared by name. A registry person the applicant did not list is flagged **undisclosed**; one who was shown the register's list and removed a name is flagged more strongly (`keyPeople.removedFromRegistry`); an applicant entry the registry does not corroborate is kept but marked unrecognised for review. ## Roles A party can hold several roles at once: `director`, `beneficial_owner`, `signatory`, `shareholder`. The webhook and API carry the full set as `roles` alongside the single headline `role` (the strongest one, beneficial owner first). Where the workflow asks different things of different roles, a party is held to the **strongest** requirement among the roles they hold, so a director who is also a beneficial owner clears the owner's bar. ## Beneficial owners and the threshold A shareholder whose stake reaches the workflow's **beneficial-owner threshold** is treated as a UBO. The default is per register: 25% in most markets (the FATF / EU / FinCEN indicative line), **10% for Nigeria**, where CAMA 2020 requires disclosure of persons with significant control from 5%. Set your own threshold on the workflow; an explicit value always wins. The test uses the party's **controlling stake: the larger of their economic ownership and their voting rights**. The two are equal until a company issues dual-class or preference shares, and testing ownership alone would file the holder of 5% of the equity and 60% of the votes as a minor shareholder, which is exactly the structure used to stay off a UBO register. Registers do not publish voting rights, so they are applicant-declared when they differ. ## Verification depth Each role is set to one of three tiers: | Tier | What it does | |---|---| | **Screening only** | AML screening: sanctions, PEP and adverse media. | | **Identified** | Screening, plus the person must be corroborated by the register and carry a distinguishing detail (date of birth or nationality). No biometric check. | | **Full KYC** | The person completes their own identity verification (document + selfie) via a personal invite link, valid for 14 days. | Invites can be emailed automatically, handed to the applicant as copyable links on the success screen, or both; a hosted **key-people portal** page lets the business chase stragglers. Forwarded on WhatsApp or email, the portal link previews as "Verify the people behind {Business}" with your organisation's name and logo, so the business admin can see who is asking before they open it. Each role can run its invite through a different published individual workflow, so a UBO can face a stricter check (NFC, proof of address) than a signatory. When the form requires email addresses, it asks only for the people who will actually be sent a link; a company is never asked for one. The decision graph can wait for the whole group: gate on `keyPeople.cddComplete` (everyone met their own tier's standard) or `keyPeople.kycComplete` (every full-KYC person finished). See [Decisioning](https://trust.myaza.co/documentation/decisioning/markdown). ## Corporate shareholders A shareholder is sometimes not a person but **another company**. A beneficial owner is by definition a natural person, so a corporate shareholder is never classified as a UBO, is never sent an identity check (there is no selfie a company can take), and never blocks the decision on its own. It is screened against sanctions lists as an entity, recorded with its registration number, and surfaced as an **unresolved branch of the ownership chain**, because the real owners sit further up. Two decisioning fields carry this: - `keyPeople.corporate`: how many in-scope shareholders are companies. - `keyPeople.ownershipUnresolved`: true while any corporate shareholder has nobody recorded above it. Route these applications to a person; every other check can pass while the actual owners remain unknown. The applicant can also mark a party as a company themselves and, where they know them, declare the people who own it. Declared owners are recorded as exactly that: disclosed, corroborated by nothing. ## Following the chain (look-through) Optionally, the workflow can **look a corporate shareholder up in its own register** and pull in the people it finds, up to three levels deep (default one). Each person found this way joins the same list, indented under the company they were found through, and is screened and verified like anyone else. - **Effective ownership multiplies down the chain.** Someone holding 75% of a company that holds 60% of the applicant owns 45% of the applicant, and 45% is what the beneficial-owner threshold tests. - **Chains cross borders.** The company search resolves which register a parent is filed in, including region-split registers such as US states, so a Delaware parent of a Nigerian company is looked up in Delaware. - **Each step up is billed** as a registry lookup, at the same rate as the workflow's own business lookup, which is why the feature is off by default. A branch that is already below the threshold is not looked up (nothing above it can cross the line), a company with no resolvable registration number is left unresolved, and a register outage refunds the lookup. A genuine "not found" keeps its charge: the register answered, and that answer is a finding about the chain. - Once a branch has been walked, `keyPeople.ownershipUnresolved` stops flagging it. In sandbox and development the chain never calls a real register. The standard sandbox company includes a corporate shareholder (`Sandbox Holdings Ltd`, RC0000900) with a person and a second company above it, so the whole feature, including the depth limit, is testable at no cost. See [Sandbox testing](https://trust.myaza.co/documentation/sandbox-testing/markdown). ## What you receive - Each `key_person.completed` [webhook](https://trust.myaza.co/documentation/webhooks/markdown) fires as a person's check resolves, carrying their `role` and full `roles`. - The verification detail (dashboard and API) lists every party with their roles, economic and voting percentages, corporate flag, registration number, and, for people found up a chain, the parent company and their effective share of the applicant business. - The decisioning roll-up: `keyPeople.status`, `anyFlagged`, `allCleared`, `total`, `inScope`, `waived`, `undisclosed`, `removedFromRegistry`, `unrecognized`, `kycRequired`, `kycVerified`, `kycFailed`, `kycPending`, `kycComplete`, `dataRequired`, `dataConfirmed`, `dataMissing`, `cddComplete`, `corporate`, `ownershipUnresolved`. ## Next steps - [Business verification (KYB)](https://trust.myaza.co/documentation/business-verification/markdown): the full KYB guide. - [Decisioning](https://trust.myaza.co/documentation/decisioning/markdown): gate approval on the key-people roll-up. - [Screening](https://trust.myaza.co/documentation/screening/markdown): what the AML screen checks and how matches are adjudicated. --- # Risk Intelligence Risk Intelligence helps your backend answer one operational question: **can this customer action proceed, does it need review, or should it stop?** Myaza connects the decision to the customer, evidence, alerts and any resulting investigation. ## Choose the job you need | Job | Start here | Result | |---|---|---| | Decide on a payment, transfer or crypto movement | `POST /api/v1/transactions` | A synchronous `allow`, `review` or `block` summary. | | Investigate a wallet | Spot Checks → Wallet, or `POST /api/v1/wallet-screenings` | A wallet risk result with exposures and triggered rule versions. A transaction hash alone is not screened. | | Decide on a login, sign-up or account change | `POST /api/v1/activities` | The same synchronous decision summary. | | Register a customer you already verified | `POST /api/v1/entities` | An Entity ID for risk assessment and monitoring. | | Screen an existing customer | `POST /api/v1/risk-assessments` | A risk snapshot using the checks you selected. | | Keep an existing customer under review | `POST /api/v1/monitoring/subscriptions` | Scheduled checks, material changes and alerts. | | Apply Travel Rule controls to crypto | Add `transaction.travelRule` | Applicability, missing information and transfer action. | | Investigate a material alert | Investigations | Owner, evidence, timeline, remediation and outcome. | ## Read the small result first Activity and transaction responses include `assessment.summary`. It contains the outcome, strongest plain-language reason, score, risk level and next action. Use that object for normal routing. Read the detailed rules, screening and billing evidence only when an operator needs to investigate. ```json { "outcome": "review", "title": "Review required", "reason": "Transaction value exceeded the active review threshold.", "score": 50, "riskLevel": "medium", "matchedRules": 1, "nextAction": { "type": "review_activity", "label": "Review transaction" } } ``` ## What is synchronous and what is not - Activity and transaction decisions return in the request. Their assessed webhooks are optional notifications carrying the same summary. - Monitoring is ongoing. Runs, changes and alerts arrive after enrolment. - Travel Rule is part of a crypto transaction. Missing information keeps the current validation generation pending until you submit the named fields. - Investigations are casework records. Assignment and status changes return immediately and remain in the timeline. All Risk Intelligence server calls use a secret key and the versioned base URL `https://sandbox.trust.myaza.app/api/v1` in Sandbox or `https://trust.myaza.app/api/v1` in Production. In the dashboard, manage one customer's monitoring state and frequency from the Entity. Use Risk Intelligence for portfolio changes, Review Queue for action, and Rules & Policies for monitoring defaults. ## Next steps - [Run your first decision](https://trust.myaza.co/documentation/risk-intelligence-quickstart/markdown). - [Read customer risk and monitoring together](https://trust.myaza.co/documentation/customer-risk-reads/markdown). - [Send complete transactions and activities](https://trust.myaza.co/documentation/monitoring-events/markdown). - [Start continuous monitoring](https://trust.myaza.co/documentation/monitoring-policies/markdown). - [Integrate Travel Rule](https://trust.myaza.co/documentation/travel-rule/markdown). - [Configure webhooks](https://trust.myaza.co/documentation/webhooks/markdown). - [Work with alerts and investigations](https://trust.myaza.co/documentation/investigations/markdown). --- # Risk Intelligence quickstart This is the shortest complete path from a Sandbox key to a transaction decision. Run these HTTP calls from your backend or a terminal. The server SDK package is not yet available on the public npm registry, so this guide does not depend on it. Your organisation needs Fraud Prevention access. The sample uses a Sandbox secret key, a sample customer and a fiat transaction. It does not require a live screening provider. ## 1. Create a secret Sandbox key Open **Developers → API Keys**, select **Sandbox**, then create a **Secret** key. Copy the full `sk_test_...` value when it appears. It is shown only once. Store it in your shell or secrets manager: ```bash export MYAZA_SECRET_KEY="sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` Risk Intelligence is server-side. Do not put this key in browser or mobile code, and do not use a publishable `pk_...` verification key. ## 2. Register a sample customer Transactions require a customer registered in the same environment. Register an unverified sample customer first. Do not claim verified KYC for a customer unless your own verification process has established it. ```bash RUN_ID="$(date +%s)-$$" CUSTOMER_ID="demo-customer-$RUN_ID" curl --fail-with-body -sS -X POST "https://sandbox.trust.myaza.app/api/identity/entities" \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" \ -H "Content-Type: application/json" \ --data-binary @- < # Read customer risk Use these read-only endpoints to explain a customer's risk or find customers who need attention. They do not buy checks, enrol customers, publish rules or release transfers. Customer risk and the risk of one transaction are different contexts. ## Availability This is a **SHADOW preview**, disabled by default. Myaza must enable the read capability for the environment; current scores additionally need a reviewed customer-risk configuration and qualified evidence. An API key alone does not activate it. Existing verification, workflow and transaction decisions are unchanged. Use a secret key on your backend. The key supplies the organisation and environment; do not send either in query parameters. Dashboard readers also need risk-read and individual- or business-read permission for the selected customer type. ## Choose a read All paths below start at your environment's `/api/v1` base URL. | Task | GET path | |---|---| | Current customer risk and its supporting evidence | `/risk-intelligence/customers/{entityId}/risk` | | Immutable assessment history | `/risk-intelligence/customers/{entityId}/risk/history` | | One exact historical assessment | `/risk-intelligence/customers/{entityId}/risk/snapshots/{snapshotId}` | | Filter by current risk | `/risk-intelligence/customers/risk-cohort?basis=CURRENT&subjectType=INDIVIDUAL` | | Filter by the latest retained assessment | `/risk-intelligence/customers/risk-cohort?subjectType=INDIVIDUAL` | Use `subjectType=BUSINESS` for business customers. Omitting `basis` deliberately selects historical, as-assessed results, not a fallback when current risk fails. In Dashboard, open **Customer assessments** and choose **Current** or **As assessed**. ## Filter risk and monitoring together For example, find currently high-risk individuals with active daily monitoring: ```bash curl --get "$MYAZA_API_URL/risk-intelligence/customers/risk-cohort" \ --header "Authorization: Bearer $MYAZA_SECRET_KEY" \ --data-urlencode 'basis=CURRENT' \ --data-urlencode 'subjectType=INDIVIDUAL' \ --data-urlencode 'includeMonitoring=true' \ --data-urlencode 'monitoringState=ACTIVE' \ --data-urlencode 'monitoringFrequency=DAILY' \ --data-urlencode 'level=HIGH' \ --data-urlencode 'pageSize=25' ``` Set `MYAZA_API_URL` to your environment's base URL including `/api/v1`. Keep `MYAZA_SECRET_KEY` in your backend secret store; never paste it into a browser. Monitoring inclusion requires Continuous Monitoring access and, for Dashboard sessions, `monitoring_subscriptions:read`. It is optional and valid only with `basis=CURRENT`. The same filtered population supplies customer rows, totals and risk-band counts; do not combine separate risk and monitoring pages yourself. Supported monitoring states are `ACTIVE`, `PAUSED`, `ENDED` and `NOT_ENROLLED`. Cadence is `HOURLY`, `DAILY`, `WEEKLY` or `MONTHLY`, including a customer override. `monitoringPolicyId` narrows by the latest subscription's policy. A null row-level `monitoring` means confirmed non-enrolment; missing or unavailable proof returns 503 for the combined read, never `NOT_ENROLLED`. Other filters are `level`, `assessmentState`, `status`, `disposition` and `search`. Search covers names and customer references, not encrypted government ID numbers. Use the returned `nextCursor` fields together and retain the same filters. Each page is a new observation, not a frozen export. The maximum page size is 100. ## Display the result safely - A numeric current headline requires `assessment.state=CURRENT` (or `current.assessmentState=CURRENT` on the single-customer endpoint). Read its score and level together. A null score is not zero. - A valid zero is Low. The versioned bands are Low 0–24, Medium 25–49, High 50–79 and Critical 80–100. The index is not a probability of fraud. - `PENDING`, `STALE`, `INCOMPLETE`, `UNAVAILABLE` and other non-current states must not show a retained number as current. Historical evidence remains a separate, explicitly as-assessed view when available. - Reconcile the selected assessment using its snapshot reference, retained factors and coverage. Contextual connections are not automatically score contributors. Do not add their values to the score again. - Refresh rows and totals together at `nextTransitionAt`, on return to the view and at least every 30 seconds. Off-page evidence may expire sooner than the visible rows. - With monitoring included, `monitoringObservation` has an independent clock. Compute its `nextTransitionAt - observedAt` duration separately from Core's duration. Use the shorter window, subtract the entire request time and measure the remaining display time monotonically. Never compare the two wall clocks. - Once that window expires, hide the current rows and totals until a valid refresh succeeds. Do not retain an old Low result during an outage. Responses are `no-store`. Invalid filters return 400; denied access returns 403; disabled preview or unavailable evidence returns 503. Unknown or inaccessible single customers return 404. These states do not authorise a transaction. ## Trace an assessment's evidence In **Risk evidence**, use **Why this score**, **Connections** and **History** for the same selected assessment. Connections offers a source map and an accessible list, with bounded pages and filters for contributions or supporting evidence. Open a finding for its retained observation time and source assessment or signal reference. Historical views never fetch today's findings to fill missing evidence. Solid connections represent retained point allocations. Supporting context adds no points; a minimum-risk adjustment comes from the model, not another check. Filtering or paging changes only what is visible, never the recorded total. The map is assessment lineage, not a live network of wallet owners or customers. Excluded or erased observations do not create invented edges. If a response's score and retained contributions conflict, the view stays unavailable. ## Integration acceptance Test individual and business reads, a real zero, unassessed customers, combined filters, cursor totals, expired evidence, missing monitoring proof, denied roles, another organisation/environment and delayed responses after a workspace switch. Reads must not create checks, charges or notifications. Compare an exact snapshot between the customer endpoint, cohort and history; do not compare independently observed live results as though time had stopped. Continue with [continuous monitoring](https://trust.myaza.co/documentation/monitoring-policies/markdown), [transaction decisions](https://trust.myaza.co/documentation/monitoring-events/markdown) and [agent integration guidance](https://trust.myaza.co/documentation/ai-integration/markdown). --- # Compliance (FRAML) Compliance is Myaza Trust's **fraud and anti-money-laundering** layer (FRAML). On top of KYC verification, it lets you keep watching the people and businesses you've onboarded: screen them against sanctions / PEP / adverse-media watchlists, score their activity in real time, and run a full investigation workflow from alerts through regulatory filings. There are two sides to it: - **The Identity Hub API**: a server-to-server API you call to sync your users as monitored **entities** and to stream their **events** (transactions and actions) for scoring. See [Identity Hub API](https://trust.myaza.co/documentation/identity-hub/markdown) and [Event monitoring](https://trust.myaza.co/documentation/monitoring-events/markdown). - **The dashboard**: where your team manages monitoring on each Entity, understands changes in **Risk Intelligence**, resolves **Review Queue** items, and edits defaults in **Rules & Policies**. Everything is **environment-scoped** (sandbox vs. production) and **organisation-scoped**, exactly like the rest of the platform; the API key you use decides which. See [Environments](https://trust.myaza.co/documentation/environments/markdown). ## Building blocks | Concept | What it is | |---|---| | **Entity** | A person (`INDIVIDUAL`) or company (`BUSINESS`) you monitor, keyed by your own `externalUserId`. Carries a KYC disposition, a risk tier, and (once resolved) a link to a global identity. | | **Screening** | An ongoing check of an entity against a watchlist type (`SANCTIONS`, `PEP`, or `ADVERSE_MEDIA`) producing zero or more **matches**. | | **Event** | A transaction or action you send for scoring (`POST /events`). The engine returns a **decision** and a **risk score**. | | **Signal** | The stored, scored record of an event: the monitored activity stream you browse in the dashboard. (A separate device/session *signal* stream is store-only.) | | **Alert** | An investigation item opened automatically when an event scores at or above the review threshold. | | **Investigation** | An owned evidence and decision record that can group one entity's related alerts and end in a disposition. | | **SAR / STR** | A Suspicious Activity / Transaction Report drafted and filed from an investigation, exportable as goAML XML. | ## Risk & disposition vocabulary An entity carries two independent axes: - **Disposition** is its KYC standing: `PENDING`, `UNDER_REVIEW`, `APPROVED`, `REJECTED`. - **Risk tier** is its monitoring risk posture: `LOW`, `MEDIUM`, `HIGH`, or `UNSCORED`. Each event yields a **decision** (`ALLOW`, `REVIEW`, or `BLOCK`) and a **risk score** from 0 to 100. ## The real-time decision lifecycle An event flows through the system like this: ``` your backend │ POST /api/identity/events ▼ Event ───score──► Signal (every event; browsable stream) │ │ decision ≥ review threshold ▼ Alert (OPEN: needs triage) │ group an entity's alerts ▼ Investigation (OPEN → IN_REVIEW → CLOSED + disposition) │ if reportable ▼ SAR / STR (DRAFT → FILED, exported as goAML XML) ``` - **Event → Signal** happens on every ingested event; the signal is stored and scored. - **Signal → Alert** happens automatically when the composite score crosses your **review threshold**. - **Alert → Investigation → SAR** are **manual** compliance actions taken in the dashboard. ## Permissions Each Compliance area has its own permission key, so you can grant read access to one surface without the others. The server enforces these; the dashboard hides what you can't use. | Key | Grants | |---|---| | `identity:read` | View individual entities, the compliance overview, and entity detail. | | `business_identity:read` | View business entities. | | `screening:read` | View screening results. | | `signals:read` | View the monitored event/signal stream. | | `alerts:read` | View alerts. | | `alerts:manage` | Open an investigation from selected alerts. | | `cases:read` | View investigations (the permission key retains its compatibility name). | | `cases:manage` | Open, assign and decide investigations; draft, file or cancel SARs. | | `monitoring:manage` | Edit monitoring rules (reading them is always allowed). | All investigation and SAR writes are audited server-side and appear in your organisation's audit log (**Dashboard → Audit logs**). ## Next steps - [Identity Hub API](https://trust.myaza.co/documentation/identity-hub/markdown): sync your users as monitored entities. - [Event monitoring](https://trust.myaza.co/documentation/monitoring-events/markdown): stream events for scoring. - [Fraud rules](https://trust.myaza.co/documentation/monitoring-rules/markdown): configure how events are scored. - [Screening](https://trust.myaza.co/documentation/screening/markdown): sanctions, PEP, and adverse-media checks. - [Investigations and reporting](https://trust.myaza.co/documentation/investigations/markdown): the investigation workflow. --- # Identity Hub API The Identity Hub is the server-to-server API you use to register the people and businesses you want to monitor as **entities**, and to read back their risk state. Once an entity exists you can stream its [events](https://trust.myaza.co/documentation/monitoring-events/markdown) for scoring. ## Base URL & authentication All Identity Hub endpoints live under a single base URL, the same for sandbox and production: ``` https://trust.myaza.app/api/identity ``` Every endpoint requires a **secret (`sk_`) key** as a Bearer token: these are backend-only, server-to-server calls that read and write identity data, so a publishable (`pk_`) key is rejected with `403 secret_key_required`. ```bash curl "https://trust.myaza.app/api/identity/entities/user_42" \ -H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` The key identifies your organisation and its environment; `sk_test_` keys operate on sandbox data, `sk_live_` on production. See [Authentication](https://trust.myaza.co/documentation/authentication/markdown). ## Register or update an entity `POST /entities`: create or update an entity, keyed by your own `externalUserId`. The call is **idempotent**: sending the same `externalUserId` again updates the existing entity rather than creating a duplicate. ### Request ```json { "externalUserId": "user_42", "metadata": { "loanId": "loan_20191", "accountId": "acc_7732" }, "type": "INDIVIDUAL", "kycProvenance": "MYAZA_VERIFIED", "kycSource": null, "profile": { "fullName": "John Doe", "dateOfBirth": "1990-01-01", "nationality": "NG", "idType": "bvn", "idNumber": "12345678901", "address": { "city": "Lagos", "country": "NG" }, "declaredMonthlyVolume": 500000, "declaredTxnTypes": ["transfer", "deposit"], "walletAddresses": [ { "address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", "chain": "bitcoin", "label": "withdrawal" } ] } } ``` | Field | Required | Notes | |---|---|---| | `externalUserId` | Yes | Your own stable reference for the user. The entity's primary key within your org + environment. | | `metadata` | No | Your own correlation fields for this entity (a loan id, an account id). Bounded (16 KiB, four levels, 100 keys), returned on every read and carried on every [webhook](https://trust.myaza.co/documentation/webhooks/markdown#your-reference-on-every-event) about the entity. Sending it again replaces the stored object. | | `type` | No | `INDIVIDUAL` (default) or `BUSINESS`. | | `kycProvenance` | No | How the entity was KYC'd: `MYAZA_VERIFIED`, `EXTERNAL_VERIFIED`, or `UNVERIFIED` (default). | | `kycSource` | No | Free-text label of the external verification source, when `kycProvenance` is `EXTERNAL_VERIFIED`. | | `profile` | No | Declared attributes, all optional. `nationality` and declared fields feed monitoring rules (e.g. cross-border). | | `profile.walletAddresses` | No | Up to 20 crypto wallet addresses (`{ address, chain?, label? }`, address 20–128 chars). Omit the key to leave the stored set unchanged; send an array (including `[]`) to replace it. Attaching a wallet enrols the entity in [`WALLET` screening](https://trust.myaza.co/documentation/screening/markdown#types); changing the set re-screens promptly. Wallets are risk attributes, never identity keys. | ### Response `201 Created` for a new entity, `200 OK` for an update: ```json { "entityId": "ent_01j9abc123", "identityId": "idn_01j9xyz456", "externalUserId": "user_42", "metadata": { "loanId": "loan_20191", "accountId": "acc_7732" }, "kycProvenance": "MYAZA_VERIFIED", "created": true, "screening": { "status": "QUEUED", "types": ["SANCTIONS", "PEP"] } } ``` - **`metadata`** echoes your customer metadata exactly as stored. - **`identityId`** is the entity's link to a **global identity**: a person or company shared across organisations and environments. It's `null` until resolution links one (see below). - **`screening`** reflects [screening](https://trust.myaza.co/documentation/screening/markdown) enrolment. It's the string `"INACTIVE"` when screening is not configured for your organisation, never a fake `QUEUED`. ### KYC provenance & identity resolution When you declare an entity **verified** (`MYAZA_VERIFIED` or `EXTERNAL_VERIFIED`) and supply an ID (`profile.idType` + `profile.idNumber`), the Hub runs **resolution**: it links the entity to a global identity, matching an existing one where the ID is already known or creating a new one. `UNVERIFIED` entities skip resolution and stay unlinked. A linked identity carries its own `trustState` (`UNVERIFIED`, `VERIFIED`, `FLAGGED`), risk tier, and the set of verified identifiers (e.g. BVN **and** NIN) the person has accumulated across your org. ## Look up an entity `GET /entities/:externalUserId`: fetch an entity by your own reference, scoped to the key's org and environment. ```json { "entity": { "entityId": "ent_01j9abc123", "externalUserId": "user_42", "metadata": { "loanId": "loan_20191", "accountId": "acc_7732" }, "identityId": "idn_01j9xyz456", "type": "INDIVIDUAL", "status": "ACTIVE", "disposition": "APPROVED", "kycProvenance": "MYAZA_VERIFIED", "kycSource": null, "kycVerifiedAt": "2026-04-27T12:00:00.000Z", "riskTier": "LOW", "riskScore": 12, "createdAt": "2026-04-27T12:00:00.000Z", "profile": { "fullName": "John Doe", "nationality": "NG" } }, "identity": { "id": "idn_01j9xyz456", "type": "INDIVIDUAL", "trustState": "VERIFIED", "riskTier": "LOW", "riskScore": 12 }, "identifiers": [ { "idType": "bvn", "idNumber": "12345678901" } ], "verifications": [] } ``` `identity` and `identifiers` are populated only when the entity is linked. `verifications` is the entity's full KYC history (newest first). A missing entity returns `404 { "error": "entity_not_found" }`. ## Update an entity `PATCH /entities/:externalUserId`: change some of what you hold about an entity without resending the rest. Only the keys you send are touched; an absent key leaves the stored value alone, and `null` clears a nullable field. (`POST /entities` remains the full upsert, which replaces the whole profile.) ```json { "metadata": { "loanId": "loan_20191", "tier": "gold" }, "profile": { "nationality": "GH", "declaredMonthlyVolume": 750000 } } ``` The body accepts the same `metadata`, `kycSource`, `kycProvenance` (`EXTERNAL_VERIFIED` or `UNVERIFIED` only) and `profile` fields as the create call. `externalUserId` and `type` cannot change: the reference is the entity's key, and a person does not become a business. **Identity facts on a Myaza-verified entity are locked.** When `kycProvenance` is `MYAZA_VERIFIED`, its `fullName`, `dateOfBirth`, `nationality`, `idType` and `idNumber` came from a verification we ran, and a patch touching them is refused with `400 field_not_editable` naming the fields. Run a new verification to change them ([verify again](https://trust.myaza.co/documentation/api-verify-again/markdown)). Everything you declare yourself, contacts, address, declared volume, wallets, your own metadata, stays editable. Changing `idType` or `idNumber` on an externally-verified entity re-runs identity resolution, exactly as the create call does. An `address` you send is filed as declared evidence in the entity's address book, as `email` and `phone` are in its contact book. It may be one `addressLine`, or structured parts (`line1`, `line2`, `city`, `state`, `country` as ISO-2, `postcode`), which are kept and composed into the line. It becomes the headline address only while nothing a verification found outranks it: an address read from a government record or a proof-of-address document keeps that place. `declaredMonthlyVolume` takes an optional `declaredVolumeCurrency` (an ISO-4217 code such as `NGN`). ### Response `200 OK` ```json { "entityId": "ent_01j9abc123", "externalUserId": "user_42", "identityId": "idn_01j9xyz456", "changedFields": ["metadata", "nationality", "declaredMonthlyVolume"], "screening": null } ``` `changedFields` names what actually moved (empty when nothing did). `screening` is set only when the wallet set changed and WALLET screening was re-queued. Your endpoints receive `entity.updated` carrying the same field names, never the values. ## Delete an entity `DELETE /entities/:externalUserId`: remove the entity from your organisation. The body may carry a `reason` (up to 500 characters) for your audit log. Deletion is **soft**. The entity leaves your dashboard and every Identity Hub read, its ongoing screening is paused (history, adjudications and cleared matches are kept), and its verifications stay where they are. The global identity it resolved to is untouched: that record is shared and belongs to the person. Myaza keeps the entity, marked as deleted, and can restore it on request; there is no restore endpoint. **Registering the same `externalUserId` again brings it back.** A new `POST /entities`, a new verification for that user, or activity for it through the [events API](https://trust.myaza.co/documentation/monitoring-events/markdown) revives the deleted entity in place rather than creating a second one, because the reference is the key. ```bash curl -X DELETE "https://trust.myaza.app/api/identity/entities/user_42" \ -H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "reason": "Account closed at the customer\u2019s request" }' ``` ### Response `200 OK` ```json { "entityId": "ent_01j9abc123", "externalUserId": "user_42", "deleted": true, "deletedAt": "2026-09-05T10:12:00.000Z", "screeningPaused": true } ``` Your endpoints receive `entity.deleted`. A deleted entity answers `404 entity_not_found` on every other endpoint, the same as one that never existed. ## Bulk import For backfilling an existing user base, `POST /entities/import` accepts **1–1000** entities in one call. It's **durable and asynchronous**: the request is persisted and acknowledged with `202` immediately, then a worker processes each item in the background (nothing is lost on a restart). ### Request ```json { "entities": [ { "externalUserId": "user_1", "type": "INDIVIDUAL", "kycProvenance": "EXTERNAL_VERIFIED", "kycSource": "acme-kyc" }, { "externalUserId": "user_2", "type": "BUSINESS" } ] } ``` Each element uses the same shape as a single [entity registration](#register-or-update-an-entity). ### Response ```json { "jobId": "job_01j9def789", "status": "PROCESSING", "total": 2 } ``` ### Poll import progress `GET /entities/import/:jobId`: ```json { "jobId": "job_01j9def789", "status": "COMPLETED", "totalCount": 2, "succeededCount": 2, "failedCount": 0, "pendingCount": 0, "createdAt": "2026-04-27T12:00:00.000Z", "completedAt": "2026-04-27T12:00:05.000Z", "failures": [] } ``` `failures` lists up to 50 failed items with `externalUserId` and an `error` string so you can retry them. An unknown job returns `404 { "error": "import_job_not_found" }`. ## Address surface Entities whose flow captured an [address pin](https://trust.myaza.co/documentation/address-intelligence/markdown) carry an address surface: | Method | Endpoint | What it does | |---|---|---| | `POST` | `/entities/:externalUserId/address/verify` | Mint a [presence watch](https://trust.myaza.co/documentation/address-intelligence/markdown#presence-verification) for the captured pin. One live watch per entity; a repeat call returns the existing one (`existing: true`). `422 no_address_pin` when no pin was ever captured. | | `POST` | `/entities/:externalUserId/address/revoke` | Stop monitoring this customer: the check that is running ends without a result, and always-on monitoring stops renewing. Optional body `{ reason }` (kept in your audit log, never sent on a webhook). Returns `{ revoked, watches }`; `revoked: false` means nothing was running. Sends `entity.address_presence_stopped`. | | `POST` | `/entities/:externalUserId/address/reattest` | A short-window watch answering "still at this address: yes / no / unknown". Billed on resolution like any watch; never live whereabouts. | | `GET` | `/entities/:externalUserId/address` | The stability attestation: capture tier, presence outcome, months of tenure, `strong`/`moderate`/`weak` grade, and moves in 24 months. | | `GET` | `/entities/:externalUserId/address/packet` | The navigation packet for delivery and recovery teams: the pin with its Plus Code, the entrance-photo URL and the directions your own flow collected. | ## Monitoring switches Two per-entity switches, the same ones the dashboard's entity page offers. Both are idempotent: switching on what is already on changes nothing and says so. | Method | Endpoint | What it does | |---|---|---| | `POST` | `/entities/:externalUserId/screening/ongoing` | Body `{ "ongoing": true }` enrols the entity in sanctions, PEP and adverse-media screening where it is not yet enrolled and resumes any paused rows, so it is re-screened on your risk-based cadence; `false` pauses them. A pause keeps every past result and adjudication. Returns `{ ongoing, active, changed }`: `changed` counts the screening rows that moved, and `active: false` means screening is not running for your organisation at all, so nothing was enrolled. | | `POST` | `/entities/:externalUserId/address/always-on` | Body `{ "enabled": true, "cadenceDays": 90 }` turns the entity's presence watch into an always-on chain that re-checks presence every `cadenceDays` (30, 60, 90, 180 or 365; default 90) and renews itself, minting a watch from the captured pin when none is live (`422 no_address_pin` when there is no pin). `{ "enabled": false }` stops the chain renewing; the check in flight still runs to its verdict. Returns `{ enabled, watchId, minted, cadenceDays }` or `{ enabled: false, stopped }`. Always-on monitoring is billed per entity per year at the cadence's rate. | ## Webhooks Registering entities emits [webhook events](https://trust.myaza.co/documentation/webhooks/markdown) you can subscribe to: | Event | When it fires | `data` | |---|---|---| | `entity.imported` | A new entity was created. | `entityId`, `identityId`, `externalUserId`, `environment` | | `identity.resolved` | An entity was linked to a global identity (new or existing). | `entityId`, `identityId`, `environment`, `matchedExisting` | | `entity.updated` | You edited an entity. | `entityId`, `externalUserId`, `identityId`, `type`, `changedFields` (names only, never values), `environment` | | `entity.deleted` | You deleted an entity. | `entityId`, `externalUserId`, `identityId`, `type`, `deletedAt`, `environment` | | `entity.restored` | Myaza restored an entity you had deleted, at your request. | `entityId`, `externalUserId`, `identityId`, `type`, `restoredAt`, `environment` | ## Errors | Status | Body | Cause | |---|---|---| | `400` | `{ "error": "invalid_request", "details": … }` | The body failed validation; `details` is a field-level breakdown. | | `401` | `{ "error": "Invalid API key" }` | Missing, unknown, or revoked key. | | `403` | `{ "error": "secret_key_required" }` | A publishable (`pk_`) key was used; Identity Hub is secret-key only. | | `400` | `{ "error": "field_not_editable", "fields": [...] }` | A patch tried to change identity facts on a Myaza-verified entity. | | `404` | `{ "error": "entity_not_found" }` | No entity with that `externalUserId` in this org + environment, or it has been deleted. | See [Errors](https://trust.myaza.co/documentation/errors/markdown) for the platform-wide list. ## Next steps - [Event monitoring](https://trust.myaza.co/documentation/monitoring-events/markdown): stream this entity's transactions for scoring. - [Screening](https://trust.myaza.co/documentation/screening/markdown): how watchlist enrolment and matches work. --- # Screening Screening checks your [entities](https://trust.myaza.co/documentation/identity-hub/markdown) against sanctions lists, politically-exposed-person (PEP) registers and adverse media, and assesses attached crypto wallets through [Wallet Intelligence](https://trust.myaza.co/documentation/wallet-screening/markdown). In the dashboard, open a customer and select **Risk** to see their compliance intelligence. Results requiring attention also appear in the **Review Queue**. Both require the `screening:read` permission. ## Enrolment An entity is enrolled in screening automatically when you register it via the [Identity Hub](https://trust.myaza.co/documentation/identity-hub/markdown#register-or-update-an-entity). The registration response echoes the screening state: ```json "screening": { "status": "QUEUED", "types": ["SANCTIONS", "PEP", "ADVERSE_MEDIA", "WALLET"] } ``` When screening is not active for your organisation, this is reported honestly as the string `"INACTIVE"` rather than a fake `QUEUED`. Nothing is screened until the service is active. A verification enrols the person (or business) when it passes, but only if the workflow it ran includes **AML Screening**. A workflow with AML Screening turned off enrols nobody, so nothing is screened or charged for screening. Its **Ongoing monitoring** setting decides whether the enrolment keeps re-screening or runs once. A verification submitted without a workflow enrols everyone who passes. ## Types | Type | Checks against | |---|---| | `SANCTIONS` | Government and international sanctions lists. | | `PEP` | Politically-exposed-person registers. | | `ADVERSE_MEDIA` | Negative news and adverse-media sources. | | `WALLET` | The entity's attached crypto wallet addresses, assessed for crypto risk, including sanctions and other exposure categories. Enrolled only when the profile carries at least one [`walletAddresses`](https://trust.myaza.co/documentation/identity-hub/markdown#register-or-update-an-entity) entry; a changed address set re-screens promptly instead of waiting for the cadence. | ## Statuses | Status | Meaning | |---|---| | `QUEUED` | Enrolled; the initial run hasn't completed. | | `CLEAR` | No matches found. | | `POTENTIAL_MATCH` | One or more possible matches need review. | | `CONFIRMED` | A match has been confirmed. | ## Matches A screening that isn't `CLEAR` carries one or more customer-safe **matches**: the matched name or wallet, a match score, match types, aliases and selected identity evidence. Internal source identity, raw watchlist records and internal provenance are not exposed. ## What you see in the dashboard Customer Risk shows the latest screening **type**, **status**, **match count**, when it was last checked, and the next monitoring check when one is scheduled. Open an evidence row to inspect the customer-safe match details and adjudication state. The detail view keeps a newest-first review history with the outcome, recorded time, review note and score at the time of the decision. It does not expose reviewer IDs, internal source names, raw datasets or source records. Screening evidence that changes a risk decision is also linked from the decision, transaction, or investigation where it mattered. When there is no recorded evidence: > Screening results appear here once entities are enrolled in ongoing sanctions / PEP / adverse-media screening. ## Related - [Identity Hub API](https://trust.myaza.co/documentation/identity-hub/markdown): where enrolment happens. - [Compliance overview](https://trust.myaza.co/documentation/compliance/markdown): how screening fits the wider FRAML picture. - [On-Demand Wallet Screening](https://trust.myaza.co/documentation/wallet-screening/markdown): address risk, exposure and decision rules. Transaction Monitoring screens a crypto counterparty address through the same engine. - `GET /api/v1/monitoring/screenings/{screeningId}`: retrieve the current customer-safe screening resource linked by a webhook. --- # Crypto operations Open **Risk Intelligence → Fraud Monitoring → Transactions**. Use **All transactions** for assessed payments, **Travel Rule** for crypto transfer review, **Unmatched deposits** for observations that need matching, **Crypto providers** for sourced provider records, and **Address book** for saved destinations. Existing transaction URLs and decisions are unchanged. For automatic counterparty wallet checks, open **Travel Rule** and select **Wallet screening settings**. Confirm the switch and provider for the active Sandbox or Live environment, then [test the wallet screening journey](https://trust.myaza.co/documentation/wallet-screening/markdown#set-up-automatic-screening). This setting applies to new crypto transactions; Spot Checks and explicit screening API requests have their own entry points. ## Connect in minutes Select **Integration guide**, choose your use case and stack, then copy the request or the Coding agent prompt. You can use cURL, Node.js, Python or Go without installing an SDK. The guide checks that your Sandbox request arrived and links to its canonical record. It never sends a Live request or places secrets in a browser URL. The five-minute target starts with an available Sandbox secret key and ends with a first decision and a signature-verified sample in your own webhook receiver. Live access still requires business approval, permissions, configured controls and production testing. Machine-readable integration resources on the API origin: - `/openapi.json`: request and response contracts. - `/integration/examples?useCase=crypto`: generated requests and a Node.js webhook verifier. - `/llms.txt` and `/llms-full.txt`: coding-agent instructions. - `/.well-known/myaza-trust.json`: API and documentation discovery. Examples support `payments`, `crypto`, `account`, `custom` and `deposits`. Use the `apiBase` returned by the example. Custom events use `/api/identity`; the other examples use `/api/v1`. Existing SDK transaction methods still work. The new crypto record endpoints are HTTP APIs; do not assume your installed SDK already has matching convenience methods. ## What each record means | Record | Meaning | Does not mean | |---|---|---| | Crypto transfer | A canonical transaction assessed by your existing controls | Myaza moved or released funds | | Deposit observation | Your backend reported an inbound chain output | A customer is matched or a decision was made | | Crypto provider evidence | Platform-reviewed, sourced identity and registration information | A compliance guarantee or active Travel Rule connection | | Approved address | Your organisation approved the saved destination | Ownership was verified or standalone screening ran | The network catalogue validates supported address syntax. It does not enable an automatic chain feed, verify checksums or prove control of a wallet. Never silently round asset amounts beyond the supported eight decimal places. ## Check a transfer's clearance Open a crypto transaction and use **Check transfer** under **Transfer clearance**. The result keeps the risk decision, information review, secure exchange and funds movement separate. **On hold** means do not release funds. **Cancel clearance** stops that clearance record; it does not reverse a blockchain transaction. Past checks remain available in the history. Team members with read-only access can view and reload results. Country coverage and exchange connections are still being qualified. **No current policy is enabled for live clearance.** Passing information checks, a risk-only **Allow**, a saved crypto provider or a matched deposit is not permission to release funds. Myaza Trust does not move funds or automatically block actions on your platform. From your backend, use the additive endpoints: - `GET /api/v1/transactions/{id}/travel-rule/transfer` reads current clearance, or `{ "transfer": null }` when no check has been started. - `POST /api/v1/transactions/{id}/travel-rule/transfer` starts or refreshes a check. Send `Idempotency-Key` and `{ "action": "refresh" }`. For an existing record, include its `expectedRevision`. Send `action: "cancel"` with that revision to cancel it. Retry an uncertain request with the **same key and unchanged body**. A replay identifies the original command revision but returns the current safe transfer state. Reload after a revision conflict. Initial expiry defaults to one hour and cannot be extended by a refresh. Cancelled and expired records cannot be reopened. These commands do not charge credit, contact another institution or move funds. Your execution backend must retain its hold when clearance is missing, unavailable, expired or `canRelease` is false. This API is not yet a signed, single-use execution authorisation; keep real transfers disabled until the complete integration is qualified. ## Manage crypto networks Platform admins can select **Manage networks** in the Crypto Provider Directory, Address book or Unmatched deposits. Add a name, stable network code, address format and audit reason. New entries become available in crypto record forms and Travel Rule policy network pickers. The network code and address format cannot change after creation. Disable an entry to stop new records; existing records, exact retries and history remain available. Concurrent edits require reloading the latest catalogue before saving again. `GET /api/v1/crypto/catalogue` returns active `networks` plus `networkOptions`, including disabled entries for historical display. Only authorised platform admins can change the catalogue. This does not add a supported funding rail: credit top-ups still use the receiving wallets separately configured by platform finance. ## Report and match a deposit Send `POST /api/v1/crypto/deposits` from your backend with a secret key and an `Idempotency-Key` header. The minimal body is available in **Integration guide → Deposit feed**. Include `externalDepositId`, `network`, `transactionHash`, `outputIndex`, `destination`, decimal-string `amount`, `currency`, `confirmations` and ISO `observedAt`. Use a stable output index, such as the transaction's log index for a token transfer. The observation appears under **Unmatched deposits**. Your team can: - **Match customer**: select an existing customer and record a reason. Myaza runs the existing transaction assessment and pricing, then links the observation. - **Link existing transaction**: choose an already assessed inbound crypto transaction. Amount, currency, network, chain hash, output and destination must all match. No second assessment or charge is created. - **Add to address book**: save the retained destination without exposing its full value in the browser. It starts as a claimed address awaiting review. - **Needs review** or **Ignored**: record the reason. Observations remain in history. For API integration, use `POST /api/v1/crypto/deposits/{id}/assessment` with `customerId`, current `version` and `reason`, or `/matches` with `transactionId`, `version` and `reason`. Use the same idempotency key and unchanged body on retries. Once assessment starts, the selected customer is fixed. A timeout can be resumed without creating a second charge. Update confirmations or report a reorganisation with `PATCH /api/v1/crypto/deposits/{id}`. A reorganisation does not erase the historical assessment or silently unmatch the deposit. It emits an event for your backend to handle. Never treat a matched record as fund release. ## Crypto provider reviews and address book Platform reviewers add crypto provider evidence with a source URL, jurisdiction and a concise review summary. Every revision is retained. An empty directory means no records have been curated; the product does not invent crypto providers or licences. Your team's **Approved**, **Watchlist** and **Avoid** review is scoped to its organisation and environment. It does not edit global evidence or automatically change transaction rules. Use Rules & Policies for enforcement. Travel Rule is shown as **Not configured** until a real connection is supported and configured. Saved addresses are encrypted at rest and masked in responses. Approval, suspension and archiving require a reason and the current record version. Address approval is distinct from ownership verification and transaction screening. ## Address tools ### Find the wallet's crypto provider When your organisation has exchange-network discovery enabled, open a saved wallet and use **Crypto provider → Find crypto provider**. Enter the asset, such as USDT. Myaza asks the connected network which institution recognises that wallet. Results appear in the same address details, beside wallet screening and control checks. A **Network match** is exchange-reported evidence, not ownership verification or transfer approval. **No crypto provider confirmed** does not mean a wallet is self-hosted. Multiple matches need clarification. The network does not reach every exchange and does not provide blockchain risk scores, address categories or transaction history. Discovery targets participating institutions' customer deposit wallets, not their cold wallets. Myaza manages the network credentials; customers do not enter signing keys here. From a backend, use the existing secret-key crypto API: ```http POST /api/v1/crypto/addresses/{id}/discovery Content-Type: application/json { "requestId": "1d526c54-e8ed-4b2f-b034-3a5ea370f9c2", "version": 1, "currency": "USDT" } ``` Use a fresh UUID for a new lookup and the address's current version. Save the UUID before sending. Poll `GET /api/v1/crypto/addresses/{id}/discovery/{requestId}` at least 12 seconds apart, including after a timeout. An exact same-body retry never reposts the vendor lookup. Do not create another request ID to recover uncertainty. The dashboard's **Check again** polls the original request. Results are `pending`, `matched`, `unknown` or `ambiguous`. A matched result can include `institutionId`, `institutionName`, `legalName` and `checkedAt`. Requests are scoped to the original organisation/environment, address and account-binding version and expire after one hour. Configuration or address changes can invalidate the result. No private address or signing key is returned. `GET /api/v1/crypto/network/status` lists locally configured discovery capabilities and enabled network codes. `GET /api/v1/crypto/network/institutions` returns the network directory. These are distinct from Myaza's reviewed crypto provider records. In **Crypto providers**, select **Browse network** to search those institutions by company or country. Browsing does not add companies to the reviewed directory or activate connections. The CodeVASP connector currently supplies discovery; its `transferExchange` capability is false. Existing TRP connections and their activation/release checks are unchanged. ### Screen, edit or verify control Open a saved address to edit its label or linked customer or crypto provider, run wallet screening, or prove control. The wallet and network cannot be edited; save a new destination instead. Relinking a customer or crypto provider returns the address to review and clears its control proof. - **Screen address** uses the existing wallet-screening price and assessment engine. Sandbox results are labelled. Waiting for credit, running, failure and a completed decision are different states. Screening never approves the address for your team. - **Verify control** creates a message that expires in ten minutes. Ask the wallet holder to sign it and paste back the signature. Ethereum, Polygon, Arbitrum and Base personal wallets are supported; smart-contract wallets are not. No funds, keys or recovery phrases are requested. A valid signature proves key control at that time, not legal ownership. - **Reveal address** requires both management access and `crypto_addresses:reveal`. Record a reason. The address hides after 30 seconds or when the page loses visibility; the audit trail records access without retaining the full address. The matching API actions live at `/api/v1/crypto/addresses/{id}/metadata`, `/screening`, `/control-challenge`, `/control-proof` and `/reveal`. Read the exact request schema before writing an integration. Each write requires `Idempotency-Key`; retry unchanged input with the same key after a timeout. A control proof returns the updated record version. ## Directory and connections Search directory names, LEIs and registration references. Filter by jurisdiction, review status, registration evidence or network. Filters are applied before pagination. An organisation review does not change the platform's sourced record or endorse a crypto provider. Select **Set up Travel Rule** from the transaction workspace to choose your company, manage exchange connections, open transfer rules and configure result notifications. The same setup is available under **Manage controls → Travel Rule setup**. Use **Advanced** for deposit-feed diagnostics, delivery history and historical wallet reviews. Sandbox and Live profiles stay separate. A company selection does not establish a connection; a successful HTTP delivery does not prove signature verification. Live exchange requires an approved, configured institution connection. In **Crypto providers**, search by company name, LEI or registration reference. Use **Filters** to narrow the directory. Open a company to see its profile, expand **Company evidence** for source records, or record your team's assessment in the review section. Directory review, team review and connection status are separate. Unmatched deposits show current totals for deposits to match, matched deposits and addresses to review. These are current queues, not time-series or financial-volume analytics. ## Coding-agent documentation server For teams with the Core repository checked out, a local, read-only MCP server supplies OpenAPI contracts and the same copy-ready examples. It cannot call your API, read keys, access files, run commands or change customer data. Install the repository dependencies using its pinned Node version, then configure your MCP client to run: ```json { "mcpServers": { "myaza-trust-integration": { "command": "node", "args": ["--import", "/absolute/path/to/myaza-kyc-core/node_modules/tsx/dist/loader.mjs", "/absolute/path/to/myaza-kyc-core/src/integration-mcp.ts"] } } } ``` Replace the paths with your checkout. No environment secrets are required. Available tools are `integration_example` and `api_schema`; `myaza://integration/openapi` is the contract resource. Alternatively, run `pnpm mcp:integration` from Core. Teams without the repository can use the public OpenAPI and llms resources above; no hosted MCP service is advertised. ## Webhooks and safe retries Subscribe to `crypto.deposit.unmatched`, `crypto.deposit.matched`, `crypto.deposit.reorged`, `crypto.address.approved` and `crypto.address.suspended` in the existing webhook settings. Payloads contain masked projections, never full wallet addresses. Use the existing webhook simulator and the guide's **Copy Node.js verifier** action. Verify `X-Myaza-Signature-V2` against the exact raw request bytes. Check the timestamp, accept a valid signature from either secret during rotation, and persist a durable inbox keyed by event `id` before acknowledging delivery. A retry may have a different `deliveryId`. Reject tampered bodies, invalid signatures and expired timestamps. A `2xx` alone does not prove signature verification. All crypto writes require an idempotency key. Reusing a key with different input returns `409`; stale review versions also return `409`. Refresh the record, explain the new decision, and use a new key only for a genuinely new operation. An error is never approval. Lists use `page` and `pageSize` (maximum 100). Detail evidence uses `historyPage`, 25 records per page, with a total count. Use the page controls to read older evidence. Read permission is `fraud_prevention:read`; review actions require `fraud_prevention:manage`. Crypto provider curation is platform-owner only with `platform.providers:write`. Production API calls keep the existing secret-key, product access and business-approval gates. ## Enforcing decisions in your app Myaza assesses and notifies. Your backend decides whether to stop a payment, restrict an account, request another check or send a case to your team. A **Block** badge in Myaza does not freeze the customer's account in another application. | What you monitor | Results to subscribe to | Your application handles | |---|---|---| | Verification | Verification results and `risk.decision.changed` when risk controls apply | Finish onboarding, request review or refuse access using the final decision | | Screening | `screening.match`, `screening.completed`, `screening.adjudicated` and `risk.decision.changed` | Hold a potential match for review; use the resolved decision, not the match alone | | Transactions and account activity | `fraud.transaction.assessed`, `fraud.activity.assessed`, `fraud.decision.changed` | Allow, hold or block the particular action | | Custom events | `event.flagged`, applicable risk decision changes and alerts | Apply the action your published policy requires | | Device reuse or linked accounts | `risk.signal.created`, `risk.decision.changed`, `alert.created` | Review the evidence or request another check; a shared device alone is not proof of fraud | Use `externalUserId` where provided to map the entity to your customer, and the transaction or activity reference for an individual action. Some events have different subject shapes; follow their documented payloads instead of assuming one shared field location. Before releasing a payment, evaluate the latest decision in your backend. Keep pending, failed or unavailable checks on hold according to your approved operating policy. For crypto, completed information validation is **not** confirmation that a counterparty received the Travel Rule information. Do not release funds based on that state alone. Verify signatures, persist each event ID once and acknowledge only after durable receipt. Webhooks can be retried or arrive out of order: fetch the latest record before applying a delayed update. Test allowed, review, blocked, pending and failed outcomes, replayed events, timeouts and manual decision changes before Live use. --- # Transaction, behaviour and event monitoring Send each transaction or customer activity when it happens. Myaza connects it to the customer, evaluates the relevant controls and returns an explainable `allow`, `review` or `block` decision. Have past transactions to assess? [Preview a historical batch](https://trust.myaza.co/documentation/import-past-transactions/markdown) before submitting it through this same endpoint. ## Choose the correct endpoint | What you are sending | Endpoint | Why | |---|---|---| | A payment, transfer, deposit or withdrawal with sender and recipient details | `POST /api/v1/transactions` | Builds the transaction relationship, records accounts or wallets, screens the correct directional party and runs fraud assessment. | | A login, sign-up, password reset, beneficiary change or other supported customer activity | `POST /api/v1/activities` | Builds Behaviour intelligence from device, IP, location and event attributes, then runs fraud assessment. | | An explicit standalone wallet check | `POST /api/v1/wallet-screenings` | Returns wallet risk without creating a monitored customer transaction. The legacy transaction-reference route also screens only the supplied wallet at the same rate. | | An existing integration using the older event shape | `POST /api/identity/events` | Compatibility only. It accepts a reduced free-form payload but cannot model complete parties and instruments. | Use a secret `sk_` key with the same base URL and authentication as the [Identity Hub](https://trust.myaza.co/documentation/identity-hub/markdown#base-url-authentication). Use the v1 endpoints for new integrations. ## Complete value reference The tables below are the complete closed-value fields accepted by the v1 transaction, activity and outcome contracts. Values are lowercase and case-sensitive in requests. If a value is not listed here, the API rejects it rather than guessing. ### Request enum values | Field | All accepted values | |---|---| | `subject.type` | `individual`, `business`, `wallet`, `bank_account`, `device`, `email`, `phone`, `ip` | | Activity `type` | `login`, `signup`, `password_reset`, `beneficiary_added`, `profile_change`, `device_change`, `payout`, `withdrawal` | | `transaction.assetClass` | `fiat`, `crypto` | | `transaction.direction` | `inbound`, `outbound` | | `transaction.parties[].role` | `customer`, `sender`, `recipient`, `counterparty` | | `transaction.parties[].type` | `individual`, `business`, `unknown` | | `transaction.parties[].instruments[].type` | `bank_account`, `crypto_wallet`, `card`, `other` | | Outcome `classification` | `fraud`, `legitimate`, `inconclusive` | | Outcome `source` | `customer`, `analyst`, `chargeback`, `case`, `sar`, `automation` | The response decision values are `allow`, `review` and `block`. Use them as machine values and show your own customer-facing copy separately. ### Open string fields These fields are intentionally not enums: `channel`, `transaction.transactionType`, `transaction.status`, `transaction.paymentMethod`, `transaction.blockchain`, `currency`, `metadata` keys and `customAttributes` keys. Send stable, documented values from your own system. Myaza stores them as evidence and may use them in authorised custom controls, but does not silently translate them into another value. `POST /api/identity/events` is a compatibility endpoint, so its `type` is also an open string. Recognised names containing transaction, payment, transfer, login, sign-in, sign-up, register, password reset, beneficiary added, profile change, device change, payout or withdrawal are routed into the canonical assessment pipeline. New integrations should use the closed v1 activity types above. ## Send a complete transaction `POST /api/v1/transactions` The `subject` is your customer. The `parties` array describes who sent and received the funds. Add each bank account, wallet, card or other payment method to the party that used it. ### Outbound bank transfer ```json { "externalActivityId": "activity_txn_10001", "subject": { "type": "individual", "externalUserId": "user_42" }, "occurredAt": "2026-08-16T09:30:00.000Z", "channel": "bank_transfer", "deviceRef": "device_7f53", "ipAddress": "102.89.12.34", "country": "NG", "location": { "city": "Lagos", "region": "Lagos", "latitude": 6.5244, "longitude": 3.3792, "accuracyRadiusKm": 10 }, "transaction": { "externalTransactionId": "txn_10001", "assetClass": "fiat", "direction": "outbound", "amount": "48000.00", "currency": "NGN", "transactionType": "bank_transfer", "status": "pending", "channel": "bank_transfer", "paymentMethod": "bank_account", "parties": [ { "role": "sender", "externalUserId": "user_42", "type": "individual", "displayName": "Sample customer", "country": "NG", "deviceRef": "device_7f53", "instruments": [ { "type": "bank_account", "externalInstrumentId": "customer_account_01", "identifier": "0123456789", "institutionName": "Sample Bank", "country": "NG", "currency": "NGN", "verified": true } ] }, { "role": "recipient", "externalPartyId": "beneficiary_998877", "type": "individual", "displayName": "Amina Bello", "country": "NG", "instruments": [ { "type": "bank_account", "externalInstrumentId": "beneficiary_account_998877", "identifier": "9988776655", "institutionName": "Recipient Bank", "country": "NG", "currency": "NGN" } ] } ], "metadata": { "paymentPurpose": "supplier_payment", "customerReference": "invoice_2026_104" } } } ``` This appears as **Sample customer → Amina Bello** throughout the dashboard. For an inbound transaction, the customer is the recipient and the title appears as **Sample customer ← sender name**. ### Inbound crypto transfer ```json { "externalActivityId": "activity_crypto_10002", "subject": { "type": "individual", "externalUserId": "user_42" }, "occurredAt": "2026-08-16T10:05:00.000Z", "channel": "blockchain", "country": "NG", "transaction": { "externalTransactionId": "crypto_10002", "assetClass": "crypto", "direction": "inbound", "amount": "0.25", "currency": "USDT", "transactionType": "transfer", "blockchain": "ethereum", "parties": [ { "role": "sender", "externalPartyId": "crypto_sender_01", "type": "individual", "displayName": "Charles Okafor", "country": "NG", "instruments": [ { "type": "crypto_wallet", "externalInstrumentId": "sender_wallet_01", "identifier": "0x8ba1f109551bd432803012645ac136ddd64dba72", "network": "ethereum", "currency": "USDT" } ] }, { "role": "recipient", "externalUserId": "user_42", "type": "individual", "displayName": "Sample customer", "country": "NG", "instruments": [ { "type": "crypto_wallet", "externalInstrumentId": "customer_wallet_01", "identifier": "0x742d35cc6634c0532925a3b844bc454e4438f44e", "network": "ethereum", "currency": "USDT", "verified": true } ] } ] } } ``` This appears as **Sample customer ← Charles Okafor**. Myaza screens the sender's crypto wallet before producing the transaction decision. ## How directional screening works The transaction direction is always from the perspective of your customer. | Direction and asset | Party screened before the decision | What is sent for screening | |---|---|---| | Outbound fiat | Recipient | The recipient's name and available identity evidence. Bank account numbers remain relationship evidence and are not used for screening. | | Inbound fiat | Sender | The sender's name and available identity evidence. Bank account numbers remain relationship evidence and are not used for screening. | | Outbound crypto | Recipient | The recipient wallet address and network. | | Inbound crypto | Sender | The sender wallet address and network. | Crypto counterparties use the same [Wallet Intelligence](https://trust.myaza.co/documentation/wallet-screening/markdown) engine as Spot Checks and the direct API. A post-settlement hash remains in Myaza's transaction record; the wallet provider receives the counterparty address only. The internal wallet check is part of the transaction assessment and does not create a second customer wallet charge. A result without wallet evidence requires review; it is never treated as clean. Individuals are checked for sanctions, PEP and adverse media. Businesses are checked for sanctions and adverse media. If the required target name or wallet address is missing, Myaza returns a review decision with an insufficient-data reason rather than treating the transaction as clear. ## Transaction fields ### Request envelope | Field | Required | Meaning | |---|---:|---| | `externalActivityId` | Yes | Stable idempotency key for this assessment. Reuse it only when retrying the same logical request. | | `subject` | Yes | Your customer. Supply exactly one of `subject.id` or `subject.externalUserId`. | | `occurredAt` | Yes | ISO 8601 time when the activity happened. It cannot be more than five minutes in the future. | | `channel` | No | Origin such as `bank_transfer`, `mobile_app`, `card` or `blockchain`. | | `deviceRef` | No | Your stable device reference. Reusing it builds relationship and new-device evidence. | | `ipAddress` | No | Valid end-user IPv4 or IPv6 address. Myaza does not substitute the calling server's IP address. | | `country` | No | Two-letter country code associated with the event. | | `location` | No | `city`, `region`, coordinates and optional accuracy radius. Latitude and longitude must be supplied together. | | `metadata` | No | Up to 50 flat fields. Values may be strings, numbers, booleans, nulls or short primitive arrays. | | `customAttributes` | No | Up to 50 flat custom values available to authorised custom controls. | ### `transaction` | Field | Required | Meaning | |---|---:|---| | `externalTransactionId` | Yes | Your stable transaction reference. It appears in APIs, evidence and webhooks. | | `assetClass` | Yes | `fiat` or `crypto`. This selects the directional screening type. | | `direction` | Yes | `inbound` or `outbound`, from your customer's perspective. | | `amount` | Yes | Positive decimal string with up to eight decimal places. Send a string to preserve exact money. | | `currency` | Yes | Fiat currency or asset code, such as `NGN`, `USD`, `USDT` or `BTC`. | | `transactionType` | Yes | Your transaction category, such as `bank_transfer`, `card_payment`, `deposit` or `withdrawal`. | | `status`, `channel`, `paymentMethod` | No | Operational context used in evidence and custom controls. | | `blockchain` | Crypto only | Network for a crypto transaction, such as `ethereum` or `bitcoin`. | | `parties` | Recommended | Structured sender and recipient records. Supply these to unlock transaction titles, relationship intelligence and directional screening. | | `metadata` | No | Flat transaction-specific values such as purpose or your own customer reference. | ### Party fields | Field | Required | Meaning | |---|---:|---| | `role` | Yes | `sender`, `recipient`, `customer` or `counterparty`. Use sender and recipient for a complete money movement. | | `externalUserId` | One stable reference | Use when this party is already an entity in your Myaza environment. | | `entityId` | One stable reference | Myaza entity ID. Prefer `externalUserId` when integrating from your own system. | | `externalPartyId` | One stable reference | Your durable reference for an external counterparty that is not yet an entity. | | `displayName` | Required for external fiat screening | Human-readable person or business name. It also becomes the dashboard transaction title. | | `type` | No | `individual`, `business` or `unknown`. It selects the relevant name-screening checks. | | `country`, `industry` | No | Counterparty context for geography, screening and relationship evidence. | | `deviceRef` | No | Device used by this party, when your system observes it. | | `instruments` | No | Bank accounts, crypto wallets, cards or other payment methods used by this party. | Every party needs at least one stable reference: `externalUserId`, `entityId` or `externalPartyId`. ### Instrument fields | Field | Required | Meaning | |---|---:|---| | `type` | Yes | `bank_account`, `crypto_wallet`, `card` or `other`. | | `externalInstrumentId` | One stable value | Your durable account, wallet or payment-method reference. | | `identifier` | One stable value | Raw account number or wallet address observed for this transaction. It is fingerprinted for relationship matching and is not stored in raw form. | | `fingerprint` | One stable value | An existing service or device token when raw identifiers should not be submitted. | | `maskedIdentifier` | No | Safe display value. It cannot identify an instrument on its own. | | `institutionName`, `country`, `currency` | No | Bank or payment-method context. | | `network` | Crypto only | Blockchain network used by the wallet. | | `verified` | No | Whether your authorised process verified that the party controls this instrument. | Each instrument needs `externalInstrumentId`, `identifier` or `fingerprint`. Myaza derives a tenant-scoped fingerprint and keeps only safe display evidence. ## Transaction assessment response The response wraps the persisted decision in `assessment`. Read `summary` first: ```json { "assessment": { "activityId": "act_01j9ghi000", "transactionId": "atx_01j9ghi001", "decisionId": "dec_01j9ghi002", "decision": "review", "summary": { "outcome": "review", "title": "Review required", "reason": "New beneficiary with value matched.", "score": 72, "riskLevel": "high", "matchedRules": 1, "nextAction": { "type": "review_activity", "label": "Review transaction" } }, "alertId": "alt_01j9ghi003", "assessedAt": "2026-08-16T09:30:00.284Z" } } ``` Use `summary.outcome` to gate your flow and `summary.nextAction` to continue it. The complete response also retains matched rules, screening, billing, signals and processing evidence when you need to audit or investigate the result. ### Wallet verification & screening For crypto, each `assessment.screening.attempts[]` contains a `walletScreenings` array. Each item is the exact saved counterparty wallet check: `id`, `status`, `wallet`, `risk`, `profile`, `provider` and `screenedAt`. Multiple screened wallets remain separate items. Read `risk.score` and `risk.level` for wallet risk; the transaction's `decision` remains the overall Myaza decision. The [address overview fields](https://trust.myaza.co/documentation/api-wallet-screenings/markdown#wallet-verification--screening) include the analytics provider (Merkle Science when returned), address, blockchain, owner and user attribution, their types and subtypes, first/latest transaction times and provider date added. They have the same names and meaning as standalone wallet results. Missing fields remain `null`. Name-screening attempts and attempts without linked wallet evidence have an empty array. If `walletEvidenceUnavailable` is `history_limit_exceeded`, the evidence read exceeded its bound (100 wallet attempts or 1,000 saved wallets). No partial list is presented; do not interpret that empty array as clean or complete evidence. Create, exact replay and `GET /api/v1/transactions/{id}` return the same saved wallet evidence without another provider call. The response excludes raw provider data and credentials. Production billing uses the component prices configured for your organisation. A transaction can contain a fraud-assessment line and one or more screening lines. Sandbox requests are non-billable. Invalid input is rejected before usage is reserved, and service failures are not treated as successful billable assessments. ## Send behaviour activity `POST /api/v1/activities` Use this endpoint for non-transaction customer behaviour. The example below connects a login to the customer, device, IP and location used at the time. ```json { "externalActivityId": "login_10001", "subject": { "type": "individual", "externalUserId": "user_42" }, "type": "login", "occurredAt": "2026-08-16T09:30:00.000Z", "channel": "mobile_app", "deviceRef": "device_7f53", "ipAddress": "102.89.12.34", "country": "NG", "location": { "city": "Lagos", "region": "Lagos", "latitude": 6.5244, "longitude": 3.3792, "accuracyRadiusKm": 10 }, "metadata": { "authenticationMethod": "password", "sessionReference": "session_20001" }, "customAttributes": { "trustedDevice": false, "failedAttemptsBeforeSuccess": 2 } } ``` ### Supported behaviour types | Type | Use it for | |---|---| | `login` | Successful or attempted customer login. | | `signup` | Account registration activity. | | `password_reset` | Password or credential recovery. | | `beneficiary_added` | A new payee or beneficiary. | | `profile_change` | Material customer-profile changes. | | `device_change` | A device replacement or trust-state change. | | `payout` | Non-transaction payout activity that does not carry full sender and recipient details. | | `withdrawal` | Non-transaction withdrawal activity that does not carry full transaction details. | Use `POST /api/v1/transactions` when money movement has sender, recipient, account or wallet evidence. Do not send a transaction through the generic activity endpoint. Each accepted activity updates the customer's Behaviour intelligence with the evidence supplied, evaluates active risk-scoring rules, and records the resulting decision. Location observations appear under **Connected records → Locations & IPs**. Device references contribute to device history and relationship evidence. ## Location and IP evidence Send only evidence you actually collected. `ipAddress` must be a valid IPv4 or IPv6 address, `country` uses a two-letter code, and latitude and longitude must be supplied together. `accuracyRadiusKm` is optional and is accepted only with coordinates. If you send both location and IP evidence, your supplied location remains primary. IP enrichment fills only missing fields. Replaying the same logical activity does not create a duplicate location observation. Location capture happens before billable fraud assessment. Invalid activity input does not create a location record or an assessment charge. A temporary location-enrichment failure does not turn otherwise valid activity into a chargeable failure. ## Idempotency and freshness - Use one `externalActivityId` per logical assessment and keep it stable across retries. - Use one `externalTransactionId` per customer transaction. - Replaying the same request returns the existing assessment with `replayed: true` and does not score or bill it again. - Reusing either identifier with different immutable transaction data returns a conflict. - Send the original `occurredAt`. Myaza retains both the occurrence time and receipt time so late events remain visible and do not distort the audit trail. ## Webhooks Subscribe to `fraud.transaction.assessed` for every transaction decision and `fraud.activity.assessed` for every behaviour decision. Transaction webhooks include the subject, external activity and transaction references, structured parties, masked instruments, screening evidence and billing breakdown. Use `event.flagged` only as a compatibility alias for non-allow decisions. See [Transaction Monitoring webhooks](https://trust.myaza.co/documentation/webhook-transaction-monitoring/markdown) and [Event Monitoring webhooks](https://trust.myaza.co/documentation/webhook-event-monitoring/markdown) for delivery contracts and retry guidance. ## Compatibility event endpoint `POST /api/identity/events` remains available for existing integrations: ```json { "externalUserId": "user_42", "externalEventId": "txn_10001", "type": "transaction.transfer", "data": { "amount": 48000, "currency": "NGN", "country": "NG", "counterparty": "beneficiary_998877", "direction": "outbound", "channel": "bank_transfer", "ipAddress": "102.89.12.34", "location": { "city": "Lagos", "region": "Lagos", "latitude": 6.5244, "longitude": 3.3792, "accuracyRadiusKm": 10 } } } ``` This reduced shape can store and score a simple event, but it cannot submit structured sender and recipient profiles or attach instruments to each party. Migrate transaction integrations to `POST /api/v1/transactions` to receive complete relationship intelligence, directional screening, billing evidence and human-readable transaction titles. ## Errors | Status | Error | Meaning | |---|---|---| | `400` | `invalid_request` | Required fields are missing, money is not an exact decimal string, location is incomplete or the payload contains unsupported nested values. | | `402` | `insufficient_wallet` | The production credit balance cannot reserve the configured assessment components. | | `404` | `subject_not_found` or `party_not_found` | The supplied customer or linked entity does not exist in this organisation and environment. | | `409` | `idempotency_conflict` or `transaction_id_conflict` | A stable identifier was reused for different immutable data. | | `422` | `subject_type_not_supported` | The subject cannot be assessed by this activity contract. | | `503` | Service failure | Assessment did not complete. Follow normal retry guidance using the same identifiers. | ## Next steps - [Fraud rules](https://trust.myaza.co/documentation/transaction-rules/markdown): install or build controls for transactions, logins and other activity. - [Ongoing monitoring](https://trust.myaza.co/documentation/monitoring-policies/markdown): configure recurring customer review. - [Investigations and reporting](https://trust.myaza.co/documentation/investigations/markdown): investigate review and block decisions. - [Webhooks](https://trust.myaza.co/documentation/webhooks/markdown): receive lifecycle events without polling. --- # Fraud rule configuration Fraud rules decide how each [event](https://trust.myaza.co/documentation/monitoring-events/markdown) is scored and which decision it earns. Configure them under **Rules & Policies → Fraud rules**. Changes apply to events scored afterwards in that environment. Start in **Rule library** to install a reviewed starter control in one click. Open **Current rules** to tune decision thresholds, currency limits and built-in signals, or choose **Build custom rule** when your integration sends organisation-specific evidence. Library rules use the same versioned, testable rule engine as custom rules, so installation does not introduce a second decision path. Viewing the configuration is open to anyone with Risk Intelligence access; **editing requires the `monitoring:manage` permission**. Until you save once, an environment runs on the built-in defaults; saving creates an explicit configuration for that environment. **Reset to defaults** reverts the form to the platform defaults. ## How scoring works 1. Every rule inspects the event and produces a **signal** from 0 (no risk) to 1 (maximum). 2. Signals are combined by **weight** using a soft-OR: any one strong signal can push the score high, and additional signals raise it further with diminishing returns. 3. The result is a **composite score, 0–1** (surfaced as 0–100 on events). 4. The score is compared to two cutoffs to produce the **decision**. ``` ALLOW REVIEW BLOCK ├──────────────┼──────────────────┼──────────────────┤ 0 review threshold block threshold 1 ``` ## Decisions & limits The first tab, **Decisions & limits**, sets the cutoffs and money ceilings. ### Decision thresholds | Setting | Meaning | |---|---| | **Review threshold** | At or above this score, the event is marked `REVIEW` and an [alert](https://trust.myaza.co/documentation/investigations/markdown) is opened. | | **Block threshold** | At or above this score, the event is marked `BLOCK`. | The review threshold must be **less than or equal to** the block threshold. **Metered billing** (toggle) charges a small per-event fee for scoring in **production** only; sandbox and development are always free. ### Money limits Amount ceilings are held in a **base currency** (e.g. `USD`) with optional **per-currency overrides**. For each event, the engine picks the ceiling for that event's currency, falling back to the base currency when there's no override. | Limit | Feeds | |---|---| | **Single transaction** | The **Threshold** rule: the most one transaction can be before it flags. | | **Rolling-window total** | The **Velocity** rule: the most an entity can move in a currency within the velocity window. | Add a currency to give it its own limits; remove it to fall back to base. Some behavioral rules also take per-currency **minimum amounts** (New-beneficiary, Round-amount). ## Built-in rules Every rule carries a **weight** (0–1): how strongly it influences the composite score. `0` turns the rule off; `1` gives it full influence. The rules are grouped into three tabs. ### Core rules Threshold-based checks that work from `amount` / `currency` / `country` / `counterparty` alone. | Rule | Flags when… | Key config | |---|---|---| | **Threshold** | A single event's amount exceeds its currency's single-transaction limit. | weight | | **Velocity** | Too many events, or too much total value in one currency, across a rolling window. | window (hours), max count, weight | | **Pattern (structuring)** | A burst of same-currency events sit just under the single-transaction limit (3+ at 70–100% of the limit). | window (hours), weight | | **Geography** | The event's `country` is on your high-risk or blocked list. | high-risk[], blocked[], weight | | **Counterparty** | The `counterparty` is on your high-risk or blocked list (one entry per line, case-insensitive). | high-risk[], blocked[], weight | | **Baseline anomaly** | The amount deviates sharply from what's normal *for this entity* (EWMA per entity, cohort fallback during warm-up). | z-score threshold, weight | ### Flow & velocity Behavioral patterns across a sequence of events. These need specific `data` fields and stay dormant until you send them. | Rule | Flags when… | Needs | Key config | |---|---|---|---| | **Dormant-then-active** | An account silent for N days suddenly transacts. | — | dormant days, weight | | **Pass-through** | Money flows in then straight back out in matched amounts (layering). | `direction` | window (hours), match ratio, weight | | **Rapid movement** | Funds leave almost as fast as they arrive (opposite-direction flow within minutes). | `direction` | window (minutes), weight | | **Smurfing** | Many distinct senders funnel money into one account. | `direction`, `counterparty` | window (hours), min sources, weight | ### More signals | Rule | Flags when… | Needs | Key config | |---|---|---|---| | **New beneficiary** | A first, sizeable payment to a counterparty never paid before. | `counterparty` | window (hours), min amount, weight | | **Round amount** | Suspiciously round amounts, exact multiples of a unit at or above a minimum. | `amount` | unit, min amount, weight | | **Cross-border** | A transaction in a country different from the entity's nationality (elevated if that country is on your geo lists). | entity `nationality`, event `country` | weight | | **Cash-intensive** | The event moved cash (`channel` = `cash`). | `channel` | weight | | **Profile change** | The entity performs a transaction `type` it hasn't done before, once it has enough history. | `type` | min history, weight | > **Match your payload to your rules.** A behavioral rule with a weight but without the fields it needs never fires. The rule-coverage panel flags rules that are weighted but unconfigured. See the [event `data` payload](https://trust.myaza.co/documentation/monitoring-events/markdown#the-data-payload). ## Custom rules When the built-ins don't cover a scenario, author your own declaratively, with no code. Each custom rule has: | Field | Purpose | |---|---| | **Name** | Machine-readable id, e.g. `high_value_crypto`. Fires as `custom:` on alerts and SAR reports. | | **Description** | Human label shown on alerts, e.g. "Large crypto transfer". | | **Enabled** | On/off toggle. | | **Condition** | The match expression (below). | | **Score** | Risk contribution (0–1) when the condition matches. | | **Weight** | How strongly that contribution affects the composite score (0–1). | You can define up to **50** custom rules per environment. Before saving a change, choose **Test changes**. Myaza replays recent scored events as a read-only historical test and shows how many decisions would change. The test creates no alerts, investigations, customer updates or charges. ### Conditions A condition is a tree combining field predicates with `all` (AND), `any` (OR), and `not`. The dashboard offers a **Builder** for flat all/any predicates and an **Advanced (JSON)** mode for arbitrary nesting. **Fields** you can test: `amount`, `type`, `currency`, `country`, `counterparty`, `direction`, `channel`, `ip`, `entity.riskTier`, `entity.nationality`, and any custom `meta_data.*` path. **Operators:** `gt` (>), `gte` (≥), `lt` (<), `lte` (≤), `eq` (=), `neq` (≠), `in` (in list), `nin` (not in list), `contains`, `exists`. ```json { "all": [ { "field": "amount", "op": "gt", "value": 50000 }, { "field": "currency", "op": "in", "value": ["BTC", "ETH"] } ] } ``` The rule above (named `high_value_crypto`, score `0.8`, weight `0.6`) flags large crypto transfers and contributes to the same composite score as the built-ins. An empty condition never fires, so the editor warns you to add at least one predicate. ## Next steps - [Event monitoring](https://trust.myaza.co/documentation/monitoring-events/markdown): send events these rules score. - [Investigations and reporting](https://trust.myaza.co/documentation/investigations/markdown): investigate what the rules flag. --- # Import past transactions Use this when you already have transactions to assess. **Preview** validates a batch without creating transactions, screening wallets or charging you. **Apply** each valid row through the existing Transaction Monitoring endpoint. Crypto rows screen the counterparty wallet; fiat rows do not call the wallet provider. Use stable, unique `externalActivityId` and `externalTransactionId` values. Register each customer first, or use an existing `externalUserId`. The `occurredAt` value is the original transaction time. Here is one complete row: ```json {"externalActivityId":"past_001","subject":{"type":"individual","externalUserId":"customer_42"},"occurredAt":"2025-01-01T00:00:00Z","transaction":{"externalTransactionId":"transfer_001","assetClass":"fiat","direction":"outbound","amount":"125.00","currency":"USD","transactionType":"bank_transfer"}} ``` Send up to 100 such rows in `transactions` to the preview endpoint: ```bash curl -X POST "https://sandbox.trust.myaza.app/api/v1/transactions/imports/preview" \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"transactions":[{"externalActivityId":"past_001","subject":{"type":"individual","externalUserId":"customer_42"},"occurredAt":"2025-01-01T00:00:00Z","transaction":{"externalTransactionId":"transfer_001","assetClass":"fiat","direction":"outbound","amount":"125.00","currency":"USD","transactionType":"bank_transfer"}}]}' ``` The response includes `valid`, `count`, row-level field errors, `providerCalls: 0` and `billableUsage: 0`. The preview rejects invalid fields and duplicate IDs within the batch. It cannot prove that an ID is unused in Myaza, that a customer exists, or what a future live decision will be. When `valid` is true, submit each unchanged row to `POST /api/v1/transactions`, sequentially. Keep the same external IDs and payload for retries. A new assessment returns `201`; an exact replay returns `200`. Stop on `202`, `409`, network failure or another error, inspect the saved result and resume with the same IDs. A preview does not reserve IDs or prices. Use a Sandbox key to test the flow first. The server-side SDK source has `transactions.previewImport(rows)` and `transactions.create(row)`. The Trust SDK package is not published yet; use HTTP until publication. Myaza operators with a Core checkout can also preview and submit a JSON Lines file through the bundled tool: ```bash pnpm import:transactions --input past-transactions.jsonl pnpm import:transactions --input past-transactions.jsonl --apply \ --environment sandbox --api-url https://sandbox.trust.myaza.app ``` The operator tool accepts at most 1,000 rows and 5 MB per file. Use `--environment production` and a matching live key only when the batch is ready for live assessment. Apply is sequential and stops at the first failed or uncertain response. There are no automatic retries after an uncertain provider call. Normal transaction pricing applies to newly assessed live rows; an internal wallet screen is not a second standalone customer charge. Sandbox rows are not billable. Read each decision through [Transaction Monitoring](https://trust.myaza.co/documentation/monitoring-events/markdown). An imported `occurredAt` remains historical, but the risk decision and provider evidence are assessed **now**. Do not present that result as the risk state that existed on the original date. For a single explicit address check, use [On-Demand Wallet Screening](https://trust.myaza.co/documentation/wallet-screening/markdown). --- # Continuous monitoring Monitoring re-checks selected evidence on a schedule and records material risk changes. The capability appears where the work happens: | Task | Dashboard location | | --- | --- | | Enable, pause, resume, turn off, or change frequency for one customer | **Entities → Customer → Risk** | | See what changed across customers | **Risk Intelligence → Overview** | | Work an actionable change | **Review Queue** | | Install or edit policy defaults | **Rules & Policies → Ongoing monitoring** | The public API remains under `/api/v1/monitoring`. ## Policy defaults and entity overrides A policy defines the checks, default frequency, material-change threshold and escalation behaviour. An Entity can follow that default or use an explicit frequency override. Supported frequencies are `hourly`, `daily`, `weekly` and `monthly`. Every subscription response identifies: - `frequency.effective`, the schedule currently used; - `frequency.policyDefault`, the active policy default; - `frequency.override`, the Entity override or `null`; - `frequency.source`, either `policy_default` or `entity_override`. Changing a policy default updates Entities that follow the default. It does not silently replace an explicit Entity override. ## Entity scope and high-risk handling `config.scope.entityTypes` defines which Entity types may use a policy version. Values are lower case: `individual`, `business`, `wallet`, `bank_account`, `device`, `email`, `phone` and `ip`. Omit `scope` to include every supported type. Scope is stored with the immutable policy version. Start, bulk enrolment, policy change and re-enable requests reject an Entity outside that version's scope. Publishing also validates the scope against affected active subscriptions. Scope controls eligibility, not risk severity. Use `alertLevels`, normally `high` and `critical`, to select the risk levels that can open an alert. Use `escalation.investigation` to choose `none`, `manual` or `automatic` investigation handling. ## Enable monitoring `POST /api/v1/monitoring/subscriptions` ```json { "subject": { "type": "individual", "id": "ent_01" }, "policyId": "pol_01", "frequency": "daily" } ``` Omit `frequency` or send `null` to use the policy default. Send an `Idempotency-Key` header for every mutation. ## Change policy or frequency `PATCH /api/v1/monitoring/subscriptions/{subscriptionId}` ```json { "frequency": "hourly" } ``` Restore the policy default: ```json { "frequency": null } ``` The same endpoint accepts `policyId` when an Entity should move to another published policy. ## Pause, resume and Off Pause and Off are different states. | Action | Endpoint | Effect | | --- | --- | --- | | Pause | `POST /api/v1/monitoring/subscriptions/{id}/pause` | Keeps configuration and history. No future scheduled monitoring checks or charges run until resumed. | | Resume | `POST /api/v1/monitoring/subscriptions/{id}/resume` | Restarts a paused subscription using its current policy and frequency. | | Off | `POST /api/v1/monitoring/subscriptions/{id}/stop` | Permanently ends that subscription. History remains and no future checks or charges run. | An ended subscription is immutable. Re-enable monitoring with: `POST /api/v1/monitoring/subscriptions/{endedSubscriptionId}/enable` ```json { "policyId": "pol_01", "frequency": null } ``` The response contains the linked new active subscription. The ended record remains available for audit history. ## Retrieve state and history `GET /api/v1/monitoring/subscriptions/{subscriptionId}` returns the current state, effective frequency, recent runs, material deltas, alerts and billing evidence. Use the Entity view for day-to-day customer management. Use Risk Intelligence for material changes across the portfolio, and Review Queue when a change requires action. ## Billing behaviour Only completed billable checks consume usage. Paused and ended subscriptions create no future scheduled monitoring checks or charges. Historical usage remains unchanged. ## Webhooks Start with `monitoring.delta.detected` for material customer-risk changes. Add `monitoring.run.failed` when failed scheduled checks need an operational response. See [Continuous Monitoring webhooks](https://trust.myaza.co/documentation/webhook-continuous-monitoring/markdown) for the exact V2 payload. --- # Fraud rules Fraud rules evaluate one activity and return **Allow**, **Review** or **Block**. They can apply to transactions, logins, sign-ups, password resets, beneficiary changes, payouts and withdrawals. The same versioned rule library includes [Wallet Intelligence templates](https://trust.myaza.co/documentation/wallet-screening/markdown#wallet-rules). Wallet decisions retain the matched conditions and values, so later edits do not change the explanation of an earlier screen. Sandbox and production installations remain separate. For a custom wallet rule, **Copy to Production** creates a production draft from the selected Sandbox version. An authorised manager reviews and publishes that draft in Production; the copy alone never changes live decisions. [Ongoing monitoring](/developers/documentation/monitoring-policies) is separate. It decides who is checked again over time and when a material change should reach your team. ## Start with the rule library Open **Rules & Policies → Fraud rules → Rule library**. Search or filter, then select **Install**. Installation publishes version 1 for the current organisation and environment. Repeating the same installation does not create a duplicate. Disabling a rule keeps its decision history. ## Test before you publish Use **Test against last 30 days** before publishing a custom rule. The test does not change activities, create alerts or create charges. ## Publish with control Publishing creates an immutable version. Later edits create another version so an earlier decision always points to the exact rule that produced it. ## Decision settings Open **Settings** to configure review and block thresholds, money limits and advanced scoring. These settings combine matched rules into the final decision. They are not a second rule library. ## API response Read `assessment.summary` first: ```json { "assessment": { "summary": { "outcome": "review", "title": "Review required", "reason": "A high-risk transaction rule matched.", "score": 82, "riskLevel": "high", "matchedRules": 1, "nextAction": { "type": "review_activity", "label": "Review transaction" } } } } ``` Use `summary.outcome` to branch and `summary.nextAction` for the next workflow step. Detailed rules, screening, billing and Travel Rule evidence remain available when you need them. ## Webhook Subscribe to `fraud.transaction.assessed` or `fraud.activity.assessed`. Both return the same `data.summary` shape. Deduplicate by the webhook envelope `id`. Use your `externalActivityId` and `externalTransactionId` to correlate the result. Do not recalculate the decision from score or detailed evidence. A later `fraud.decision.changed` event carries a new summary when the effective decision changes. --- # Investigations and reporting This is where flagged activity becomes an owned, evidence-backed decision. [Events](https://trust.myaza.co/documentation/monitoring-events/markdown) that score high raise **alerts**; your team opens an **investigation**, assigns an owner, reviews the evidence and records an outcome. Where activity is reportable, the investigation can produce a **SAR** or **STR**. Every action is audited server-side. ## Alert or investigation? An **alert** is a signal that needs triage. It shows what happened, why Myaza raised it and the rules or screening evidence involved. An **investigation** is the casework record. Use it when the alert needs an accountable owner, more evidence, remediation, escalation or a formal outcome. This separation keeps the alert queue fast without losing the deeper audit trail required for material decisions. ## Alerts An **alert** is opened automatically when an event scores at or above your [review threshold](https://trust.myaza.co/documentation/monitoring-rules/markdown#decision-thresholds). Review alerts under **Compliance → Alerts** (`alerts:read`). | Field | Notes | |---|---| | **Decision** | The event's decision: typically `REVIEW` or `BLOCK`. | | **Risk score** | The score that triggered it (0–100). | | **Status** | `OPEN`, `RESOLVED`, or `DISMISSED`. | | **Reasons** | The rules that contributed, each with a score and a detail string (e.g. `amount 48000 > 10000`). | The list shows each alert's entity, decision, risk score, status, and how many investigations it belongs to. > Alerts appear here when monitoring flags an event for review or block. ## Opening and assigning an investigation Open an alert to review its decision evidence. If more work is needed, choose an investigator from your organisation and select **Open investigation**. You can also select multiple alerts from one entity and open a grouped investigation. All linked alerts must belong to the same entity so the investigation remains scoped to one customer. ## Investigation workspace The investigation workspace puts the decision controls first: 1. **State** shows where the investigation is in its lifecycle. 2. **Why** summarises the strongest reasons that raised the linked alerts. 3. **Evidence** keeps rule explanations, linked records and uploaded attachments together. 4. **Owner** assigns accountability to a member of your organisation. 5. **Next action** starts review, escalates, records an outcome, closes or reopens the investigation. | Attribute | Values | |---|---| | **Status** | `OPEN` → `IN_REVIEW` → `ESCALATED` or `RESOLVED` → `CLOSED`, with reopening when new evidence appears. | | **Outcome** | `CLEARED`, `CONFIRMED`, `ESCALATED` or `INCONCLUSIVE`, supported by an analyst rationale. | | **Assignee** | A team member, or unassigned. | | **Alerts** | The grouped alerts; add more of the entity's alerts at any time. | Resolving and closing are separate. **Resolve** records the evidence-backed outcome; **close** completes the operational work. The rationale, actor, timestamps and later reopening remain in the immutable timeline. > Open an investigation from the Alerts view when the evidence needs an owner or outcome. ## Server-side SDK The public SDK calls this resource `investigations`: ```js const investigation = await myaza.investigations.retrieve(investigationId); const evidence = await myaza.investigations.evidence.list(investigationId); const timeline = await myaza.investigations.timeline(investigationId); ``` The canonical HTTP paths are `/api/v1/investigations`. The deprecated `/api/v1/cases` paths and `myaza.cases` SDK resource remain available for existing integrations; new integrations should use `myaza.investigations` and the primary `Investigation` SDK types. ## SARs & STRs When an investigation is reportable, draft a regulatory filing from it. The existing permission key for these actions is `cases:manage`. | Attribute | Values | |---|---| | **Report type** | `SAR` (Suspicious Activity Report) or `STR` (Suspicious Transaction Report). | | **Status** | `DRAFT` → `FILED`, or `CANCELLED`. | | **Reference** | Your external filing reference (e.g. the regulator's number), set when filing. | The lifecycle: 1. **Draft**: choose `SAR` or `STR` and a reason. The report is generated server-side in **goAML** format. 2. **Review & export**: inspect the goAML payload on the SAR detail view and **export it as XML** for submission to your FIU/regulator. 3. **File**: record the external reference and file path; the SAR moves to `FILED`. 4. **Cancel**: a `DRAFT` can be cancelled if it's no longer needed. ## Audit Every investigation and SAR write (open, assign, add evidence, transition, resolve, close, reopen, draft, file, cancel) is recorded in your organisation's audit log (**Dashboard → Audit logs**), with the actor and timestamp. ## Related - [Event monitoring](https://trust.myaza.co/documentation/monitoring-events/markdown): how alerts get raised. - [Monitoring rules](https://trust.myaza.co/documentation/monitoring-rules/markdown): the thresholds that decide `REVIEW` vs `BLOCK`. - [Compliance overview](https://trust.myaza.co/documentation/compliance/markdown): the end-to-end lifecycle. --- # Decision effectiveness Risk scores are useful only when you can prove what happened afterwards. Myaza connects each fraud or transaction decision to a downstream outcome so your team can measure captured fraud, false positives, check quality and financial impact. This gives your organisation three concrete benefits: - **Protect more revenue:** identify the controls and check types associated with confirmed fraud, prevented loss and recovery. - **Reduce customer friction:** measure legitimate customers interrupted by review or block decisions. - **Improve safely:** review drift, thresholds and check reliability without silently changing a live policy. Open **Risk Intelligence → Effectiveness** to see the current state, why Myaza reached it and the next safe action. Metrics remain marked as **Not enough evidence** until enough confirmed outcomes exist. Myaza does not turn missing labels into a reassuring zero. ### Understand the four headline measures - **Fraud captured** is the share of confirmed fraud linked to an interrupted transaction. - **Precision** is the share of interrupted, labelled transactions that were confirmed as fraud. - **False positives** are legitimate customers interrupted by a review or block decision. - **Outcome coverage** is the share of decisions with a confirmed downstream result. Low coverage means the other measures are not yet trustworthy. ## Record an outcome Send the final known result for a transaction. Use the same idempotency key when retrying the same outcome. The path reference may be a Myaza activity ID, external activity ID, Myaza transaction ID or your external transaction ID. Include `externalUserId` as an ownership guard when you have it. Myaza rejects the request with `409 external_user_mismatch` if that customer does not own the resolved activity. ```bash curl --request POST \ --url https://trust.myaza.app/api/v1/transactions/{transactionId}/outcomes \ --header 'Authorization: Bearer sk_live_…' \ --header 'Idempotency-Key: outcome-2026-08-16-001' \ --header 'Content-Type: application/json' \ --data '{ "externalUserId": "customer-8421", "classification": "fraud", "source": "chargeback", "occurredAt": "2026-08-16T10:30:00.000Z", "reasonCode": "chargeback.confirmed", "currency": "USD", "lossAmount": "125.50000000", "recoveredAmount": "25.00000000" }' ``` Outcome records are append-only. Replaying an identical request is safe. Reusing the idempotency key with different data is rejected. Resolved investigations are recorded automatically as fraud, legitimate or inconclusive outcomes for every linked transaction decision. Filed SARs add inconclusive escalation evidence and never assert guilt. For chargebacks, call this endpoint from the final dispute webhook with the event ID as the idempotency key. ## Read effectiveness evidence ```bash curl --request GET \ --url 'https://trust.myaza.app/api/v1/risk-intelligence/effectiveness?from=2026-08-01T00:00:00.000Z&to=2026-09-01T00:00:00.000Z' \ --header 'Authorization: Bearer sk_live_…' ``` The response includes decision distribution, outcome coverage, fraud-capture rate, false-positive rate, precision, exact decimal financial impact, segment comparisons and per-rule effectiveness. Check `completeness.complete` before using a report for policy decisions. The dashboard lets authorised teams filter the evidence window, review recommendations, manage immutable system versions and run shadow experiments. ## Drift, check reliability and relationships Use the authorised Risk Intelligence endpoints to retrieve: - `GET /api/v1/risk-intelligence/drift` - `GET /api/v1/risk-intelligence/providers` - `GET /api/v1/risk-intelligence/threshold-recommendations` - `GET /api/v1/risk-intelligence/system-versions` - `GET /api/v1/risk-intelligence/experiments` - `GET /api/v1/risk-intelligence/network/{subjectKind}/{subjectId}` Network exploration is bounded by depth and node limits. Reliability comparisons show availability, latency, disagreement and adjudicated false positives without ranking internal sources from incomplete evidence. ## Safe optimisation Recommendations never change a live customer decision automatically. They require labelled evidence, review, backtesting and a separately published system or rule version. Shadow experiments use the same canonical decision context but cannot change live decisions, alerts or billing. Myaza refreshes fixed daily evidence windows on a schedule. Re-running the same window updates its canonical snapshot rather than creating duplicate drift evidence. The full improvement loop is **rule → safe historical test → governed publish → decision evidence → confirmed outcome → effectiveness review**. Read [Protect transactions with rules](/dashboard/developers/documentation/transaction-rules) to install controls and test changes before they affect customers. ## Webhooks Subscribe to the lifecycle events below to keep your project in sync: - `fraud.outcome.recorded` - `risk.drift.detected` - `risk.threshold.recommended` - `risk.system_version.changed` Each customer-facing event includes `externalUserId` when the activity is linked to one, together with the internal activity or transaction reference used by Myaza. --- # On-Demand Wallet Screening Check one wallet address before a transfer or during an investigation. Myaza returns its risk score, fund-flow exposure and rule decision. The check does not create a transaction. | What you need | Where to start | |---|---| | Check an address by hand | **Spot Checks → Wallet** | | Check an address from your backend | [Wallet Screening API](https://trust.myaza.co/documentation/api-wallet-screenings/markdown) | | Assess a customer payment | [Transaction Monitoring](https://trust.myaza.co/documentation/monitoring-events/markdown) | For a crypto transaction, **Myaza** screens the counterparty wallet through the same engine. Myaza does not send the transaction or its hash to the wallet provider. Keep the hash in your transaction record for audit. The [legacy transaction-reference route](https://trust.myaza.co/documentation/api-transaction-screenings/markdown) remains available for existing integrations; it uses the same Wallet Screening rate and screens the supplied wallet, not the hash. ## Try it in Sandbox 1. Create a Sandbox secret key under **Developers → API Keys**. 2. Open **Spot Checks → Wallet**, enter a valid address and choose a test scenario. Or use the [curl example](https://trust.myaza.co/documentation/api-wallet-screenings/markdown) with `sandboxScenario: "wallet.sanctions.direct"`. 3. Read the **risk**, **exposures**, **decision** and **rulesTriggered** fields. Direct sanctions exposure is blocked by the default rule; its `hops` value may be zero. 4. Save `screening.id` to retrieve the result or link a case. Sandbox scenarios run your published rules without calling the live provider or creating billable wallet usage. Your key selects the environment; the request has no `sandbox` switch. ## Understand the result The result shows what was screened, how risky it is, why, which rule fired and what Myaza decided. `allow`, `review` and `block` are the API decisions. A provider's display band `UNKNOWN` for scores 0–9 is different from an operationally unknown result, which requires review. Malformed network identifiers and locally invalid address formats return `422` before a provider call. Enter a network name or code. Myaza translates known aliases and forwards other well-formed identifiers to the configured provider. There is no fixed network allowlist; the provider decides whether it can screen the network and address. A successful Sandbox scenario does not confirm live coverage. The **Exposure map** groups reported source and destination exposures. It is not a verified path of on-chain transfers. Myaza stores the exact encrypted provider response for audit and can render a signed provider PDF for an eligible completed address screen. Neither raw provider data nor internal cost appears in the public response. ## Automatic checks and rules Under **Risk Intelligence → Rules & Policies → Fraud rules → Settings**, turn **Wallet verification & screening** on for new crypto transactions. Then review the wallet templates in **Rule library**. The switch controls automatic counterparty screening; it does not disable Spot Checks or the direct API. Seven safety templates are enabled by default. The 27 optional templates cover score thresholds, direct and nearby exposure, source and destination concentration, and transfer context. Duplicate a template to change its action or threshold. Each saved decision keeps the matched rule version and values. Test a rule in Sandbox, then copy it to a Production draft and publish it separately. See [Wallet Screening API](https://trust.myaza.co/documentation/api-wallet-screenings/markdown), [Transaction Monitoring](https://trust.myaza.co/documentation/monitoring-events/markdown), [past transaction import](https://trust.myaza.co/documentation/import-past-transactions/markdown) and [wallet webhooks](https://trust.myaza.co/documentation/webhook-wallet-intelligence/markdown). --- # Platform simulator The interactive platform simulator is part of the authenticated Myaza Trust documentation experience. It uses your current sandbox environment and the canonical Risk Intelligence, Monitoring, Transaction Intelligence, and Investigations workflows. [Open the platform simulator](/developers/simulator) Production execution is disabled. Scenarios that require controlled service fixtures remain clearly locked until those fixtures are configured. ## Test monitoring in context Create a Sandbox customer, then filter the catalogue to **Monitoring**. The lifecycle journeys cover: - immutable policy scope across all supported lower-case Entity types; - pause and resume without a scheduled check or charge while paused; - permanent Off with retained history; - an hourly Entity override against a daily policy default; - removing the override and returning to the policy default; - re-enabling an ended period as a distinct active subscription. Monitoring scenarios reuse stable Simulator policies by check set. Running the same journey again does not create another policy version or a new timestamped policy. Results link to the Entity for state and history, Risk Intelligence for material changes, Review Queue for action, and Rules & Policies for defaults. The public integration remains under `/api/v1/monitoring`. ## Set up Sandbox Travel Rule Travel Rule scenarios need a compatible published policy. If the Simulator shows **Setup required** or **Policy incompatible**, select **Set up Travel Rule**. The action opens [**Risk Intelligence → Rules & Policies → Fraud rules → Travel Rule**](/risk-intelligence/rules-policies/decisions?view=travel-rule&preset=sandbox-travel-rule-simulator) with the server-owned `sandbox-travel-rule-simulator` preset selected. 1. Select **Create Sandbox policy draft** or **Replace Sandbox policy draft**. This creates a new draft, or supersedes the current unpublished draft. Your published Sandbox policy stays unchanged until you publish the draft; publishing updates Sandbox transfer checks and enables the test scenarios. 2. Review the draft, then select **Publish draft** separately. 3. Return to the Simulator and refresh. After the Simulator refetches the published policy, the setup shows **Ready for Simulator** and the scenario shows **Runnable**. The explicit Simulator fixture is deterministic and has no charge in local development and Sandbox. Ordinary Sandbox requests without a scenario still use configured pricing, and Production behaviour is unchanged. Remove `sandboxScenario` from Production requests. See [Travel Rule for crypto transfers](https://trust.myaza.co/documentation/travel-rule/markdown) for policy design, API requests, remediation and webhook events. --- # Travel Rule for crypto transfers Myaza Travel Rule helps your organisation decide when a crypto transfer requires originator, beneficiary and counterparty information. It assesses that information with identity, sanctions, PEP, adverse-media and wallet evidence, then recommends whether the transfer can proceed, must be held for review or must be stopped. The operational result always answers three questions: 1. **What happened?** The assessment is not required, needs information, needs review, is blocked, can proceed or failed validation. 2. **Why?** Myaza records the matched policy rule, missing fields, identity evidence, VASP evidence and screening results. 3. **What next?** Supply missing information, open the exact investigation, retry a failed validation or take no action. ## Choose your next step | Task | Guide | |---|---| | Your company, institutions and application. | [Set up Travel Rule](https://trust.myaza.co/documentation/travel-rule-setup/markdown) | | Choose rules, information packages and publication. | [Transfer requirements](https://trust.myaza.co/documentation/travel-rule-policies/markdown) | | Send the information you already hold in one request. | [Create a transfer](https://trust.myaza.co/documentation/travel-rule-transactions/markdown) | | Recover incomplete transfers without creating duplicates. | [Supply missing information](https://trust.myaza.co/documentation/travel-rule-information/markdown) | | Secure messaging and the customer wallet's execution gate. | [Exchange and authorise](https://trust.myaza.co/documentation/travel-rule-execution/markdown) | | Review incoming information and admit a verified deposit. | [Receive a transfer](https://trust.myaza.co/documentation/travel-rule-incoming/markdown) | | Report wallet confirmation and track message delivery. | [Confirm an outgoing transfer](https://trust.myaza.co/documentation/travel-rule-confirmation/markdown) | | Events, current status and safe notification handling. | [Receive Travel Rule updates](https://trust.myaza.co/documentation/travel-rule-webhooks/markdown) | | Signed control proof and separate address review. | [Self-hosted wallets](https://trust.myaza.co/documentation/travel-rule-self-hosted/markdown) | | Exercise information gaps, review and stopped transfers. | [Test and troubleshoot](https://trust.myaza.co/documentation/travel-rule-testing/markdown) | ## What this feature includes When a policy permits identity alternatives, the missing-information form lets you choose one accepted option. For a government ID, enter its number, choose the actual ID type and provide its issuing country. Business-only rules can accept an LEI. Do not guess an identifier's type. Existing untyped records are not converted, and completing these fields does not verify the person's or company's identity. - A governed rule library and guided rule builder. - Organisation and environment-scoped policy versions. - Impact testing against recent crypto transactions before publication. - Draft-first publication with an immutable publisher record for every active version. - Immutable assessment generations and policy snapshots. - Plain-language API and webhook summaries with the complete evidence retained. - Idempotent information submission when a transfer is missing required data. - Deterministic, non-billable Sandbox scenarios through the API. Myaza combines information checks with a Crypto Provider Directory, address book and governed institutional connections. A directory entry does not prove wallet ownership or establish a messaging connection. Live exchange requires a reviewed connection with the receiving institution. Your organisation remains responsible for the jurisdictions, thresholds and information requirements applicable to its regulated activity. ## Integrate with an AI agent Use the [AI integration guide](https://trust.myaza.co/documentation/ai-integration/markdown) for the product index, downloadable skills and machine-readable API contracts. Start with the Travel Rule skill and fetch only the guides needed for your workflow. --- # Set up Travel Rule ## Get started in the dashboard 1. Open **Fraud Monitoring → Transactions → Travel Rule** and select **Set up Travel Rule**. 2. **Your company:** confirm the company you represent. Myaza reuses your company verification and reviews the directory match. 3. **Institutions:** choose the exchanges you send to or receive from. Review each company before approving information exchange. Save your selection. 4. **Your application:** follow the integration guide and configure your result webhook. You can do this while Myaza prepares the connections. 5. **Test and activate:** Myaza checks the institution connection automatically; you can also rerun its test. Send a test transaction and enter its external activity reference. Complete the displayed checks and request activation. Setup is saved for your organisation and environment. Close it and return later to continue. Myaza manages certificate issuance and renewal in the background; customers do not upload certificates, edit server configuration or restart anything. Direct TRP connections use managed certificates; CodeVASP connections use the approved institution registration. CodeVASP-only setup does not require a TRP certificate. An independent Myaza reviewer confirms company representation and activation evidence. Activation checks institution communication, configured notifications, the received test and country policy reviews. The reviewer also checks evidence of real result delivery/recovery and the organisation's wallet enforcement. A configured webhook is not proof that your application verified its signature or acted on a decision. **Active** means this organisation's setup is activated in the selected environment. Every transfer still needs its own current clearance. **Pause Travel Rule** stops new exchange and execution; it does not reverse a transfer that already completed. ### Connect a crypto company If your company is missing from the directory, a Myaza platform administrator can add it through **Crypto providers → Add crypto company**, with its registration evidence. Myaza establishes the partner arrangement once, then provisions customer connections covered by that arrangement automatically. **Awaiting institution** means Myaza is still arranging that channel. Selecting a name cannot make another company trust it. Once the connection appears in **Set up Travel Rule**, use **Test connection** to check it or **Pause** to stop new exchanges. **Messaging setup needed** means identity testing passed but the exchange configuration is incomplete. Open the adjacent information button for the next action. A verified connection is not an approval for any transfer. Technical diagnostics and previous configuration remain under **Existing connections and diagnostics**. Keep Sandbox and Live separate. ### Add another destination Open **Institutions**, select the company and save. Existing active connections keep working. Myaza reuses the platform's approved partner route and provisions it for your organisation without a restart or another company activation review. **Connection requested** means the destination is still being arranged; **Connected** means its current connection checks passed. Each transfer still has its own clearance checks. Your team's institution approval and the destination's secure communication and country reviews remain required. An unavailable destination does not disable other destinations. Removing an institution stops new exchanges with that institution only. Company identity changes, expired company approval or pausing the whole service still require setup attention. ## A practical example You operate **Exchange A**. Alice is your customer and wants to send USDT to Bob at **Exchange B**. 1. Your backend sends Myaza one transaction containing Alice's and Bob's available information, the amount and the destination institution. 2. Myaza uses the approved connection to Exchange B to exchange the required information. You do not build another integration for every transfer or handle certificates. 3. Myaza reports what is missing, needs review or is ready for the next step. Your backend retrieves the current clearance, consumes its short-lived authorisation once, and instructs **your own wallet** to send. For a held transfer it does not send. 4. If Bob's required details were missing, your backend submits only the additional information against the same transaction. If the first request was complete, that follow-up request is unnecessary. Adding Exchange B to a directory is like adding a business contact. Establishing its connection is like verifying that you have the right secure delivery address. A contact record alone cannot receive a message. The connection is reused, not rebuilt per user. ## Your company and your counterparties **Your company** identifies the organisation you represent. **Add crypto company** adds a directory record when an institution is missing; it does not connect an application or enable secure messaging. **Connections** are authenticated channels between institutions. Before exchanging regulated information, Myaza needs a reviewed channel to the institution involved. A saved directory entry is not enough. You do not need to configure a connection for each transfer or each individual customer. Reuse the institution connection within the correct organisation and environment. Your application connects to **Myaza's API and webhooks**. The institution connection is a different path: Myaza exchanges the required information with the other institution. Your wallet still enforces the current decision. ### What if you only have a wallet address? A blockchain wallet address does not tell Myaza the owner's name, their exchange or how to contact them. Myaza does not guess ownership or send personal information to an endpoint discovered from an unverified address. Ask your sender for the recipient's exchange and available recipient details. The recipient can obtain a **Travel Address** from a supporting exchange and share it with the sender. This is a secure information-exchange address, not a crypto wallet address. With a Travel Address, Myaza can select an unambiguous existing approved route automatically. The receiving exchange privately resolves its own customer. If information is missing, Myaza returns the required fields to **your application**. Your application asks your sender to supply or obtain them. Myaza cannot independently contact an unknown wallet owner. Details must still be checked; supplying a name is not proof that the person controls the wallet. | Recipient destination | What happens | | --- | --- | | Connected institution | Exchange through its authenticated route, whether or not it is a Myaza customer. | | Institution without a working route | Request the destination in setup. Keep the transfer held while Myaza arranges a compatible, approved channel. | | Confirmed self-hosted wallet | Follow the separate self-hosted wallet flow and required control checks. Do not call an unknown wallet self-hosted just to continue. | | Unknown owner or institution | Obtain more information; do not guess a route or release a held transfer. | Myaza is not a closed customer-only network. External institutions can communicate through an approved CodeVASP route or compatible TRP connection without purchasing Myaza. A supported protocol does not mean every exchange already has a working connection. For a new transfer, choose the receiving institution. Myaza offers one ready route per institution; where both are available, the managed CodeVASP route is preferred. A prepared or uncertain message keeps its original route. Do not create another message or switch protocols to work around a lost response. --- # Transfer requirements ## Create a Travel Rule rule Open **Risk Intelligence → Rules & Policies → Fraud rules → Travel Rule**. 1. Select **Rule library** and compare the reviewed starting points in the table. Select **Install** beside one to open a prefilled draft, or select **Create rule** to start from a blank scope. 2. Name the rule and choose its transfer direction, counterparty type, jurisdictions, assets and networks. 3. Choose the standard, enhanced or **Supporting detail with alternatives** package. 4. Add an optional amount threshold only when your governing policy requires one. 5. Select **Test impact**. This previews the complete draft against recent crypto transfers without changing transactions, creating alerts or charging customers. 6. Select **Save draft**. 7. Select **Publish draft** when you are ready to make the saved policy version active. The active policy can contain several ordered rules. The first rule matching a transfer determines the information requirements. A transfer outside every explicit scope remains unknown and is held rather than treated as exempt. ### Let customers provide a permitted alternative **Supporting detail with alternatives** collects the core party information plus one supporting sender detail: a customer reference or a structured address. Choose **Individuals** for **Sender type** under **Advanced transfer filters** to also allow date and place of birth together. Business rules do not offer birth details. Review the chosen package against your country requirements before publication; these are configurable building blocks, not automatic country approval. On a transaction with missing information, select **Add missing information**. When the policy permits alternatives, choose the detail you can provide and fill the displayed fields. Myaza reassesses the transaction and retains the previous attempt for audit. A partial address correction preserves the address fields already supplied. Hidden alternatives are not submitted by the form. For API clients, `fields.baseRequired` lists always-required paths, while `fields.alternatives` lists groups of fully qualified paths. Supply **every** field in **one** group for each requirement. Send nested address fields as an object: ```json { "originator": { "residentialAddress": { "city": "Lagos", "country": "NG" } } } ``` The example completes a previously supplied street address; it is not a complete address by itself. Post corrections to `/api/v1/transactions/{id}/travel-rule/information` using the same idempotency key and body on retries. Missing/unknown evidence still holds the transfer, and the organisation's wallet must use the separate execution authorisation flow. ## Publish a draft Saving a rule never activates it. Myaza stores the complete configuration as a new draft version so the active policy and historical assessments remain unchanged. Any authorised Travel Rule manager can publish the draft, including the person who created it. Publication records the publisher and activation time, then freezes that policy version so historical decisions remain reproducible. ## Information packages **Standard party information** collects legal names and account identifiers for the originator and beneficiary. VASP transfers also collect the counterparty VASP legal name and jurisdiction. **Enhanced party and VASP information** additionally collects party residential addresses and countries of residence, plus VASP registration and licence identifiers. These packages are product starting points, not a legal determination. Review and adjust the saved draft for the laws, corridors and services applicable to your organisation. --- # Create a transfer ## Submit a crypto transaction Use a secret API key and a unique idempotency key. Set `MYAZA_API_BASE_URL` to the versioned API base, such as `http://localhost:32101/api/v1` locally: ```bash curl -X POST "$MYAZA_API_BASE_URL/transactions" \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" \ -H "Idempotency-Key: crypto-transfer-1042-v1" \ -H "Content-Type: application/json" \ -d '{ "externalActivityId": "activity-1042", "subject": { "type": "individual", "externalUserId": "customer-42" }, "occurredAt": "2026-08-25T20:00:00.000Z", "transaction": { "externalTransactionId": "transfer-1042", "assetClass": "crypto", "direction": "outbound", "amount": "2500", "currency": "USDT", "transactionType": "crypto_transfer", "blockchain": "TRON", "travelRule": { "sourceJurisdiction": "NG", "destinationJurisdiction": "GB", "counterpartyType": "vasp", "walletType": "hosted", "originator": { "partyType": "individual", "data": { "legalName": "Originator name", "accountIdentifier": "customer-account-reference" } }, "beneficiary": { "partyType": "individual", "data": { "legalName": "Beneficiary name", "accountIdentifier": "beneficiary-account-reference" } }, "counterpartyVasp": { "partyType": "business", "data": { "legalName": "Counterparty VASP", "jurisdiction": "GB" } } } } }' ``` Do not send regulated party information in generic metadata. Use the dedicated `travelRule` party fields so Myaza can minimise, encrypt, retain and purge the evidence under the Travel Rule privacy controls. ### Node.js SDK The same request is available from the server-side Trust SDK: ```js const transaction = await myaza.transactions.create({ externalActivityId: 'activity-1042', subject: { type: 'individual', externalUserId: 'customer-42' }, occurredAt: '2026-08-25T20:00:00.000Z', transaction: { externalTransactionId: 'transfer-1042', assetClass: 'crypto', direction: 'outbound', amount: '2500', currency: 'USDT', transactionType: 'crypto_transfer', blockchain: 'TRON', travelRule: { sourceJurisdiction: 'NG', destinationJurisdiction: 'GB', counterpartyType: 'vasp', walletType: 'hosted', originator: { partyType: 'individual', data: { legalName: 'Originator name', accountIdentifier: 'customer-account-reference' }, }, beneficiary: { partyType: 'individual', data: { legalName: 'Beneficiary name', accountIdentifier: 'beneficiary-account-reference' } }, counterpartyVasp: { partyType: 'business', data: { legalName: 'Counterparty VASP', jurisdiction: 'GB' }, }, }, }, }, { idempotencyKey: 'crypto-transfer-1042-v1' }); if (transaction.travelRule) { console.log(transaction.travelRule.summary.title, transaction.travelRule.summary.nextAction); } ``` ## What happens next? This example includes both parties for the standard information package. Your published policy can require additional fields or evidence. Read the returned summary, then follow its next action. You do **not** need to submit the same information again. Use the returned `activityId` for later requests, not a guessed identifier. A read retrieves the current assessment; it does not create another transaction. If information is missing, use [Supply missing information](https://trust.myaza.co/documentation/travel-rule-information/markdown). For institutional messaging and the wallet gate, continue to [Exchange and authorise](https://trust.myaza.co/documentation/travel-rule-execution/markdown). These are ordinary API requests and use your configured pricing. For labelled, non-billable scenario traffic, use [Test in Sandbox](https://trust.myaza.co/documentation/travel-rule-testing/markdown). --- # Supply missing information The first transaction request can contain all known party information. A second information submission is needed **only** when fields are missing or need correction. In the missing-beneficiary Sandbox scenario, the sender is present and the recipient is deliberately empty. ## Read the result ```bash curl "$MYAZA_API_BASE_URL/transactions/activity-1042/travel-rule" \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" ``` Read `assessment.summary` first: ```json { "outcome": "information_required", "title": "Beneficiary information needed", "description": "Add the beneficiary legal name and beneficiary account identifier before this transfer can be reviewed again.", "transferStatus": "hold", "missingInformation": [ { "key": "beneficiary.legalName", "party": "beneficiary", "field": "legalName", "label": "Beneficiary legal name" } ], "checks": { "passed": 4, "needsReview": 0, "failed": 0, "pending": 0 }, "nextAction": { "type": "submit_information", "label": "Add missing information" } } ``` The rest of the response contains the exact policy version, required and received fields, issues, party references, VASP verification provenance and screening attempt references. Keep this evidence available for investigation, but use the summary for normal operational routing. ## Supply missing information ```bash curl -X POST "$MYAZA_API_BASE_URL/transactions/activity-1042/travel-rule/information" \ -H "Authorization: Bearer $MYAZA_SECRET_KEY" \ -H "Idempotency-Key: activity-1042-beneficiary-v1" \ -H "Content-Type: application/json" \ -d '{ "beneficiary": { "legalName": "Beneficiary name", "accountIdentifier": "beneficiary-account-reference" } }' ``` The response returns the updated assessment. An exact retry with the same idempotency key returns the same generation and does not create a second charge. Different information requires a new idempotency key and creates a new immutable validation generation. Completing the named fields resolves data completeness only. Always route the updated `assessment.summary`: identity, VASP or wallet evidence can still require review or block the transfer even when `fields.missing` is empty. ## Node.js correction flow After a webhook or an interrupted workflow, read the current result. Only submit the missing beneficiary when requested; do not create another transaction: ```js const current = await myaza.transactions.travelRule.retrieve(transaction.activityId); if (current.summary.nextAction.type === 'submit_information') { const result = await myaza.transactions.travelRule.submitInformation( transaction.activityId, { beneficiary: { legalName: 'Beneficiary name', accountIdentifier: 'beneficiary-account-reference' } }, { idempotencyKey: 'activity-1042-beneficiary-v1' }, ); console.log(result.assessment.summary); } ``` --- # Exchange and authorise ## Exchange information and authorise execution An assessment result of **Allow** is not permission to move funds. Keep your payment job on hold until its current requirements and the separate execution check pass. The execution path covers outgoing and incoming institutional transfers. [Self-hosted wallets](https://trust.myaza.co/documentation/travel-rule-self-hosted/markdown) use a separate signed-wallet path. Below-threshold authorisation requires an explicit qualified policy, not just a low amount. 1. Complete the information review and the independent country reviews for the exact active policy version. Review all source, destination and organisation countries. 2. Create or refresh clearance with `POST /transactions/{id}/travel-rule/transfer`. 3. Prepare the message with `POST /transactions/{id}/travel-rule/exchanges`, using the current `expectedRevision` and the receiving institution's `travelAddress`. Myaza selects the matching existing approved route automatically. `connectionId` remains supported and is required if multiple scoped routes match that origin. An ordinary wallet address cannot select a route. Use a UUID v4 `Idempotency-Key`. 4. Send once with `POST /transactions/{id}/travel-rule/exchanges/{messageId}/send`. Wait for an authenticated acceptance. An uncertain or conflicting reply stays held. 5. From your backend, request and consume an execution authorisation as shown below. Your original transaction must identify exactly one `recipient` crypto-wallet instrument, with its real `identifier` and `network`. An opaque wallet reference, masked address or customer-supplied verified flag is not sufficient. The assessed address and institution-approved address must both match `recipientAddress` exactly. The amount, asset and network come from the immutable assessed transaction. ### Request a short-lived authorisation ```http POST /api/v1/transactions/{id}/travel-rule/execution/authorisations Authorization: Bearer Idempotency-Key: Content-Type: application/json { "expectedRevision": 2, "recipientAddress": "" } ``` The response contains `authorisation.id`, `authorisation.token` and `authorisation.expiresAt`. The signed token expires within 60 seconds. Keep it on your backend and out of logs, analytics, URLs and browser storage. Do not treat a valid signature or successful issuance as permission to send funds. ### Consume once, immediately before execution Your organisation's wallet executes the transfer; Myaza supplies the compliance authorisation. Integrate this gate into your backend before the wallet sends funds. You do not need to give Myaza custody of your funds. Your system owns submission, duplicate prevention, transaction confirmation and reconciliation. A webhook or an assessment's Allow is not a payment instruction: check current authorisation. Persist a unique UUID `externalExecutionId` in your own durable payment job before this request. Use the same secret API key that requested the authorisation. ```http POST /api/v1/transactions/{id}/travel-rule/execution/consume Authorization: Bearer Content-Type: application/json { "expectedRevision": 2, "recipientAddress": "", "token": "", "externalExecutionId": "" } ``` Myaza checks the current policy, country reviews, fraud decision, recipient, exchange evidence, connection, network catalogue and retention approval again. A changed or expired prerequisite holds the transfer even when the token has not expired. - `action: "execute_once"`: execute that exact durable job once. Your payment or custody integration must enforce the same job identifier against duplicate sends. - `action: "reconcile_execution"`: this is a replay, not another permission. Look up the existing job and reconcile it; never create a new payment. - Any error, missing response or ambiguous result: keep the job held. Read `GET /transactions/{id}/travel-rule/execution` to reconcile the recorded receipt. This read remains available after token expiry; it does not grant new permission. A transfer can have only one consumption receipt, across every token and revision. The receipt's `settlementStatus: "UNKNOWN"` is intentional: authorisation is not evidence of a broadcast or settlement. Myaza does not move funds. If the execution result is lost or uncertain, resolve it with your wallet system before taking any further action. Do not automatically retry a funds-moving operation with a new ID. --- # Receive a transfer ## Receive information for an incoming transfer First record the incoming transfer through the transaction API with the exact customer, recipient wallet, network, asset, amount and available party details. Keep its current transfer evidence unexpired. Myaza matches one recorded intent; a saved external address or shared wallet alone cannot identify the receiving customer. Your organisation's deposit ledger remains authoritative. ### CodeVASP For an approved CodeVASP destination, Myaza manages the receiving endpoint. Do not generate a TRP receiving address. The gateway compares the sender's selected claims with the recorded incoming transfer and checks the current policy and reviews. It saves the encrypted response before replying; exact retries reuse that response. Rejected requests do not occupy the transfer's incoming-message slot. The transaction page shows received information and the recorded response separately. **Response recorded** does not prove that the sender received it. Use **Compare information** for an audited view; do not send a TRP approval for a CodeVASP inquiry. Wait for the authenticated sender confirmation and your own wallet's chain checks before the incoming authorisation step below. Unknown or ambiguous recipients remain held. ### Direct TRP Open the incoming crypto transaction and find **Receive required information**. Choose **Get receiving address**, select a verified messaging connection and give the address to the sending institution. It is an information address, not a wallet address. It is limited to that institution and the current transfer revision. When information arrives, the page shows **Information received** and your webhook receives `travel_rule.exchange.updated` with `change: inquiry_received`. The transfer stays on hold. Receiving a message does not confirm the information is complete, authorise a deposit, or prove that funds arrived. Choose **Compare information** to see the sender's supplied values alongside your current record. Only fields required by the policy are shown, and access is recorded in the audit trail. Differences, missing values and formats needing specialist review are explicit. A match does not verify an identity, update your records or approve the transfer. Read-only team members do not have access to this sensitive comparison. To decline, choose **Decline request**, then **Prepare decline**. Review the transfer and select **Send decline response** when ready. The local decision and delivery status are shown separately. If a response is lost, use **Check delivery** or refresh; do not start a new exchange to work around uncertainty. To approve matching information, choose **Prepare approval** in the comparison. The server checks the current decisions, country reviews, recorded recipient wallet and network again. You cannot override the wallet or enter a callback URL here. Then choose **Send approval response**. Preparation and delivery are separate, so an interrupted response can be retried using the same operation without another approval. For backend integrations: | Action | Endpoint under `/api/v1` | Body | |---|---|---| | Get receiving address | `POST /transactions/{id}/travel-rule/exchanges/invitations` | `{ "connectionId": "reviewed-sender", "expectedRevision": 1 }` | | Prepare decline | `POST /transactions/{id}/travel-rule/exchanges/{incomingMessageId}/rejection` | `{ "expectedRevision": 1 }` | | Prepare approval | `POST /transactions/{id}/travel-rule/exchanges/{incomingMessageId}/approval` | `{ "expectedRevision": 1 }` | | Compare selected information | `POST /transactions/{id}/travel-rule/exchanges/{incomingMessageId}/review` | `{ "expectedRevision": 1 }` | | Send prepared response | `POST /transactions/{id}/travel-rule/exchanges/{responseMessageId}/send` | `{}` | Use your secret API key. Receiving-address and approval/decline preparation require their own UUIDv4 `Idempotency-Key`; preserve the exact key and body across retries. Read the current transfer for the correct revision and message IDs. Only a configured, independently verified connection can submit a message to the native mutual-TLS receiver. No customer API accepts an arbitrary peer callback or raw approval. The approved sender can confirm its transaction through the authenticated receiving connection. `exchangeProgress.confirmation` distinguishes `CONFIRMED`, `CANCELLED` and `CONFLICT`, with `settlementStatus: "UNKNOWN"`. This is the sender's report, not proof that a deposit arrived. Use the separate incoming authorisation gate in this guide after your wallet independently verifies the deposit. Never credit or release funds from message status alone. ## Authorise an incoming deposit After the information review and approval, wait for the authenticated sender confirmation. Your wallet must independently verify its transaction identifier, amount, asset, network, recipient, finality and whether it was already credited. The sender's report alone is not chain evidence. Use the [issue and consume endpoints](https://trust.myaza.co/documentation/travel-rule-execution/markdown). Include `incomingTxid` in **both** requests, with the exact transaction identifier your wallet checked. It must match the retained sender confirmation for this transfer revision and approval. Keep the same `recipientAddress` and persist one UUID `externalExecutionId` for the credit job. ```json { "expectedRevision": 3, "recipientAddress": "", "incomingTxid": "" } ``` Consumption also includes `token` and `externalExecutionId`, as in the execution guide. An incoming receipt has `purpose: "INBOUND_CREDIT"`; outgoing receipts have `purpose: "OUTBOUND_TRANSFER"`. Only a fresh `execute_once` result can admit the bound job. Your wallet must enforce duplicate prevention for credits as well as sends. On a replay or missing response, reconcile the existing job instead of crediting again. Myaza does not credit balances, and settlement remains `UNKNOWN` in its receipt. Cancellation, conflicting sender reports, stale policy or country reviews, paused connections and changed recipient evidence block consumption. A message acknowledgement without a sender confirmation is insufficient. Do not send `incomingTxid` for an outgoing transfer or use the outbound confirmation endpoint for an incoming job. --- # Confirm an outgoing transfer ## Tell the receiving institution when your wallet confirms an outgoing transaction After your wallet confirms the **same authorised transaction**, report its identifier from your backend using the original secret API key. Do not send this for a merely broadcast transaction, an uncertain payment, or a different payment job. ```http POST /api/v1/transactions/{id}/travel-rule/execution/confirmation Authorization: Bearer Content-Type: application/json { "expectedRevision": 2, "externalExecutionId": "", "txid": "", "confirmedAt": "2026-09-06T12:00:00.000Z" } ``` Use the original consumed revision. `confirmedAt` must be at or after consumption and not in the future. `txid` is an asset-specific identifier (letters, digits, underscores or hyphens; at most 256 characters), not an explorer URL. Your system is responsible for checking that it identifies the exact authorised transaction. The response returns `confirmation.messageId`, `confirmedAt`, `source: "CUSTOMER"` and the message delivery `status`. Persist the exact request in your durable job: repeating it returns the same message; changing its transaction ID or timestamp conflicts. There is only one confirmation per consumed execution, regardless of HTTP retries. No extra execution is authorised. Send the prepared message through the existing endpoint: ```http POST /api/v1/transactions/{id}/travel-rule/exchanges/{confirmation.messageId}/send Authorization: Bearer Content-Type: application/json {} ``` Myaza uses the institution's authenticated callback from the approval that preceded execution. Your request cannot choose another URL, institution, amount or asset. The transaction identifier is encrypted in the exchange journal; audit entries and webhooks contain metadata only. Peer and retention checks still apply, even when the original clearance has expired. `ACKNOWLEDGED` means the institution received the confirmation message, not that Myaza independently verified settlement. `UNCERTAIN` means delivery needs reconciliation; sending again does not repeat the network POST. Subscribe to `travel_rule.exchange.updated` and read the execution or transfer endpoint for the latest status. The dashboard separately shows **Reported by your system** and **Transfer confirmation · Message received**. Settlement remains `UNKNOWN` until independent evidence is available. The operator must provision execution signing configuration, reviewed institution connections and country-specific requirements before enabling live use. Product templates and local tests do not qualify a country or prove live interoperability. --- # Receive Travel Rule updates ## Webhook events Subscribe to the Travel Rule events in **Developers → Webhooks**: | Event | Meaning | |---|---| | `travel_rule.required` | The active policy requires Travel Rule information. | | `travel_rule.data.pending` | Required information is still missing. | | `travel_rule.assessment.completed` | The current validation generation completed. | | `travel_rule.data.completed` | The required information package is complete. | | `travel_rule.mismatch.detected` | Identity, sanctions, VASP or wallet evidence needs action. | | `travel_rule.failed` | Validation failed because of a service problem. | | `travel_rule.exchange.updated` | Preparation, delivery or an authenticated institutional reply changed. Read current clearance; this event does not grant execution permission. | Every event includes the assessment ID, validation generation, status, field coverage, exact API paths and the same plain-language `summary`. Webhooks never contain the regulated party values submitted to Myaza. Use the shared [webhook authentication](https://trust.myaza.co/documentation/webhook-authentication/markdown) and [testing](https://trust.myaza.co/documentation/webhook-testing/markdown) guides. Verify the signature against raw bytes, reject stale signatures and deduplicate the event ID before applying an action. Delivery can be repeated or arrive after a newer revision. Fetch the current transfer before deciding what to do; a historical event never grants fresh wallet authority. --- # Self-hosted wallets ## Self-hosted wallets In the Travel Rule rule library, install **Unhosted wallet transfers** and turn on **Signed-wallet authorisation** in the draft. This applies to all amounts in that rule and requires an approved address plus a signed wallet proof from the last 24 hours. Publish the draft separately. The exact policy still needs the relevant country reviews before execution is available. Use the existing Address Book APIs to add the wallet, request its signed-message challenge, verify the signature and approve the address record. Both transfer parties must have current Myaza-verified customer records; submit their names and customer identifiers with the transaction. Supported Ethereum-compatible personal wallets use this path; contract wallets and other signature formats remain held. Add `selfHostedAddressId` to both [execution requests](https://trust.myaza.co/documentation/travel-rule-execution/markdown): ```json { "expectedRevision": 1, "recipientAddress": "0x1111111111111111111111111111111111111111", "selfHostedAddressId": "your-approved-address-record" } ``` For outgoing transfers, that record identifies the recipient wallet. For incoming transfers, it identifies the sender wallet; also include the same `incomingTxid` in both requests. Add the issued `token` and your stable `externalExecutionId` when consuming. Execute only on `execute_once`; reconcile the existing job on retries. Your wallet verifies the chain transaction and finality before crediting a deposit. No information exchange with an institution is fabricated for a self-hosted wallet. --- # Test and troubleshoot ## Test in Sandbox Use a Sandbox secret key and set `sandboxScenario` on a transaction request. Start with these scenarios: - **Travel Rule not required** for an explicit below-threshold result. - **Missing beneficiary information** to test the information collection loop. - **Travel Rule potential sanctions** to test a review outcome and specialist investigation. - **Travel Rule confirmed sanctions** to test a stopped transfer. After the request completes, open the transaction, inspect the summary, submit missing information when requested and verify the matching webhook deliveries. Scenario traffic is labelled and non-billable, but it still uses the governed Travel Rule assessment path. Travel Rule scenarios require a compatible published policy. Create a Sandbox policy draft from the simulator preset under **Risk Intelligence → Rules & Policies → Travel Rule → Transfer rules**, then publish it before testing. Publishing that draft also updates Sandbox transfer checks. For an API-driven deterministic test, set the top-level `sandboxScenario` to `TRAVEL_RULE_MISSING_BENEFICIARY`, only in this recovery test. The scenario is included in the request hash, follows the real policy and evidence path, and is non-billable. It is accepted only in Sandbox and development. Production returns `422 sandbox_scenario_not_available_in_production`; remove the field for live traffic. Ordinary Sandbox requests without a scenario use configured pricing and are billable. ## Acceptance checklist - Complete data: one transaction, no duplicate information submission. - Missing data: collect only the named fields and retain the original transaction. - Response loss: reuse the exact request and idempotency key; do not charge or execute twice. - Information accepted: still check current clearance and wallet authorisation. - Invalid or expired webhook signature: reject it without changing customer access. - Review or block: enforce the outcome in your application and make the reason visible to authorised staff. - Live activation: test an actual institution channel and your wallet enforcement separately from synthetic scenarios. --- # Webhook authentication Verify every delivery before parsing or acting on its JSON. Myaza signs the exact request bytes with the secret assigned to that endpoint. ## Signed headers | Header | Example | Required handling | |---|---|---| | `X-Myaza-Signature-V2` | `t=1786441200,v1=8df1…` | Parse the Unix timestamp and every `v1` candidate. Multiple `v1` values are present during secret rotation. | | `X-Myaza-Timestamp` | `1786441200` | Same signed timestamp, exposed separately for diagnostics. | | `X-Myaza-Replay-Window` | `300` | Endpoint replay window in seconds. Reject signatures outside it. | | `X-Myaza-Event` | `risk.signal.created` | Event type for routing. The signed JSON remains authoritative. | | `X-Myaza-Event-Id` | `evt_…` | Stable business-deduplication key. | | `X-Myaza-Delivery` | `del_…` | Endpoint delivery record ID. | | `X-Myaza-Correlation-Id` | `corr_…` | Optional end-to-end correlation value when the producer supplied one. | | `X-Myaza-Webhook-Version` | `v2` | Endpoint body contract. | | `X-Myaza-Signature` | `sha256=…` | Exact-body compatibility signature. New integrations should verify V2. | ## Verification algorithm 1. Read the raw HTTP request body as bytes. 2. Parse `t` and all `v1` values from `X-Myaza-Signature-V2`. 3. Reject a missing, invalid, too-old or implausibly future timestamp. 4. Compute lowercase hex `HMAC-SHA256(".", endpointSecret)`. 5. Compare the expected value with every supplied `v1` using a constant-time comparison. 6. Only after a match, parse the JSON, deduplicate on `id`, enqueue it durably and return `2xx`. Do not re-serialise parsed JSON before verification. Whitespace and property order change the signed bytes. ## Node.js example ```js import crypto from "node:crypto"; import express from "express"; const app = express(); app.post("/webhooks/myaza", express.raw({ type: "application/json" }), (req, res) => { const parts = String(req.header("X-Myaza-Signature-V2") || "").split(","); const timestamp = parts.find((part) => part.startsWith("t="))?.slice(2); const candidates = parts .filter((part) => part.startsWith("v1=")) .map((part) => part.slice(3)); if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) { return res.status(401).send("Expired signature"); } const expected = crypto .createHmac("sha256", process.env.MYAZA_WEBHOOK_SECRET) .update(`${timestamp}.`) .update(req.body) .digest("hex"); const valid = candidates.some((candidate) => candidate.length === expected.length && crypto.timingSafeEqual(Buffer.from(candidate), Buffer.from(expected)) ); if (!valid) return res.status(401).send("Invalid signature"); const event = JSON.parse(req.body.toString("utf8")); // Persist event.id before dispatching asynchronous business work. res.status(200).send("OK"); }); ``` ## Secret rotation During the configured grace period Myaza signs with the active secret and every unexpired grace secret. The V2 header therefore carries more than one `v1` value. Keep both endpoint secrets available and accept the request if either expected signature matches. Remove the old secret only after its dashboard grace period ends. ## Failure responses Return `401` for missing, stale or invalid signatures. Return `2xx` only after the event is durably accepted. Other non-`2xx` responses and timeouts are retried according to the [delivery schedule](https://trust.myaza.co/documentation/webhooks/markdown#delivery-semantics). --- # Verification webhooks Verification events describe one individual verification from processing to a terminal result. All five names use the same `data` schema, so a handler can upsert one record by `verificationId` and branch on `type` and `status`. ## Lifecycle | Event | When it fires | Recommended handling | |---|---|---| | `verification.started` | The verification enters processing. | Record the IDs and show a processing state. Do not treat it as a completed check. | | `verification.completed` | The verification completes successfully. | Persist the result and continue the approved customer flow. | | `verification.failed` | The check completes with a failed identity, data or biometric result. | Show the public `reason`; route according to your policy. | | `verification.not_found` | The submitted identity cannot be found. | Ask the customer to verify the identifier or use another supported method. | | `verification.error` | A verification service error stops processing. | Keep the customer retryable and use `reasonCode` for support diagnostics. | ## Complete successful payload ```json { "version": "2026-08-11", "id": "evt_4d6628b920d940d48ae7ac2e", "type": "verification.completed", "createdAt": "2026-08-15T12:14:37.150Z", "deliveryId": "del_01j9verification01", "data": { "verificationId": "ver_sample0000000000", "requestId": "req_sample_verified", "externalId": "your_user_123", "workflowId": "wf_sample0000000000", "workflowVersion": 3, "status": "VERIFIED", "reason": null, "reasonCode": null, "verificationMethod": "sandbox", "assuranceLevel": null, "idType": "bvn", "country": "NG", "subjectType": "individual", "idNumber": "00000000001", "business": null, "questionnaire": null, "proofOfAddress": null, "nfc": null, "emailVerification": null, "phoneVerification": null, "deviceIntel": null, "deviceRules": null, "userData": { "firstName": "Amina", "lastName": "Okafor", "middleName": null, "dateOfBirth": "1992-04-18", "gender": "female" }, "facialMatch": true, "facialConfidence": 92, "dataMatch": true, "media": { "selfie": "/api/verifications/ver_sample0000000000/media/selfie", "livenessVideo": "/api/verifications/ver_sample0000000000/media/liveness-video" }, "environment": "SANDBOX", "createdAt": "2026-08-15T12:14:31.004Z" } } ``` Media values are absolute authenticated URLs in a live delivery. Fetch them with your API key. The illustrative relative paths above show the resource shape without exposing a deployment host. ## Event-specific examples The repeated identity result fields follow the complete schema above. These fixtures show the fields that distinguish the other four event types. ```json [ { "version": "2026-08-11", "id": "evt_started_01", "type": "verification.started", "createdAt": "2026-08-15T12:14:31.004Z", "deliveryId": "del_started_01", "data": { "verificationId": "ver_sample0000000000", "requestId": "req_sample_pending", "externalId": "your_user_123", "workflowId": "wf_sample0000000000", "workflowVersion": 3, "status": "PENDING", "reason": null, "reasonCode": null, "verificationMethod": "sandbox", "assuranceLevel": null, "idType": "bvn", "country": "NG", "subjectType": "individual", "idNumber": "00000000001", "environment": "SANDBOX", "createdAt": "2026-08-15T12:14:31.004Z" } }, { "version": "2026-08-11", "id": "evt_failed_01", "type": "verification.failed", "createdAt": "2026-08-15T12:15:03.421Z", "deliveryId": "del_failed_01", "data": { "verificationId": "ver_failed_01", "requestId": "req_sample_failed", "externalId": "your_user_124", "workflowId": "wf_sample0000000000", "workflowVersion": 3, "status": "FAILED", "reason": "The user's selfie does not match the photo on file with the government database (match confidence 38%, minimum 70% required).", "reasonCode": "selfie_mismatch", "verificationMethod": "sandbox", "assuranceLevel": null, "idType": "bvn", "country": "NG", "subjectType": "individual", "idNumber": "00000000002", "facialMatch": false, "facialConfidence": 38, "dataMatch": false, "environment": "SANDBOX", "createdAt": "2026-08-15T12:14:58.050Z" } }, { "version": "2026-08-11", "id": "evt_not_found_01", "type": "verification.not_found", "createdAt": "2026-08-15T12:16:18.120Z", "deliveryId": "del_not_found_01", "data": { "verificationId": "ver_not_found_01", "requestId": "req_sample_not_found", "externalId": "your_user_125", "workflowId": null, "workflowVersion": null, "status": "NOT_FOUND", "reason": "The BVN number was not found in the government database.", "reasonCode": "identity_not_found", "verificationMethod": "gov_db", "assuranceLevel": null, "idType": "bvn", "country": "NG", "subjectType": "individual", "idNumber": "00000000003", "environment": "SANDBOX", "createdAt": "2026-08-15T12:16:11.004Z" } }, { "version": "2026-08-11", "id": "evt_error_01", "type": "verification.error", "createdAt": "2026-08-15T12:17:40.902Z", "deliveryId": "del_error_01", "data": { "verificationId": "ver_error_01", "requestId": "req_sample_error", "externalId": "your_user_126", "workflowId": "wf_sample0000000000", "workflowVersion": 3, "status": "ERROR", "reason": "A temporary system error prevented this verification from being processed. No charge was applied.", "reasonCode": "system_error", "verificationMethod": "gov_db", "assuranceLevel": null, "idType": "bvn", "country": "NG", "subjectType": "individual", "idNumber": "00000000004", "environment": "SANDBOX", "createdAt": "2026-08-15T12:17:35.418Z" } } ] ``` ## Field reference | Field | Type | Nullable | Values and meaning | |---|---|---:|---| | `verificationId`, `requestId` | string | No | Stable verification and request identifiers. | | `externalId` | string | Yes | Your customer reference supplied with the request. | | `workflowId` | string | Yes | Published workflow that configured the run. | | `workflowVersion` | integer | Yes | Exact published version that ran. Pair it with `workflowId`. | | `status` | string | No | `PENDING`, `VERIFIED`, `FAILED`, `NOT_FOUND` or `ERROR`. | | `reason`, `reasonCode` | string | Yes | Populated for failure outcomes. Codes are stable for routing; copy is suitable for display. | | `verificationMethod` | string | Yes | `gov_db`, `document_ocr` or `sandbox`. Internal source names are deliberately hidden. | | `assuranceLevel` | string | Yes | For a verified result: `chip`, `gov_db` or `document`. | | `idType`, `country` | string | No | Submitted ID type and ISO 3166-1 alpha-2 country. | | `subjectType` | string | No | `individual` or `business`. Business fields are documented separately. | | `idNumber` | string | Yes | Submitted identifier where the method uses one. | | `business` | object | Yes | Customer-safe registry result for business checks. | | `questionnaire`, `proofOfAddress`, `nfc` | object | Yes | Workflow sub-results when those steps ran. | | `emailVerification`, `phoneVerification` | object | Yes | Contact possession sub-results when enabled. | | `deviceIntel` | object | Yes | Device, IP and soft-signal result when enabled. | | `deviceRules` | object | Yes | What the workflow's Device Intelligence settings decided: `result` (the deciding finding or `clear`), `action` (`approve`, `review` or `decline`), every finding in `cases`, `ipCountry` and `countryAllowed` (the geofence verdict, null when it does not apply). Null when the settings were not applied. | | `userData` | object | Yes | Individual identity fields. Null for a business subject. | | `facialMatch`, `dataMatch` | boolean | Yes | Biometric and identity-data match results. | | `facialConfidence` | number | Yes | Facial match confidence from the completed check. | | `media` | object | Yes | Authenticated URLs for captured media. | | `environment` | string | No | Environment that produced the event. | | `createdAt` | ISO 8601 string | No | Verification record creation time. This differs from the envelope event time. | ## Related APIs and events Read the verification resource when you need the latest state or full nested sub-results. `workflow.run.completed`, `credits.deducted`, `entity.created` and `identity.resolved` can refer to the same customer journey, but they are separate logical events and can arrive in a different order. See [Authentication](https://trust.myaza.co/documentation/webhook-authentication/markdown) for signature verification and [Webhook Testing](https://trust.myaza.co/documentation/webhook-testing/markdown) for signed simulator delivery. --- # Business verification webhooks These events connect a completed KYC or KYB journey to the durable entity, identity and workflow records created for it. They can arrive independently of `verification.completed`, so deduplicate each event by envelope `id` and join records by the IDs in `data`. ## Events and handling | Event | When it fires | Recommended handling | |---|---|---| | `entity.created` | A successful Myaza verification creates an entity. | Store the entity ID against your customer. | | `entity.imported` | An externally verified entity is created through an import or Identity API call. | Store its provenance and follow the import job if `jobId` exists. | | `identity.resolved` | An entity is linked to a global identity. | Add the identity ID without replacing the entity ID. | | `entity.reverification_due` | The entity reaches its re-verification due date. | Start a new check before `dueAt`. | | `workflow.run.completed` | A workflow reaches a terminal customer outcome. | Persist the version, outcome and tags. | | `workflow.run.failed` | A workflow stops because of an engine fault. | Keep the journey retryable and inspect the run. | | `workflow.action` | A configured workflow webhook action executes. | Route the action by `nodeId`; do not infer a final outcome. | | `key_person.completed` | A director or beneficial-owner verification resolves. | Update that person inside the parent business review. | ## Complete entity example ```json { "version": "2026-08-11", "id": "evt_entity_created_01", "type": "entity.created", "createdAt": "2026-08-15T13:06:44.002Z", "deliveryId": "del_entity_created_01", "data": { "entityId": "ent_2405a1d091cf154b1ecfc9689e75f37b", "orgId": "org_01j9myaza", "environment": "SANDBOX", "externalUserId": "business_1048", "type": "business", "kycProvenance": "MYAZA_VERIFIED", "verificationId": "ver_business_1048", "createdAt": "2026-08-15T13:06:43.711Z" } } ``` ## Event-specific `data` contracts Insert each object below into the shared V2 envelope. Optional fields appear only when the producing operation has them. ```json [ { "type": "entity.imported", "data": { "entityId": "ent_imported_01", "identityId": "idn_01", "externalUserId": "customer_882", "environment": "SANDBOX", "jobId": "imp_01" } }, { "type": "identity.resolved", "data": { "entityId": "ent_imported_01", "identityId": "idn_01", "environment": "SANDBOX", "matchedExisting": true } }, { "type": "entity.reverification_due", "data": { "entityId": "ent_2405a1d091cf154b1ecfc9689e75f37b", "externalUserId": "business_1048", "environment": "PRODUCTION", "dueAt": "2026-09-15T00:00:00.000Z" } }, { "type": "workflow.run.completed", "data": { "runId": "wfr_01", "workflowId": "wf_kyb_01", "workflowVersion": 7, "verificationId": "ver_business_1048", "entityId": "ent_2405a1d091cf154b1ecfc9689e75f37b", "environment": "SANDBOX", "outcome": "APPROVE", "tags": ["registry-match", "screening-clear"], "completedAt": "2026-08-15T13:06:44.002Z" } }, { "type": "workflow.run.failed", "data": { "runId": "wfr_02", "workflowId": "wf_kyb_01", "workflowVersion": 7, "verificationId": "ver_business_1049", "environment": "SANDBOX", "error": "A workflow action could not be completed.", "failedAt": "2026-08-15T13:11:04.192Z" } }, { "type": "workflow.action", "data": { "runId": "wfr_01", "workflowId": "wf_kyb_01", "verificationId": "ver_business_1048", "entityId": "ent_2405a1d091cf154b1ecfc9689e75f37b", "nodeId": "node_notify_legal", "note": "Notify the compliance operations queue", "environment": "SANDBOX" } }, { "type": "key_person.completed", "data": { "keyPersonId": "kp_01", "verificationId": "ver_business_1048", "personVerificationId": "ver_person_01", "role": "DIRECTOR", "isApplicant": false, "status": "VERIFIED", "environment": "SANDBOX" } } ] ``` ## Field reference | Field | Type | Nullable | Meaning | |---|---|---:|---| | `entityId`, `identityId` | string | `identityId` can be null | Organisation-scoped entity and resolved global identity IDs. | | `externalUserId` | string | No when present | Your stable customer reference. | | `type` | string | No | `individual` or `business`. | | `kycProvenance` | string | No | Verification provenance, for example `MYAZA_VERIFIED`. | | `workflowId`, `runId`, `verificationId` | string | Event-dependent | IDs for the exact journey. | | `workflowVersion` | integer | Event-dependent | Published workflow version that ran. | | `outcome` | string | Event-dependent | Terminal workflow outcome such as `APPROVE`, `DECLINE` or `REVIEW`. | | `tags` | string[] | No on completion | Workflow-generated classification tags. | | `role` | string | No for key people | Business relationship, for example `DIRECTOR`. | | `status` | string | No for key people | Result status, for example `VERIFIED` or `FAILED`. | | `environment` | string | No | `SANDBOX` or `PRODUCTION`. | | `createdAt`, `completedAt`, `failedAt`, `dueAt` | ISO 8601 string | Event-dependent | Resource lifecycle time. | Business verification result fields such as registry records, directors and ownership remain on the verification resource. Behaviour and reauthentication events are documented under [Event Monitoring](https://trust.myaza.co/documentation/webhook-event-monitoring/markdown). ## Related APIs and events Use entity, verification and workflow-run read APIs for current state. Related events include `verification.completed`, `screening.completed`, `credits.deducted` and `risk.assessment.completed`. They are not guaranteed to arrive in that order. --- # Risk Intelligence webhooks Risk Intelligence turns checks and observed activity into explainable assessment, signal, level and decision events. `risk.signal.created` is the supported signal event. There is no `risk.signal.detected` or signal-updated webhook. ## Events | Event | When it fires | Recommended handling | |---|---|---| | `risk.assessment.completed` | An assessment finishes with a score, level and decision. | Upsert by `assessmentId`; use the decision for routing. | | `risk.assessment.failed` | The assessment engine cannot produce a result. | Keep the subject retryable and inspect `failureCode`. | | `risk.signal.created` | New durable risk evidence is recorded. | Append the immutable signal; do not overwrite earlier evidence. | | `risk.level.changed` | Aggregate risk moves between levels. | Re-evaluate access and monitoring policy. | | `risk.decision.changed` | The customer decision changes. | Apply the newest current decision after reading the resource if events arrive out of order. | ## Verification device and face findings When verification observes a device shared across identities or a legacy face finding without the opt-in workflow rule below, Myaza creates a review alert under **Risk Intelligence → Investigations → Review Queue**. No custom signal policy is required. Subscribe your organisation endpoint to `risk.signal.created` and `alert.created` in the same environment as your verification. These findings include `signalType` (`device.multi_entity` or `biometric.face_match`; legacy `biometric.face_duplicate` is also supported), `signalId`, `verificationId`, `alertId` and `subject.externalUserId`, your own customer reference. The signal recommends review; it does not block an account, merge identities or confirm fraud. Match images and other customers' identifiers are not included in the webhook. Verify the webhook signature, store its event ID once, and use `externalUserId` to route the right customer to your review process. Retryable delivery keeps the same event ID. A compliance officer can inspect and resolve the alert using the existing investigation workflow. More than one ordinary device is not, by itself, a fraud finding; activity rules assess new-device and shared-device behaviour. Face-reuse detection requires the deployed face model and eligible verification captures. No match result is fabricated when the model or capture is unavailable. The search runs after a successful verification, within the same organisation, environment and face model, excluding the same entity. Production and development captures are indexed by default. Sandbox stock personas are excluded unless the deployment explicitly enables sandbox indexing. A missing finding does not prove that an account is unique. ### Review one face across multiple accounts Open **Workflow builder → Presence Intelligence → Rules**, turn on **Review face reuse**, and set the number of **other accounts** that should trigger review. Publish the workflow when ready. A threshold of 2 means at least two other accounts: the current customer and repeated checks on one account do not count. This edits the same rules used by Decisioning. For a custom graph, use `verification.faceMatchedAccounts` (number) and `verification.faceSearchStatus` (`complete` or `unavailable`). The Presence control also routes an unavailable search to review. Similarity is evidence to investigate, not proof of fraud. With this rule enabled, review stays on the verification. **No separate Review Queue item is created.** A match sends `risk.signal.created` with `reviewDestination: "verification"` and `recommendedAction: "follow_workflow_decision"`, without an `alertId`. The final `workflow.run.completed` event includes the workflow outcome, `externalUserId`, and a summary such as `"faceReuse": { "status": "complete", "matchedAccountCount": 2 }`. An unavailable search has a null count, not zero. The deployed face engine and eligible captures must be available. Sandbox stock personas are not evidence of real duplicates; indexing remains deployment-controlled. Existing published workflows are unchanged until you enable and publish this rule. ### Apply the workflow decision in your platform 1. Send your stable `externalUserId` with each customer's verification. The raw verification result describes the identity check, not permission to keep using an account. A later face finding does not rewrite that result. 2. Subscribe to `risk.signal.created` for findings and `workflow.run.completed` for the final workflow outcome. Do not grant access based only on the earlier `verification.completed` event while Decisioning is still running. 3. For independent ongoing findings, a **signal-triggered policy** can match `biometric.face_match` (and the legacy alias where needed). Branch on `signal.type` and `signal.strength`, then use a review outcome or a step-up challenge. A similarity score alone is not proof of fraud and should not automatically merge customers or impose a permanent ban. 4. Subscribe to `workflow.run.completed` for your policy's outcome. Your backend applies your access policy using the customer reference. Review can mean a temporary hold; a confirmed decision can lead to a restriction. Myaza cannot disable a login, account or wallet hosted on your own platform. Workflow completion notifications have durable intent saved with the completed run and retry with a stable event ID. Notification replay does not reapply an old disposition. Read the current customer/decision state before relaxing restrictions, especially when events arrive late or out of order. A delivered outcome describes that run, not proof that every later customer-state change has been applied. Screening uses its own `screening.match` and `screening.completed` events; see [Screening webhooks](/docs/webhook-screening). ## Complete assessment example ```json { "version": "2026-08-11", "id": "evt_risk_assessment_01", "type": "risk.assessment.completed", "createdAt": "2026-08-15T10:42:13.914Z", "deliveryId": "del_risk_assessment_01", "data": { "riskAssessmentId": "ras_01j9risk001", "assessmentId": "ras_01j9risk001", "entityId": "ent_2405a1d091cf154b1ecfc9689e75f37b", "subjectType": "INDIVIDUAL", "score": 72, "level": "HIGH", "decision": "REVIEW", "checks": ["SANCTIONS", "PEP"], "signals": ["SANCTIONS_MATCH"] } } ``` ## Event-specific `data` contracts ```json [ { "type": "risk.assessment.failed", "data": { "riskAssessmentId": "ras_02", "assessmentId": "ras_02", "entityId": "ent_02", "subjectType": "BUSINESS", "failureCode": "provider_unavailable" } }, { "type": "risk.signal.created", "data": { "entityId": "ent_02", "riskSignalId": "sig_01", "signalId": "sig_01", "assessmentId": "ras_02", "subjectType": "BUSINESS", "signalType": "SANCTIONS_MATCH", "strength": 0.91, "detectedAt": "2026-08-15T11:02:00.000Z" } }, { "type": "risk.level.changed", "data": { "entityId": "ent_02", "subscriptionId": "msub_01", "previousLevel": "MEDIUM", "currentLevel": "HIGH" } }, { "type": "risk.decision.changed", "data": { "entityId": "ent_02", "subscriptionId": "msub_01", "previousDecision": "REVIEW", "currentDecision": "BLOCK" } } ] ``` ## Field reference | Field | Type | Nullable | Values and meaning | |---|---|---:|---| | `riskAssessmentId`, `assessmentId`, `entityId`, `riskSignalId`, `signalId`, `subscriptionId` | string | Event-dependent | Stable customer-addressable resource IDs. `riskAssessmentId`/`assessmentId` and `riskSignalId`/`signalId` intentionally identify the same resources. | | `subjectType` | string | No on assessment-originated events | `INDIVIDUAL` or `BUSINESS`. | | `score` | number | Absent on failure | Normalised risk score. | | `level`, `previousLevel`, `currentLevel` | string | Event-dependent | Current transition values such as `LOW`, `MEDIUM`, `HIGH` or `CRITICAL`. | | `decision`, `previousDecision`, `currentDecision` | string | Event-dependent | `ALLOW`, `REVIEW` or `BLOCK`. | | `checks`, `signals` | string[] | No on completed assessments | Checks completed and signal types produced by the assessment. | | `failureCode` | string | No on failure | Stable diagnostic code. | | `signalType` | string | No | Evidence class such as `NEW_DEVICE`, `IP_RISK`, `SANCTIONS_MATCH`, `BEHAVIOURAL_ANOMALY`, `COUNTERPARTY_RISK` or `WALLET_RISK`. | | `strength` | number | Producer-dependent | Confidence from `0` to `1` for assessment-originated signals. | | `severity`, `confidence`, `activityId`, `decisionId` | string, number, string, string | Producer-dependent | Present on behaviour or transaction-originated signals instead of `strength` and `assessmentId`. Severity is `LOW`, `MEDIUM`, `HIGH` or `CRITICAL`; confidence is `0` to `1`. | | `detectedAt` | ISO 8601 string | No | Time the evidence was observed. | ## Semantics and related resources Assessments are snapshots; signals are append-only evidence; level and decision events are material transitions. Ordering is not guaranteed, so read the entity risk resource before irreversible action. Screening, monitoring, fraud and alert events can refer to the same assessment without sharing a delivery order. --- # Activity decision webhooks Fraud rules can assess transactions and non-transaction activity through the same outcome contract. ## Events | Event | Use it for | | --- | --- | | `fraud.activity.assessed` | Apply the first decision for a non-transaction activity. | | `fraud.decision.changed` | Replace the previous decision with the new effective decision. | | `entity.step_up_requested` | Send or display the hosted reauthentication challenge. | | `entity.step_up_completed` | Continue only when `passed` is true. | | `entity.biometric_enrolled` | Record that biometric reauthentication is available. | | `entity.biometric_authenticated` | Apply the resolved biometric result. | ## Assessment example ```json { "id": "evt_activity_01", "event": "fraud.activity.assessed", "createdAt": "2026-08-15T14:20:09.000Z", "data": { "externalActivityId": "login_928401", "activityId": "act_01", "decisionId": "dec_01", "decision": "review", "summary": { "outcome": "review", "title": "Review required", "reason": "A new device and unusual location were detected.", "score": 78, "riskLevel": "high", "matchedRules": 2, "nextAction": { "type": "review_activity", "label": "Review activity" } } } } ``` Use the summary for the immediate answer. Detailed rule and screening evidence remains available in the activity response for audit and investigation. ## Step-up example ```json { "event": "entity.step_up_requested", "data": { "stepUpId": "stu_01", "externalUserId": "customer_1", "url": "https://trust.myaza.app/reauth/stu_01", "expiresAt": "2026-08-15T14:35:09.000Z", "environment": "SANDBOX" } } ``` Treat the hosted challenge URL as sensitive. Deduplicate every webhook by its envelope `id` and use the related read API whenever current state matters. --- # Transaction decision webhooks Myaza sends one concise summary when a transaction is assessed and again if its effective decision changes. ## Events | Event | Use it for | | --- | --- | | `fraud.transaction.assessed` | Apply the first transaction decision. | | `fraud.decision.changed` | Replace the previous decision with the new effective decision. | | `event.flagged` | Legacy compatibility for non-allow decisions. New integrations should use the events above. | ## Example ```json { "id": "evt_transaction_01", "event": "fraud.transaction.assessed", "createdAt": "2026-08-15T15:02:12.884Z", "data": { "externalTransactionId": "txn_928401", "activityId": "act_tx_01", "decisionId": "dec_tx_01", "decision": "review", "summary": { "outcome": "review", "title": "Review required", "reason": "A high-risk transaction rule matched.", "score": 82, "riskLevel": "high", "matchedRules": 1, "nextAction": { "type": "review_activity", "label": "Review transaction" } } } } ``` Use `data.summary` for normal integration logic: 1. Deduplicate by the envelope `id`. 2. Branch on `summary.outcome`. 3. Display `summary.reason` when a person needs context. 4. Follow `summary.nextAction` when it is not `none`. The same payload can include detailed rule, screening, billing and Travel Rule evidence. Store that evidence for audit or open the transaction read API when you need it, but do not recompute Myaza's decision from those details. Delivery order is not guaranteed. A later `fraud.decision.changed` event is authoritative for the effective decision. --- # Screening webhooks Screening event names are list-agnostic. Inspect `data.type` and the linked screening resource to distinguish sanctions, PEP, adverse media and wallet screening. Internal source records and raw watchlist documents are not included in webhook payloads. ## Events | Event | When it fires | Recommended handling | |---|---|---| | `screening.completed` | A screening run completes. | Persist status and match count. | | `screening.match` | A non-clear match needs review. | Open or update review using the screening ID and alert ID. | | `screening.insufficient_data` | The customer profile cannot support the check. | Collect the missing customer information before retrying. | | `screening.failed` | A screening run cannot complete. | Inspect the public reason code and `retryScheduled`. | | `screening.stale` | The profile changed while the check ran. | Wait for or trigger the fresh run. | | `screening.adjudicated` | An analyst confirms or clears a match. | Apply the recorded outcome to your review workflow. | | `screening.adjudication_overdue` | A match passes its review deadline. | Escalate to the assigned compliance queue. | ## Complete match example ```json { "version": "2026-08-11", "id": "evt_screening_match_01", "type": "screening.match", "createdAt": "2026-08-15T16:20:00.000Z", "deliveryId": "del_screening_match_01", "data": { "screeningId": "scr_01", "entityId": "ent_01", "subject": { "entityId": "ent_01", "externalUserId": "customer-123", "identityId": "idn_01", "type": "individual" }, "type": "SANCTIONS", "status": "POTENTIAL_MATCH", "assurance": "STANDARD", "reasonCode": "name_plus_one", "matchCount": 2, "alertId": "alt_01", "environment": "SANDBOX" } } ``` ## Field reference | Field | Type | Nullable | Values and meaning | |---|---|---:|---| | `screeningId`, `entityId` | string | No | Screening and subject Entity IDs. | | `subject` | object | Yes | Customer-safe Entity correlation, including your `externalUserId` when available. | | `type` | string | No | `SANCTIONS`, `PEP`, `ADVERSE_MEDIA` or `WALLET`. | | `status` | string | No | Current screening state such as `CLEAR`, `POTENTIAL_MATCH`, `INSUFFICIENT_DATA`, `FAILED` or `STALE`. | | `assurance`, `reasonCode` | string | Yes | Customer-safe quality level and explanation. | | `matchCount` | integer | Yes | Matches in this result. | | `alertId` | string | Yes | Review alert opened or reused for a non-clear result. | | `outcome` | string | Yes | Analyst outcome on `screening.adjudicated`. | | `retryScheduled` | boolean | Yes | Whether a failed or stale check will retry. | | `environment` | string | Yes | Environment that produced the result. | Retrieve `GET /api/v1/monitoring/screenings/{screeningId}` for current match records, attempts and the customer-safe adjudication history. In the dashboard, screening evidence is shown in the customer, risk decision, transaction, or investigation that gives it context. Internal source identity, attempt IDs, profile digests, billing flags, datasets and raw responses are intentionally absent. --- # Continuous Monitoring webhooks For a minimal integration, subscribe to `monitoring.delta.detected`. Add `monitoring.run.failed` when your operations team must react to an incomplete check. Do not subscribe to high-volume `monitoring.run.completed` unless your system needs every successful run. ## Events | Event | When it fires | | --- | --- | | `monitoring.subscription.started` | Monitoring becomes active for an Entity. | | `monitoring.subscription.paused` | Scheduled monitoring is paused. | | `monitoring.subscription.resumed` | A paused subscription becomes active. | | `monitoring.subscription.ended` | A subscription is permanently turned off. | | `monitoring.subscription.updated` | An Entity's policy or frequency changes. | | `monitoring.run.completed` | One monitoring run completed. | | `monitoring.run.failed` | One monitoring run could not complete. | | `monitoring.delta.detected` | Monitoring found one or more material changes. | `monitoring.failed` is not a public event name. ## Exact V2 delta envelope ```json { "version": "2026-08-11", "id": "evt_monitoring_delta_01", "type": "monitoring.delta.detected", "createdAt": "2026-08-26T17:00:00.000Z", "deliveryId": "del_monitoring_delta_01", "data": { "subscriptionId": "sub_01", "entityId": "ent_01", "externalUserId": "customer_42", "subject": { "entityId": "ent_01", "externalUserId": "customer_42", "identityId": "identity_01", "type": "individual" }, "detectedAt": "2026-08-26T17:00:00.000Z", "deltas": [ { "type": "SANCTIONS_MATCH", "direction": "ADDED", "severity": "HIGH" } ] } } ``` This is the complete V2 envelope. Product fields live directly inside `data`. ## Run failure example ```json { "version": "2026-08-11", "id": "evt_monitoring_failed_01", "type": "monitoring.run.failed", "createdAt": "2026-08-26T17:04:00.000Z", "deliveryId": "del_monitoring_failed_01", "data": { "subscriptionId": "sub_01", "entityId": "ent_01", "externalUserId": "customer_42", "failureCode": "provider_unavailable", "failedAt": "2026-08-26T17:04:00.000Z" } } ``` ## Public data contract | Field | Events | Meaning | | --- | --- | --- | | `subscriptionId` | All | Customer-addressable monitoring subscription. | | `entityId`, `externalUserId`, `subject` | Entity-related events | Customer correlation without internal source data. | | `status` | Subscription lifecycle | `active`, `paused` or `ended`. | | `frequency` | Subscription updated | Effective frequency, policy default, Entity override and source. | | `materialChange`, `completedAt` | Run completed | Whether the successful run found a material change and when it completed. | | `failureCode`, `failedAt` | Run failed | Stable failure classification and time. | | `deltas`, `detectedAt` | Delta detected | Customer-visible material changes and detection time. | Internal `policyVersionId`, `monitoringRunId` and `assessmentId` fields are not included. Read current state and history from `GET /api/v1/monitoring/subscriptions/{subscriptionId}`. Verify the raw request body, deduplicate by envelope `id`, return `2xx` quickly, and use the read API whenever current state matters. --- # Alerts & Investigations webhooks Alerts group risk occurrences that need attention. Investigations are the owned human-review records opened from one or more alerts. The product and API use the name Investigation. Webhook event names retain the established `case.*` prefix so existing receivers do not break. ## Alert events | Event | When it fires | Recommended handling | |---|---|---| | `alert.created` | A new actionable alert opens. | Create a linked review item. | | `alert.updated` | Decision, score, occurrence count or status changes. | Update the existing item by `alertId`. | ```json { "version": "2026-08-11", "id": "evt_alert_01", "type": "alert.created", "createdAt": "2026-08-15T17:32:00.000Z", "deliveryId": "del_alert_01", "data": { "alertId": "alt_01", "entityId": "ent_01", "status": "OPEN", "decision": "REVIEW", "riskScore": 86, "occurrences": 1, "resolution": null } } ``` ```json [ { "type": "alert.updated", "data": { "alertId": "alt_01", "entityId": "ent_01", "status": "IN_REVIEW", "decision": "REVIEW", "riskScore": 89, "occurrences": 3, "resolution": null } }, { "type": "alert.updated", "data": { "alertId": "alt_01", "entityId": "ent_01", "status": "RESOLVED", "decision": "ALLOW", "riskScore": 22, "occurrences": 3, "resolution": "FALSE_POSITIVE" } } ] ``` ## Investigation events | Event | When it fires | |---|---| | `case.created` | An investigation opens from related evidence. | | `case.assigned` | An owner is assigned. | | `case.review_started` | Human review begins. | | `case.escalated` | Priority or review tier is escalated. | | `case.resolved` | An analyst records a disposition. | | `case.closed` | Operational work closes. | | `case.reopened` | A closed investigation returns to review. | | `case.sla_at_risk` | The due time is approaching. | | `case.overdue` | The due time has passed. | Every event uses the standard webhook envelope. Route it by `data.caseId`, then retrieve `/api/v1/investigations/{caseId}` when your workflow needs the complete current Investigation. `caseId` is retained inside webhook payloads as a stable compatibility field. ```json { "version": "2026-08-11", "id": "evt_investigation_01", "type": "case.assigned", "createdAt": "2026-08-15T17:35:00.000Z", "deliveryId": "del_investigation_01", "data": { "id": "evt_case_assigned_inv_01_current", "caseId": "inv_01", "entityId": "ent_01", "subject": { "type": "entity", "id": "ent_01" }, "assigneeId": "usr_reviewer_01", "occurredAt": "2026-08-15T17:35:00.000Z" } } ``` ## Field reference | Field | Type | Nullable | Meaning | |---|---|---:|---| | `alertId`, `caseId`, `entityId` | string | No where present | Stable alert, investigation and subject IDs. | | `status` | string | No | Current lifecycle status shown in the examples. | | `decision` | string | Yes | Alert decision such as `ALLOW`, `REVIEW` or `BLOCK`. | | `riskScore` | number | Yes | Alert risk score. | | `occurrences`, `alertCount` | integer | No | Grouped evidence or linked alert counts. | | `resolution`, `disposition` | string | Yes | Recorded alert resolution or investigation outcome. | | `priority` | string | No | For example `LOW`, `MEDIUM`, `HIGH` or `CRITICAL`. | | `assigneeId` | string | Yes | Assigned user ID. | | `dueAt` | ISO 8601 string | Yes | SLA deadline. | Private notes, attachments, analyst evidence and SAR filing state are not included. There are no customer-facing `sar.*` events in the current catalogue. --- # Wallet Intelligence webhooks Subscribe to the wallet events on an endpoint in the same environment as the screening. Sandbox and production endpoints and deliveries are separate. Verify the normal webhook signature and deduplicate by the envelope `id`. | Event | When it arrives | |---|---| | `wallet.screening.completed` | An address result is available. | | `wallet.screening.review` / `wallet.screening.declined` | The completed address or transaction result requires review or was blocked. | | `wallet.screening.failed` | An address screen failed. | | `crypto.transaction.screening.completed` | A crypto transaction result is available. | | `crypto.transaction.screening.failed` | A crypto transaction screen failed. | The event body includes the environment, organisation and screening IDs, masked wallet, network and asset, transaction hash when applicable, risk score and level, decision, triggered rule IDs and occurrence time. It does not contain the raw analytics response, secrets or internal cost data. Fetch the [screening result](https://trust.myaza.co/documentation/api-wallet-screenings/markdown) for exposures and rule explanations. ```json { "environment": "sandbox", "organisationId": "org_example", "screeningId": "wsc_example", "assessmentId": "ras_example", "wallet": { "maskedAddress": "0xaaaa…aaaa", "blockchain": "ETH", "asset": "USDT" }, "transaction": null, "risk": { "score": 95, "level": "CRITICAL", "status": "SCREENED" }, "decision": "BLOCK", "triggeredRuleIds": ["wallet_sanctions_direct"], "occurredAt": "2026-09-30T12:00:00.000Z" } ``` The delivery worker uses the existing signed webhook envelope, retries and idempotency behaviour. `screening.*` events for ongoing entity screening and `fraud.transaction.assessed` for transaction decisions remain available. A wallet screening event reports the intelligence result; use the transaction assessment event for the final action on a monitored transaction. See [Webhooks](https://trust.myaza.co/documentation/webhooks/markdown) and [Wallet screening](https://trust.myaza.co/documentation/wallet-screening/markdown). --- # Billing webhooks Billing events describe posted credit changes. API-key events provide a security audit signal but never expose the secret key. ## Events and payloads | Event | When it fires | Recommended handling | |---|---|---| | `credits.deducted` | Billable verification usage is posted. | Reconcile by `verificationId`. | | `credits.low` | Balance crosses the configured threshold. | Notify billing owners once per logical event ID. | | `credits.topped_up` | A top-up posts successfully. | Reconcile the new balance. | | `credits.granted` | Promotional credit is granted. | Record the grant and expiry policy. | | `credits.promo_expired` | Unused promotional credit expires. | Reconcile the reduction without treating it as usage. | | `api_key.created` | A key is created. | Audit the name, environment, type and prefix. | | `api_key.revoked` | A key is revoked. | Remove dependent access and investigate unexpected revocation. | ```json { "version": "2026-08-11", "id": "evt_credit_deducted_01", "type": "credits.deducted", "createdAt": "2026-08-15T18:00:00.000Z", "deliveryId": "del_credit_deducted_01", "data": { "verificationId": "ver_01", "amount": "0.2000", "currency": "USD" } } ``` ```json [ { "type": "credits.low", "data": { "orgId": "org_01", "balance": "20.0000", "threshold": "25.0000", "currency": "USD" } }, { "type": "credits.topped_up", "data": { "orgId": "org_01", "amount": "500.0000", "balanceAfter": "1020.0000", "currency": "USD" } }, { "type": "credits.granted", "data": { "orgId": "org_01", "amount": "50.0000", "balanceAfter": "1070.0000", "currency": "USD", "grantId": "grt_01" } }, { "type": "credits.promo_expired", "data": { "orgId": "org_01", "amount": "10.0000", "balanceAfter": "1060.0000", "currency": "USD", "grantId": "grt_01" } }, { "type": "api_key.created", "data": { "apiKeyId": "key_01", "name": "Production server", "environment": "PRODUCTION", "keyType": "SECRET", "keyPrefix": "sk_live_8p4" } }, { "type": "api_key.revoked", "data": { "apiKeyId": "key_01", "name": "Production server", "environment": "PRODUCTION" } } ] ``` ## Field reference | Field | Type | Nullable | Meaning | |---|---|---:|---| | `orgId`, `verificationId`, `grantId`, `apiKeyId` | string | Event-dependent | Related organisation, usage, grant and key IDs. | | `amount`, `balance`, `balanceAfter`, `threshold` | string | Event-dependent | Fixed-scale decimal currency amounts with four fractional digits. Parse with a decimal library; do not use binary floating-point for ledger arithmetic. | | `currency` | string | No for credit events | ISO 4217 currency code such as `USD`. | | `name` | string | No for key events | Customer-visible key name. | | `environment` | string | No for key events | `SANDBOX` or `PRODUCTION`. | | `keyType` | string | No on creation | Public key classification, for example `SECRET` or `PUBLISHABLE`. | | `keyPrefix` | string | No on creation | Non-secret prefix for identifying the key. The full secret is never delivered. | Use billing ledger and API-key read endpoints for authoritative current state. Financial side effects must be idempotent by envelope `id`, while reconciliation should also use the referenced verification, grant or top-up record. --- # Webhook testing Test with a sandbox endpoint before creating a production subscription. The simulator builds the same envelope, signs the same raw bytes and creates a real delivery record, so your receiver exercises production verification and retry behaviour without running an entire product flow. ## Recommended test sequence 1. Create a sandbox endpoint and copy its `whsec_…` secret into your receiver's secret store. 2. Choose one event from each family your integration handles. 3. Send a simulated delivery from the endpoint's **Testing** action. 4. Confirm your receiver verifies `X-Myaza-Signature-V2`, persists `id`, and returns `2xx` quickly. 5. Inspect the endpoint delivery history and correlate it with `deliveryId` or `X-Myaza-Delivery`. 6. Resend the delivery and confirm your business side effect happens only once. 7. Temporarily return a non-`2xx` response and confirm a retry is scheduled. 8. Rotate the secret and confirm both V2 `v1` signatures are accepted during the grace period. 9. If migrating from an alias, subscribe to the canonical name and alias together and confirm the shared event `id` prevents duplicate work. ## Catalogue-driven fixtures `GET /api/v1/webhooks/catalogue` returns every supported event and its generated sample. Generate typed fixtures from this response rather than copying an invented schema into a test suite. ```bash curl -H "Authorization: Bearer sk_test_…" \ https://trust.myaza.app/api/v1/webhooks/catalogue ``` Samples show realistic public field shapes, but sample identifiers are not reusable API resources. Runtime events can contain additional optional fields documented on each event-family page. ## Idempotency assertion Use the envelope `id`, not `deliveryId`, as the unique business key: ```sql insert into received_myaza_events (event_id, event_type, payload) values (:id, :type, :payload) on conflict (event_id) do nothing; ``` A manual resend creates another delivery record but preserves the logical event ID. A compatibility alias also shares the canonical event's ID. ## Ordering assertion Do not write a test that assumes a cross-type sequence such as `risk.signal.created` always arriving before `alert.created`. Delay or reverse two accepted events and confirm your handler either applies them by resource version/current API state or processes them independently. ## Production readiness checklist - HTTPS endpoint uses a valid public certificate and does not redirect. - Raw-body signature verification and replay rejection are covered by tests. - Event IDs have a durable unique constraint. - Slow processing runs outside the request. - Unknown event types are safely stored or ignored and still acknowledged. - Alerts exist for repeated delivery failures and dead-lettered deliveries. - Secrets are environment-scoped, stored outside source code and rotation-tested. --- # How to manage workspace access and costs with Myaza Trust Manage team access, environments and cost controls deliberately. Give each teammate the permissions needed for their job and keep Sandbox preparation separate from Production activity. Key takeaways: - A reader cannot perform management actions. - Environment switching shows the intended records and keys. - Secret values are absent from client code and public images. - A saved change survives reload and has audit evidence. - Pricing uses the current organisation’s quoted components. Manage team access, environments and cost controls deliberately. Give each teammate the permissions needed for their job and keep Sandbox preparation separate from Production activity. ## Before you begin - Authorised organisation or billing access - Current workspace and environment Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Check your organisation and environment Use the dashboard workspace selector to confirm the organisation and environment before viewing data or changing settings. Sandbox and Production separate keys, workflows, results and receivers. A URL alone may not identify which environment you are viewing. Confirm the displayed state after switching, especially before editing a published control or a billing setting. ![Integration reference: Switching environments in the dashboard](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-02facc5e03316e037558/7ee99e7313b7cf2d5c31fc854636a0b2/reference-environments-101.jpg) *Integration reference: Switching environments in the dashboard* ### Step 2: Review the team’s actual permissions Open the organisation’s **Team** settings and inspect the member and role you intend to manage. Separate readers from people who can publish workflows, manage rules, review evidence or change billing. Use the supported role controls and required confirmation. A visible page or shared organisation membership does not grant authority to every record or action. ![Integration reference: Creating and managing keys](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-ebd2c7a503487a7bdb4c/7aad60a6d2a03bf533222c18ac1033b4/reference-authentication-111.jpg) *Integration reference: Creating and managing keys* ### Step 3: Keep organisation security separate from personal settings Use **Organisation settings → Security** for organisation-wide requirements and personal account security for the current user’s sign-in. Preserve the required two-factor confirmation before privileged changes. Give teammates a usable enrolment and recovery path. A local personal change cannot substitute for an organisation-wide policy, and a role change should not be used to bypass a missing security requirement. ![Integration reference: Best practices](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-9403f7388d1f2aba8da3/1897541734497cd098c6cffb86c53f26/reference-authentication-112.jpg) *Integration reference: Best practices* ### Step 4: Create credentials for the right service Open **Developers → API Keys** in the intended environment. Choose Publishable for supported client capture and Secret for backend results or Risk Intelligence. Secret values are shown once: store them immediately in the backend secret manager. Use separate service credentials where appropriate so revocation does not disrupt every integration. Do not place secret keys in screenshots, source code or a mobile build. ![Integration reference: Creating and managing keys](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-ebd2c7a503487a7bdb4c/7aad60a6d2a03bf533222c18ac1033b4/reference-authentication-111.jpg) *Integration reference: Creating and managing keys* ### Step 5: Review current prices and balance Open **Billing → Rate card** for the organisation’s current component pricing, then inspect balance and low-balance settings. Review a workflow’s per-check estimate and a batch or recurring operation’s actual quote before activation. Eligible Usage Credits apply to the components and remaining pool the service returns; do not divide or invent free allowances. A covered step can coexist with payable checks in the same journey. ![Integration reference: Billing](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-ab162a36c9886f2d9dd4/c81f8aeff9e28cf9b3b2bde0ef44ad3c/reference-environments-102.jpg) *Integration reference: Billing* ### Step 6: Verify changes and retain audit evidence Reload the relevant setting or record after a permitted save and inspect the organisation’s **Audit Logs** for the action. Review user, environment and resource binding when troubleshooting an unexpected change. Keep key and webhook rotation evidence separate from product activation. Do not assume a saved team, billing or security setting proves that a customer-facing integration is fully live. ![Integration reference: Best practices](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-9403f7388d1f2aba8da3/1897541734497cd098c6cffb86c53f26/reference-authentication-112.jpg) *Integration reference: Best practices* ## Get more from the capability - Separate routine reader access from publication and investigation authority. - Review service keys and members periodically, revoking access through the supported process when it is no longer needed. - Review batch and recurring cost estimates before activation, then reconcile recorded usage rather than guessing from the number of visible rows. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - A reader cannot perform management actions. - Environment switching shows the intended records and keys. - Secret values are absent from client code and public images. - A saved change survives reload and has audit evidence. - Pricing uses the current organisation’s quoted components. ## Troubleshooting ### A control is missing or disabled Check role, product access, environment and security requirements before assuming the feature is broken. ### A secret key cannot be shown again It is intentionally shown once. Use the supported new-key and controlled replacement process. ### The balance does not match my estimate Inspect actual usage components, credits and recorded charges in the intended workspace. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/organisation/billing/rate-card) - [Authentication](https://trust.myaza.co/documentation/authentication) - [Environments](https://trust.myaza.co/documentation/environments) - [Webhook billing](https://trust.myaza.co/documentation/webhook-billing) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Authentication](https://trust.myaza.co/documentation/authentication) - [Environments](https://trust.myaza.co/documentation/environments) - [Webhook billing](https://trust.myaza.co/documentation/webhook-billing) --- # How to test and troubleshoot an integration with Myaza Trust Test the complete integration, including failures and recovery. Choose the supported fixture for the API family you use, then inspect both Myaza’s record and your application’s behaviour. Key takeaways: - All decision branches reach their intended application path. - Lost replies and repeated events create no duplicate effect. - Pending and unavailable remain distinct. - Fixtures remain explicitly labelled. - Production-only claims require live evidence. Test the complete integration, including failures and recovery. Choose the supported fixture for the API family you use, then inspect both Myaza’s record and your application’s behaviour. ## Before you begin - A supported Sandbox scenario or labelled sample - Use the correct endpoint family and SDK version Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Match the product, host and credentials Choose Sandbox and record the API family you are testing. Identity verification uses the documented /api/kyc host and test key. Risk Intelligence uses its Sandbox /api/v1 host and secret key. Identity Hub has its own /api/identity contract. Do not move request fields between families or infer that every Sandbox request is free. Explicit Risk Intelligence scenarios are non-billable; ordinary requests can use configured pricing. ![Integration reference: Sandbox](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-7a341a2bc9343eca9567/8984c38138e24ee879fc83facf7fde40/reference-environments-100.jpg) *Integration reference: Sandbox* ### Step 2: Choose a supported verification scenario Open the **Sandbox & test IDs** guide and select an ID that fits the country and document type. For example, the Nigerian BVN fixture ending in 01 passes and 04 produces selfie mismatch. Document-only outcomes need a document ID. Use the published scenario or supported override; real identity numbers are refused by the Sandbox verification contract. ![Integration reference: Scenario reference](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-187b56e58d730c1debd2/1345d54d65f9bae29fdcfc95c655e229/reference-sandbox-testing-109.jpg) *Integration reference: Scenario reference* ### Step 3: Use the Simulator for supported risk journeys Open **Developers → Simulator** in Sandbox. Select a supported scenario and inspect its eligibility. Locked or Setup required scenarios need their displayed prerequisites; do not remove a gate to run them. Travel Rule fixtures need a compatible separately published policy. Running a deterministic scenario proves its controlled handling, not a live provider connection or actual institution exchange. ![Integration reference: Set up Sandbox Travel Rule](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-f26b11e7c60e076da660/557ed2117d7b60ccd8b1f6e7ff8bc8da/reference-platform-simulator-110.jpg) *Integration reference: Set up Sandbox Travel Rule* ### Step 4: Test the signed receiver independently Create the Sandbox webhook endpoint and use **Testing** to simulate a supported event. Verify the signature against raw bytes and persist its logical id. Test a resend, invalid signature, expired signature and a receiver failure. Generated sample identifiers demonstrate shape; they are not reusable API resources for a later retrieve call. ![Integration reference: Catalogue-driven fixtures](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-0ee1c8eac3e8871339ed/bccd4e37800f530f224130d546812180/reference-webhook-testing-108.jpg) *Integration reference: Catalogue-driven fixtures* ### Step 5: Exercise uncertainty and ordering Simulate a lost response after a request was accepted, then retry the exact operation with its original references and idempotency key where the contract supports it. Delay or reorder accepted notifications and reconcile current state. Test a pending and unavailable result separately from a decline. Inspect the Myaza record and your durable application state for duplicates or an incorrect access change. ![Integration reference: Ordering assertion](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-27a93e113e2a47595767/6b47d03631eab32515da0e5b6f533669/reference-webhook-testing-107.jpg) *Integration reference: Ordering assertion* ### Step 6: Keep a concrete Production readiness record Record the tested scenarios, intended decision, observed decision, backend action and recovery evidence. Check supported SDK versions, permission recovery and representative devices. Set up the separate Production workflow, keys and receiver only after business and capability requirements are satisfied. Remove Sandbox-only scenario fields. A green local test or example score is not evidence of live customer behaviour. ![Integration reference: Checklist for going live](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-bc617abc11aa9a1725fd/a225e53d45fd64a4261691830ec26b60/reference-environments-99.jpg) *Integration reference: Checklist for going live* ## Get more from the capability - Keep fixtures in the same form as the public contracts so your test suite catches field and version changes. - Test late final decisions for workflows that wait on screening, key people or presence. - Give operational failures their own customer-safe message and internal recovery path rather than asking the applicant to fix a service outage. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - All decision branches reach their intended application path. - Lost replies and repeated events create no duplicate effect. - Pending and unavailable remain distinct. - Fixtures remain explicitly labelled. - Production-only claims require live evidence. ## Troubleshooting ### My real ID was rejected in Sandbox Use a published test ID. Sandbox verification does not validate real identity numbers. ### A Simulator scenario is locked Complete its displayed policy or controlled-fixture prerequisites. ### A sample resource cannot be retrieved Catalogue samples illustrate shapes; their IDs are not live stored resources. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/documentation/sandbox-testing) [Read the integration reference](https://trust.myaza.co/documentation/sandbox-testing) - [Sandbox testing](https://trust.myaza.co/documentation/sandbox-testing) - [Platform simulator](https://trust.myaza.co/documentation/platform-simulator) - [Errors](https://trust.myaza.co/documentation/errors) - [Status and reliability](https://trust.myaza.co/documentation/status-and-reliability) - [Ai integration](https://trust.myaza.co/documentation/ai-integration) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Sandbox testing](https://trust.myaza.co/documentation/sandbox-testing) - [Platform simulator](https://trust.myaza.co/documentation/platform-simulator) - [Errors](https://trust.myaza.co/documentation/errors) - [Status and reliability](https://trust.myaza.co/documentation/status-and-reliability) - [Ai integration](https://trust.myaza.co/documentation/ai-integration) --- # How to receive results reliably with Myaza Trust Receive Myaza results reliably with a signed webhook receiver. Verify the exact bytes, store each event durably and recover retries without repeating the customer action. Key takeaways: - Tampered and expired signatures are rejected. - A logical resend triggers one business action. - Receiver recovery survives a process restart. - Delayed events cannot overwrite newer state. - Rotation works through the full grace period. Receive Myaza results reliably with a signed webhook receiver. Verify the exact bytes, store each event durably and recover retries without repeating the customer action. ## Before you begin - A server endpoint that verifies signatures - Durable duplicate handling and retry recovery - An authorised API key for server result retrieval Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Prepare the backend receiver Create a public HTTPS endpoint with a valid certificate and no redirect. It must receive the exact raw request body before JSON middleware changes it. Plan a durable inbox keyed by logical event ID and background processing for slow work. The receiver belongs on your backend; a browser callback is not a substitute for reliable result delivery. ![Integration reference: Verification algorithm](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-8ec82c22353b55454eb4/f9eed57a2fc9e584f1c92ea45825bcec/reference-webhook-authentication-103.jpg) *Integration reference: Verification algorithm* ### Step 2: Create the environment’s webhook endpoint In Sandbox, open **Developers → Webhooks → Add endpoint**. Enter the **Endpoint URL**, choose the supported body version and select the event families your integration handles. Select **Create endpoint** and save the displayed endpoint signing secret in your backend secret store. Keep it separate from your API key. Sandbox and Production need environment-appropriate endpoints and secrets. ![Integration reference: Recommended test sequence](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-4c4e92ef1bba7806229e/ce6c9299530be789dae927df281183fa/reference-webhook-testing-105.jpg) *Integration reference: Recommended test sequence* ### Step 3: Verify the V2 signature before parsing Read **X-Myaza-Signature-V2**, parse the signed timestamp and v1 candidates, reject invalid or expired timestamps and verify HMAC-SHA256 over timestamp, a full stop and the exact raw bytes. Use the endpoint secret and constant-time comparison. During rotation accept a valid signature from the active or unexpired grace secret. Use the documented SDK or receiver example rather than a made-up signing format. ![Integration reference: Signed headers](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-b95518054fd05b17464b/d6743472e473c60e58c88e2fe8413733/reference-webhook-authentication-104.jpg) *Integration reference: Signed headers* ### Step 4: Persist the logical event before acknowledgement After signature verification, parse the JSON and store its envelope **id** under a durable unique constraint. Deduplicate business work using that id, not deliveryId. A resend or compatibility alias can carry the same logical event under another delivery. Return 2xx only after durable acceptance; queue slow work separately. A fast 2xx with no durable record can lose the result after a crash. ![Integration reference: Idempotency assertion](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-f4012ffd49ca0853edc7/b7ac97c47794129fa207724581f5af8b/reference-webhook-testing-106.jpg) *Integration reference: Idempotency assertion* ### Step 5: Reconcile the current product result Use the documented customer, verification, transaction or activity reference for the event family. Keep status and checkStatus distinct for verifications. Events can arrive out of order, so retrieve current resource state or use its supported version before a sensitive change. Unknown optional fields and event types should not crash your receiver. A notification alone is not permission to release funds. ![Integration reference: Ordering assertion](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-27a93e113e2a47595767/6b47d03631eab32515da0e5b6f533669/reference-webhook-testing-107.jpg) *Integration reference: Ordering assertion* ### Step 6: Test delivery, replay and rotation Open the endpoint’s **Testing** action and send a supported simulated event. Inspect delivery history and your receiver’s durable record. Resend it and prove the business action occurs once. Test a non-2xx response, recovery after timeout, a tampered body and an expired signature. Rotate using the grace period and test both signatures before retiring the older receiver secret. ![Integration reference: Recommended test sequence](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-4c4e92ef1bba7806229e/ce6c9299530be789dae927df281183fa/reference-webhook-testing-105.jpg) *Integration reference: Recommended test sequence* ## Get more from the capability - Subscribe to the canonical event families needed by your product rather than every catalogue entry without a handler. - Alert your team on repeated failed or dead-lettered deliveries and keep a reconciliation path to the current resource. - Store the exact decision references and explanation with your own action so support can prove what the application did. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - Tampered and expired signatures are rejected. - A logical resend triggers one business action. - Receiver recovery survives a process restart. - Delayed events cannot overwrite newer state. - Rotation works through the full grace period. ## Troubleshooting ### Signatures fail after parsing JSON Verify raw bytes before parsing. Re-serialised JSON changes the signed input. ### A resend acted twice Use the logical event id as the durable unique key instead of deliveryId. ### Delivery shows success but the account did not change Inspect queued business processing and correlation. HTTP delivery is not proof of the downstream side effect. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/developers/webhooks) [Read the integration reference](https://trust.myaza.co/documentation/webhooks) - [Webhooks](https://trust.myaza.co/documentation/webhooks) - [Webhook authentication](https://trust.myaza.co/documentation/webhook-authentication) - [Webhook testing](https://trust.myaza.co/documentation/webhook-testing) - [Authentication](https://trust.myaza.co/documentation/authentication) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Webhooks](https://trust.myaza.co/documentation/webhooks) - [Webhook authentication](https://trust.myaza.co/documentation/webhook-authentication) - [Webhook testing](https://trust.myaza.co/documentation/webhook-testing) - [Authentication](https://trust.myaza.co/documentation/authentication) --- # How to connect your customer records with Myaza Trust Connect the customers already in your application to Myaza records using stable references. Retain what you know about their verification source rather than upgrading imported claims into verified facts. Key takeaways: - One external reference resolves to the intended record. - Import acknowledgement and completion are distinct. - Failed rows remain explainable and retryable. - Imported claims retain truthful provenance. - A scoped recapture attaches to the existing customer. Connect the customers already in your application to Myaza records using stable references. Retain what you know about their verification source rather than upgrading imported claims into verified facts. ## Before you begin - Stable customer references - Authorised import or profile-management access Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Choose a stable customer reference Use the durable customer or business ID from your own system as **externalUserId**. Keep one reference for the same subject across imports, verification and activity. Decide whether the subject is an individual or business. Do not use an email that can change as the only customer key. Obtain authority to transfer the profile details you intend to register. ![Integration reference: Register or update an entity](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-b9a7781af4c5e662c35a/11ce1992e8ab1b3094789e31b1e70b89/reference-identity-hub-41.jpg) *Integration reference: Register or update an entity* ### Step 2: Choose the correct provenance Use **UNVERIFIED** for a record that has not completed verification. Use **EXTERNAL_VERIFIED** only when your own authorised KYC process verified it, with a truthful **kycSource**. Do not claim MYAZA_VERIFIED for an imported external record merely to unlock a feature. Identity links and risk evidence depend on their own contracts; a claimed wallet is not ownership evidence. ![Integration reference: KYC provenance & identity resolution](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-02914f0aeb1614511d72/915aaef4a5854e7677cacccba352b4f6/reference-identity-hub-43.jpg) *Integration reference: KYC provenance & identity resolution* ### Step 3: Register one record from your backend Call **POST /api/identity/entities** with a secret key and the customer’s externalUserId, type and permitted profile. Supply only known details. Reusing the same customer reference updates the existing entity rather than creating another one. Keep metadata replacement semantics deliberate, and store the returned entityId with your application record. ![Integration reference: Request](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-8d383fa2325a64dcfe51/f906beebcb3018a6322feb2d2264c196/reference-identity-hub-44.jpg) *Integration reference: Request* ### Step 4: Read back the record before sending activity Retrieve **GET /api/identity/entities/{externalUserId}** and compare the returned subject, provenance and permitted profile with the intended record. Review screening enrolment separately. **INACTIVE** means screening is not active; **QUEUED** is not a completed clear check. Keep Myaza entity IDs and your external references distinct so later assessments use the endpoint’s expected subject form. ![Integration reference: Look up an entity](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-cf97046f545fa07d7a76/b08be73f054ede39f8c6d23564ff69bd/reference-identity-hub-45.jpg) *Integration reference: Look up an entity* ### Step 5: Import a larger book using the async job For a supported batch, call **POST /api/identity/entities/import** with 1–1,000 entries using the documented registration shape. Store the returned jobId from the 202 response. Poll the job’s progress until it settles, then inspect succeeded, failed and pending counts and the bounded failure details. An acknowledged import is not proof every row succeeded. ![Integration reference: Bulk import](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-f6605b1fe80fcec7704a/ce45eeb1ca85e6d395653bad0607f4c0/reference-identity-hub-46.jpg) *Integration reference: Bulk import* ### Step 6: Fix failures and connect the next capability Reconcile failed customer references against your source data before retrying them. Do not reimport successful records with fabricated values. Once a record exists, link capture using the same reference or send supported account activity. Explicit risk assessments and monitoring may need the returned entity ID and separate activation. Maintain customer-level errors and job evidence for support. ![Integration reference: Assess or monitor an existing customer](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-c9a4885d7d0e5371c827/0a62748475bf2280fbb50164b1cec806/reference-risk-intelligence-quickstart-68.jpg) *Integration reference: Assess or monitor an existing customer* ## Get more from the capability - Keep the customer reference stable when a profile name or email changes. - Use truthful external-verification provenance and retain the source so reviewers can understand the origin. - Treat wallet addresses as scoped risk attributes, not global identity keys or permission to transfer. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - One external reference resolves to the intended record. - Import acknowledgement and completion are distinct. - Failed rows remain explainable and retryable. - Imported claims retain truthful provenance. - A scoped recapture attaches to the existing customer. ## Troubleshooting ### A batch returned 202 but records are missing Read the durable job’s progress and failures instead of assuming acknowledgement means completion. ### A profile edit is refused Check which fields the contract allows for that verified record. Do not overwrite verified identity facts through a general patch. ### An assessment cannot find the customer Check the organisation, environment, reference type and entity ID expected by that endpoint. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/customers/individuals) [Read the integration reference](https://trust.myaza.co/documentation/identity-hub) - [Identity hub](https://trust.myaza.co/documentation/identity-hub) - [Risk intelligence quickstart](https://trust.myaza.co/documentation/risk-intelligence-quickstart) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Identity hub](https://trust.myaza.co/documentation/identity-hub) - [Risk intelligence quickstart](https://trust.myaza.co/documentation/risk-intelligence-quickstart) --- # How to add verification to your app with Myaza Trust Add verification to your app using a published workflow and a supported SDK or hosted session. Keep capture in the customer journey and final result handling on your backend. Key takeaways: - The app opens the intended published workflow. - Secret credentials stay on the backend. - Cancelled, expired and abandoned journeys are handled. - Final outcomes map to the intended customer. - Production uses its own workflow, keys and receiver. Add verification to your app using a published workflow and a supported SDK or hosted session. Keep capture in the customer journey and final result handling on your backend. ## Before you begin - A published workflow - The supported SDK or hosted session path - Your server retrieves the final result Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Publish the workflow customers should use Open **Configuration → Workflows** in Sandbox. Create or open the appropriate individual, business or scoped workflow. Configure only the required capture and review checks. Save and publish it, then copy the workflow ID. Check the organisation and environment before integrating. An unpublished workflow is not a reusable live capture configuration. ![Step 2: Individual verification workflow editor and its workflow ID](https://images.ctfassets.net/6x586jzal49f/solution-guide-workflow-editor/2fdf15980d29f92aa030a84bb8826d99/workflow-editor.jpg) *Dashboard example: Publish the workflow customers should use* ### Step 2: Choose embedded capture or a hosted session Use the Web SDK for browser capture, React Native or Flutter for native mobile capture, and a hosted session when you need a link for a known applicant. Native-only capabilities such as NFC require a native path. A shared workflow link serves many applicants; a server-created session belongs to one applicant. Choose the path that matches your product rather than mixing their credentials. ![Integration reference: Three ways to run a workflow](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-5bf08fe6f3cc6e24f543/3779ebedfe3b3cf4a3196d1f533a0689/reference-workflows-8.jpg) *Integration reference: Three ways to run a workflow* ### Step 3: Install the current supported SDK and configure capture Follow your platform’s SDK guide for the supported package, required peer dependencies and permission declarations. Pass **workflowId**, the matching publishable **apiKey** and your stable **userId**. The workflow supplies the configured steps; runtime subject data stays with your app. Check the documented minimum workflow-enabled version before troubleshooting a flow that ignores the workflow ID. ![Integration reference: Workflows](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-68f9a3d95ab73a3d09d7/367e7e02cf36e5874555ff05726818c5/reference-workflows-9.jpg) *Integration reference: Workflows* ### Step 4: For hosted capture, mint the session on your backend Call **POST /api/kyc/sessions** with a secret key, published workflow ID and intended externalUserId. Add permitted prefills and metadata only when needed. Store the session reference and give the private URL to the intended applicant. The workflow snapshot is fixed at mint time. Do not place a secret key in a WebView, mobile build or browser bundle. ![Integration reference: Request](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-22e57e1100aea0075f1a/bf77b91e383ef3c8bd5c3de0a632450d/reference-api-create-session-1.jpg) *Integration reference: Request* ### Step 5: Record submission, then wait for the outcome The SDK’s submission callback identifies a submission; it is not the final approval callback. Store its verification reference on your backend. Read current status with the permitted API or receive signed webhooks. Keep processing, human review, awaiting resubmission, error and final outcomes distinct. Read final **status** with **checkStatus** and its explanation, especially when decisioning waits on other checks. ![Integration reference: States](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-9453693b41325843a7f6/1dd2e8c023452b38d9eaa9921531a6df/reference-verifications-5.jpg) *Integration reference: States* ### Step 6: Test the full journey and prepare Production separately Use supported Sandbox IDs and fixtures for capture, status and receiver tests. Exercise permission refusal, abandonment, retries, Review and declines. For Production, obtain business approval, publish the Production workflow, configure environment-matched keys and result receiver, and review current pricing. A Sandbox pass validates your handling; it is not proof that every live check or device works. ![Integration reference: Checklist for going live](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-bc617abc11aa9a1725fd/a225e53d45fd64a4261691830ec26b60/reference-environments-99.jpg) *Integration reference: Checklist for going live* ## Get more from the capability - Let workflow publication change capture configuration without duplicating that configuration in every app build. - Use metadata for permitted correlation such as an application reference, keeping it within the documented bounds. - Test phone handoff, permission recovery and browser-hosted events for the journey you actually ship. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - The app opens the intended published workflow. - Secret credentials stay on the backend. - Cancelled, expired and abandoned journeys are handled. - Final outcomes map to the intended customer. - Production uses its own workflow, keys and receiver. ## Troubleshooting ### The SDK ignores workflowId Check the installed package version against the documented minimum and verify the published workflow. ### The workflow is not found Match key, organisation, environment and published ID. ### My app onboarded immediately after onSubmit Wait for final authoritative status instead of treating capture submission as approval. ## Open the setup and integration references [Open Workflows](https://trust.myaza.co/configure/workflows?create=1&template=standard-kyc&type=individual) [Read the integration reference](https://trust.myaza.co/documentation/quickstart) - [Quickstart](https://trust.myaza.co/documentation/quickstart) - [Sdks](https://trust.myaza.co/documentation/sdks) - [Sdk react](https://trust.myaza.co/documentation/sdk-react) - [Sdk react native](https://trust.myaza.co/documentation/sdk-react-native) - [Sdk flutter](https://trust.myaza.co/documentation/sdk-flutter) - [Hosted events](https://trust.myaza.co/documentation/hosted-events) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Quickstart](https://trust.myaza.co/documentation/quickstart) - [Sdks](https://trust.myaza.co/documentation/sdks) - [Sdk react](https://trust.myaza.co/documentation/sdk-react) - [Sdk react native](https://trust.myaza.co/documentation/sdk-react-native) - [Sdk flutter](https://trust.myaza.co/documentation/sdk-flutter) - [Hosted events](https://trust.myaza.co/documentation/hosted-events) --- # How to verify a business with Myaza Trust Verify a business against a supported registry and choose the additional evidence your onboarding policy needs. Company existence and authority to represent it are different questions. Key takeaways: - The business workflow uses the supported country and product. - Found, not-found, inactive and unknown-status fixtures are handled. - Owner due diligence can keep the final decision pending. - The final business outcome maps to the correct company reference. Verify a business against a supported registry and choose the additional evidence your onboarding policy needs. Company existence and authority to represent it are different questions. ## Before you begin - Business verification enabled for your organisation - Supported registry and required company details - Separate requirements for representatives and key people Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Check business access and country support Choose the intended organisation and Sandbox. Confirm that business verification is enabled and that the current country catalogue supports the required registry product. Read the business guide’s current availability rather than assuming every individual ID market also supports KYB. Keep the company and its representative as separate subjects. ![Integration reference: Products](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-b2f1d86c6f890a245b91/ad2412f8df746cffb99c3d55bd2431ea/reference-business-verification-96.jpg) *Integration reference: Products* ### Step 2: Create the business workflow Open **Configuration → Workflows → Create workflow** and choose **Business**, then a suitable **Standard KYB**, screening or enhanced-declarations template. Set its name and inspect the subject type. Select the country and supported business product. A production KYB capture requires a published business workflow; an individual workflow cannot silently become KYB. ![Step 1: Workflows in the example organisation, with Sandbox selected](https://images.ctfassets.net/6x586jzal49f/solution-guide-workflow-list/1be948252b740cf0faf6b32ccb29de5b/workflow-list.jpg) *Dashboard example: Create the business workflow* ### Step 3: Choose the application evidence Configure the offered company profile, documents, key people, applicant verification and declarations that your policy needs. Registry lookup establishes that a company exists; it does not establish that the submitter controls it. Where required, key people are screened and invited to their own identity-verification journeys. Treat corporate ownership-chain gaps as unresolved evidence, not as no beneficial owners. ![Integration reference: The application (optional layers)](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-7f53782c61139ae8b787/573c3d043d04dd97c82417a64ca2bb94/reference-business-verification-97.jpg) *Integration reference: The application (optional layers)* ### Step 4: Review the business decision gates Inspect the decision graph for company status and the relevant key-people completion requirements. Decide whether the journey must wait for due diligence before acceptance. A registry-check completion can precede the overall workflow decision. Keep unavailable registry evidence, inactive company status and incomplete owner review distinct. Do not approve a company solely because a document was uploaded. ![Integration reference: Decisioning fields](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-d7c8f0020f3f139b337d/bc6464aeabe71736f6ab687f1dbfe889/reference-business-verification-98.jpg) *Integration reference: Decisioning fields* ### Step 5: Save and publish the workflow Wait for **Saved** in the editor. Select **Publish**, or **Publish changes** for an existing workflow. In the publication review, check the target environment, workflow ID, price and readiness warnings. Resolve blocking issues, then confirm **Publish version**. A saved draft is not the version customers use. New sessions use the new published snapshot; sessions already created retain their original version. ![Step 8: Publication review showing the target environment, workflow ID and confirmation button](https://images.ctfassets.net/6x586jzal49f/solution-guide-workflow-publish-review/bc49f1bbace1e12fe91ca244a111a42d/workflow-publish-review.jpg) *Dashboard example: Save and publish the workflow* ### Step 6: Send the company its published journey Embed the business workflow through a supported SDK or use a customer-bound hosted session. Bind externalUserId to the business reference; prefilled business details remain claims until checked. For the session’s business block, follow the documented country and registration fields. Return the link to the intended applicant and retain the workflow version. ![Integration reference: Request body](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-e023a34994aa42a86b82/1d89fd0819eab90923eac0473ca23832/reference-api-create-session-2.jpg) *Integration reference: Request body* ### Step 7: Read both the checks and final workflow outcome Inspect the business result, registry record and current verification status. Confirm whether the workflow is still waiting for owner or representative due diligence. Store the overall decision and explanation in your business onboarding record. Use the latest authoritative status or workflow result rather than granting access from the first registry notification. Your application still enforces the onboarding decision. ![Integration reference: Status and checkStatus](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-a3e6a3047d3e79b1398d/f10e55c302c0f1cbe58d25b7c0e4f932/reference-verifications-4.jpg) *Integration reference: Status and checkStatus* ## Get more from the capability - Require representative verification where your onboarding policy needs authority beyond company existence. - Use owner and corporate-shareholder evidence with its source and unresolved-chain state. - Test country and product differences with the current catalogue instead of hardcoding a global registry promise. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - The business workflow uses the supported country and product. - Found, not-found, inactive and unknown-status fixtures are handled. - Owner due diligence can keep the final decision pending. - The final business outcome maps to the correct company reference. ## Troubleshooting ### Business verification is unavailable Check organisation access and the supported registry product for that country. ### The registry check passed but onboarding is waiting Inspect the workflow’s owner, representative and decisioning requirements. ### The representative changed a prefilled name Prefill is a convenience, not verified evidence. Use the registry and captured checks as the source of result. ## Open the setup and integration references [Open Workflows](https://trust.myaza.co/configure/workflows?create=1&template=standard-kyb&type=business) [Open the dashboard tool](https://trust.myaza.co/verification/spot-checks/business) [Read the integration reference](https://trust.myaza.co/documentation/business-verification) - [Business verification](https://trust.myaza.co/documentation/business-verification) - [Key people](https://trust.myaza.co/documentation/key-people) - [Countries](https://trust.myaza.co/documentation/countries) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Business verification](https://trust.myaza.co/documentation/business-verification) - [Key people](https://trust.myaza.co/documentation/key-people) - [Countries](https://trust.myaza.co/documentation/countries) --- # How to check and monitor address presence with Myaza Trust Verify address presence over a supported observation window. Configure capture, native reporting and customer consent together, then distinguish verified, failed and inconclusive evidence. Key takeaways: - A zero-progress watch has a clear diagnostic path. - Permission refusal has a usable foreground or recovery route. - Verified, failed and inconclusive remain distinct. - Sandbox’s compressed clock is not presented as live residency proof. - Recurring cost and opt-out are clear before ongoing activation. Verify address presence over a supported observation window. Configure capture, native reporting and customer consent together, then distinguish verified, failed and inconclusive evidence. ## Before you begin - An eligible address and customer consent - Supported mobile location declarations - Separate opt-in for ongoing monitoring Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Start with an eligible captured address Confirm that the customer has an authorised address pin and stable external reference. In the workflow, enable **Address Intelligence** and open **Presence**. Choose the supported observation window and evidence requirements. A one-time pin is a claim; a multi-day watch needs separate reporting. Business premises and unsupported browser-only journeys cannot be assumed to produce residential presence evidence. ![Integration reference: Presence verification](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-63a67563e3670ba32dc5/ee3083ec6edffdbb754048e5b069d187/reference-address-intelligence-24.jpg) *Integration reference: Presence verification* ### Step 2: Configure the native app and consent journey Use the React Native or Flutter SDK and pass the stable userId. Follow the background-location setup for your platform when enabling background reporting. React Native needs the documented registration at startup; Flutter needs its manifest and Info.plist declarations. Explain what is collected and let the person control permission. Enabling the workflow switch without app setup leaves the watch short of evidence. ![Integration reference: What your app has to do](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-b328c769cc074bbf1a06/62f17239d7e8f129206af96ec7c972e1/reference-address-intelligence-25.jpg) *Integration reference: What your app has to do* ### Step 3: Report presence on app open and foreground return Call the documented presence reporter for the same externalUserId whenever the app opens or returns to the foreground. This supports people who decline background permission and phones whose battery manager restricts it. Do not fabricate coordinates or substitute an IP location. If capture occurred in a hosted flow, use the documented authorised pin handoff before expecting native reports. ![Integration reference: Reporting presence from your app](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-6a3618bfdfdbdc1d5a72/1d5278a54092b74008ece2b6094fb924/reference-address-intelligence-26.jpg) *Integration reference: Reporting presence from your app* ### Step 4: Check the watch’s actual progress Open **Risk Intelligence → Address verification** or retrieve **GET /api/kyc/address/presence/{externalUserId}**. Inspect status, progress, observation counts and evidence tier. The endpoint does not return the pin. A watch stuck at zero needs reporting recovery. Use the reporter’s reason and the SDK’s presenceStatus to distinguish missing pin, permission denial and disabled location services. ![Integration reference: Showing the person where the check stands](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-aa694e3aebf9372853eb/45e4750bed99cfae67b615f28015bbc7/reference-address-intelligence-27.jpg) *Integration reference: Showing the person where the check stands* ### Step 5: Interpret the three resolution paths **verified** means the recorded evidence met the policy. **failed** requires an integrity contradiction such as spoofed-location evidence; absence is not a failure. **inconclusive** means the window ended without enough evidence and is not a finding against the customer. Use signed result events to update your application. A background-only requirement must inspect the appropriate policy evidence rather than relying on total days alone. ![Integration reference: Holding the decision on it](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-4095f213f70336274489/63813a20654591e5c70e6f0263c50cc6/reference-address-intelligence-28.jpg) *Integration reference: Holding the decision on it* ### Step 6: Enable ongoing cycles only when intended For ongoing address confirmation, review the separate always-on setting, cadence and annual price. Supported cadence choices are 30, 60, 90, 180 or 365 days. Keep a one-off presence check distinct from annual renewal coverage. Turning renewal off stops future cycles; a check already in flight still has its own resolution. Review permissions and revocation recovery in your app. ![Integration reference: Always-on monitoring](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-a0b7a179c56f557d7f99/b2cca54f8becae94601043c6386233a6/reference-address-intelligence-29.jpg) *Integration reference: Always-on monitoring* ## Get more from the capability - Use foreground reporting as a supported fallback and show the evidence tier actually running. - Test manufacturer battery restrictions on the Android devices your customers use; use the documented foreground-service option only with explicit setup and customer-visible disclosure. - Show progress without exposing the person’s address or live whereabouts. Notify through your app when the recorded outcome changes. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - A zero-progress watch has a clear diagnostic path. - Permission refusal has a usable foreground or recovery route. - Verified, failed and inconclusive remain distinct. - Sandbox’s compressed clock is not presented as live residency proof. - Recurring cost and opt-out are clear before ongoing activation. ## Troubleshooting ### Days stay at zero Check reporter calls, stored pin, user reference, services and permissions. ### Background reporting stopped Read the actual SDK status and use the supported settings recovery path. Do not keep labelling it background. ### The window was inconclusive Collect the missing permitted evidence or offer the appropriate next journey. Absence does not prove a false address. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/risk-intelligence/presence) [Read the integration reference](https://trust.myaza.co/documentation/address-intelligence) - [Address intelligence](https://trust.myaza.co/documentation/address-intelligence) - [Background location declarations](https://trust.myaza.co/documentation/background-location-declarations) - [Monitoring policies](https://trust.myaza.co/documentation/monitoring-policies) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Address intelligence](https://trust.myaza.co/documentation/address-intelligence) - [Background location declarations](https://trust.myaza.co/documentation/background-location-declarations) - [Monitoring policies](https://trust.myaza.co/documentation/monitoring-policies) --- # How to review existing customer addresses with Myaza Trust Review the address evidence already in your customer book. Upload a permitted CSV, inspect the quote and use the resulting grades to choose the right next capture or presence check. Key takeaways: - The quote matches the intended row count and choice. - Previewed and ungraded rows remain distinct. - Export retains the row-level grades. - Recapture attaches to the intended existing customer. - A pending job is not presented as a completed audit. Review the address evidence already in your customer book. Upload a permitted CSV, inspect the quote and use the resulting grades to choose the right next capture or presence check. ## Before you begin - An authorised customer book in the supported format - Available existing address evidence - Review the quote before running triage Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Prepare the customer-address CSV Create a UTF-8 CSV with an **address** column. Add **customer_id**, **name** and **country** where supported and known. Use the customer reference your organisation actually holds; preserve the original address text rather than inventing a structured house number. Review the current file and row limits in the upload requirements. Include only records your team is permitted to process. ![Integration reference: Book triage](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-4516d669fd75d46c24cf/398cc060db7e9ce2270e461d5fd94eca/reference-address-intelligence-21.jpg) *Integration reference: Book triage* ### Step 2: Open Address checks in the right workspace Select the organisation and environment, then open **Risk Intelligence → Address checks**. The retained route is address-triage. Choose **Sample CSV** to inspect the current format before using **Upload addresses**. Read access and upload-management access are separate. If uploading is unavailable, ask for the correct role rather than sending the book to another workspace. ![Integration reference: Book triage](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-4516d669fd75d46c24cf/398cc060db7e9ce2270e461d5fd94eca/reference-address-intelligence-21.jpg) *Integration reference: Book triage* ### Step 3: Review the quote before choosing a run After selecting the file, inspect the parsed row count, file issues and current quote. Choose the free first-row grading option when offered or the full-book option at its displayed amount. If the entire book fits the free sample, the interface offers that complete choice. Resolve insufficient credit or malformed columns before running a paid full-book job. ![Integration reference: Book triage](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-4516d669fd75d46c24cf/398cc060db7e9ce2270e461d5fd94eca/reference-address-intelligence-21.jpg) *Integration reference: Book triage* ### Step 4: Wait for the job and inspect row coverage Open the resulting run and monitor progress until its recorded result is ready. Distinguish graded rows from rows that remain ungraded after a preview. Review the distribution and each row’s actual grade. A pending or failed job is not a finished address audit, and a small preview cannot establish the quality of the unprocessed remainder. ![Integration reference: Book triage](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-4516d669fd75d46c24cf/398cc060db7e9ce2270e461d5fd94eca/reference-address-intelligence-21.jpg) *Integration reference: Book triage* ### Step 5: Choose remediation from the recorded grade **corroborated** means held evidence agrees at locality level. **confirmable_in_app** points to a short recapture journey for an existing customer relationship. **needs_presence** points to a presence-verification path. Use the run’s available remediation controls and the customer’s correct reference. A triage grade is a next-step recommendation; it does not independently prove residency or complete a new verification. ![Integration reference: Address-only workflows (standalone, no identity step)](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-7527ff6cee5873677127/0a1cb2bb8235eb61d55b9fea2f86466d/reference-address-intelligence-22.jpg) *Integration reference: Address-only workflows (standalone, no identity step)* ### Step 6: Export and follow through on the next capture Use the supported CSV export to preserve the row-level plan. Send an address-scoped session to the intended customer or add the relevant address step to their next workflow. Multi-day presence still needs the native app’s reporter and consent setup. Review later capture results against the original plan rather than treating the upload itself as proof that every gap is fixed. ![Integration reference: Stability, re-attestation and the certificate](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-f37ce0abbef97f7162be/5bac39515cc215bd0dabc3b448a2a9fa/reference-address-intelligence-23.jpg) *Integration reference: Stability, re-attestation and the certificate* ## Get more from the capability - Run an offered preview before committing to a large paid run, then keep the ungraded population explicit. - Use stable customer IDs so remedial capture lands on the right existing record. - Prioritise the grade-specific next step instead of forcing every customer through an unnecessary full KYC journey. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - The quote matches the intended row count and choice. - Previewed and ungraded rows remain distinct. - Export retains the row-level grades. - Recapture attaches to the intended existing customer. - A pending job is not presented as a completed audit. ## Troubleshooting ### The CSV will not parse Compare its columns and encoding with Sample CSV and inspect the returned file issues. ### The full run is disabled Check the quote and balance. A free preview may still be available. ### The customer needs another capture Use an address-only session with their external reference; triage cannot invent missing evidence. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/risk-intelligence/address-triage) [Read the integration reference](https://trust.myaza.co/documentation/address-intelligence) - [Address intelligence](https://trust.myaza.co/documentation/address-intelligence) - [Identity hub](https://trust.myaza.co/documentation/identity-hub) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Address intelligence](https://trust.myaza.co/documentation/address-intelligence) - [Identity hub](https://trust.myaza.co/documentation/identity-hub) --- # How to manage crypto destinations and observations with Myaza Trust Keep crypto destination records and deposit observations organised. A deposit observation, customer match, screening decision and settled funds are separate facts. Key takeaways: - An unchanged deposit retry creates no duplicate observation. - A matching existing transaction creates no second assessment charge. - A chain reorganisation preserves history and reaches the backend. - Address control and approval remain separate. - A matched deposit does not automatically credit funds. Keep crypto destination records and deposit observations organised. A deposit observation, customer match, screening decision and settled funds are separate facts. ## Before you begin - Crypto-operation permissions - Reviewed provider and address evidence where required Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Open the crypto transaction workspace Open **Risk Intelligence → Fraud Monitoring → Transactions → Crypto** in the intended environment. Use **Crypto transfers**, **Unmatched deposits**, **Crypto Provider Directory** and **Address book** for their different jobs. Read permission allows inspection; reviews require management permission. Check the current network catalogue for supported record syntax, without assuming it supplies an automatic blockchain feed. ![Integration reference: What each record means](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-a8994d5be5eda55b9627/9382df7130ff3cf742699111a2d19171/reference-crypto-operations-74.jpg) *Integration reference: What each record means* ### Step 2: Connect your deposit-observation feed Open **Integration guide → Deposit feed**, choose your stack and follow the exact backend request. **POST /api/v1/crypto/deposits** needs a secret key and Idempotency-Key. Include the stable deposit reference, network, transaction hash, output index, destination, exact decimal-string amount, currency, confirmations and observedAt. Keep the original chain output identity stable across retries. ![Integration reference: Report and match a deposit](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-8ee311f36e257a874bba/a3795ba31640478a9bb11fd158421a1b/reference-crypto-operations-75.jpg) *Integration reference: Report and match a deposit* ### Step 3: Review unmatched observations Open **Unmatched deposits**. Find the reported deposit by its reference and check the network, asset, amount and chain-output evidence. An observation does not yet identify a customer or contain a completed risk decision. Use **Needs review** or **Ignored** with a reason when your evidence cannot justify a match. Do not credit a customer merely because an observation arrived. ![Integration reference: Report and match a deposit](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-8ee311f36e257a874bba/a3795ba31640478a9bb11fd158421a1b/reference-crypto-operations-75.jpg) *Integration reference: Report and match a deposit* ### Step 4: Match the customer or link an existing transaction Choose **Match customer** to select the known customer and run the existing assessment and pricing. Choose **Link existing transaction** only for a matching assessed inbound crypto transaction; amount, currency, network, hash, output and destination must agree. Linking does not create another assessment charge. Keep the current version and reason with the operation and reuse unchanged retry keys after a timeout. ![Integration reference: Report and match a deposit](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-8ee311f36e257a874bba/a3795ba31640478a9bb11fd158421a1b/reference-crypto-operations-75.jpg) *Integration reference: Report and match a deposit* ### Step 5: Manage destination records with separate reviews In **Address book**, save an authorised destination and review its status deliberately. Screen it through **Screen address** when required. Use **Verify control** only for supported personal wallets and ask for a signed message, never a key or recovery phrase. A control proof establishes key control at that time, not legal ownership. Approval, screening and provider discovery do not replace one another. ![Integration reference: Screen, edit or verify control](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-385a96dac16392b8ede4/43ed451dde8f2b7d96faba5cc89e8b32/reference-crypto-operations-73.jpg) *Integration reference: Screen, edit or verify control* ### Step 6: Handle confirmations, reorganisations and enforcement Update deposit confirmations or report a chain reorganisation through the documented API. Reorganisation retains historical assessment evidence rather than silently erasing the match. Subscribe to supported deposit and address events, verify signatures and deduplicate by logical event ID. Your backend checks chain finality and current clearance before funds movement or credit; a matched observation is not settlement. ![Integration reference: Webhooks and safe retries](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-3397781f22312f7d4a3e/d15c8b2a417981467df1d3eb108af3c5/reference-crypto-operations-76.jpg) *Integration reference: Webhooks and safe retries* ## Get more from the capability - Use stable output indices to avoid treating two outputs from one chain transaction as the same deposit. - Keep team reviews scoped to your organisation without changing platform-sourced provider evidence. - Use address reveal only with the appropriate permission and recorded reason; masked records are the default. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - An unchanged deposit retry creates no duplicate observation. - A matching existing transaction creates no second assessment charge. - A chain reorganisation preserves history and reaches the backend. - Address control and approval remain separate. - A matched deposit does not automatically credit funds. ## Troubleshooting ### The deposit has no customer Resolve the relationship using authorised evidence; an observation is not automatic attribution. ### A review save conflicts Reload the current record version and reconcile any earlier uncertain operation. ### An institution lookup is unknown Discovery coverage is separate from ownership and screening. Do not infer self-hosting or approval. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/risk-intelligence/fraud-prevention/transactions/crypto) [Read the integration reference](https://trust.myaza.co/documentation/crypto-operations) - [Crypto operations](https://trust.myaza.co/documentation/crypto-operations) - [Travel rule self hosted](https://trust.myaza.co/documentation/travel-rule-self-hosted) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Crypto operations](https://trust.myaza.co/documentation/crypto-operations) - [Travel rule self hosted](https://trust.myaza.co/documentation/travel-rule-self-hosted) --- # How to set up Travel Rule exchange with Myaza Trust Set up your organisation’s Travel Rule information-exchange journey, test it and follow the displayed activation requirements. Every transfer still needs its own current authorisation before your wallet acts. Key takeaways: - Missing beneficiary data is collected against the original transaction. - Potential and confirmed sanctions have separate Review and stopped paths. - Lost responses reuse the same request and route. - A test fixture is never treated as live institution proof. - Real wallet enforcement is qualified before live release. Set up your organisation’s Travel Rule information-exchange journey, test it and follow the displayed activation requirements. Every transfer still needs its own current authorisation before your wallet acts. ## Before you begin - Institution and company review - Supported TRP connection and secure-channel setup - Current policy and applicable information requirements Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Open the saved setup for your environment Choose Sandbox and open **Fraud Monitoring → Transactions → Travel Rule → Set up Travel Rule**. The setup is saved for the organisation and environment so you can return later. Review the current supported connection path. New R1 setup uses managed **TRP** connections; do not select a parked legacy connector as a new activation route. ![Integration reference: Get started in the dashboard](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-581db41f28b5d9b9c6cd/2e9a9015775b0577e11f65c8c962962e/reference-travel-rule-setup-90.jpg) *Integration reference: Get started in the dashboard* ### Step 2: Confirm the company you represent In **Your company**, select the correct company and inspect its verification and directory evidence. A company selection is not approval to represent it. Myaza’s independent review confirms representation. If the company is missing, use the supported request process for a platform administrator to add its sourced record; do not enter another company just to finish setup. ![Integration reference: Your company and your counterparties](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-9aa1152311ada4bff507/84af8398754f7490ba8fed40df5bcc85/reference-travel-rule-setup-91.jpg) *Integration reference: Your company and your counterparties* ### Step 3: Choose and review the destination institutions In **Institutions**, select the counterparties your organisation intends to exchange with and save. Review each offered connection’s actual readiness. **Awaiting institution**, **Messaging setup needed** and a connected route are different states. A directory listing or successful identity test cannot establish a working information channel. Existing active destinations keep their own state when you request another institution. ![Integration reference: Add another destination](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-77bf4eec9e425fce6062/398aa00cc8b95420f46dc6a2410ebed6/reference-travel-rule-setup-92.jpg) *Integration reference: Add another destination* ### Step 4: Publish the applicable transfer rules Open the linked **Fraud rules → Travel Rule** controls. Install a suitable rule or create a draft, then inspect the scope, network, information requirements and deliberate authorisation opt-ins. Save and **Publish draft** separately. Country qualification and execution requirements remain additional checks. A threshold amount alone does not enable funds movement. ![Integration reference: Test in Sandbox](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-dbecb5338679004e59b0/83afeaa4f5d88c80abae9e706533bdd5/reference-travel-rule-testing-94.jpg) *Integration reference: Test in Sandbox* ### Step 5: Connect your application and result receiver In **Your application**, follow the exact server integration and configure the result webhook. Verify raw-body signatures, deduplicate events and recover failed delivery. Your backend asks the sender only for fields the current transfer reports missing. An unknown wallet address does not identify the recipient or their exchange; obtain the institution or supported Travel Address rather than guessing. ![Integration reference: What if you only have a wallet address?](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-81dccccccfe8583b57dd/efa519478df1701c7064c75f465165f3/reference-travel-rule-setup-93.jpg) *Integration reference: What if you only have a wallet address?* ### Step 6: Test, request activation and retain holds In **Test and activate**, run the offered connection check, send the supported Sandbox transaction and provide its external activity reference. Complete the displayed requirements and request activation when eligible. Independent review checks more than a saved webhook count. After activation, each transfer still needs fresh clearance and the required single-use backend authorisation. Your wallet moves or credits funds; Myaza does not. ![Integration reference: Acceptance checklist](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-e760ecc77ec349dcbb4a/74b282e617a9fa60cfc0d7f11a2cc47c/reference-travel-rule-testing-95.jpg) *Integration reference: Acceptance checklist* ## Get more from the capability - Add destinations through Institutions so their readiness remains distinct from your organisation’s overall setup. - Use a supported Travel Address to select an already approved route where available, rather than treating a blockchain address as an exchange address. - Keep self-hosted wallet controls separate from institution exchange. Do not label an unknown wallet self-hosted to avoid requirements. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - Missing beneficiary data is collected against the original transaction. - Potential and confirmed sanctions have separate Review and stopped paths. - Lost responses reuse the same request and route. - A test fixture is never treated as live institution proof. - Real wallet enforcement is qualified before live release. ## Troubleshooting ### A selected institution is awaiting connection Its route still needs preparation or review. Keep the affected transfer held; other ready destinations have separate state. ### A webhook is configured but activation is blocked Check the displayed requirements, actual signature verification, delivery recovery and wallet enforcement evidence. ### A response was lost Retain the prepared route, body and request key. Reconcile the existing operation before sending another message. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/risk-intelligence/rules-policies/travel-rule) [Read the integration reference](https://trust.myaza.co/documentation/travel-rule-setup) - [Travel rule setup](https://trust.myaza.co/documentation/travel-rule-setup) - [Travel rule](https://trust.myaza.co/documentation/travel-rule) - [Travel rule testing](https://trust.myaza.co/documentation/travel-rule-testing) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Travel rule setup](https://trust.myaza.co/documentation/travel-rule-setup) - [Travel rule](https://trust.myaza.co/documentation/travel-rule) - [Travel rule testing](https://trust.myaza.co/documentation/travel-rule-testing) --- # How to screen a crypto wallet with Myaza Trust Screen a saved crypto wallet and inspect the decision with its exposure evidence. Keep blockchain risk, address approval, signed control and transfer clearance separate. Key takeaways: - The selected network belongs to the actual wallet. - Sandbox results remain labelled. - Failed and completed decisions are distinct. - An unchanged retry cannot create another charge. - Risk-only Allow does not release funds. Screen a saved crypto wallet and inspect the decision with its exposure evidence. Keep blockchain risk, address approval, signed control and transfer clearance separate. ## Before you begin - Wallet address with explicit network context - An available screening provider and current price Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Open the crypto Address book Choose the correct organisation and Sandbox. Open **Risk Intelligence → Fraud Monitoring → Transactions → Crypto**, then the **Address book** view. You need the supported crypto read access; screening actions require management access and available product access. Use the current network catalogue rather than guessing a chain from the appearance of an address. ![Integration reference: Crypto operations](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-bdda54b66627de8681e4/307ef6d21d681b8beeebe5a56272698d/reference-crypto-operations-71.jpg) *Integration reference: Crypto operations* ### Step 2: Save the destination with its network context Use the address-book add action and provide the permitted wallet address, selected network and label. Link the intended customer or provider only when your evidence supports that relationship. Check the address format and asset precision. A saved destination starts as a claimed record for review; it does not prove ownership and does not automatically run screening. ![Integration reference: Crypto provider reviews and address book](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-c45ecb482148c810bfa1/4288665df6f70350a70df1a386896510/reference-crypto-operations-72.jpg) *Integration reference: Crypto provider reviews and address book* ### Step 3: Open the saved wallet’s screening tool Open the address details and select **Screen address**. Review the displayed current screening price and the available check. Keep the address and network unchanged for this run. If credit or provider access is missing, resolve that state before expecting a decision. Running, waiting for credit, failed and completed are different states; none should be replaced with a clear label. ![Integration reference: Screen, edit or verify control](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-385a96dac16392b8ede4/43ed451dde8f2b7d96faba5cc89e8b32/reference-crypto-operations-73.jpg) *Integration reference: Screen, edit or verify control* ### Step 4: Inspect the risk decision and exposure Read the completed wallet result and its supporting exposure details. Where available, review sanctions exposure, category, direct versus indirect exposure and the recorded path distance. Use the actual returned evidence; the Solutions page’s 92/100 example is deterministic demonstration data, not a promised score. A low score does not establish legal ownership or permit a transfer on its own. ![Integration reference: Types](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-b6f274715a2630631168/9dc7b87f2d6eaa8051ed01be84d1d99c/reference-screening-67.jpg) *Integration reference: Types* ### Step 5: Integrate saved-address screening on your server Use the documented **POST /api/v1/crypto/addresses/{id}/screening** action with a secret key, its required body and an **Idempotency-Key**. Read the current address and request schema before sending. Keep the original key and unchanged body after a timeout, and reconcile current state instead of starting another paid check blindly. Store the assessment reference against the saved address and network. ![Integration reference: Screen, edit or verify control](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-385a96dac16392b8ede4/43ed451dde8f2b7d96faba5cc89e8b32/reference-crypto-operations-73.jpg) *Integration reference: Screen, edit or verify control* ### Step 6: Use the result in your review and payment policy Record the screening result with its checked time and apply the authorised decision to the intended action. Keep address approval and control proof separate: a passed screen does not approve the address for your team. Crypto funds release needs its separately qualified clearance and wallet-enforcement path. Keep pending, failed or unavailable checks on hold according to your policy. ![Integration reference: Enforcing decisions in your app](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-908ffbd67ae37905790e/466525f2117fb04945695a42005a0b91/reference-crypto-operations-70.jpg) *Integration reference: Enforcing decisions in your app* ## Get more from the capability - Screen the actual sender for inbound crypto and recipient for outbound crypto when submitting a complete transaction. - Review direct and indirect exposure separately where the result supports that distinction. - Re-screen deliberately when freshness or changed evidence requires it, with the current price and a genuine new operation. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - The selected network belongs to the actual wallet. - Sandbox results remain labelled. - Failed and completed decisions are distinct. - An unchanged retry cannot create another charge. - Risk-only Allow does not release funds. ## Troubleshooting ### Screening is waiting for credit Review the current rate and available credit; opening details does not complete a paid check. ### No crypto provider was found Discovery has different coverage from screening. Unknown discovery does not mean self-hosted. ### Screening passed but transfer remains held Check information requirements, approved routes, country review and current clearance separately. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/risk-intelligence/fraud-prevention/transactions/crypto) [Read the integration reference](https://trust.myaza.co/documentation/screening) - [Screening](https://trust.myaza.co/documentation/screening) - [Webhook wallet intelligence](https://trust.myaza.co/documentation/webhook-wallet-intelligence) - [Crypto operations](https://trust.myaza.co/documentation/crypto-operations) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Screening](https://trust.myaza.co/documentation/screening) - [Webhook wallet intelligence](https://trust.myaza.co/documentation/webhook-wallet-intelligence) - [Crypto operations](https://trust.myaza.co/documentation/crypto-operations) --- # How to measure decision quality with Myaza Trust Measure decision quality using confirmed downstream outcomes. Review fraud capture and customer friction without silently changing the live policy. Key takeaways: - An identical outcome replay has no duplicate effect. - A changed payload with the same key conflicts. - Incomplete coverage remains visible. - A shadow experiment does not change live decisions. - A published improvement has a traceable version. Measure decision quality using confirmed downstream outcomes. Review fraud capture and customer friction without silently changing the live policy. ## Before you begin - Recorded decisions and supported outcome feedback - Authorised reporting access Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Open Effectiveness and choose a meaningful window Open **Risk Intelligence → Effectiveness** with authorised reporting access. Select the evidence window you intend to evaluate. Check whether the report has enough confirmed outcomes and whether the response says its completeness is valid. **Not enough evidence** is an honest state; it is not zero fraud or perfect performance. ![Integration reference: Understand the four headline measures](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-9913ea0bd415af5a8685/007264450ac1bc481b50f3e263922bee/reference-decision-effectiveness-60.jpg) *Integration reference: Understand the four headline measures* ### Step 2: Record what happened after the decision From your backend, send **POST /api/v1/transactions/{transactionId}/outcomes** with the final known classification and source. Use **fraud**, **legitimate** or **inconclusive** according to evidence. Include occurredAt and a reason code. Include externalUserId as the ownership guard when available. Keep an idempotency key stable for that exact outcome. Do not label an unresolved chargeback as confirmed fraud. ![Integration reference: Record an outcome](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-7b0031edf6cc9a9e7d9e/a26cf530b3cd594ec519a3e7f526190c/reference-decision-effectiveness-61.jpg) *Integration reference: Record an outcome* ### Step 3: Keep exact financial evidence with the outcome When recording loss and recovery, send the applicable currency and exact decimal-string amounts. Keep the confirmed financial event separate from a risk score. Store the transaction and decision reference so the label can be traced to the control that acted. Outcome records are append-only; an unchanged replay is safe, but reusing the key for different data is refused. ![Integration reference: Record an outcome](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-7b0031edf6cc9a9e7d9e/a26cf530b3cd594ec519a3e7f526190c/reference-decision-effectiveness-61.jpg) *Integration reference: Record an outcome* ### Step 4: Read coverage before judging performance Review **Outcome coverage** first, then **Fraud captured**, **Precision** and **False positives**. Low outcome coverage weakens what the other measures can establish. Compare supported segments and per-rule evidence within the same period. Do not call every blocked payment prevented fraud or compare incomplete periods as if they had equal labels. ![Integration reference: Read effectiveness evidence](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-50897b75c76f0fbe9a20/da8a103d3d58a3b8b96abd1314805f1c/reference-decision-effectiveness-62.jpg) *Integration reference: Read effectiveness evidence* ### Step 5: Inspect drift and recommendations Review supported drift, reliability and threshold recommendations. Read the evidence and the next action, not only a headline score. Recommendations cannot change customer decisions automatically. Bounded relationship evidence can help investigation, but it does not replace labelled outcomes. An apparent change may reflect a different customer mix or missing labels rather than a worse control. ![Integration reference: Drift, check reliability and relationships](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-48be49af67a998d08edd/a0f4c6a0e8144fee48cb9fb67c970466/reference-decision-effectiveness-59.jpg) *Integration reference: Drift, check reliability and relationships* ### Step 6: Test and publish a deliberate improvement Backtest the proposed rule or threshold against suitable historical evidence. Use an available shadow experiment when you need comparison without changing live decisions. Review the result, then publish a separately governed rule or system version when authorised. Keep the earlier version and decisions intact. Continue recording outcomes so the improvement can be assessed after activation. ![Integration reference: Safe optimisation](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-717c686619ce6a46a33b/b52f2f17c119706430d3977dcfe3bc4e/reference-decision-effectiveness-63.jpg) *Integration reference: Safe optimisation* ## Get more from the capability - Automate final dispute and confirmed case outcomes with stable external event IDs, rather than manually labelling only memorable fraud cases. - Track legitimate interrupted customers alongside confirmed fraud to reveal friction. - Review completeness and coverage for every window before using the figures for a policy change. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - An identical outcome replay has no duplicate effect. - A changed payload with the same key conflicts. - Incomplete coverage remains visible. - A shadow experiment does not change live decisions. - A published improvement has a traceable version. ## Troubleshooting ### All metrics say not enough evidence Record confirmed outcomes and check the supported window and completeness; do not fill gaps with invented zeroes. ### An outcome ownership guard conflicts Check that externalUserId belongs to the resolved transaction. ### A recommendation did not change the policy That is expected. Review, testing and separate publication are required. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/risk-intelligence/effectiveness) [Read the integration reference](https://trust.myaza.co/documentation/decision-effectiveness) - [Decision effectiveness](https://trust.myaza.co/documentation/decision-effectiveness) - [Investigations](https://trust.myaza.co/documentation/investigations) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Decision effectiveness](https://trust.myaza.co/documentation/decision-effectiveness) - [Investigations](https://trust.myaza.co/documentation/investigations) --- # How to review and investigate findings with Myaza Trust Turn a risk alert into owned casework. Review the reason, gather evidence and record a clear outcome that the next team member can understand. Key takeaways: - The case stays scoped to the intended customer. - The assignee can inspect the linked evidence. - Every material transition records its rationale. - Resolution and closure remain distinguishable. - A report retains the external filing reference when actually submitted. Turn a risk alert into owned casework. Review the reason, gather evidence and record a clear outcome that the next team member can understand. ## Before you begin - Investigation permissions and a review owner - Available alert or case evidence Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Open the relevant review queue Choose your organisation and environment, then open **Risk Intelligence → Investigations** and the alert or Review Queue view offered there. Search or filter for the known customer and finding. An alert is a triage signal; it is not yet a complete investigation. Check permission to read the alert before opening evidence. ![Integration reference: Alerts](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-dc80f16f74e20d9ea9a8/7efa4a7ed92cc7832d59ca5f850aa9f6/reference-investigations-50.jpg) *Integration reference: Alerts* ### Step 2: Read the decision and its strongest reasons Open the alert. Check the customer, activity or transaction reference, decision, risk score and reasons. Inspect the linked rule and screening evidence. Record what is actually known and what still needs confirmation. A Review or Block result explains the control’s response; it does not automatically establish criminal conduct or require a regulatory filing. ![Integration reference: Alert or investigation?](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-a45535ac36539ef854c6/cfb8dc28050140fdf1468b9d57ec1a00/reference-investigations-51.jpg) *Integration reference: Alert or investigation?* ### Step 3: Open an investigation when more work is needed Select **Open investigation** and choose an investigator from your organisation. You can group multiple alerts only when they belong to the same entity. Check the linked alerts and owner before saving. Use a concise investigation purpose so the assignee knows the question to resolve, rather than copying every alert detail into an unexplained case title. ![Integration reference: Opening and assigning an investigation](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-ea850a9f632122a27265/15c41ad0b272685ffda59b354ea15942/reference-investigations-49.jpg) *Integration reference: Opening and assigning an investigation* ### Step 4: Work the evidence and timeline Use the investigation workspace to inspect State, Why, Evidence, Owner and Next action. Add permitted supporting evidence and record review notes. Move through review or escalation with a rationale. Keep the original decision evidence available. New evidence belongs in the timeline; do not edit source records solely to make an investigation appear consistent. ![Integration reference: Investigation workspace](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-1dc2e1591c59ee62aaf1/a37ee3866c56ec57394342fa372c5c7c/reference-investigations-47.jpg) *Integration reference: Investigation workspace* ### Step 5: Resolve the finding, then close operational work Choose the appropriate cleared, confirmed, escalated or inconclusive outcome and record the analyst rationale. **Resolve** records the evidence-backed outcome; **Close** completes the operational work. Reopening retains the earlier timeline. Clearing the investigation does not approve the customer’s KYC or confirm that funds moved. Apply any customer action through its separately authorised decision process. ![Integration reference: Investigation workspace](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-1dc2e1591c59ee62aaf1/a37ee3866c56ec57394342fa372c5c7c/reference-investigations-47.jpg) *Integration reference: Investigation workspace* ### Step 6: Prepare regulatory reporting only when required Where your authorised team determines the case is reportable, draft the supported SAR or STR with a reason. Review and export the goAML XML for submission through your regulator’s process. Record the external filing reference when filed. A drafted report and a filed report are different states, and a report does not assert guilt. Keep the external submission and internal audit evidence together. ![Integration reference: SARs & STRs](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-c66f2399c9b3dd3a1b0d/2b3b54ca2f089722be37a19d8667f7ba/reference-investigations-52.jpg) *Integration reference: SARs & STRs* ## Get more from the capability - Assign a named owner and a clear review question early so an alert cannot remain indefinitely unowned. - Use grouped investigations for related alerts on the same customer, preserving each originating decision. - Read the audit trail and resolution rationale when reopening. Use confirmed downstream outcomes to support effectiveness analysis. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - The case stays scoped to the intended customer. - The assignee can inspect the linked evidence. - Every material transition records its rationale. - Resolution and closure remain distinguishable. - A report retains the external filing reference when actually submitted. ## Troubleshooting ### I cannot change the case Check investigation-management permissions; read access does not grant transition authority. ### Resolve did not close the case Resolution and operational closure are separate steps. ### I need to add another customer’s alert Keep different entity investigations separate rather than merging their personal evidence. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/risk-intelligence/investigations/alerts) [Read the integration reference](https://trust.myaza.co/documentation/investigations) - [Investigations](https://trust.myaza.co/documentation/investigations) - [Webhook alerts cases](https://trust.myaza.co/documentation/webhook-alerts-cases) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Investigations](https://trust.myaza.co/documentation/investigations) - [Webhook alerts cases](https://trust.myaza.co/documentation/webhook-alerts-cases) --- # How to understand customer risk with Myaza Trust Read customer risk with its evidence and assessment basis. Customer risk helps explain a customer’s recorded state; it is different from the risk decision for a specific payment. Key takeaways: - A real zero differs from unassessed or incomplete evidence. - History pins an exact snapshot. - Expired current data disappears until refresh succeeds. - A workspace switch discards earlier customer evidence. - Reads create no checks, charges or execution authority. Read customer risk with its evidence and assessment basis. Customer risk helps explain a customer’s recorded state; it is different from the risk decision for a specific payment. ## Before you begin - The relevant customer-risk read capability enabled - Authorised typed customer access - Evidence basis and freshness available Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Confirm that customer-risk reads are available This read capability is a **Shadow preview** and is disabled by default. Myaza must enable it for the environment. Current scores also require reviewed configuration and qualified evidence. Check your organisation’s product access and individual- or business-read permissions. An API key or a legacy risk tier does not activate this preview or supply a live decision policy. ![Integration reference: Availability](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-1b66b89201139b8ca025/6399cdb46279ade2dfe27e45d08acb3d/reference-customer-risk-reads-85.jpg) *Integration reference: Availability* ### Step 2: Choose Current or As assessed Open **Customer assessments** and choose the basis deliberately. **Current** asks for qualified current evidence. **As assessed** reads retained historical assessment evidence. When viewing one customer, use the corresponding Risk evidence view and selected snapshot. Do not switch to an older score and label it Current if the current read is unavailable. ![Integration reference: Choose a read](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-fd36d682d7f2fcd2faac/995b59bb4cb3c18dfde11f47e5b26d92/reference-customer-risk-reads-86.jpg) *Integration reference: Choose a read* ### Step 3: Read one customer from your backend Use **GET /api/v1/risk-intelligence/customers/{entityId}/risk** with the environment’s secret key. Use the history endpoint for previous assessments and the snapshots endpoint for one exact historical assessment. The key supplies organisation and environment. Request the customer type you are permitted to access; inaccessible or unknown customers must not become a neutral score. ![Integration reference: Choose a read](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-fd36d682d7f2fcd2faac/995b59bb4cb3c18dfde11f47e5b26d92/reference-customer-risk-reads-86.jpg) *Integration reference: Choose a read* ### Step 4: Check state, score and coverage together A current number needs the response’s Current assessment state. A null score is not zero; a valid zero is Low. The documented bands are Low 0–24, Medium 25–49, High 50–79 and Critical 80–100. This index is not a probability of fraud. Pending, stale, incomplete or unavailable evidence must retain its state instead of borrowing a retained number. ![Integration reference: Display the result safely](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-c1cd72db34ba86221040/efd07c81e32fac6699229807d4f7815f/reference-customer-risk-reads-87.jpg) *Integration reference: Display the result safely* ### Step 5: Trace why the selected score exists Use **Why this score**, **Connections** and **History** for the same assessment. Inspect retained factors and coverage. Supporting context adds no points, and a minimum-risk adjustment is model evidence rather than another paid check. Paging the map changes visible evidence, not the recorded score. A score-lineage map is not a live network of customer or wallet owners. ![Integration reference: Read customer risk](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-de35793f766169d8cb0d/ca799e0004ba15a786d93ccc4f3f7e03/reference-customer-risk-reads-88.jpg) *Integration reference: Read customer risk* ### Step 6: Refresh combined customer views correctly For current cohort filtering, use the documented **basis=CURRENT** and typed subject filters. Include monitoring only through the supported combined read. Refresh rows and totals together at the returned transition window; do not join independently paginated lists. When the validity window expires, remove the current numbers until refresh succeeds. These reads do not buy checks, publish rules or authorise payments. ![Integration reference: Filter risk and monitoring together](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-270c648560a9c6ca76bc/8b681854ca123a713f652d9688102fb7/reference-customer-risk-reads-89.jpg) *Integration reference: Filter risk and monitoring together* ## Get more from the capability - Use exact snapshot links for an audit question so historical evidence cannot silently change to today’s evidence. - Keep coverage visible next to the score. A complete Low result and an incomplete assessment require different interpretation. - Treat connections as explanation for the selected assessment and avoid adding context into the score twice. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - A real zero differs from unassessed or incomplete evidence. - History pins an exact snapshot. - Expired current data disappears until refresh succeeds. - A workspace switch discards earlier customer evidence. - Reads create no checks, charges or execution authority. ## Troubleshooting ### The preview is unavailable Check environment activation, current configuration and permissions. Do not fall back to a legacy tier as current proof. ### The score is missing Read the assessment state and coverage. Missing does not mean zero or Low. ### Risk and monitoring totals disagree Use the supported single combined population rather than intersecting separately paginated results. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/customers/individuals) [Read the integration reference](https://trust.myaza.co/documentation/customer-risk-reads) - [Customer risk reads](https://trust.myaza.co/documentation/customer-risk-reads) - [Risk intelligence](https://trust.myaza.co/documentation/risk-intelligence) - [Identity hub](https://trust.myaza.co/documentation/identity-hub) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Customer risk reads](https://trust.myaza.co/documentation/customer-risk-reads) - [Risk intelligence](https://trust.myaza.co/documentation/risk-intelligence) - [Identity hub](https://trust.myaza.co/documentation/identity-hub) --- # How to assess transaction risk with Myaza Trust Assess transaction risk before your application moves money. Submit an exact amount, the right direction and the parties involved, then use the decision and its explanation. Key takeaways: - A complete request shows the intended sender and recipient. - Allow, Review, Block and service failures use the correct payment paths. - A retry does not score or bill again. - The displayed decision matches the API record. - Crypto release uses its separate qualified clearance integration. Assess transaction risk before your application moves money. Submit an exact amount, the right direction and the parties involved, then use the decision and its explanation. ## Before you begin - Exact amount, currency, direction and stable reference - Published rules and applicable transfer requirements Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Configure transaction controls in Sandbox Open **Risk Intelligence → Fraud Monitoring → Transactions** and review the setup requirements. In **Rules & Policies → Fraud rules**, install suitable rules from the library or test and publish a custom rule. Review thresholds and money limits. Installation publishes a rule version for the selected organisation and environment. A page preview with a risk score is demonstration data, not a configured decision system. ![Integration reference: Start with the rule library](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-441530d2c43b9bad2d33/25e48bd9cc1120d54cf977693666271e/reference-transaction-rules-83.jpg) *Integration reference: Start with the rule library* ### Step 2: Prepare one complete transaction request Assign stable external activity and transaction references. Specify **subject**, original **occurredAt**, **assetClass**, **direction**, **amount**, **currency** and **transactionType**. Send amount as an exact positive decimal string, not a floating-point calculation. Direction is from your customer’s perspective: inbound means received by the customer, outbound means sent by them. Put each bank account or wallet under the party that used it. ![Integration reference: Send a complete transaction](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-a9bd0e74e7ea42a223a9/2efb37623bd82ff436745fedec973547/reference-monitoring-events-56.jpg) *Integration reference: Send a complete transaction* ### Step 3: Include the party that needs screening For an outbound fiat payment, provide the recipient’s permitted name and identity context; inbound fiat screens the sender. For crypto, provide the relevant external party’s wallet address and explicit network. Each party needs a stable reference. Missing target evidence can produce Review for insufficient data instead of a clear result. A masked display value alone cannot identify a payment instrument. ![Integration reference: How directional screening works](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-bcde5bdeff41a5fb5948/e8fd6ca95b40e36eb52dfad3f5c76c11/reference-monitoring-events-57.jpg) *Integration reference: How directional screening works* ### Step 4: Submit once and read the authoritative summary Call **POST /api/v1/transactions** from your backend with a secret key and a stable **Idempotency-Key**. Store the returned activity, transaction and decision references. Read **assessment.summary.outcome**, reason and nextAction. Keep Review and Block separate from Allow. Detailed matched rules, screening and billing explain the result; do not sum them yourself to replace the returned outcome. ![Integration reference: Transaction assessment response](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-388c6465ed4a3980edfc/f8d786991cf22160ac5da54f94ae8246/reference-monitoring-events-58.jpg) *Integration reference: Transaction assessment response* ### Step 5: Enforce the result before funds movement Your payment backend holds Review, stops Block and continues Allow according to its authorised operating policy. Keep unavailable or pending results on the required hold. For crypto, a risk-only Allow is not Travel Rule clearance or single-use execution authorisation. Myaza does not move funds. Search the transaction reference in the dashboard and inspect the evidence before a manual exception. ![Integration reference: Enforcing decisions in your app](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-908ffbd67ae37905790e/466525f2117fb04945695a42005a0b91/reference-crypto-operations-70.jpg) *Integration reference: Enforcing decisions in your app* ### Step 6: Test retries, notifications and final outcomes Retry unchanged requests with the original references and key after uncertainty. Subscribe to **fraud.transaction.assessed** and applicable decision changes. Verify signatures, store event IDs durably and retrieve current state for delayed events. Send confirmed downstream outcomes when known so Effectiveness can measure performance. A successfully delivered webhook does not establish that your payment system enforced the decision. ![Integration reference: Idempotency and freshness](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-0d5cb818b49aaada2bef/44cd36911deccfed1d88a7a4bd2f6543/reference-monitoring-events-55.jpg) *Integration reference: Idempotency and freshness* ## Get more from the capability - Include structured senders, recipients and instruments to make screening and relationship evidence more informative. - Backtest rule changes before publishing a new version and retain the exact version with the decisions it produced. - Record confirmed fraud, legitimate and inconclusive outcomes. Avoid tuning thresholds from unlabelled decisions alone. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - A complete request shows the intended sender and recipient. - Allow, Review, Block and service failures use the correct payment paths. - A retry does not score or bill again. - The displayed decision matches the API record. - Crypto release uses its separate qualified clearance integration. ## Troubleshooting ### A transaction needs review despite no rule hit Inspect directional screening and insufficient-data evidence, not only matched rule count. ### Amount or location was rejected Use exact decimal strings and supply both latitude and longitude when sending coordinates. ### Retry produced a conflict The same identifiers were used for changed immutable data. Reconcile the original transaction before a genuinely new operation. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/risk-intelligence/fraud-prevention/transactions) [Read the integration reference](https://trust.myaza.co/documentation/risk-intelligence-quickstart) - [Transaction rules](https://trust.myaza.co/documentation/transaction-rules) - [Risk intelligence quickstart](https://trust.myaza.co/documentation/risk-intelligence-quickstart) - [Travel rule](https://trust.myaza.co/documentation/travel-rule) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Transaction rules](https://trust.myaza.co/documentation/transaction-rules) - [Risk intelligence quickstart](https://trust.myaza.co/documentation/risk-intelligence-quickstart) - [Travel rule](https://trust.myaza.co/documentation/travel-rule) --- # How to assess account activity with Myaza Trust Assess logins, account recovery and other non-payment activity. Send the actual customer and device context, read the returned decision and enforce it in your own app. Key takeaways: - Allow, Review and Block use distinct application paths. - Missing device evidence is not invented. - A same-request retry produces no duplicate side effect. - A delayed decision change is reconciled against current state. Assess logins, account recovery and other non-payment activity. Send the actual customer and device context, read the returned decision and enforce it in your own app. ## Before you begin - Stable event and customer references - Published rules and a review owner - Your app handles the returned decision Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Enable the relevant Fraud Monitoring area Open **Risk Intelligence → Fraud Monitoring** in Sandbox. Follow the setup for account activity and review the activation requirements displayed for your organisation. Check your Manage fraud rules access. A saved setup is different from enabled decisions. Do not enable unrelated monitoring areas just to remove a setup warning. ![Integration reference: Risk Intelligence quickstart](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-53593a9d6faf06767072/7379afb3e4f07cc57270d739fc9bf69f/reference-risk-intelligence-quickstart-69.jpg) *Integration reference: Risk Intelligence quickstart* ### Step 2: Install or configure the relevant fraud rules Open **Rules & Policies → Fraud rules → Rule library**. Search for the controls appropriate to the activity you send, then inspect them before selecting **Install**. Installation publishes a version in the current organisation and environment. Test a custom rule against historical evidence before publishing it. Review decision thresholds and money limits in Settings separately from individual rule conditions. ![Integration reference: Start with the rule library](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-441530d2c43b9bad2d33/25e48bd9cc1120d54cf977693666271e/reference-transaction-rules-83.jpg) *Integration reference: Start with the rule library* ### Step 3: Send a named activity from your backend Use **POST /api/v1/activities** with a secret key. Supply a stable **externalActivityId**, typed **subject**, the supported activity **type** and original **occurredAt**. Types include login, signup, password_reset, beneficiary_added, profile_change, device_change, payout and withdrawal. Include deviceRef and end-user IP only when observed. Use the complete transaction endpoint for money movement with sender and recipient evidence. ![Integration reference: Send behaviour activity](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-5ab579b6c9bec5834939/2d1936809d34af30eda91b8bf3aef55d/reference-monitoring-events-53.jpg) *Integration reference: Send behaviour activity* ### Step 4: Read and apply the decision summary Read **assessment.summary.outcome**, reason and nextAction. **allow** permits the action under your policy; **review** requires the hold or review path; **block** stops it. Store the activity and decision references. Do not reconstruct the outcome from the score or count of matched rules. Myaza does not automatically revoke a login session or change access in your application. ![Integration reference: API response](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-7d1ef43e5c94b780dbb5/ab4bea7cfceb53d81e0cd779748c1b89/reference-transaction-rules-84.jpg) *Integration reference: API response* ### Step 5: Find the same activity in the dashboard Open the Fraud Monitoring account-activity view and search by the external activity reference. Inspect the decision, matched rules and supporting evidence. Confirm that the customer and device are the ones your application intended. Shared device evidence can have a legitimate explanation. Use an investigation when the finding needs an owner or a deeper outcome. ![Integration reference: Send behaviour activity](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-5ab579b6c9bec5834939/2d1936809d34af30eda91b8bf3aef55d/reference-monitoring-events-53.jpg) *Integration reference: Send behaviour activity* ### Step 6: Handle retries and later decision changes Keep the same logical activity reference and idempotency key for an unchanged retry after response loss. Subscribe to **fraud.activity.assessed** and applicable **fraud.decision.changed** updates. Verify signatures and deduplicate by the envelope event ID. Re-read current state for a delayed update. A retry must not create another assessment, charge or account side effect. ![Integration reference: Idempotency and freshness](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-0d5cb818b49aaada2bef/44cd36911deccfed1d88a7a4bd2f6543/reference-monitoring-events-55.jpg) *Integration reference: Idempotency and freshness* ## Get more from the capability - Use the stable end-user device reference across journeys so new-device and relationship evidence can be useful. - Record only context your application actually observed. Do not substitute your server’s IP for the user’s IP. - Combine a review decision with a subject-bound extra check when your policy needs more login assurance. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - Allow, Review and Block use distinct application paths. - Missing device evidence is not invented. - A same-request retry produces no duplicate side effect. - A delayed decision change is reconciled against current state. ## Troubleshooting ### No rules match Check the published rule scope and the fields your activity actually supplies. ### The same event conflicts on retry Compare immutable input. A stable reference cannot be reused for a different activity. ### A Block decision did not stop the action Inspect the backend enforcement path. Myaza records the decision; your app enforces it. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/risk-intelligence/fraud-prevention) [Read the integration reference](https://trust.myaza.co/documentation/risk-intelligence-quickstart) - [Monitoring events](https://trust.myaza.co/documentation/monitoring-events) - [Risk intelligence quickstart](https://trust.myaza.co/documentation/risk-intelligence-quickstart) - [Monitoring rules](https://trust.myaza.co/documentation/monitoring-rules) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Monitoring events](https://trust.myaza.co/documentation/monitoring-events) - [Risk intelligence quickstart](https://trust.myaza.co/documentation/risk-intelligence-quickstart) - [Monitoring rules](https://trust.myaza.co/documentation/monitoring-rules) --- # How to keep customer screening up to date with Myaza Trust Continuous monitoring rechecks selected customer evidence over time. Configure a policy, enrol eligible customers and give material changes a clear review owner. Key takeaways: - The effective frequency matches the selected default or override. - Pause creates no future scheduled check or charge. - Resume restarts the retained configuration. - Off and re-enable preserve separate historical periods. - A material change reaches an accountable review process. Continuous monitoring rechecks selected customer evidence over time. Configure a policy, enrol eligible customers and give material changes a clear review owner. ## Before you begin - A published monitoring policy - Eligible customer references - Explicit enrolment and cost review Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Choose the checks and monitoring policy Open **Risk Intelligence → Rules & Policies → Ongoing monitoring**. Inspect the available policy defaults. Decide which evidence needs rechecking, the default frequency, material-change threshold and escalation behaviour. Monitoring is a recurring operation, so review the current price and your expected customer volume before enrolment. A one-time clear screen does not establish future screening coverage. ![Integration reference: Policy defaults and entity overrides](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-cfda4dc6862704262654/10ada2e2469c6fc277779b2de288c044/reference-monitoring-policies-77.jpg) *Integration reference: Policy defaults and entity overrides* ### Step 2: Publish a policy with the correct customer scope Review the policy’s supported entity types and publish the intended version through the policy controls. Scope belongs to the version and governs which subjects may subscribe. Start with one test customer in Sandbox. A policy saved as a draft is not necessarily the version a subscription uses. Keep organisation and environment consistent throughout the setup. ![Integration reference: Entity scope and high-risk handling](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-003ffdf789df3d2942c0/30700a4e5e8976031d54f80aff82f189/reference-monitoring-policies-78.jpg) *Integration reference: Entity scope and high-risk handling* ### Step 3: Enable monitoring for an eligible customer Open the customer’s **Risk** view and use its monitoring controls, or call **POST /api/v1/monitoring/subscriptions** on your backend with the typed entity ID and published **policyId**. Send an **Idempotency-Key** for the mutation. Omit **frequency**, or use null, to follow the policy default. Send a supported explicit frequency only when the customer needs an override. ![Integration reference: Enable monitoring](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-c085f3d12d37f3106851/0ddc4a266aaf83214d72f96418201f60/reference-monitoring-policies-79.jpg) *Integration reference: Enable monitoring* ### Step 4: Check the effective schedule and current state Read the subscription result and the customer view. Compare **frequency.effective**, **policyDefault**, **override** and **source** so you know whether the customer follows the default or an override. Review runs, deltas, alerts and billing evidence. A changed policy default updates customers following it; it does not silently overwrite an explicit customer frequency. ![Integration reference: Retrieve state and history](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-8c72c8706f3af2ee4e9a/20852ba8db0e04ab15f2542859adfe3c/reference-monitoring-policies-80.jpg) *Integration reference: Retrieve state and history* ### Step 5: Wire material changes into the team’s workflow Subscribe to **monitoring.delta.detected**, and add **monitoring.run.failed** if your team must respond to failed checks. Verify and durably store deliveries before acknowledging them. Use Risk Intelligence for portfolio changes and the Review Queue for actionable findings. Give escalations an owner. A scheduled run or delivery notification is not proof that your team investigated the change. ![Integration reference: Webhooks](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-4b5081ea200e0e7ad629/3574c46d73fc23c0fd4178e8fc79c92c/reference-monitoring-policies-81.jpg) *Integration reference: Webhooks* ### Step 6: Use Pause, Resume and Off correctly Pause temporarily stops future scheduled checks while keeping configuration and history. Resume restarts a paused subscription. **Off** ends that subscription permanently; re-enabling creates a linked new active period instead of rewriting the ended one. Restore **frequency: null** to remove an override. Test these actions on Sandbox records before using them to manage your customer book. ![Integration reference: Pause, resume and Off](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-7c4b6f6e15fd74b861c4/256a34b9fbd6da53fc56ecf8d67b2cff/reference-monitoring-policies-82.jpg) *Integration reference: Pause, resume and Off* ## Get more from the capability - Apply defaults to suitable groups and reserve overrides for a documented customer need. - Review material changes rather than treating every repeated check as a new alert requiring the same work. - Monitor failed runs and spend as operational signals. A recurring check needs both evidence collection and a response process. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - The effective frequency matches the selected default or override. - Pause creates no future scheduled check or charge. - Resume restarts the retained configuration. - Off and re-enable preserve separate historical periods. - A material change reaches an accountable review process. ## Troubleshooting ### A customer cannot enrol Check the entity type against the published policy version’s scope and the organisation’s product access. ### A policy edit did not change a customer’s frequency Check whether the customer has an explicit override. ### Off cannot be resumed An ended period is terminal. Use the documented enable operation to create its linked new period. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/risk-intelligence/rules-policies/monitoring) [Read the integration reference](https://trust.myaza.co/documentation/monitoring-policies) - [Monitoring policies](https://trust.myaza.co/documentation/monitoring-policies) - [Webhook continuous monitoring](https://trust.myaza.co/documentation/webhook-continuous-monitoring) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Monitoring policies](https://trust.myaza.co/documentation/monitoring-policies) - [Webhook continuous monitoring](https://trust.myaza.co/documentation/webhook-continuous-monitoring) --- # How to screen customers for risk with Myaza Trust Screen customers for sanctions, politically exposed person and adverse-media findings. Start with a correctly linked customer, run the supported checks, then review possible matches with the evidence available. Key takeaways: - Inactive, queued, clear and potential-match results are distinct. - Person and business checks use the correct subject. - Review outcome and history are visible to the authorised team. - The application receives and handles the resolved decision once. Screen customers for sanctions, politically exposed person and adverse-media findings. Start with a correctly linked customer, run the supported checks, then review possible matches with the evidence available. ## Before you begin - Screening enabled and available - Sufficient person or business information - A team member to review possible matches Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Check screening access and the customer Select your organisation and environment. Open the customer record and confirm the reference and subject type. Screening requires the product to be active and your role to have the appropriate access. A customer registration can report **INACTIVE** when screening is not running. Do not treat an inactive or queued state as a clear screening result. ![Integration reference: Enrolment](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-c1e1a8401db76cb495d8/71b8504e052c8ac4d5a754051f760101/reference-screening-64.jpg) *Integration reference: Enrolment* ### Step 2: Supply usable person or business details Use the customer’s permitted name and identity context to reduce ambiguity. For a customer already verified elsewhere, upsert the record with your stable external reference and truthful verification provenance before requesting an assessment. Keep individuals and businesses separate. A common name without supporting context can create a possible match; inventing a date of birth to make a check clearer creates incorrect evidence. ![Integration reference: Assess or monitor an existing customer](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-c9a4885d7d0e5371c827/0a62748475bf2280fbb50164b1cec806/reference-risk-intelligence-quickstart-68.jpg) *Integration reference: Assess or monitor an existing customer* ### Step 3: Choose onboarding screening or an explicit assessment For onboarding, open the customer’s workflow and turn on **Screening & AML**. Review its ongoing-monitoring choice, then publish the workflow. Passing a workflow with screening switched off does not automatically enrol its customers through that workflow. For a specific server-side check, use the documented risk-assessment request with the returned entity ID and the selected sanctions, PEP and adverse-media checks. ![Dashboard example: Choose onboarding screening or an explicit assessment](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-5dd3855e24ba98048a32/b2288ee26dc0e1300d4eff5df11b8771/setup-screening-aml.jpg) *Dashboard example: Choose onboarding screening or an explicit assessment* ### Step 4: Read the recorded screening result In the customer’s **Risk** view, inspect each screening type, status, match count and checked time. The supported states distinguish queued, clear, potential match and confirmed findings. Open the evidence row to read the selected match details. No recorded evidence is not equivalent to a completed clear check, and a PEP finding is not a finding that the person committed a crime. ![Integration reference: What you see in the dashboard](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-1757f7f4befd24855fc3/04df37ddbb5e1bd271aa46cf5796a1d7/reference-screening-65.jpg) *Integration reference: What you see in the dashboard* ### Step 5: Review possible matches before applying policy Compare the customer and match evidence, including names, aliases and available identity context. Use the permitted adjudication process and record the outcome with a concise reason. Review history remains part of the evidence. Use Investigations when the finding needs accountable casework. Do not restrict another customer because their name resembles a listed person. ![Integration reference: Matches](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-f56fbfbdb4c7d0f34932/91852e9b1288d9acf187997f65fc2304/reference-screening-66.jpg) *Integration reference: Matches* ### Step 6: Deliver the resolved decision to your application Subscribe to the screening and risk events your backend actually handles. Verify webhook signatures, deduplicate by logical event ID and correlate the documented subject reference. Retrieve current screening or decision state before applying a delayed update. Your application applies the restriction or next check required by its policy; a Myaza match alone does not automatically block an account. ![Integration reference: Enforcing decisions in your app](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-908ffbd67ae37905790e/466525f2117fb04945695a42005a0b91/reference-crypto-operations-70.jpg) *Integration reference: Enforcing decisions in your app* ## Get more from the capability - Collect useful identity context before screening to make review more reliable. - Enable ongoing monitoring deliberately when your policy needs fresh screening, with explicit cost and cadence review. - Retain both the original finding and the adjudication reason. A cleared name match should remain explainable. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - Inactive, queued, clear and potential-match results are distinct. - Person and business checks use the correct subject. - Review outcome and history are visible to the authorised team. - The application receives and handles the resolved decision once. ## Troubleshooting ### Screening says INACTIVE Check organisation access and service activation. Nothing has been screened in that state. ### A customer was verified but never enrolled Inspect the published workflow’s screening setting and the applicable enrolment path. ### There is a possible match Review the recorded evidence and adjudicate it; do not replace uncertainty with a clear label. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/risk-intelligence/screening) [Read the integration reference](https://trust.myaza.co/documentation/screening) - [Screening](https://trust.myaza.co/documentation/screening) - [Webhook screening](https://trust.myaza.co/documentation/webhook-screening) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Screening](https://trust.myaza.co/documentation/screening) - [Webhook screening](https://trust.myaza.co/documentation/webhook-screening) --- # How to investigate connected customers with Myaza Trust Investigate connections that may explain suspicious activity. Shared devices, instruments and locations help you ask better questions; they do not establish that every connected customer is the same person. Key takeaways: - Each reviewed connection has a source and observation time. - A shared device is not automatically treated as a shared identity. - The investigation stays scoped to the intended customer. - An outcome and its rationale remain in the case history. Investigate connections that may explain suspicious activity. Shared devices, instruments and locations help you ask better questions; they do not establish that every connected customer is the same person. ## Before you begin - Authorised access to relevant customer evidence - An existing finding or supported relationship view Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Open the customer you are investigating Choose the correct organisation and environment, then open **Customers → Individuals** or **Businesses**. Search by the known customer reference and open the record. Check the customer type and reference before reviewing connections. Use the permissions appropriate to that customer, and start from an actual finding rather than browsing unrelated personal records. ![Integration reference: Identity Hub API](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-b40ce42fbd77c157840c/f1dca6963ba27bed4457082a7b9bc622/reference-identity-hub-42.jpg) *Integration reference: Identity Hub API* ### Step 2: Inspect the available connected records Open the customer’s supported connected-record or relationship view. Review devices, payment instruments, locations and other evidence actually present. Keep the source record and observation time with every connection. An empty view can mean no recorded evidence; it cannot establish that the customer has never used another device or account. ![Integration reference: Send behaviour activity](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-5ab579b6c9bec5834939/2d1936809d34af30eda91b8bf3aef55d/reference-monitoring-events-53.jpg) *Integration reference: Send behaviour activity* ### Step 3: Read the source of each connection Open a connected record and inspect the activity, transaction or captured evidence that created the relationship. A deviceRef connects observations supplied by your application. A shared address can represent a household. A fingerprinted instrument can represent an observed payment method. None of those, on its own, proves fraud, legal ownership or identity equivalence. ![Integration reference: Instrument fields](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-59dd7a6a1e0e0993014f/2ea3e199772aeb5d153ac48812eb9e25/reference-monitoring-events-54.jpg) *Integration reference: Instrument fields* ### Step 4: Use bounded network exploration where available For authorised server-side investigation, use **GET /api/v1/risk-intelligence/network/{subjectKind}/{subjectId}** with the supported depth and node limits. Keep results within the organisation and environment identified by your secret key. Follow the returned bounds rather than treating a partial network as a complete customer graph. Do not substitute historical score-lineage connections for a live ownership network. ![Integration reference: Drift, check reliability and relationships](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-48be49af67a998d08edd/a0f4c6a0e8144fee48cb9fb67c970466/reference-decision-effectiveness-59.jpg) *Integration reference: Drift, check reliability and relationships* ### Step 5: Create accountable casework for material findings When an alert needs more evidence, open it in **Investigations** and select **Open investigation** with an investigator. Group alerts only when they belong to the same entity. Record why the connection matters and what evidence could confirm or disprove your concern. Reviewers should see the originating finding, not an unsupported claim that every connected account is fraudulent. ![Integration reference: Opening and assigning an investigation](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-ea850a9f632122a27265/15c41ad0b272685ffda59b354ea15942/reference-investigations-49.jpg) *Integration reference: Opening and assigning an investigation* ### Step 6: Record an evidence-backed outcome Review the case evidence and record the appropriate outcome with a rationale. Resolve and close are different actions. A cleared investigation is not KYC approval, and a graph edge is not permission to freeze an account. Your application handles any authorised restriction through its own decision process, using the current result and the right customer reference. ![Integration reference: Investigation workspace](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-1dc2e1591c59ee62aaf1/a37ee3866c56ec57394342fa372c5c7c/reference-investigations-47.jpg) *Integration reference: Investigation workspace* ## Get more from the capability - Send stable device and instrument references with activity so relationships have useful provenance. - Record uncertainty and alternative explanations such as shared households or corporate devices. - Compare findings over the relevant time window; do not mix different historical snapshots as though they were simultaneous. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - Each reviewed connection has a source and observation time. - A shared device is not automatically treated as a shared identity. - The investigation stays scoped to the intended customer. - An outcome and its rationale remain in the case history. ## Troubleshooting ### Connections are absent Check that your integration sends stable supported references. Do not invent missing relationships. ### The network is truncated Respect depth and node bounds and narrow the investigation rather than claiming full coverage. ### The score map shows different links Assessment lineage explains retained score contributors; it is not a customer-ownership network. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/customers/individuals) - [Identity hub](https://trust.myaza.co/documentation/identity-hub) - [Investigations](https://trust.myaza.co/documentation/investigations) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Identity hub](https://trust.myaza.co/documentation/identity-hub) - [Investigations](https://trust.myaza.co/documentation/investigations) --- # How to manage reviewed lists with Myaza Trust Use organisation lists to apply reviewed evidence consistently. Keep the decision to add an entry separate from the rules that decide what a list match does. Key takeaways: - A manager can save an authorised entry with a reason. - Read-only members cannot mutate the list. - The intended published rule handles a match. - Historical decisions retain their original explanation. Use organisation lists to apply reviewed evidence consistently. Keep the decision to add an entry separate from the rules that decide what a list match does. ## Before you begin - Authorised list-management permissions - A documented review reason and workflow policy Use a team member with permission for this control. Check the organisation and environment before changing a setting. Review the current price before a paid check or ongoing enrolment. Screenshots show an example workspace or the public integration reference; they do not show a real customer or a completed paid check. ## Set up and use the capability ### Step 1: Open your organisation’s Lists Select the intended organisation and environment, then open **Configuration → Lists**. Choose the relevant list type from the available catalogue. Use face lists for face evidence and the appropriate document, user, device or contact list for that type of evidence. A team member with read access may be able to inspect entries without being allowed to change them. ![Dashboard example: Choose the intended list and review purpose](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-c71d7e8d1d71a21368b0/ac17164ae602f10324e9803bfb802ffb/lists-catalogue.jpg) *Dashboard example: Open your organisation’s Lists* ### Step 2: Choose the intended list and review purpose In **Configuration → Lists**, choose **Face blocklist** or **Face allowlist**. The catalogue explains what each list does: the blocklist declines a matching face, while the allowlist skips duplicate-face rules for a matching face. Open the intended list and review existing entries before adding another. Keep a documented purpose for the exception; an allowlist entry is not universal identity approval. ![Dashboard example: Choose the intended list and review purpose](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-c71d7e8d1d71a21368b0/ac17164ae602f10324e9803bfb802ffb/lists-catalogue.jpg) *Dashboard example: Choose the intended list and review purpose* ### Step 3: Add a supported entry with a reason For a face blocklist entry, select **Add face**. Upload one clear, front-facing photo in JPEG, PNG or WebP, up to **5 MB**. Fill **Name or reference (optional)** with the reviewed case reference and **Comment (optional)** with why the face belongs here. Select **Add to blocklist** only after checking the intended organisation and source evidence. Other list types have their own fields; use their catalogue action rather than this face form. ![Dashboard example: Add a supported entry with a reason](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-c2b78c7d203a01416a76/a018cf4d3c4285ec209ed7b8f614fbeb/add-face-list.jpg) *Dashboard example: Add a supported entry with a reason* ### Step 4: Connect list findings to workflow rules Open the workflow that should use the evidence and inspect the relevant **Rules** or decision graph. For face matches, enable **Face search** and review the offered list and duplicate handling. Test the configured action and its rule order. Adding an entry by itself is not proof that every workflow or transaction will block it. ![Step 5: Separate Approve, Review and Decline choices for each face-search finding](https://images.ctfassets.net/6x586jzal49f/solution-guide-duplicate-match-actions/3ec37dc7865b7af6c6ec4140ffc9bb83/duplicate-match-actions.jpg) *Dashboard example: Connect list findings to workflow rules* ### Step 5: Save and publish the workflow Wait for **Saved** in the editor. Select **Publish**, or **Publish changes** for an existing workflow. In the publication review, check the target environment, workflow ID, price and readiness warnings. Resolve blocking issues, then confirm **Publish version**. A saved draft is not the version customers use. New sessions use the new published snapshot; sessions already created retain their original version. ![Step 8: Publication review showing the target environment, workflow ID and confirmation button](https://images.ctfassets.net/6x586jzal49f/solution-guide-workflow-publish-review/bc49f1bbace1e12fe91ca244a111a42d/workflow-publish-review.jpg) *Dashboard example: Save and publish the workflow* ### Step 6: Review entries and outcomes over time After testing a permitted fixture, inspect the verification or activity result to confirm the expected finding and decision. Keep the review rationale and list access limited to authorised managers. When an entry is no longer justified, use the supported management action and retain the relevant review history. Do not rewrite past verification or decision evidence to make it match the current list. ![Integration reference: Audit](https://images.ctfassets.net/6x586jzal49f/solution-guide-image-24ae88ebf748441878f7/8931ebc2035cad26475728f9eb3b5809/reference-investigations-48.jpg) *Integration reference: Audit* ## Get more from the capability - Use the list type that matches the evidence so a phone finding cannot be mistaken for identity proof. - Review stale entries periodically and keep a reason for additions and changes. - Test a listed and unlisted subject plus an unavailable-check path. A lookup failure cannot become an automatic allowlist match. ## Test before relying on it Run the supported Sandbox or simulator scenarios for this product. Where a tool is Production only, check access and scope first, then use only a permitted, deliberately reviewed Production test. A documentation screenshot or a successful page load is not an end-to-end integration test. - A manager can save an authorised entry with a reason. - Read-only members cannot mutate the list. - The intended published rule handles a match. - Historical decisions retain their original explanation. ## Troubleshooting ### The add action is missing Ask for the list-management permission appropriate to your role. ### A saved entry had no effect Check the exact list, environment, published workflow and rules using that type of finding. ### A match looks incorrect Inspect source evidence and record review. Do not compensate by changing unrelated customer records. ## Open the setup and integration references [Open the dashboard tool](https://trust.myaza.co/configure/lists) [Read the integration reference](https://trust.myaza.co/documentation/workflows) - [Workflows](https://trust.myaza.co/documentation/workflows) - [Identity hub](https://trust.myaza.co/documentation/identity-hub) - [Decisioning](https://trust.myaza.co/documentation/decisioning) [Back to Solutions](https://trust.myaza.co/explore). Choose the capability you want to activate, then follow its own guide. ### Sources - [Workflows](https://trust.myaza.co/documentation/workflows) - [Identity hub](https://trust.myaza.co/documentation/identity-hub) - [Decisioning](https://trust.myaza.co/documentation/decisioning)