Skip to main content

Common Flows

Complete, working code examples for the most common Dakota API integration patterns.

Critical Concepts to Understand

Prerequisites and Dependencies

Before calling Dakota APIs, understand these mandatory requirements:
To CreateYou Must First Have
RecipientCustomer with kyb_status: "approved"
DestinationRecipient
Onramp AccountDestination (crypto type) + Customer with kyb_status: "approved"
Offramp AccountDestination (bank type) + Customer with kyb_status: "approved"
WalletSigner Group
Signer GroupES256 public keys (see key generation below)
KYB Approval is Required: Customers must complete KYB verification and have kyb_status: "approved" before you can create onramp accounts, offramp accounts, or process transactions. Attempting these operations with pending/in_progress KYB will fail with customer_not_approved.

Validation Rules

FieldConstraint
customer.name3-100 characters
recipient.nameMust be unique per customer (no duplicates)
recipient.address.street1Required
recipient.address.cityRequired
recipient.address.countryRequired (ISO 3166-1 alpha-2, e.g., “US”, “DE”)
destination.crypto_addressMust be valid for the network (EVM: 0x + 40 hex chars, Solana: base58)

Ownership Model

Dakota uses client isolation - you can only access your own resources:
  • You can only view/modify customers you created
  • You can only view/modify recipients belonging to your customers
  • You can only view/modify destinations belonging to your recipients
  • Attempting to access another client’s resources returns 403 Forbidden

Required Headers for All POST Requests

Every POST request to Dakota API requires these headers:
-H "X-API-Key: your-api-key"
-H "X-Idempotency-Key: $(uuidgen)"   # REQUIRED for all POST requests
-H "Content-Type: application/json"
HeaderRequiredDescription
X-API-KeyAll requestsYour API key for authentication
X-Idempotency-KeyAll POST requestsUUID to prevent duplicate operations. Generate a new UUID for each request.
Content-TypeRequests with bodyMust be application/json
Missing X-Idempotency-Key on POST requests will fail.

ID Types - Do Not Confuse These

Dakota uses different ID types that are NOT interchangeable:
ID TypeCreated ByUsed ForExample
wallet_idPOST /walletsQuerying balances, wallet management2Nq8G7xK9mR4Ls6DyJ1Uf3Tp
destination_idPOST /recipients/{id}/destinationsOnramp, offramp, transactions31TgvySz1ARnqMZUdbuxykqqxGV
recipient_idPOST /customers/{id}/recipientsCreating destinations31TgvwFzi3rstV0DEDzQtuBfwFR
customer_idPOST /customersAll operations for that customer31Tgw0zSyDVo4Az66kmzUjMuwxx
signer_group_idPOST /signer-groupsCreating wallets2Nq8H8yL0nS5Mt7EzK2Vg4Uq
Common Mistake: Using a wallet_id as a destination_id will return destination_not_found error.

Scenario 1: Creating a Wallet (Non-Custodial)

Wallets require signer groups to be created first. The signer_groups field takes an array of signer group IDs (strings), not objects.

Public Key Format Requirements

The member_keys array requires ECDSA P-256 (ES256) public keys in the following format:
  • Algorithm: ECDSA with P-256 curve (also known as secp256r1 or prime256v1)
  • Encoding: PKIX/SPKI DER format, then Base64 encoded
  • Key Type: ES256 (or WEBAUTHN for passkey-based keys)

Generating ES256 Keys

JavaScript (Web Crypto API):
async function generateES256KeyPair() {
  const keyPair = await crypto.subtle.generateKey(
    { name: "ECDSA", namedCurve: "P-256" },
    true,  // extractable
    ["sign", "verify"]
  );

  // Export public key in SPKI format
  const publicKeyBuffer = await crypto.subtle.exportKey("spki", keyPair.publicKey);
  const publicKeyBase64 = btoa(String.fromCharCode(...new Uint8Array(publicKeyBuffer)));

  // Export private key (store securely - never send to server)
  const privateKeyBuffer = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
  const privateKeyBase64 = btoa(String.fromCharCode(...new Uint8Array(privateKeyBuffer)));

  return { publicKeyBase64, privateKeyBase64 };
}

