> For the complete documentation index, see [llms.txt](https://docs.zebec.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.zebec.io/developer-docs/partner-api/topup-flow.md).

# Top-up flow

Top up a Carbon card through the Zebec Partner API.

This is the shared lifecycle for every token returned by `GET /partner/v1/topup/programs`. The payment itself is chain-specific; use the quote as the authority for the chain, network, token, amount, and payment instructions.

For implementation examples, see [EVM](/developer-docs/partner-api/topup-flow/evm.md), [Solana](/developer-docs/partner-api/topup-flow/solana.md), [Canton](/developer-docs/partner-api/topup-flow/canton.md), or [Zano](/developer-docs/partner-api/topup-flow/zano.md).

## Flow overview

1. Discover the partner's enabled programs and tokens.
2. Request a short-lived quote.
3. Run preflight checks for the wallet that will pay.
4. Send the chain-native payment described by the quote.
5. Sign the quote's canonical signing message and submit the receipt.
6. Poll the returned order until it reaches a terminal state.

## 1. Discover programs

```http
GET /partner/v1/topup/programs
Authorization: Bearer <accessToken>
```

The response contains the available Carbon programs, target currencies, and the tokens enabled for the authenticated partner. Choose a token, then retain its `chain`, `sourceTokenMint`, and `decimals` for the quote request. Do not hardcode an address, mint, network, or token precision.

```json
{
  "programs": [
    {
      "programId": "carbon-intl",
      "displayName": "Carbon International",
      "currencies": ["USD"],
      "acceptedTokens": [
        {
          "symbol": "USDC",
          "chain": "SOLANA",
          "contractAddress": "<token-address-or-mint>",
          "sourceTokenMint": "<token-address-or-mint>",
          "decimals": 6,
          "platformFeePercentage": "0.025"
        }
      ]
    }
  ],
  "currencies": { "USD": { "rate": 1, "symbol": "$" } }
}
```

## 2. Build a quote

```http
GET /partner/v1/topup/quote?amount=100&currencyCode=USD&sourceChain=<chain>&sourceTokenMint=<token-address-or-mint>
Authorization: Bearer <accessToken>
```

Copy `sourceChain` and `sourceTokenMint` from the selected accepted-token entry. A quote is single-use and valid for two minutes; `expiresIn` is its Unix-millisecond expiration time.

Save the following values:

* `id` — use as `quoteId`.
* `cardId`, `requestedAmount`, and `payment` — bind the payment to this quote.
* `payment.chain`, `payment.network`, `payment.contractAddress`, `payment.tokenAmount`, and `payment.tokenDecimals` — exact payment inputs.
* `signingMessage` — sign this exact, unmodified UTF-8 message after payment.
* Any additional rail-specific fields, such as a destination address, payment ID, contract route, or swap data.

`payment.tokenAmount` is a base-unit string. Interpret it using `payment.tokenDecimals`, not a token precision stored in your app.

## 3. Preflight

Run preflight before submitting a payment. Include the wallet identifier that will send the payment; it must match the receipt submitted later.

```http
POST /partner/v1/topup/preflight
Authorization: Bearer <accessToken>
Content-Type: application/json

{
  "quoteId": "<quote-id>",
  "amount": { "amount": 100, "currencyCode": "USD" },
  "programId": "<program-id>",
  "receipt": {
    "deposit": { "buyerAddress": "<paying-wallet-identifier>" }
  }
}
```

Proceed only when `canProceed` is `true`. A `fail` check makes it false; a `warn` is informational. The successful result is tied to the quote, expires with it, and is consumed when the top-up is submitted.

## 4. Send the payment

Execute the payment with the user's wallet, using the values returned by the quote. Wait until the wallet reports a confirmed transaction identifier (`txHash`).

* **EVM and Solana:** use the Partners Card SDK's `purchaseCard()` method. It selects and submits the quote-authorized card purchase path.
* **Canton, Zano, and other supported rails:** use the wallet's native token transfer function, supplying the quote's network, token, destination, and base-unit amount exactly as returned.

| Payment rail                             | Example                                                           |
| ---------------------------------------- | ----------------------------------------------------------------- |
| Ethereum, Base, BNB Smart Chain, Polygon | [EVM top-up](/developer-docs/partner-api/topup-flow/evm.md)       |
| Solana                                   | [Solana top-up](/developer-docs/partner-api/topup-flow/solana.md) |
| Canton                                   | [Canton top-up](/developer-docs/partner-api/topup-flow/canton.md) |
| Zano                                     | [Zano top-up](/developer-docs/partner-api/topup-flow/zano.md)     |

## 5. Sign and submit

After the payment is confirmed, sign the exact `signingMessage` from the quote and submit the receipt. Use the signature scheme appropriate to the quoted chain; the chain examples show the required identifiers and encodings.

```http
POST /partner/v1/topup
Authorization: Bearer <accessToken>
Content-Type: application/json

{
  "quoteId": "<quote-id>",
  "receipt": {
    "deposit": {
      "txHash": "<confirmed-transaction-id>",
      "buyerAddress": "<paying-wallet-identifier>"
    }
  },
  "userSignature": {
    "signature": "<signature-of-signingMessage>",
    "publicKey": "<signer-public-key-or-wallet-identifier>",
    "scheme": "<evm|solana|canton|zano>"
  }
}
```

For Canton, include `cantonAlgorithm` when the wallet does not use the default `ed25519` algorithm. The server derives chain and token metadata from the quote, so do not add a client-defined token, network, or payment ID.

The endpoint returns `202 Accepted`:

```json
{
  "orderId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "PENDING",
  "statusUrl": "/partner/v1/topup/550e8400-e29b-41d4-a716-446655440000"
}
```

Submits are idempotent for seven days by `receipt.deposit.txHash`. Reuse the same receipt only to retry an interrupted request; do not use a confirmed transaction for another quote.

## 6. Poll status

```http
GET /partner/v1/topup/<orderId>
Authorization: Bearer <accessToken>
```

Poll `statusUrl` until the order reaches a terminal status. Do not infer card balance changes from transaction broadcast alone; the top-up completes through the validation and card-provider pipeline.

## Errors and retries

* Request a new quote after expiry, a failed preflight, or a failed payment.
* Correct a signature or receipt error, then start again with a new quote and preflight because preflight is one-shot.
* Retry temporary network failures with the same `txHash`; do not send the payment again until you have confirmed whether it settled.
* Treat the partner [sandbox Swagger UI](https://dev-super.api.zebec.io/api/partner) as the source for complete schemas and current error responses.
