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

# Card funding (wallet & BYO)

> Fund cards with stablecoins using Apocor wallet service or a bring-your-own (BYO) external wallet.

Cards are topped up from **stablecoins** deposited for the cardholder (applicant). Apocor exposes one receive address and a one-click fund call. You choose the product mode **per program**.

## How money moves

Each applicant has three balances, and funds flow through them in order:

```mermaid theme={null}
flowchart LR
  D[Deposit wallet<br/>USDC / stables] -->|swap on fund| S[Spend account<br/>USD]
  S -->|transfer| B[Card funds<br/>budget or prepaid balance]
  B --> C[Card spending]
```

1. **Deposit wallet** — where on-chain stablecoin deposits land (`GET …/funding/deposit-address`).
2. **Spend account** — the applicant's USD balance. Deposits are swapped in automatically when a funding call needs them.
3. **Card funds** — the budget (debit programs) or per-card balance (prepaid programs) that transactions draw from. Top up with `POST /v1/cards/{id}/fund-from-wallet`, or move USD explicitly with `POST /v1/accounts/{id}/transfers`.

You never have to move money between pots 1 and 2 yourself: both `fund-from-wallet` and `transfers` (direction `"in"`) auto-convert deposited stablecoins to USD when the spend balance is short, and report what they converted in a `swapped` array.

<Warning>
  **Card issuance charges an opening fee from the card funds pot.** Make sure
  the applicant's funding is in place *before* the first `POST /v1/cards`, or
  issuance fails with an issuer `Balance insufficient` error. The whole
  first-card flow is three steps: deposit stablecoins → one
  `POST /v1/accounts/{id}/transfers` with `direction: "in"` (auto-converts the
  deposit and fills the budget) → issue the card.
</Warning>

| Mode                       | Who sends                                        | Your job                                                                                    |
| -------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| `wallet_service` (default) | End-user (or you) to Apocor’s deposit address    | Show deposit address → wait for balance → `fund-from-wallet`                                |
| `byo_wallet`               | End-user **from** a wallet address you configure | Store their wallet → show deposit address → they send from that wallet → `fund-from-wallet` |

Both modes use the **same** deposit address and the **same** `POST /v1/cards/{id}/fund-from-wallet` call. BYO only adds the configured send-from wallet on the program and on deposit-address responses.

<Note>
  Upstream issuer / crypto-rail names never appear in responses. You only see Apocor fields.
</Note>

## Testing deposits with TEST keys

The TEST environment runs on issuer **testnets**. Deposits are only detected for:

