> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dakota.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Common Flows

> One section per Dakota resource — customer, counterparty, onramp, offramp, swap, wallet.

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](/documentation/advanced-flows).

**On this page:**

* [Create a Customer](#create-a-customer)
* [Create a Recipient and Destination](#create-a-recipient-and-destination)
* [Create an Onramp](#create-an-onramp-usd-→-stablecoin)
* [Create an Offramp](#create-an-offramp-stablecoin-→-usd)
* [Create a Swap](#create-a-swap-stablecoin-→-stablecoin)
* [Create a Wallet](#create-a-wallet-non-custodial)
* [Dedicated References](#dedicated-references)

### Required Headers

Every Dakota request uses the same three headers:

| Header              | Required           | Description                |
| ------------------- | ------------------ | -------------------------- |
| `X-API-Key`         | All requests       | Your API key               |
| `X-Idempotency-Key` | All POST requests  | UUID to prevent duplicates |
| `Content-Type`      | Requests with body | `application/json`         |

## 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.

```mermaid theme={null}
sequenceDiagram
    participant App as Your App
    participant EndUser as End User
    participant Dakota
    App->>Dakota: POST /customers
    Dakota-->>App: customer_id + application_url
    Note over App: Share application_url with end user
    EndUser->>Dakota: Completes hosted onboarding form
    Dakota->>App: Webhook: customer.kyb_status.updated
    App->>Dakota: GET /customers/{id}
    Dakota-->>App: kyb_status: "active"
```

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/customers \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Corporation",
    "customer_type": "business",
    "external_id": "your_internal_id"
  }'
```

`external_id` is optional; set it to your internal user ID to simplify reconciliation later.

**Response:**

```json theme={null}
{
  "id": "2LfTd6QQrUyPKwRR9qMMyk7CMHS",
  "kyb_links": [],
  "application_id": "2WGC9cKv9P4K8eGzqY6qJ3Xz7Qm",
  "application_url": "https://apply.dakota.com/applications/2WGC9cKv9P4K8eGzqY6qJ3Xz7Qm?token=kJ8xN3zQ9mL2pR5vY7wC1aF4dG6hK0sT8uW3nB5eM9",
  "application_expires_at": 1734567890000000000
}
```

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](/documentation/webhooks#customer-events) for payload shape and signature verification.
* **Sandbox:** advance KYB manually with the simulation endpoint:

```bash theme={null}
curl -X POST https://api.platform.sandbox.dakota.xyz/sandbox/simulate/onboarding \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "kyb_approve",
    "applicant_id": "'"$APPLICANT_ID"'",
    "simulation_id": "sim_'$(uuidgen)'"
  }'
```

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.

```mermaid theme={null}
sequenceDiagram
    participant App as Your App
    participant Dakota

    App->>Dakota: POST /customers/{id}/recipients
    Dakota-->>App: ...
    App->>Dakota: POST /recipients/{id}/destinations (crypto or bank)
    Dakota-->>App: ...
```

### Create a Recipient

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/customers/$CUSTOMER_ID/recipients \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Treasury Operations",
    "recipient_type": "business",
    "address": {
      "street1": "123 Main St",
      "city": "San Francisco",
      "region": "CA",
      "postal_code": "94102",
      "country": "US"
    }
  }'
```

**Response:**

```json theme={null}
{
  "id": "2LfXk8NNpSwMHuRP8oKKwi5AKFQ"
}
```

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

### Add a Crypto Destination (for onramps and swaps)

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/recipients/$RECIPIENT_ID/destinations \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "destination_type": "crypto",
    "name": "USDC on Ethereum",
    "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2",
    "network_id": "ethereum-mainnet"
  }'
```

**Supported `network_id` values.** USDC and USDT are available on every mainnet.

| Mainnet            | Sandbox testnet    |
| ------------------ | ------------------ |
| `ethereum-mainnet` | `ethereum-sepolia` |
| `polygon-mainnet`  | `polygon-amoy`     |
| `arbitrum-mainnet` | `arbitrum-sepolia` |
| `base-mainnet`     | `base-sepolia`     |
| `optimism-mainnet` | `optimism-sepolia` |
| `solana-mainnet`   | `solana-devnet`    |

<Note>
  **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`).
</Note>

### Add a Bank Destination (for offramps)

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/recipients/$RECIPIENT_ID/destinations \
  -H "X-API-Key: $DAKOTA_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": "987654321",
    "account_type": "checking",
    "account_holder_name": "Acme Corporation",
    "bank_name": "Chase Bank",
    "bank_address": {
      "street1": "383 Madison Ave",
      "city": "New York",
      "region": "NY",
      "postal_code": "10179",
      "country": "US"
    }
  }'
```

For international rails (IBAN / SEPA), use `destination_type: "fiat_iban"` — see [Destinations & Recipients](/documentation/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.

```mermaid theme={null}
sequenceDiagram
    participant App as Your App
    participant EndUser as End User
    participant Bank
    participant Dakota
    participant Blockchain

    App->>Dakota: POST /accounts (onramp)
    Dakota-->>App: ... + bank_account
    App->>EndUser: Share bank details
    EndUser->>Bank: Wire USD
    Bank->>Dakota: USD received
    Dakota->>App: Webhook: transaction.auto.created
    Dakota->>Blockchain: Deliver stablecoin to destination
    Dakota->>App: Webhook: transaction.auto.updated (completed)
```

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/accounts \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "account_type": "onramp",
    "crypto_destination_id": "2LfYm5KMnRvLFtRP7nJJug4zJEP",
    "source_asset": "USD",
    "destination_asset": "USDC",
    "destination_network_id": "ethereum-mainnet",
    "capabilities": ["ach"],
    "rail": "ach"
  }'
```

**Supported `rail` values** (also used by offramps):

| Rail      | Currencies | Speed             |
| --------- | ---------- | ----------------- |
| `ach`     | USD        | 1–3 business days |
| `fedwire` | USD        | Same day          |

**Response:**

```json theme={null}
{
  "id": "2LfZn6LNoSvMGuSQ0pLLxj6BLGR",
  "account_type": "onramp",
  "bank_account": {
    "aba_routing_number": "021000021",
    "account_number": "123456789012",
    "account_type": "checking",
    "capabilities": ["ach"]
  }
}
```

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](/documentation/webhooks#transaction-events).

### 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.created` → `transaction.auto.updated` → `wallet.deposit` if the Destination is a Dakota Wallet) you'd see in production.

```bash theme={null}
curl -X POST https://api.platform.sandbox.dakota.xyz/sandbox/simulate/inbound \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "simulation_id": "sim_onramp_001",
    "type": "ach_inbound",
    "account_id": "2LfZn6LNoSvMGuSQ0pLLxj6BLGR",
    "amount": "1.00",
    "currency": "USD",
    "scenario": "success_immediate"
  }'
```

**Response** (synchronous — the deposit event itself processes async):

```json theme={null}
{
  "simulation_id": "sim_onramp_001",
  "state": "accepted",
  "trace_id": "trace_abc123"
}
```

* `type` — `ach_inbound` or `wire_inbound` for USD rails.
* `scenario` — `success_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.

<Warning>
  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](/documentation/testing#sandbox-restrictions).
</Warning>

***

## 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.

```mermaid theme={null}
sequenceDiagram
    participant App as Your App
    participant Blockchain
    participant Dakota
    participant Bank

    App->>Dakota: POST /accounts (offramp)
    Dakota-->>App: ... + source_crypto_address
    App->>Blockchain: Send stablecoin to source_crypto_address
    Blockchain->>Dakota: Stablecoin received
    Dakota->>App: Webhook: transaction.auto.created
    Dakota->>Bank: Wire USD to destination
    Dakota->>App: Webhook: transaction.auto.updated (completed)
```

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/accounts \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "account_type": "offramp",
    "fiat_destination_id": "2LfYm5KMnRvLFtRP7nJJug4zBAN",
    "source_asset": "USDC",
    "source_network_id": "ethereum-mainnet",
    "destination_asset": "USD",
    "rail": "ach"
  }'
```

**Response:**

```json theme={null}
{
  "id": "2LfZn6LNoSvMGuSQ0pLL9Nrw7qZ",
  "account_type": "offramp",
  "source_crypto_address": "0xabc...",
  "source_network_id": "ethereum-mainnet",
  "source_asset": "USDC"
}
```

Each stablecoin transfer to `source_crypto_address` triggers a USD wire to the bank Destination. Same `transaction.auto.*` webhooks as onramp.

<Warning>
  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](/documentation/testing#sandbox-restrictions).
</Warning>

***

## 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.

```mermaid theme={null}
sequenceDiagram
    participant App as Your App
    participant Source as Source chain
    participant Dakota
    participant Target as Target chain

    App->>Dakota: POST /accounts (swap)
    Dakota-->>App: ... + source_crypto_address
    App->>Source: Send source stablecoin to source_crypto_address
    Source->>Dakota: Source stablecoin received
    Dakota->>App: Webhook: transaction.auto.created
    Dakota->>Target: Deliver target stablecoin to destination
    Dakota->>App: Webhook: transaction.auto.updated (completed)
```

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/accounts \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "account_type": "swap",
    "crypto_destination_id": "2LfYm5KMnRvLFtRP7nJJug4zJEP",
    "source_asset": "USDC",
    "source_network_id": "ethereum-mainnet",
    "destination_asset": "USDT",
    "destination_network_id": "polygon-mainnet"
  }'
```

**Response:**

```json theme={null}
{
  "id": "2LfZn6LNoSvMGuSQ0pLLsWp42pQ",
  "account_type": "swap",
  "source_crypto_address": "0xabc...",
  "source_network_id": "ethereum-mainnet",
  "source_asset": "USDC",
  "destination_asset": "USDT",
  "destination_network_id": "polygon-mainnet"
}
```

Send USDC on Ethereum to `source_crypto_address`; receive USDT on Polygon at the Destination. Same `transaction.auto.*` webhooks.

<Warning>
  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](/documentation/testing#sandbox-restrictions).
</Warning>

***

## 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](/documentation/wallets). Signing reference (intent schema, RFC 8785 canonicalization, DER encoding, the browser P1363→DER trap): [Wallet Transaction Signing](/documentation/wallet-signing).

```mermaid theme={null}
sequenceDiagram
    participant App as Your App
    participant Dakota
    participant Custody as Custody / KMS
    App->>App: Generate ES256 keypair
    App->>Dakota: POST /signers (per key)
    App->>Dakota: POST /signer-groups
    App->>Dakota: POST /policies (optional)
    App->>Dakota: POST /wallets
    Dakota->>Custody: Create wallet
    Custody-->>Dakota: Wallet address
    Dakota-->>App: wallet_id + address
    App->>Dakota: POST /wallets/{id}/transactions (signed intent)
```

### 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:

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/signers \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Alice",
    "public_key": "MFkw...",
    "key_type": "ES256"
  }'
```

**Response:**

```json theme={null}
{
  "id": "2LfNs7VVqTwNJxRP4mFFth3yGDM",
  "client_id": "1NFHrqBHb3cTfLVkFSGmHZqdDPi",
  "name": "Alice",
  "public_key": "MFkw...",
  "key_type": "ES256"
}
```

`public_key` is a base64-encoded X.509 SubjectPublicKeyInfo (raw DER or PEM).

### Create a Signer Group

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/signer-groups \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Treasury Team",
    "member_keys": [
      "MFkw...",
      "MFkw..."
    ]
  }'
