Skip to main content
Webhooks allow Dakota Platform to send real-time notifications about events in your account directly to your application.

Overview

Instead of constantly polling our API for updates, webhooks deliver event notifications instantly when something happens:
  • Transaction status changes
  • Customer onboarding updates
  • Account changes
  • System events

Webhook Signature Verification

Dakota signs every webhook with an Ed25519 signature (not HMAC). To verify a delivery, you need Dakota’s public key — set it in your environment as DAKOTA_WEBHOOK_PUBLIC_KEY.

Dakota Public Keys

Each key is 64 hex characters (32 raw bytes).

Signature Headers

Dakota includes these headers in every webhook request: Full verification implementations for Node.js, Python, Go, Rust, and Java are in Verification Code Examples below.

Setting Up Webhooks

1. Create a Webhook Endpoint

Create an endpoint in your application to receive webhook notifications:

2. Register Your Webhook

Register your endpoint with Dakota Platform:

Webhook Target Fields

event_types is an exact-match delivery filter. With global: false, Dakota delivers only the event types you list. An event whose type is absent from the list is never delivered and never retried — it is not queued for your target at all, so there is nothing to redeliver later. List every type you consume, and add a new type to the list before you rely on it. Set global: true to receive every event type for your client; event_types is then ignored.
cURL
Response:

Webhook Events

The envelope schema defines data.previous_attributes and a top-level metadata object, and the API reference lists both as optional. In practice no event populates either today — they are absent from every delivered payload. Drive your state machine off data.object alone; do not write change-detection logic that depends on previous_attributes being present.

Transaction Events

Transaction lifecycle events fire for auto-account (onramp/offramp/swap) and one-off transactions. There is no separate failed event — failure is reflected in the status field of the *.updated event.

Status progression

The data.object.status field on transaction webhooks progresses through the following observable values. Inspect this field on every *.updated event to drive your own state machine — there is no separate “status rank” or priority concept beyond this order. A transaction only ever moves forward through this sequence — statuses never regress. Treat completed and failed as terminal; no further *.updated events fire after either. Additional internal statuses (for example, awaiting_confirmation, broadcasted, pending_return) may appear on the underlying transaction object but are not surfaced as distinct webhook lifecycle stages.

Fee fields in the receipt

The data.object.receipt object breaks the transaction’s fees into separate fields. The same receipt shape is returned by GET /transactions/{transaction_id}.
developer_fee_bps is write-only: it is accepted on account create, account update, and one-off transaction create, but is not returned by GET /accounts/{account_id} or transaction reads. Record the value you set, and verify the fee actually charged via client_fee on each receipt. Updating developer_fee_bps with PUT /accounts/{account_id} applies to future transactions only.
transaction.auto.created — emitted when Dakota creates a transaction for an onramp/offramp/swap account (e.g., after detecting a bank deposit). There is no top-level amount — amount-like fields live inside data.object.receipt.
transaction.auto.updated — emitted on every status change or detail update (inspect data.object.status to distinguish processing, completed, failed).
transaction.one_off.created / transaction.one_off.updated — same shape, emitted for single-use transactions created via POST /transactions/one-off. Covers both off-ramp (crypto → fiat with payment_reference support) and swap (crypto → crypto with a destination network override). The one-off artifact is discarded after the transaction completes. data.object carries a one-off transaction snapshot — the abbreviated { id, customer_id, status } object shown in some list examples is just an example view, not a smaller payload. It is close to the response body of GET /transactions/{transaction_id} but not field-for-field identical — several keys are spelled differently on the event: resource_type and destination_payment_rail are not emitted on the event at all. Fields that carry no value are omitted rather than sent as null. The example below illustrates the shape; log a delivered payload and code against what you actually receive.
For the full field set — including return and reversal fields on failed/returned/reversed one-offs — see the OneOffTransaction schema on GET /transactions/{transaction_id} and the One-off transaction lifecycle.

Customer Events