// Usage
const { publicKeyBase64, privateKeyBase64 } = await generateES256KeyPair();
// Send publicKeyBase64 to Dakota API
// Store privateKeyBase64 securely on client side
Node.js:
const crypto = require('crypto');

function generateES256KeyPair() {
  const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', {
    namedCurve: 'P-256'
  });

  // Export public key in SPKI/DER format, then base64 encode
  const publicKeyDer = publicKey.export({ type: 'spki', format: 'der' });
  const publicKeyBase64 = publicKeyDer.toString('base64');

  // Export private key (store securely)
  const privateKeyDer = privateKey.export({ type: 'pkcs8', format: 'der' });
  const privateKeyBase64 = privateKeyDer.toString('base64');

  return { publicKeyBase64, privateKeyBase64 };
}
Python:
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
import base64

def generate_es256_keypair():
    private_key = ec.generate_private_key(ec.SECP256R1())
    public_key = private_key.public_key()

    # Export public key in SPKI/DER format, then base64 encode
    public_key_der = public_key.public_bytes(
        encoding=serialization.Encoding.DER,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    )
    public_key_base64 = base64.b64encode(public_key_der).decode('utf-8')

    # Export private key (store securely)
    private_key_der = private_key.private_bytes(
        encoding=serialization.Encoding.DER,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption()
    )
    private_key_base64 = base64.b64encode(private_key_der).decode('utf-8')

    return public_key_base64, private_key_base64
Go:
import (
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/rand"
    "crypto/x509"
    "encoding/base64"
)

func generateES256KeyPair() (string, string, error) {
    privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    if err != nil {
        return "", "", err
    }

    // Export public key in PKIX/SPKI format
    pubKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
    if err != nil {
        return "", "", err
    }
    publicKeyBase64 := base64.StdEncoding.EncodeToString(pubKeyBytes)

    // Export private key
    privKeyBytes, err := x509.MarshalECPrivateKey(privateKey)
    if err != nil {
        return "", "", err
    }
    privateKeyBase64 := base64.StdEncoding.EncodeToString(privKeyBytes)

    return publicKeyBase64, privateKeyBase64, nil
}

Step 1: Create a Signer Group

# Create signer group with ES256 public keys (base64-encoded SPKI format)
curl -X POST https://api.platform.dakota.xyz/signer-groups \
  -H "X-API-Key: your-api-key" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Treasury Team",
    "member_keys": [
      "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    ]
  }'
Response:
{
  "id": "2Nq8H8yL0nS5Mt7EzK2Vg4Uq",
  "client_id": "2Nq8G7xK9mR4Ls6DyJ1Uf3Tp",
  "name": "Treasury Team",
  "members": [
    {
      "id": "2Nq8H9zM1oT6Nu8FzL3Wh5Vr",
      "name": "Signer 1",
      "public_key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...",
      "key_type": "ES256"
    }
  ]
}

Step 2: Create the Wallet Using Signer Group ID

# signer_groups is an array of STRING IDs, NOT objects
curl -X POST https://api.platform.dakota.xyz/wallets \
  -H "X-API-Key: your-api-key" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Main Treasury Wallet",
    "family": "evm",
    "signer_groups": ["2Nq8H8yL0nS5Mt7EzK2Vg4Uq"],
    "policies": []
  }'
CORRECT signer_groups format:
{
  "signer_groups": ["2Nq8H8yL0nS5Mt7EzK2Vg4Uq"]
}
WRONG signer_groups format (causes “value must be a string” error):
{
  "signer_groups": [{"id": "2Nq8H8yL0nS5Mt7EzK2Vg4Uq"}]
}
Response:
{
  "id": "2Nq8I9AM2pU7Ov9GaM4Xi6Ws",
  "client_id": "2Nq8G7xK9mR4Ls6DyJ1Uf3Tp",
  "name": "Main Treasury Wallet",
  "family": "evm",
  "address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2"
}