```

**Response:**

```json theme={null}
{
  "id": "2LfPqT9VmQzKDvQP9rGHth3yHCN",
  "client_id": "1NFHrqBHb3cTfLVkFSGmHZqdDPi",
  "name": "Treasury Team",
  "members": [
    {
      "id": "2LfNs7VVqTwNJxRP4mFFth3yGDM",
      "client_id": "1NFHrqBHb3cTfLVkFSGmHZqdDPi",
      "name": "Alice",
      "public_key": "MFkw...",
      "key_type": "ES256"
    },
    {
      "id": "2LfNt8WWrUxOKyRP5nGGui4zHEN",
      "client_id": "1NFHrqBHb3cTfLVkFSGmHZqdDPi",
      "name": "Bob",
      "public_key": "MFkw...",
      "key_type": "ES256"
    }
  ]
}
```

`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**:

| Rule type            | Controls                                                         |
| -------------------- | ---------------------------------------------------------------- |
| `approval_threshold` | How many signatures from the policy's signer group are required. |
| `amount_threshold`   | Per-transaction value limit, in a chosen currency.               |
| `address_list`       | Allow- or deny-listed destination addresses.                     |

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](/documentation/policies).

The minimal permissive policy — any single member of the signer group can authorize, no other restrictions — is one rule:

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/policies \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Default single-signature policy",
    "description": "Allows any transaction with one signature from the policy'\''s signer group",
    "signer_group_id": "'"$SIGNER_GROUP_ID"'",
    "rules": [
      {
        "rule_type": "approval_threshold",
        "action": "allow",
        "definition": { "threshold": 1 }
      }
    ]
  }'
