api.platform.dakota.xyz); swap to api.platform.sandbox.dakota.xyz for sandbox testing. For single-use payouts and international rails, see Advanced Flows.
On this page:
- Create a Customer
- Create a Recipient and Destination
- Create an Onramp
- Create an Offramp
- Create a Swap
- Create a Wallet
- Dedicated References
Required Headers
Every Dakota request uses the same three headers:Create a Customer
A Customer is the legal entity Dakota processes payments for — typically a business your app onboards. It’s a separate object from your internal user record because regulated money movement requires verified KYB, and Dakota attaches the review to this object once and reuses it forever. Untilkyb_status is "active", no recipients, destinations, accounts, or transactions can be created for the Customer. Map one Customer per business entity, not per user session.
external_id is optional; set it to your internal user ID to simplify reconciliation later.
Response:
application_url with the end user. They complete the hosted KYB form (business details, beneficial ownership, legal agreements).
Complete KYB
- Production: Dakota’s compliance team reviews the application. Subscribe to the
customer.kyb_status.updatedwebhook — the payload carries the newkyb_status, so no follow-up GET is needed. See Webhooks for payload shape and signature verification. - Sandbox: advance KYB manually with the simulation endpoint:
GET /customers/{id} after the simulate call is usually simpler than wiring up a webhook listener. Use polling as a reconciliation fallback in production if you miss a webhook (outage, 5xx on your receiver). Operations on non-approved customers return customer_not_approved.
Create a Recipient and Destination
Before money can move, you need a Recipient (the legal beneficiary of the funds — separate from the paying Customer because regulators report on beneficiaries) and a Destination (the concrete endpoint where funds land — a crypto address + network, or a bank account). One Customer can own many Recipients; one Recipient can own many Destinations across different networks and currencies.Create a Recipient
Add a Crypto Destination (for onramps and swaps)
network_id values. USDC and USDT are available on every mainnet.
RD is available only on Base — use
base-mainnet (production) or base-sepolia (sandbox). A Destination or Account that pairs RD with any other network is rejected when creating the Account (no provider available for the requested capabilities).Add a Bank Destination (for offramps)
destination_type: "fiat_iban" — see Destinations & Recipients.
Create an Onramp (USD → Stablecoin)
An onramp account takes a crypto Destination and returns real ACH or Fedwire bank details. Your end user wires USD to those details; Dakota converts the USD to the stablecoin you configured and delivers it to the Destination automatically. The onramp account is where setup ends and money movement begins.rail values (also used by offramps):
Response:
bank_account with your end user. Each inbound wire triggers an automatic conversion and on-chain delivery. Subscribe to auto_account.created, transaction.auto.created, and transaction.auto.updated. If the Destination is a Dakota Wallet, wallet.deposit also fires on arrival. See transaction webhooks.
Simulate the USD Deposit (Sandbox)
You can’t actually wire USD into a sandbox account, so you trigger the onramp lifecycle by callingPOST /sandbox/simulate/inbound. The provider then fires the same webhook sequence (transaction.auto.created → transaction.auto.updated → wallet.deposit if the Destination is a Dakota Wallet) you’d see in production.
type—ach_inboundorwire_inboundfor USD rails.scenario—success_immediate(default) orsuccess_delayedwith adelay_secondsfield (1–86400).simulation_id— your idempotency key for the simulation. Repeating with the same ID + params returns the original response; conflicting params return 409.
Create an Offramp (Stablecoin → USD)
An offramp account takes a bank Destination and returns a crypto deposit address. You send stablecoins there; Dakota converts them to USD and wires to the bank account.source_crypto_address triggers a USD wire to the bank Destination. Same transaction.auto.* webhooks as onramp.
Create a Swap (Stablecoin → Stablecoin)
A swap account takes a crypto Destination on the target asset and network and returns asource_crypto_address on the source asset and network. Dakota converts and delivers cross-chain. Because crypto_destination_id can point at any Recipient’s Destination — including a third party’s — a swap is also how you pay a counterparty in their preferred stablecoin while holding a different one.
source_crypto_address; receive USDT on Polygon at the Destination. Same transaction.auto.* webhooks.
Create a Wallet (Non-Custodial)
Non-custodial wallets are a different resource shape: no Recipient or Account chain. A wallet owns an on-chain address and authorizes movements via client-signed intents instead of Dakota-triggered conversions — Dakota never holds the private keys. Architecture: Wallets. Signing reference (intent schema, RFC 8785 canonicalization, DER encoding, the browser P1363→DER trap): Wallet Transaction Signing.Register Signers
Generate an ECDSA P-256 (ES256) keypair client-side — the private half never leaves the client. Register each public key as a Signer:public_key is a base64-encoded X.509 SubjectPublicKeyInfo (raw DER or PEM).
Create a Signer Group
member_keys is an array of public-key strings, not objects. The group is the authorization unit — wallets and policies attach to groups, so you can add or remove members without redeploying.
Create a Policy
A policy governs which transactions a wallet can authorize. Every wallet must have at least one policy attached before it can sign outbound transactions — Dakota’s policy engine default-denies any transaction on a wallet with zero attached policies. Inbound deposits work without a policy: an on-chain address can always receive crypto. A policy is asigner_group_id (the group authorized to mutate the policy itself) plus one or more rules:
Multiple rules can layer in a single policy, and multiple policies can attach to a single wallet — the policy engine evaluates them all and applies deny-wins-then-allow logic. Full reference: Policies.
The minimal permissive policy — any single member of the signer group can authorize, no other restrictions — is one rule:
id — you’ll pass it as the policy_id when creating the wallet in the next step. Policy creation itself is a regular API call; subsequent mutations (adding rules, deleting, attaching to a wallet) go through the endorsed-request flow.
Create the Wallet
Pass thepolicy_id from the previous step in the policies array. The field is required — omitting it returns 400 validation_error.
Deposit-only wallets. It is valid to create a wallet with
"policies": []. The wallet still gets a real on-chain address and can receive crypto deposits — those are not policy-gated, anyone can send to an address. But the wallet cannot send transactions until at least one policy is attached: the policy engine returns 403 Forbidden with "transaction denied: No policies found for wallet" on every POST /wallets/{id}/transactions call. Use this if you want to provision the address first (e.g. share it for inbound funding) and decide governance later — see Attach a Policy to an Existing Wallet below.Attach a Policy to an Existing Wallet
You can attach additional policies to a wallet at any time — to ratchet up controls (amount caps, sanctions blocklists, higher approval thresholds), or to bring a deposit-only wallet online by attaching its first policy. The endpoint isPUT /policies/{policy_id}/wallets/{wallet_id}, and the attach_policy_to_wallet intent must be signed by a member of a signer group already attached to the wallet (not the policy’s own signer group).
204 No Content on success — the empty body is the success signal. The wallet is now bound to the policy; subsequent GET /wallets/{id}/policies will include it.
The signatures array holds base64-encoded ASN.1 DER ECDSA signatures over the canonicalized intent. The signing pipeline (RFC 8785 JCS → SHA-256 → ECDSA P-256 → DER → base64) is the same one used for wallet transactions; the full reference, code samples in Node / Python / Go / browser, and the browser P1363→DER helper live on Wallet Transaction Signing.
After attachment, the wallet can submit transactions immediately. You can repeat this call with additional policy_ids to layer on stricter rules — Dakota’s deny-wins evaluation means adding policies can only tighten controls, never relax them.
Sign and Send a Transaction
Dakota has three transaction families — auto (triggered by funds arriving at an Account’s deposit address, no API call to send), one-off (POST /transactions/one-off, for single-use payouts and swaps — see Advanced Flows), and wallet (POST /wallets/{id}/transactions, covered here). Only wallet transactions require a Dakota Wallet — auto and one-off transactions accept any on-chain address as source or destination.
A wallet transaction is a canonical JSON intent plus one or more ECDSA P-256 signatures. You build the intent (what the wallet should do), canonicalize it per RFC 8785 JCS, hash with SHA-256, sign in ASN.1 DER, base64-encode the signature, and POST both intent and signatures together. The server re-canonicalizes and verifies; the policy engine checks approval thresholds; then the transaction executes on-chain.
wallet.transaction.created and wallet.transaction.updated to follow status through pending → in_progress → completed | failed. Full walkthrough — intent schema, canonicalization code in Node / Python / Go / browser, and the browser P1363→DER conversion helper — lives on Wallet Transaction Signing.
Dedicated References
- Wallets — architecture, signer groups, policies
- Wallet Transaction Signing — intent schemas, canonicalization, signing code in Node / Python / Go / browser
- Webhooks — event types, delivery, signature verification
- API Reference — full endpoint reference and OpenAPI spec