| Currency | Chain | Testnet          | Faucet                                         |
| -------- | ----- | ---------------- | ---------------------------------------------- |
| `USDC`   | `ETH` | Ethereum Sepolia | [faucet.circle.com](https://faucet.circle.com) |

Request the deposit address with `?chain=ETH&currency=USDC`, then send testnet
USDC (Ethereum Sepolia) from the Circle faucet. Credits appear in
`funding/balances` within a couple of minutes.

<Warning>
  Other chains (Base, Arbitrum, Optimism, Polygon, Solana) generate valid
  addresses in TEST but **deposits on them never credit** — the issuer sandbox
  supports withdrawals only on those chains. Testnet funds sent there are
  unrecoverable. In LIVE, all configured chains support deposits normally.
</Warning>

## 1. Set the program funding mode

### Wallet service (default)

```bash theme={null}
curl -s -X PUT 'https://sandbox.apocor.ai/v1/programs/PROGRAM_ID/funding-config' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "mode": "wallet_service",
    "preferred_chain": "BASE",
    "preferred_currency": "USDC"
  }'
```

### Bring-your-own (BYO) wallet

`byo.address` is **required** — the user’s external wallet they will send from.

```bash theme={null}
curl -s -X PUT 'https://sandbox.apocor.ai/v1/programs/PROGRAM_ID/funding-config' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "mode": "byo_wallet",
    "preferred_chain": "BASE",
    "preferred_currency": "USDC",
    "byo": {
      "address": "0xUserWalletAddress",
      "chain": "BASE",
      "currency": "USDC",
      "provider": "metamask"
    }
  }'
```

Read back with `GET /v1/programs/{id}/funding-config`.

## 2. Get the deposit address

After the applicant has completed identity verification (`status: APPROVED`):

```bash theme={null}
curl -s 'https://sandbox.apocor.ai/v1/applicants/APPLICANT_ID/funding/deposit-address?program_id=PROGRAM_ID&chain=BASE&currency=USDC' \
  -H 'Authorization: Bearer YOUR_TOKEN'
```

<Tip>
  With TEST keys use `chain=ETH` — the issuer sandbox only credits deposits on
  Ethereum Sepolia (see [Testing deposits](#testing-deposits-with-test-keys)).
</Tip>

Example **BYO** response:

```json theme={null}
{
  "data": {
    "applicant_id": "app_abc123",
    "mode": "byo_wallet",
    "address": "0xDepositReceiveAddress",
    "currency": "USDC",
    "chain": "BASE",
    "instructions": "Send from your configured external wallet to this address, then call fund-from-wallet on the card.",
    "byo_wallet": {
      "address": "0xUserWalletAddress",
      "chain": "BASE",
      "currency": "USDC",
      "provider": "metamask"
    }
  }
}
```

* **`address`** — always the **receive** address (send *to* this).
* **`byo_wallet.address`** — only in `byo_wallet` mode; the configured **send-from** wallet.

## 3. Check balances

```bash theme={null}
curl -s 'https://sandbox.apocor.ai/v1/applicants/APPLICANT_ID/funding/balances' \
  -H 'Authorization: Bearer YOUR_TOKEN'
```

Use `available_to_fund_usd` to know when you can fund: it is the sum of the USD spend balance plus stablecoin deposits that can still be converted. It does **not** include amounts already moved into budgets — check those with `GET /v1/accounts/{id}/budgets`.

## 4. Fund the card

```bash theme={null}
curl -s -X POST 'https://sandbox.apocor.ai/v1/cards/CARD_ID/fund-from-wallet' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: fund-CARD_ID-25' \
  -d '{ "amount": "25.00" }'
```

This swaps deposited stables → USD and credits the card's spend pot. Minimums are enforced by the rail (typically ≥ \$0.50). `Idempotency-Key` is **required** on `fund`, `fund-from-wallet`, and `defund` — retries with the same key replay the original response for 72 hours.

## Budgets (debit programs)

Debit programs spend from **budgets** — named pots of USD that cards draw from. Every applicant gets a **default budget** automatically the first time funds move or a card is issued; you can create more to ring-fence spend (e.g. "Marketing" vs "Travel").

### Top up a budget

```bash theme={null}
# Fund the budget (e.g. before first issuance, to cover the opening fee).
# If the spend balance is short, deposited stablecoins are converted automatically.
curl -s -X POST 'https://sandbox.apocor.ai/v1/accounts/ACCOUNT_ID/transfers' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: topup-ACCOUNT_ID-001' \
  -d '{ "applicant_id": "APPLICANT_ID", "direction": "in", "amount": "15.00" }'
```

```json theme={null}
{
  "data": {
    "account_id": "acct_abc123",
    "budget_id": "bud_xyz789",
    "direction": "in",
    "amount": "15.00",
    "type": "budget",
    "swapped": [{ "currency": "USDC", "amount": "15.00" }]
  }
}
```

`direction: "in"` funds the budget (auto-converting deposits if needed); `direction: "out"` withdraws back to the spend account. Pass `budget_id` to target a specific budget — omitted, the default budget is used. If deposits plus spend balance still don't cover the amount, you get `402 INSUFFICIENT_FUNDS`. `Idempotency-Key` is optional here but honored — send one to make retries safe.

### Named budgets

```bash theme={null}
# Create an extra budget
curl -s -X POST 'https://sandbox.apocor.ai/v1/accounts/ACCOUNT_ID/budgets' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "applicant_id": "APPLICANT_ID", "name": "Marketing" }'

# List budgets with balances
curl -s 'https://sandbox.apocor.ai/v1/accounts/ACCOUNT_ID/budgets?applicant_id=APPLICANT_ID' \
  -H 'Authorization: Bearer YOUR_TOKEN'

# Rename
curl -s -X PATCH 'https://sandbox.apocor.ai/v1/budgets/BUDGET_ID' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "name": "Growth" }'
```

Bind a card to a budget at issuance by passing `budget_id` to `POST /v1/cards` (defaults to the default budget). The card's spending and its `fund-from-wallet` top-ups then use that budget.

## Flow

```mermaid theme={null}
sequenceDiagram
  participant App as Your app
  participant Apocor as Apocor API
  participant User as End-user wallet

  App->>Apocor: PUT funding-config (wallet_service or byo_wallet)
  App->>Apocor: GET deposit-address
  Apocor-->>App: receive address (+ byo send-from if BYO)
  User->>Apocor: Send USDC to receive address
  App->>Apocor: GET funding/balances
  App->>Apocor: POST cards/{id}/fund-from-wallet
  Apocor-->>App: Card spend balance updated
```

## Errors to expect

| Situation                                                           | Typical result                                                                                                                     |
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `byo_wallet` without `byo.address`                                  | `400` — address required                                                                                                           |
| Applicant KYC incomplete                                            | `400` — no issuer account yet                                                                                                      |
| Amount below rail minimum / insufficient deposit                    | `400` / issuer error                                                                                                               |
| Transfer-in larger than deposits + spend balance                    | `402 INSUFFICIENT_FUNDS` — deposit more, wait for confirmation, retry                                                              |
| Wrong chain or currency                                             | Deposit may not credit — match `preferred_*` / query params                                                                        |
| Missing `Idempotency-Key` on `fund` / `fund-from-wallet` / `defund` | `400 IDEMPOTENCY_KEY_REQUIRED`                                                                                                     |
| Card issued before funding is in place                              | `card.issuance.failed` event — `Balance insufficient` (opening fee). Fund, transfer in, and retry with a **new** `Idempotency-Key` |
| TEST deposit sent on an unsupported chain (e.g. Base Sepolia)       | Never credits — use USDC on Ethereum Sepolia                                                                                       |

## Related

* [Programs & accounts](/concepts/programs-accounts) — create the program first
* [Cards](/concepts/cards) — issue the prepaid card before funding
* API reference: `PUT /v1/programs/{id}/funding-config`, `GET …/funding/deposit-address`, `POST /v1/cards/{id}/fund-from-wallet`