customer.kyb_status.created — emitted once per customer, when Dakota first records a KYB status for that customer. kyb_status carries whatever status Dakota records first: approved for a customer that provisioning approves, or auto_declined for an application that is auto-declined. Every later change to that status arrives as customer.kyb_status.updated.
Subscribe to customer.kyb_status.created as well as customer.kyb_status.updated. A target that lists only customer.kyb_status.updated never learns the customer’s first KYB status, because event_types is an exact-match filter. Unlike customer.kyb_status.updated, this event never carries a reason_code. customer.kyb_status.updated
Some customer.kyb_status.updated events carry an additional reason_code that explains why the status changed. It is present only on transitions that have a defined reason; on all other status updates it is absent. Example payload with reason_code:
customer.created
external_id is present only when you set one for the customer. customer.updated — defined and subscribable, but no Dakota flow currently emits it. Do not build on it. The customer changes Dakota does deliver today are KYB status (customer.kyb_status.created / customer.kyb_status.updated), capability standing (customer.capability_status.updated), and deletion (customer.deleted, below). Changes to a customer’s name or email — which Dakota may apply itself during KYB provisioning — are not surfaced on any webhook; re-read GET /customers/{customer_id} if you need them. customer.deleted
customer.deleted is delivered to targets subscribed to all events (global: true), but it is not yet in the public EventType enum, so it cannot be named in an event_types filter today.
Token-sharing imports carry import_reference. For a customer created via a token-sharing import (Persona, Sumsub), the customer.created payload includes data.object.import_reference, a { source, reference } object where reference is the token you submitted for that import (for Persona, the cnst_... share token). Use it to tie the event back to the source record you minted the token from, with no extra lookup:
Notes:
  • source tells you which import produced the customer (persona, sumsub), so you branch on one field rather than a per-vendor key.
  • We cannot include your source Inquiry id itself: Persona does not share it with Dakota as the destination organization. The token you submitted is the reliable correlation key.
  • import_reference is a correlation hint, not a primary key: in rare import-retry cases the same reference can appear on more than one customer.created. If you would rather not track tokens at all, the import job maps every token to its result: GET /customers/persona-import-jobs/{job_id} returns each token alongside its customer_id and application_id.
Apart from import_reference on token-sharing imports, the payload carries no other provider-side metadata (no Persona Inquiry IDs, no Sumsub applicant IDs). To resolve a Dakota customer back to a provider identifier, either:
  • Look it up on the imported application. GET /customers/{customer_id} returns application_id; GET /applications/{application_id} exposes provider (persona or sumsub) and provider_applicant_id (the Persona inquiry ID or Sumsub applicant ID). See Persona Token Import and Sumsub Token Import.
  • For Persona imports, correlate through the import job. GET /customers/persona-import-jobs/{job_id} returns each redeemed token with its customer_id and application_id, so you can map the token you originally submitted back to a Dakota customer without keeping a separate table.
customer.kyb_application.submitted

Request for Information (RFI) Events

During review, compliance may need more information about a customer you onboarded. The application stays open and that customer stays blocked until the information arrives. You own the relationship with your customer. Dakota never contacts them. These events are how you learn a customer is blocked and what is still owed, so you can collect it in your own product. customer.rfi.requested — a reviewer opened a request. requirements lists everything the customer must supply.
customer.rfi.responded — the information arrived and the application is back with a reviewer. Stop chasing the customer.
customer.application.withdrawn — the application was withdrawn, either by the applicant or by you. Withdrawal is final.

Reading requirements

Each entry is one thing the customer must supply. A document requirement names what is needed in exactly one of two ways, never both:
  • document_type — a specific kind of document, e.g. bank_statement. Supply that document.
  • purpose — a compliance requirement that several document types could satisfy, e.g. business_proof_of_address. Let your customer choose among the documents that satisfy it, rather than demanding one specific type.
Switch on whichever field is present. A value from one vocabulary never appears in the other field. entity says who a document belongs to: {"kind": "business"} for the business, {"kind": "individual", "id": "..."} for a named individual, or {"kind": "edd"} for enhanced due diligence. status is missing when nothing is on file for the requirement, and on_file when a document is already there and the reviewer is asking for it again — send a replacement. Answer every question. A resubmission that carries documents but leaves questions unanswered goes back to a reviewer who still cannot act on it. When require_document is true the answer must also carry an upload; a text-only answer leaves the request open. status is scoped to the party in entity. A beneficial owner having supplied a proof of address does not make the business’s proof of address on_file. message_id identifies the request. Use it to deduplicate redeliveries and to pair a later customer.rfi.responded with the request it answers. requirements is always present and always an array. It is empty when a reviewer sent a plain request with no machine-readable scope; the request is still open and still needs a response. The event does not contain the resubmission link. Fetch it from the customer:
resubmit_url is present only while a request is open (status is info_requested). It opens a Dakota-hosted page that lists the same requirements and accepts uploads and answers. Forward it to your customer and the request closes when they submit.
resubmit_url contains an access token. Anyone holding it can open that application. Treat it like a password-reset link: send it to your customer, do not post it in a shared channel, and do not log it.This is exactly why it is not in the webhook body. A webhook body comes to rest in your application logs, APM traces, proxy logs, dead-letter queues and retry buffers, and delivery is retried, so one event can land in several of them. Identifiers like customer_id are harmless there. A credential is not. Fetching the link over your own authenticated request keeps it out of all of those, and keeps webhook bodies safe to log in full while debugging.

Resolving a request

Withdraw when the customer will not or cannot continue:
reason is optional and is recorded for audit. Withdrawal is final: the customer’s status becomes withdrawn, and onboarding them later requires a new application. An application that already has a decision returns 409.

Handling the events

An RFI ends in a decision, not in a separate event. Keep handling customer.kyb_status.updated for the approval or decline. Do not treat “we sent the link” as resolution. The request stays open, and reviewers see nothing, until the customer actually responds. Drive any follow-up you do from customer.rfi.responded.