```

**Response:**

```json theme={null}
{
  "id": "2LfQm5KMnRvLFtRP7nJJug4zJEP",
  "client_id": "1NFHrqBHb3cTfLVkFSGmHZqdDPi",
  "signer_group_id": "2LfPqT9VmQzKDvQP9rGHth3yHCN",
  "version": 1,
  "name": "Default single-signature policy",
  "description": "Allows any transaction with one signature from the policy's signer group",
  "rules": [
    {
      "id": "2LfRn6LOoSwMGuSQ8oKKvi5AKFQ",
      "policy_id": "2LfQm5KMnRvLFtRP7nJJug4zJEP",
      "rule_type": "approval_threshold",
      "action": "allow",
      "definition": { "threshold": 1 },
      "created_at": 1640995200
    }
  ],
  "created_at": 1640995200,
  "updated_at": 1640995200
}
```

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](/documentation/wallet-signing#modifying-policies-wallets-and-signer-groups).

### 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`.

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/wallets \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Treasury Wallet",
    "family": "evm",
    "signer_groups": ["'"$SIGNER_GROUP_ID"'"],
    "policies": ["'"$POLICY_ID"'"]
  }'
```

**Response:**

```json theme={null}
{
  "id": "2LfZm5KMnRvLFtRP7nJJug4zJEP",
  "client_id": "1NFHrqBHb3cTfLVkFSGmHZqdDPi",
  "name": "Treasury Wallet",
  "family": "evm",
  "address": "0x165cd37b4c644c2921454429e7f9358d18a45e14"
}
```

<Note>
  **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](#attach-a-policy-to-an-existing-wallet) below.
</Note>

### 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).

```bash theme={null}
curl -X PUT https://api.platform.dakota.xyz/policies/$POLICY_ID/wallets/$WALLET_ID \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "signatures": ["MEUCIQDXA8sfHIe...base64-encoded-DER..."],
    "intent": {
      "type": "attach_policy_to_wallet",
      "policy_id": "'"$POLICY_ID"'",
      "wallet_id": "'"$WALLET_ID"'",
      "idempotency_key": "a6f8c8c0-6f0a-4a24-a3a3-9e8a0cf2f7c0"
    }
  }'
```

**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](/documentation/wallet-signing#modifying-policies-wallets-and-signer-groups).

After attachment, the wallet can submit transactions immediately. You can repeat this call with additional `policy_id`s 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](/documentation/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.

```bash theme={null}
curl -X POST https://api.platform.dakota.xyz/wallets/$WALLET_ID/transactions \
  -H "X-API-Key: $DAKOTA_API_KEY" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "signatures": ["MEQCIEtPHo4edFaeOAWql3CHzcEJTX0MlUxjnqdlQwv+FYbr..."],
    "intent": {
      "wallet_id": "2LfZm5KMnRvLFtRP7nJJug4zJEP",
      "caip2": "eip155:1",
      "operation": {
        "kind": "transfer",
        "from": "0xYourWalletAddress...",
        "to": "0xDestinationAddress...",
        "amount": "10.5",
        "asset_id": "USDC"
      },
      "idempotency_key": "a6f8c8c0-6f0a-4a24-a3a3-9e8a0cf2f7c0"
    }
  }'
```

**Response:**

```json theme={null}
{
  "id": "2LfWtXnLkRvMFuPR8mLLjgHZ4QE",
  "resource_type": "wallet",
  "wallet_id": "2LfZm5KMnRvLFtRP7nJJug4zJEP",
  "network_id": "ethereum-mainnet",
  "from": "0x165cd37b4c644c2921454429e7f9358d18a45e14",
  "to": "0xDestinationAddress...",
  "transaction_type": "transfer",
  "amount": "10.5",
  "asset": "USDC",
  "status": "pending"
}
```

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](/documentation/wallet-signing).

***

## Dedicated References

* [Wallets](/documentation/wallets) — architecture, signer groups, policies
* [Wallet Transaction Signing](/documentation/wallet-signing) — intent schemas, canonicalization, signing code in Node / Python / Go / browser
* [Webhooks](/documentation/webhooks) — event types, delivery, signature verification
* [API Reference](/api-reference/introduction) — full endpoint reference and OpenAPI spec
