Skip to main content
A reference of common Transaction Monitoring issues, with symptom, cause, and fix.

Duplicate txn_id error

Symptom. Submitting a transaction returns 400 with A transaction with this transaction_id already exists. on the transaction_id field, or 409 when two identical requests race each other. Cause. txn_id (the transaction_id you send) is unique per application. Didit rejects a second transaction with the same transaction_id and does not return the existing record. Fix.
  • Generate txn_id deterministically on your side (e.g. use your own transaction DB ID).
  • On a 400 or 409 for a duplicate transaction_id during a retry, look the existing transaction up instead of resubmitting: read it with GET /v3/transactions/{uuid}/ using the uuid from the first response, or use the transaction_id (the Didit uuid) that the transaction.created webhook carries next to your txn_id.
  • If the request was not a retry, give the new transaction its own transaction_id.

Rule not firing

Symptom. You created a rule that should match, but no transactions are showing it in rule_runs[] with matched: true. Causes and fixes.
  • The application is in sandbox mode - a sandbox application stores the transaction and skips rule evaluation, AML screening, webhooks, and billing; only a Travel Rule exchange still runs. rule_runs comes back empty and the score stays 0, whatever mode your rules are in, and the transaction stays APPROVED unless an expiring Travel Rule transfer applies your timeout_outcome. Check environment on the transaction response: "sandbox" means it was never evaluated. See Transaction Monitoring in sandbox for how to validate a rule before it goes live.
  • Rule mode is DISABLED or TEST — check Rules → [rule] → Mode. Only ACTIVE rules contribute to scoring; TEST rules evaluate but don’t affect status.
  • Scope filter excludes the transaction — e.g. scope set to transaction_type = finance won’t match travelRule transactions.
  • Aggregation window is shorter than needed — a “20 transactions in 30 days” rule won’t match after only 15 transactions.
  • Condition field is null — e.g. your rule checks counterparty.country_code but you didn’t send a counterparty.
  • Currency conversion — the rule thresholds on amount_in_default_currency; if your application’s default currency differs from the transaction currency, the converted amount may be below threshold.
Use the Rules → [rule] → Backtest feature to replay a past transaction and see why the rule did / didn’t fire.

Rule firing on everything

Symptom. A rule fires on 90%+ of transactions — your review queue is flooded. Cause. Conditions are too broad, or thresholds are too low. Fix.
  • Add scope filters to narrow applicability (e.g. only outbound, only certain transaction types).
  • Raise thresholds (amounts, velocity counts).
  • Put the rule in TEST mode for a week — analyze the rule_runs to see the match rate and tighten conditions before re-enabling.

Crypto screening timed out

Symptom. Transaction completed with status: APPROVED but the provider_results[] array shows status: FAILED for a wallet, transaction hash, or crypto screening provider. Causes.
  • Provider API was briefly unavailable.
  • Query timed out (typically ~10s).
Fix.
  • Transactions never fail because of provider issues — enrichment is best-effort.
  • If a specific provider fails repeatedly, check its status page or open a support ticket.
  • Consider configuring provider preferences in Settings → Transaction Monitoring → Providers to route to alternative providers.

IP enrichment missing

Symptom. A transaction’s applicant.ip_enrichment is absent or partial. Causes.
  • No applicant.device.ip in the submission.
  • IP is private / reserved (not routable).
  • All enrichment providers were temporarily unavailable.
Fix.
  • Capture the submitter’s IP in your client/server and pass it on every transaction.
  • Use only routable IPs — Didit silently skips private ranges.
  • Check Settings → Status in the console for provider health.

Webhook signature mismatch

Symptom. Your endpoint rejects events because the signature doesn’t match. Causes.
  • Raw body was modified (JSON re-stringified with different whitespace).
  • Wrong secret.
  • Multiple destinations with the same URL — signatures differ per destination.
Fix.
  • Sign over the raw request body, not the parsed JSON. Many frameworks expose a raw-body middleware.
  • Rotate the secret if compromised: Webhook destinations → [destination] → Rotate secret.
  • Keep one destination per URL to avoid secret confusion.

Webhook not arriving

Symptom. Transactions are being created but webhooks never arrive. Causes.
  • Destination enabled is false.
  • URL not publicly reachable.
  • Destination auto-disabled after repeated failures.
  • Event not in subscribed_events.
Fix.
  • Send a test event: Webhook destinations → [destination] → Send test.
  • Check recent deliveries: Webhooks → [destination] → Deliveries. See 4xx/5xx responses with body/headers.
  • Re-enable disabled destinations.

Currency conversion looks wrong

Symptom. amount_in_default_currency doesn’t match your expected rate. Causes.
  • FX rate is the mid-market rate at txn_date.
  • Backdated transactions use the historical rate at that date.
  • Some currencies (exotic FX) have coarser update intervals.
Fix.
  • Use amount and currency (native) for display.
  • Use amount_in_default_currency only for internal rule thresholding.
  • For audit, the transaction record pins the conversion rate used at evaluation time.

Counterparty not matching my blocklist

Symptom. You blocklisted a business by vendor_data, but a transaction with that counterparty still went through. Causes.
  • Counterparty in the transaction payload has no vendor_data — stored as external snapshot.
  • vendor_data casing differs.
Fix.
  • Always pass counterparty.vendor_data when the counterparty is an entity on your platform.
  • Use the same casing everywhere — vendor_data is case-sensitive.

AWAITING_USER transactions stuck

Symptom. Transactions are stuck in AWAITING_USER because the user never completes the remediation session. Causes.
  • User didn’t open the remediation session URL.
  • Remediation session expired.
  • The transaction is a Travel Rule transfer that a library rule such as travel-rule-pending-action moved to AWAITING_USER: Didit does not create its default remediation session for a Travel Rule transaction, so unless the rule names its own remediation workflow there is no link to deliver.
Fix.
  • The transaction response’s remediation_session_url is the link to deliver. Re-deliver to the user.
  • For a Travel Rule transaction, resolve the transfer from its Travel rule tab, then change the transaction status yourself, because resolving the transfer does not by itself move the transaction out of AWAITING_USER.
  • Remediation sessions follow the same KYC quickstart pattern — subscribe to completion webhooks.
  • Set a timeout in your own system to expire stale transactions if remediation never completes.

Rate limit on create

Symptom. 429 Too Many Requests during high-volume bursts. Fix.
  • Respect the Retry-After header.
  • Throttle client-side for burst traffic.
  • Contact support for higher limits on proven production volume.
See rate limiting.

Next steps

Integration guide

Full integration patterns.

Rules

How rule evaluation works.

Webhooks

Webhook mechanics.