Wallet Family Options

FamilyDescriptionAddress Format
evmEthereum, Polygon, Arbitrum, Base0x... (42 chars)
solanaSolanaBase58 (32-44 chars)

Scenario 2: Setting Up Onramp (Fiat to Crypto)

Key Understanding: Onramp requires a Destination ID, NOT a Wallet ID. You must create the full chain: Customer → Recipient → Destination.

The Complete Flow

1. Customer (KYB approved) → Already exists
2. Recipient → Create for the customer
3. Destination → Create crypto destination under recipient
4. Onramp Account → Use destination_id from step 3

Step 1: Verify Customer is KYB Approved

curl -X GET https://api.platform.dakota.xyz/customers/31Tgw0zSyDVo4Az66kmzUjMuwxx \
  -H "X-API-Key: your-api-key"
Customer must have "kyb_status": "approved" to create onramp accounts.

Step 2: Create a Recipient

curl -X POST https://api.platform.dakota.xyz/customers/31Tgw0zSyDVo4Az66kmzUjMuwxx/recipients \
  -H "X-API-Key: your-api-key" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Corp Treasury",
    "address": {
      "street1": "123 Main St",
      "city": "San Francisco",
      "region": "CA",
      "postal_code": "94102",
      "country": "US"
    }
  }'
Response:
{
  "data": {
    "id": "31TgvwFzi3rstV0DEDzQtuBfwFR",
    "name": "Acme Corp Treasury",
    "address": { ... }
  }
}

Step 3: Create a Crypto Destination

This is where you specify the wallet address that will receive funds:
curl -X POST https://api.platform.dakota.xyz/recipients/31TgvwFzi3rstV0DEDzQtuBfwFR/destinations \
  -H "X-API-Key: your-api-key" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "destination_type": "crypto",
    "name": "USDC Receiving Wallet",
    "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2",
    "network_id": "ethereum-mainnet"
  }'
Response:
{
  "data": {
    "id": "31TgvySz1ARnqMZUdbuxykqqxGV",
    "destination_type": "crypto",
    "name": "USDC Receiving Wallet",
    "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2",
    "network_id": "ethereum-mainnet",
    "status": "active"
  }
}
Save this id - this is your destination_id for onramp.

Step 4: Create Onramp Account

Now use the Destination ID (not wallet ID) to create the onramp:
curl -X POST https://api.platform.dakota.xyz/accounts/onramp \
  -H "X-API-Key: your-api-key" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "destination_id": "31TgvySz1ARnqMZUdbuxykqqxGV",
    "source_asset": "USD",
    "destination_asset": "USDC",
    "capabilities": ["ach"]
  }'
Response:
{
  "id": "31TgwOnrampAccountID",
  "bank_account": {
    "account_holder_name": "Acme Corp Treasury",
    "aba_routing_number": "123456789",
    "account_number": "9876543210",
    "account_type": "checking",
    "capabilities": ["ach", "wire"]
  },
  "destination": {
    "destination_type": "crypto",
    "name": "USDC Receiving Wallet",
    "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2",
    "network_id": "ethereum-mainnet"
  },
  "destination_asset": "USDC",
  "rail": "ach"
}

Common Onramp Error

Error:
{
  "code": "destination_not_found",
  "message": "Destination not found"
}
Cause: You used a wallet ID instead of a destination ID. Solution: Create a destination first using /recipients/{id}/destinations, then use that destination’s id field.

Scenario 3: Creating an Offramp (Crypto to Fiat)

Step 1: Create a Bank Destination

curl -X POST https://api.platform.dakota.xyz/recipients/31TgvwFzi3rstV0DEDzQtuBfwFR/destinations \
  -H "X-API-Key: your-api-key" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "destination_type": "fiat_us",
    "name": "Business Checking",
    "aba_routing_number": "021000021",
    "account_number": "123456789",
    "account_type": "checking",
    "account_holder_name": "Acme Corporation",
    "bank_name": "Chase Bank",
    "bank_address": {
      "street1": "383 Madison Avenue",
      "city": "New York",
      "region": "NY",
      "postal_code": "10179",
      "country": "US"
    }
  }'