Application status changes that do not emit a webhook

declined transitions driven by KYC/KYB rejection do not emit their own application event; they arrive as customer.kyb_status.updated. If you need to track a state that has no event of its own, poll GET /applications/{application_id} and inspect application_status.

Verification Code Examples

Ed25519 verification implementations — pick your language. See the public keys and required headers in Webhook Signature Verification above.

Managing Webhooks

List Webhooks

List all webhook targets configured for your account:
cURL

Update Webhook

Update an existing webhook target:
cURL

Delete Webhook

Delete a webhook target:
cURL

Webhook Delivery

Retry Policy

Dakota automatically retries failed webhook deliveries using exponential backoff over approximately 48 hours: Retry behavior:
  • Max attempts: 10 total
  • Total retry window: ~48 hours
  • Timeout per attempt: 20 seconds

Success Criteria

A webhook delivery is considered successful when:
  • Your endpoint returns an HTTP status code in the 2xx range (200-299)
  • Response is received within 20 seconds
A webhook delivery fails and triggers a retry when:
  • Your endpoint returns a non-2xx status code (4xx, 5xx)
  • Connection timeout (20 seconds exceeded)
  • Connection refused or DNS failure

Failure Handling

If all 10 delivery attempts fail:
  • Webhook is marked as permanently failed
  • You can view failed webhooks in the dashboard
  • Failed webhooks can be manually retried

Event Ordering

Webhooks are not guaranteed to arrive in lifecycle order. Two events fired close together for the same resource (e.g. wallet.transaction.updated for Broadcasted and Success on a fast testnet, or transaction.auto.deposit.received and transaction.auto.deposit.succeeded for an instant rail) may reach your endpoint out of order due to network races, retry timing, and the fact that the envelope created field is second-resolution. This is most visible in sandbox because testnet broadcasts and the simulate endpoints transition states sub-second. In production, longer chain confirmations and bank-rail latencies usually space the events out enough that ordering is incidental — but the contract still does not guarantee it. How to handle it:
  • Treat status as the source of truth, not the order of arrival. Each event carries the full resource state; the latest event by terminal-state semantics wins.
  • Use X-Dakota-Event-ID for idempotency — duplicate deliveries (retries or out-of-order replays) carry the same ID.
  • For ordered processing, derive a sequence from the resource’s own state machine. For wallet transactions: Waiting For Signature → Broadcasted → Success | Failed. For auto-account deposits: received → succeeded | failed. If you receive a “later” status before an “earlier” one, accept it; the earlier one is informational.
  • Don’t strictly require monotonic timestampscreated and X-Webhook-Timestamp are second-resolution, and two events can share the same value.
If your processing must be strictly serial per resource, queue incoming events by resource ID (e.g. wallet_id, transaction_id) and reorder by status before applying.

Webhook Headers

Every webhook request includes these headers: Use X-Dakota-Event-ID for idempotency - it remains the same across all retry attempts for the same event.

Best Practices

Endpoint Requirements

  • HTTPS only: Dakota Platform only sends webhooks to HTTPS endpoints
  • Fast response: Respond within 30 seconds
  • 2xx status codes: Return 200-299 status for successful processing
  • Idempotent: Handle duplicate webhooks gracefully

Security

  • Always verify webhook signatures
  • Use HTTPS for your webhook endpoint
  • Validate event data before processing
  • Log webhook events for debugging

Processing Best Practices

Implement robust webhook event processing with error handling, retries, and idempotency:

Testing Webhooks

Local Development

Use tools like ngrok to expose local endpoints:

Webhook Testing

Test your webhook endpoint manually:

Troubleshooting

Common Issues

Webhooks Not Received
  • Check that your endpoint returns 2xx status
  • Verify your URL is accessible from the internet
  • Ensure HTTPS is properly configured
  • Check firewall/proxy settings
Signature Verification Failing
  • Ensure you’re using Dakota Platform’s correct Ed25519 public key
  • Verify the signature calculation matches our Ed25519 implementation
  • Check that the timestamp and payload haven’t been modified
  • Ensure you’re using the correct header names (X-Webhook-Signature, X-Webhook-Timestamp)
Duplicate Webhooks
  • Implement idempotency using the event ID
  • Store processed event IDs to prevent duplicates
  • Use database constraints where possible

Monitoring

  • Set up alerts for webhook failures
  • Log all webhook events for debugging
  • Monitor endpoint response times
  • Track webhook delivery success rates

Event Types Reference

Below are the commonly used webhook event types. This list is not exhaustive — the authoritative list of subscribable event types is the EventType enum in the API reference.

Next Steps

After setting up webhooks:
  1. Testing Your Integration - Test webhook delivery and processing
  2. Transactions - Process transactions that trigger webhook events
  3. Customer Onboarding - Set up KYB status webhooks

API Reference

For detailed endpoint documentation, see: