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

# Customers Quickstart

> Onboard a KYC'd customer in Sandbox, fund and move USDC, hit a capability error, then resolve it — all with the CDP CLI.

<Note>
  The Customers API is available to select partners during onboarding, and currently supports KYC of US individuals only. If you're interested in using this product, [get in touch](https://www.coinbase.com/developer-platform/developer-interest) and our team will follow up to discuss fit.
</Note>

<Note>
  Not yet onboarded for Live? Try the Customers API in [Sandbox](/get-started/sandbox/overview). Live access is limited to onboarded partners; Sandbox is available to everyone.
</Note>

This guide walks the full customer lifecycle in **Sandbox** using the **[CDP CLI](/get-started/build-with-ai/cdp-for-agents)**. You will:

1. Create an individual customer and request custody and transfer capabilities.
2. Create an entity-owned account and a customer-owned account.
3. Fund the entity account with USDC and transfer some to the customer.
4. Try to convert the customer's USDC to USD — and watch it fail because the customer is missing a capability.
5. Request the missing capability and retry the conversion successfully.

**Base URL (Sandbox):** `https://sandbox.cdp.coinbase.com/platform/v2` (for `cdp api` and `cdp env`)

## Prerequisites

* A CDP login with access to the [CDP Portal](https://portal.cdp.coinbase.com)
* [Node.js](https://nodejs.org/) 22 or later
* A **Sandbox** Secret API Key JSON from the Portal (Sandbox environment)

<Warning>
  Never commit API keys to source control. Store them in environment variables or a secrets manager. Do not use real personal data in Sandbox.
</Warning>

### Install and configure the CDP CLI

Requires [Node.js](https://nodejs.org/) 22 or later.

```bash theme={null}
npm install -g @coinbase/cdp-cli@latest --force
cdp --version
```

Configure a Sandbox environment from your downloaded key file. Point it at the Sandbox platform base so `cdp api` paths resolve correctly:

```bash theme={null}
cdp env sandbox --key-file ./cdp-api-key.json --url https://sandbox.cdp.coinbase.com/platform/v2
```

Every command below passes `-e sandbox` to target that environment. For CLI basics — field syntax, `--jq`, `--dry-run` — see [CDP for Agents](/get-started/build-with-ai/cdp-for-agents).

## 1. Create a KYC'd customer

Create an `individual` customer, submit their identity information, record Terms of Service and tax attestations, and request the custody and transfer capabilities they need. Note that `tradeStablecoin` is **not** requested yet — you add it later in step 7.

In Sandbox, the `fullSsn` value `000-00-0000` is a [magic value](/customers-kyc/requirements#testing-in-sandbox) that forces a deterministic **approval**, so the requested capabilities verify and become `active` without real PII.

```bash theme={null}
CUSTOMER_ID=$(cdp api -X POST /platform/v2/customers -e sandbox --jq=.customerId - <<'JSON'
{
  "type": "individual",
  "individual": {
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "jane.doe@example.com",
    "phoneNumber": "+16175551212",
    "fullSsn": "000-00-0000",
    "dateOfBirth": { "day": "1", "month": "1", "year": "1987" },
    "address": {
      "line1": "500 Main St",
      "city": "Boston",
      "state": "MA",
      "postCode": "02108",
      "countryCode": "US"
    },
    "purposeOfAccount": "investing",
    "sourceOfFunds": "salary"
  },
  "tosAcceptances": [
    { "versionId": "us_individual_2026-05-29", "language": "en", "acceptedAt": "2026-04-17T20:00:00Z" }
  ],
  "taxAttestations": [
    { "form": "us_w9", "isExemptBackupWithholding": true, "edeliveryConsent": true, "acceptedAt": "2026-04-17T20:00:00Z" }
  ],
  "compliance": { "requesterIpAddress": "203.0.113.10" },
  "capabilities": {
    "custodyCrypto": { "requested": true },
    "custodyFiat": { "requested": true },
    "custodyStablecoin": { "requested": true },
    "transferCrypto": { "requested": true },
    "transferFiat": { "requested": true },
    "transferStablecoin": { "requested": true }
  }
}
JSON
)
echo "CUSTOMER_ID=$CUSTOMER_ID"
```

The request body carries the full baseline that every capability requires: identity fields, Terms of Service acceptance, a W-9 tax attestation, and the end-user's IP address on `compliance.requesterIpAddress`. See [Requirements](/customers-kyc/requirements#which-capabilities-require-which-requirements) for the field-to-capability mapping.

<Note>
  Verification is asynchronous. With the approval magic value, the requested capabilities settle to `active` quickly, but in production you should subscribe to the [`customers.capability.changed`](/customers-kyc/capabilities#webhooks) webhook rather than poll.
</Note>

## 2. Confirm the customer's capabilities are active

```bash theme={null}
cdp api /platform/v2/customers/$CUSTOMER_ID -e sandbox
```

<Accordion title="Example response">
  ```json theme={null}
  {
    "customerId": "customer_af2937b0-9846-4fe7-bfe9-ccc22d935114",
    "type": "individual",
    "individual": {
      "firstName": "Jane",
      "lastName": "Doe",
      "email": "jane.doe@example.com"
    },
    "capabilities": {
      "custodyCrypto": { "requested": true, "status": "active" },
      "custodyFiat": { "requested": true, "status": "active" },
      "custodyStablecoin": { "requested": true, "status": "active" },
      "tradeCrypto": { "requested": false, "status": "unrequested" },
      "tradeStablecoin": { "requested": false, "status": "unrequested" },
      "transferCrypto": { "requested": true, "status": "active" },
      "transferFiat": { "requested": true, "status": "active" },
      "transferStablecoin": { "requested": true, "status": "active" }
    },
    "createdAt": "2026-04-17T20:00:00Z",
    "updatedAt": "2026-04-17T20:00:05Z"
  }
  ```
</Accordion>

All six requested capabilities are `active`. `tradeStablecoin` is still `unrequested` — that's intentional.

## 3. Create an entity account

Create an account owned by your entity (your own treasury). Omitting `owner` makes the account entity-owned.

```bash theme={null}
ENTITY_ACCOUNT_ID=$(cdp api -X POST /platform/v2/accounts -e sandbox \
  name="Treasury account" --jq=.accountId)
echo "ENTITY_ACCOUNT_ID=$ENTITY_ACCOUNT_ID"
```

<Accordion title="Example response">
  ```json theme={null}
  {
    "accountId": "account_af2937b0-9846-4fe7-bfe9-ccc22d935114",
    "type": "cdp",
    "owner": "entity_af2937b0-9846-4fe7-bfe9-ccc22d935114",
    "name": "Treasury account",
    "createdAt": "2026-04-17T20:01:00Z",
    "updatedAt": "2026-04-17T20:01:00Z"
  }
  ```
</Accordion>

## 4. Create the customer's account

Pass the customer ID as `owner` to create a customer-owned account. This requires the customer to hold the `custodyCrypto` capability, which Jane does. Customer-facing operations also require the end-user's IP on `compliance.requesterIpAddress`.

```bash theme={null}
CUSTOMER_ACCOUNT_ID=$(cdp api -X POST /platform/v2/accounts -e sandbox \
  owner=$CUSTOMER_ID name="Jane Doe account" \
  compliance.requesterIpAddress=8.8.8.8 \
  --jq=.accountId)
echo "CUSTOMER_ACCOUNT_ID=$CUSTOMER_ACCOUNT_ID"
```

<Accordion title="Example response">
  ```json theme={null}
  {
    "accountId": "account_b1748c92-12ef-4a0b-9d3e-77a1e5f0c233",
    "type": "cdp",
    "owner": "customer_af2937b0-9846-4fe7-bfe9-ccc22d935114",
    "name": "Jane Doe account",
    "createdAt": "2026-04-17T20:02:00Z",
    "updatedAt": "2026-04-17T20:02:00Z"
  }
  ```
</Accordion>

<Note>
  If the customer is missing a required capability (for example `custodyCrypto`), this call returns `403` with `errorType: customer_not_authorized`. Resolve the customer's [requirements](/customers-kyc/requirements) before retrying.
</Note>

## 5. Fund the entity account

Sandbox does not move real funds. Seed a USDC balance on the entity account with the Sandbox-only funding helper. This endpoint lives at the host root (`/fake/balances`), not under `/platform/v2`.

```bash theme={null}
cdp api -X POST /fake/balances -e sandbox - <<JSON
{
  "account_id": "$ENTITY_ACCOUNT_ID",
  "balances": [
    {
      "asset": { "symbol": "usdc", "type": "crypto", "name": "USD Coin", "decimals": 6 },
      "availableAmount": "10.00",
      "totalAmount": "10.00"
    }
  ]
}
JSON
```

<Warning>
  `/fake/balances` is a Sandbox-only helper for seeding test balances. It does not exist in production, where accounts are funded by real deposits.
</Warning>

Confirm the balance landed:

```bash theme={null}
cdp api /platform/v2/accounts/$ENTITY_ACCOUNT_ID/balances -e sandbox
```

<Accordion title="Example response">
  ```json theme={null}
  {
    "balances": [
      {
        "asset": { "symbol": "usdc", "type": "crypto", "name": "USD Coin", "decimals": 6 },
        "amount": {
          "usdc": { "available": "10.00", "total": "10.00" }
        }
      }
    ]
  }
  ```
</Accordion>

## 6. Transfer USDC to the customer

Move USDC from the entity account to the customer's account. Because USDC is a stablecoin, the receiving customer must hold the `custodyStablecoin` capability — Jane does.

```bash theme={null}
cdp api -X POST /platform/v2/transfers -e sandbox \
  source.accountId=$ENTITY_ACCOUNT_ID source.asset=usdc \
  target.accountId=$CUSTOMER_ACCOUNT_ID target.asset=usdc \
  amount=5.00 asset=usdc \
  execute:=true \
  compliance.requesterIpAddress=203.0.113.10
```

<Accordion title="Example response">
  ```json theme={null}
  {
    "transferId": "transfer_8a2c0e7f-194d-4e9a-b5f6-d8a2c0e7f194",
    "status": "processing",
    "source": { "accountId": "account_af2937b0-9846-4fe7-bfe9-ccc22d935114", "asset": "usdc" },
    "target": { "accountId": "account_b1748c92-12ef-4a0b-9d3e-77a1e5f0c233", "asset": "usdc" },
    "sourceAmount": "5.00",
    "sourceAsset": "usdc",
    "targetAmount": "5.00",
    "targetAsset": "usdc",
    "createdAt": "2026-04-17T20:03:00Z",
    "updatedAt": "2026-04-17T20:03:00Z"
  }
  ```
</Accordion>

Confirm the customer account now holds USDC:

```bash theme={null}
cdp api /platform/v2/accounts/$CUSTOMER_ACCOUNT_ID/balances -e sandbox
```

## 7. Convert USDC to USD (this fails)

Now convert the customer's USDC to USD. A conversion is a transfer where `source` and `target` are the **same account** but different assets. Set `amountType` to `target` so the customer receives an exact USD amount.

```bash theme={null}
cdp api -X POST /platform/v2/transfers -e sandbox \
  source.accountId=$CUSTOMER_ACCOUNT_ID source.asset=usdc \
  target.accountId=$CUSTOMER_ACCOUNT_ID target.asset=usd \
  amount=2.00 asset=usd amountType=target \
  execute:=true \
  compliance.requesterIpAddress=203.0.113.10
```

This returns `403 Forbidden`:

<Tabs>
  <Tab title="CDP CLI">
    ```text theme={null}
    error: customer_not_authorized — Customer is not authorized for one or more capabilities required by this action. (HTTP 403) [unauthorized capabilities: tradeStablecoin]
    ```
  </Tab>

  <Tab title="HTTP response">
    ```json theme={null}
    {
      "errorType": "customer_not_authorized",
      "errorMessage": "Customer is not authorized for one or more capabilities required by this action.",
      "unauthorizedCapabilities": ["tradeStablecoin"]
    }
    ```
  </Tab>
</Tabs>

### Capabilities required to convert USDC to USD

A conversion changes an asset's denomination within a single account, so it gates on the customer holding **both** of these:

| Capability        | Why it's required                                                |
| ----------------- | ---------------------------------------------------------------- |
| `tradeStablecoin` | Performs the trade between a stablecoin (USDC) and another asset |
| `custodyFiat`     | Holds the resulting USD                                          |

A conversion needs **no** `transfer*` capability — the funds never leave the account. (The reverse direction, USD to USDC, would instead require `tradeStablecoin` + `custodyStablecoin`.)

Jane already holds `custodyFiat`, so the only missing capability is `tradeStablecoin` — exactly what `unauthorizedCapabilities` reports.

<Note>
  Always read `unauthorizedCapabilities` to learn precisely which capabilities the customer is missing for an operation, then request them. The error message itself is intentionally generic — never surface compliance-specific reasons to your end-users.
</Note>

## 8. Request the missing capability

Add `tradeStablecoin` with an update. The baseline requirements were already satisfied in step 1, so in Sandbox the capability verifies and becomes `active`.

```bash theme={null}
cdp api -X PUT /platform/v2/customers/$CUSTOMER_ID -e sandbox - <<'JSON'
{
  "capabilities": { "tradeStablecoin": { "requested": true } },
  "compliance": { "requesterIpAddress": "203.0.113.10" }
}
JSON
```

Confirm `tradeStablecoin` is now `active`:

```bash theme={null}
cdp api /platform/v2/customers/$CUSTOMER_ID -e sandbox --jq=.capabilities.tradeStablecoin
```

```json theme={null}
{ "requested": true, "status": "active" }
```

<Note>
  If `tradeStablecoin` comes back `pending` instead of `active`, inspect the customer's `requirements` map for outstanding items and resolve them. See [Requirements](/customers-kyc/requirements#the-resolution-loop).
</Note>

## 9. Retry the conversion

With `tradeStablecoin` active, run the same conversion again — it now succeeds.

```bash theme={null}
cdp api -X POST /platform/v2/transfers -e sandbox \
  source.accountId=$CUSTOMER_ACCOUNT_ID source.asset=usdc \
  target.accountId=$CUSTOMER_ACCOUNT_ID target.asset=usd \
  amount=2.00 asset=usd amountType=target \
  execute:=true \
  compliance.requesterIpAddress=203.0.113.10
```

<Accordion title="Example response">
  ```json theme={null}
  {
    "transferId": "transfer_5d747be1-60e2-4805-04c0-99d984bcfe01",
    "status": "processing",
    "source": { "accountId": "account_b1748c92-12ef-4a0b-9d3e-77a1e5f0c233", "asset": "usdc" },
    "target": { "accountId": "account_b1748c92-12ef-4a0b-9d3e-77a1e5f0c233", "asset": "usd" },
    "sourceAmount": "2.00",
    "sourceAsset": "usdc",
    "targetAmount": "2.00",
    "targetAsset": "usd",
    "exchangeRate": { "sourceAsset": "usdc", "targetAsset": "usd", "rate": "1" },
    "createdAt": "2026-04-17T20:05:00Z",
    "updatedAt": "2026-04-17T20:05:00Z"
  }
  ```
</Accordion>

You've onboarded a customer, provisioned accounts, moved funds, and learned the core gating pattern: **check the capability, resolve what's missing, then retry**.

## Move to production

To run this flow for real, switch the Sandbox base URL to the production base URL and use a production API key. In production there are no magic values and no `/fake/balances` helper — customers complete real KYC, and accounts are funded by real deposits.

## What to read next

<CardGroup cols={2}>
  <Card title="Capabilities" icon="circle-check" href="/customers-kyc/capabilities">
    The full capability set, statuses, and how to gate operations
  </Card>

  <Card title="Requirements" icon="list-check" href="/customers-kyc/requirements">
    Resolve outstanding requirements, Terms of Service, and tax attestations
  </Card>

  <Card title="Customers" icon="user" href="/customers-kyc/customers">
    The customer record, identity fields, and the verification lifecycle
  </Card>

  <Card title="Customers API reference" icon="code" href="/api-reference/v2/rest-api/customers/customers">
    REST reference for customer, account, and transfer endpoints
  </Card>
</CardGroup>