Response:
{
  "data": {
    "id": "31TgvtxUdXi95dUN4M8X1rhSCNS",
    "destination_type": "fiat_us",
    "name": "Business Checking",
    "aba_routing_number": "*****0021",
    "account_number": "*****6789",
    "account_type": "checking",
    "status": "active"
  }
}

Step 2: Create Offramp Account

curl -X POST https://api.platform.dakota.xyz/accounts/offramp \
  -H "X-API-Key: your-api-key" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "destination_id": "31TgvtxUdXi95dUN4M8X1rhSCNS",
    "source_asset": "USDC",
    "source_network_id": "ethereum-mainnet",
    "destination_asset": "USD",
    "rail": "ach"
  }'

Scenario 4: One-Off Transaction

For single transactions where you don’t need a recurring account:
curl -X POST https://api.platform.dakota.xyz/transactions/one-off \
  -H "X-API-Key: your-api-key" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": "31Tgw0zSyDVo4Az66kmzUjMuwxx",
    "destination_id": "31TgvtxUdXi95dUN4M8X1rhSCNS",
    "source_asset": "USDC",
    "source_network_id": "ethereum-mainnet",
    "destination_asset": "USD",
    "amount": "1000.00"
  }'

Quick Reference: Required Headers

HeaderWhen RequiredValue
X-API-KeyAll requestsYour API key
X-Idempotency-KeyAll POST requestsUUID (e.g., from uuidgen)
Content-TypeRequests with bodyapplication/json

Quick Reference: Endpoint Patterns

Customer Flow

POST /customers                              → Create customer, get customer_id
GET  /customers/{customer_id}                → Check KYB status

Recipient & Destination Flow

POST /customers/{customer_id}/recipients     → Create recipient, get recipient_id
POST /recipients/{recipient_id}/destinations → Create destination, get destination_id

Account Creation (Requires destination_id)

POST /accounts/onramp                        → Fiat → Crypto (requires crypto destination)
POST /accounts/offramp                       → Crypto → Fiat (requires bank destination)
POST /accounts/swap                          → Crypto → Crypto (requires crypto destination)

Wallet Flow (Separate from Payment Flow)

POST /signer-groups                          → Create signer group, get signer_group_id
POST /wallets                                → Create wallet using signer_group_ids
GET  /wallets/{wallet_id}/balances           → Check wallet balances

Destination Types Reference

Crypto Destination

{
  "destination_type": "crypto",
  "name": "My USDC Wallet",
  "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2",
  "network_id": "ethereum-mainnet"
}

US Bank (ACH) - All Required Fields

{
  "destination_type": "fiat_us",
  "name": "Business Checking",
  "aba_routing_number": "021000021",
  "account_number": "123456789",
  "account_type": "checking",
  "account_holder_name": "Acme Corporation",
  "bank_name": "Chase Bank",
  "bank_address": {
    "street1": "383 Madison Avenue",
    "city": "New York",
    "region": "NY",
    "postal_code": "10179",
    "country": "US"
  }
}

International Bank (IBAN) - All Required Fields

{
  "destination_type": "fiat_iban",
  "name": "European Account",
  "iban": "DE89370400440532013000",
  "bic": "DEUTDEFFXXX",
  "account_holder_name": "Acme GmbH",
  "account_holder_address": {
    "street1": "Friedrichstraße 123",
    "city": "Berlin",
    "postal_code": "10117",
    "country": "DE"
  },
  "bank_name": "Deutsche Bank",
  "bank_address": {
    "street1": "Taunusanlage 12",
    "city": "Frankfurt",
    "postal_code": "60325",
    "country": "DE"
  },
  "assets": ["EUR"],
  "capabilities": ["sepa"]
}

Supported Assets (Tokens)

