Skip to main content
Dakota’s API turns a handful of REST resources into fiat-to-crypto onramps, crypto-to-fiat offramps, cross-chain swaps, and non-custodial wallets. Follow the sections end-to-end to set up a fiat-to-crypto onramp, or skip to the section you need — offramp, swap, or non-custodial wallet. URLs are production (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:

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. Until kyb_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:
Share 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.updated webhook — the payload carries the new kyb_status, so no follow-up GET is needed. See Webhooks for payload shape and signature verification.
  • Sandbox: advance KYB manually with the simulation endpoint:
In sandbox, a single 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

Response:
Reuse the same Recipient for all Destinations belonging to the same counterparty.

Add a Crypto Destination (for onramps and swaps)

Supported 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)

For international rails (IBAN / SEPA), use 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.
Supported rail values (also used by offramps): Response:
Share 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 calling POST /sandbox/simulate/inbound. The provider then fires the same webhook sequence (transaction.auto.createdtransaction.auto.updatedwallet.deposit if the Destination is a Dakota Wallet) you’d see in production.
Response (synchronous — the deposit event itself processes async):
  • typeach_inbound or wire_inbound for USD rails.
  • scenariosuccess_immediate (default) or success_delayed with a delay_seconds field (1–86400).
  • simulation_id — your idempotency key for the simulation. Repeating with the same ID + params returns the original response; conflicting params return 409.
Sandbox object-create endpoints reject mainnet network IDs and cap per-request amount at $2 USD. USDT is not yet supported in sandbox (coming soon); USD, USDC, and RD work today. See Testing — Sandbox restrictions.

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.
Response:
Each stablecoin transfer to source_crypto_address triggers a USD wire to the bank Destination. Same transaction.auto.* webhooks as onramp.
Sandbox object-create endpoints reject mainnet network IDs and cap per-request amount at $2 USD. USDT is not yet supported in sandbox (coming soon); USD, USDC, and RD work today. See Testing — Sandbox restrictions.

Create a Swap (Stablecoin → Stablecoin)

A swap account takes a crypto Destination on the target asset and network and returns a source_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.
Response:
Send USDC on Ethereum to source_crypto_address; receive USDT on Polygon at the Destination. Same transaction.auto.* webhooks.
Sandbox object-create endpoints reject mainnet network IDs and cap per-request amount at $2 USD. USDT is not yet supported in sandbox (coming soon); USD, USDC, and RD work today. See Testing — Sandbox restrictions.

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:
Response:
public_key is a base64-encoded X.509 SubjectPublicKeyInfo (raw DER or PEM).

Create a Signer Group

Response:
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 a signer_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:
Response:
Save the returned 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 the policy_id from the previous step in the policies array. The field is required — omitting it returns 400 validation_error.
Response:
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 is PUT /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).
Response: 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.
Response:
Subscribe to 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