AssetNameType
USDUS DollarFiat
EUREuroFiat
USDCUSD CoinStablecoin
USDTTetherStablecoin
DKUSDDakota USDStablecoin
ETHEthereumNative token
SOLSolanaNative token

Payment Rails (Capabilities)

RailDescriptionSupported Assets
achUS ACH transfersUSD
fedwireUS Fedwire transfersUSD
swiftInternational SWIFT transfersUSD, EUR
sepaEuropean SEPA transfersEUR

Supported Networks

Mainnet (Production)

Network IDChainFamily
ethereum-mainnetEthereumEVM
polygon-mainnetPolygonEVM
arbitrum-mainnetArbitrumEVM
base-mainnetBaseEVM
optimism-mainnetOptimismEVM
solana-mainnetSolanaSolana

Testnet (Sandbox)

Network IDChainFamily
ethereum-sepoliaEthereum SepoliaEVM
ethereum-holeskyEthereum HoleskyEVM
base-sepoliaBase SepoliaEVM
arbitrum-sepoliaArbitrum SepoliaEVM
optimism-sepoliaOptimism SepoliaEVM
polygon-amoyPolygon AmoyEVM
solana-devnetSolana DevnetSolana
solana-testnetSolana TestnetSolana

Token Availability by Network

NetworkUSDCUSDTDKUSD
ethereum-mainnetYesYes-
polygon-mainnetYesYes-
arbitrum-mainnetYesYes-
base-mainnetYesYesYes
optimism-mainnetYesYes-
solana-mainnetYesYes-

Troubleshooting Common Errors

Authentication & Headers

ErrorCauseFix
401 UnauthorizedMissing or invalid API keyInclude X-API-Key: your-api-key header
400 Bad Request (missing idempotency)Missing X-Idempotency-Key on POSTAdd X-Idempotency-Key: <uuid> header
409 ConflictReused idempotency key for different requestGenerate new UUID for each unique request

Customer & KYB Errors

ErrorCauseFix
customer_not_foundCustomer ID doesn’t existVerify customer was created successfully
customer_not_approvedCustomer KYB status is not “approved”Wait for KYB completion or check kyb_status field
customer name must be at least 3 charactersName too shortUse 3-100 characters for customer name
customer name cannot exceed 100 charactersName too longUse 3-100 characters for customer name
a customer with that name already existsDuplicate customer name for your clientUse unique name per customer

Recipient & Destination Errors

ErrorCauseFix
recipient_not_foundRecipient ID doesn’t exist or belongs to another clientVerify recipient exists and you own it
duplicate_nameRecipient name already exists for this customerUse unique recipient name per customer
destination_not_foundUsing wallet_id instead of destination_id, or destination doesn’t existCreate destination via /recipients/{id}/destinations first
403 ForbiddenTrying to access resource owned by another clientYou can only access your own resources

Wallet & Signer Errors

ErrorCauseFix
field 'signer_groups/0': value must be a stringPassing objects instead of stringsUse ["id"] format, not [{"id": "..."}]
Invalid public key formatKey not in ES256 SPKI base64 formatGenerate key using ECDSA P-256, export as SPKI DER, base64 encode
signer_group_not_foundSigner group ID doesn’t existCreate signer group first via /signer-groups

Onramp/Offramp Errors

ErrorCauseFix
customer_not_approvedCustomer KYB not completeCustomer must have kyb_status: "approved"
destination_not_foundInvalid destination_idCreate crypto destination for onramp, bank destination for offramp
invalid_request (non-crypto destination)Using bank destination for onrampOnramp requires crypto destination type
invalid_request (non-bank destination)Using crypto destination for offrampOfframp requires fiat_us or fiat_iban destination

General Validation Errors

ErrorCauseFix
invalid_identifierMalformed KSUIDEnsure ID is valid 27-character KSUID
invalid_requestMissing required field or invalid valueCheck request body against required fields
asset_not_foundInvalid asset symbolUse valid asset: USD, EUR, USDC, USDT, ETH, etc.
network_not_foundInvalid network_idUse valid network from supported networks list