Card Top-Up
Estimate and execute a top-up that funds a user wallet from an external card, Apple Pay, or Google Pay.
Before You Start
Read the following guides before proceeding:
| Guide | Why |
|---|---|
| Getting Started | Platform overview and setup |
| Api Basics | Required headers and request configuration |
| Authentication | How to obtain access tokens |
| Onboarding | User and wallet registration |
| KYC | KYC verification requirements |
| Top-Up Card Registration | Register the source card |
Overview
A card top-up funds a user's wallet with an on-chain token, debited from one of three instruments,
named by payment_method on the estimate:
external_card(default) — an external payment card (Visa/Mastercard) via AFT (Account Funding
Transaction) rails, registered through Top-Up Card Registrationapple_pay— the card behind an Apple Pay device payment sheetgoogle_pay— the card behind a Google Pay device payment sheet
The top-up flow:
- Estimate — Size the debit for the token amount the user wants to receive, naming the instrument
- Execute — Start the pull from the registered card, or relay the payment-sheet payload for a wallet method
- 3DS — Complete the 3DS challenge when required
The user specifies the destination token and amount they want to receive. The estimate returns the source (fiat) amount their instrument will be charged, the fee, and the exchange rate.
Capability Check
An external_card top-up requires the CardTopup capability to be active. An apple_pay or google_pay
top-up requires WalletTopup instead — a separate capability, so a partner can be live on card top-up while
wallet-pay enrolment is still pending, or the reverse. Check capability status via GET /api/v2/user before
initiating top-ups.
Estimate Top-Up
Get a top-up estimation for the token amount the user wants to receive.
Endpoint
POST /api/v1/cards/topup/estimate
Request
{
"external_card_id": "c34908da-d980-4a9c-9b39-4dabd6f6144e",
"destination_amount": "100000000",
"destination_token_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"payment_method": "external_card"
}| Field | Type | Required | Description |
|---|---|---|---|
external_card_id | string (UUID) | Conditional | card_id from card registration. Required when payment_method is external_card (or omitted); rejected for apple_pay / google_pay. Can be retrieved via GET {pci_base_url}/b2b/cards/topup |
destination_amount | string | Yes | Token amount to receive, as an integer string scaled by the token's decimals (e.g. "100000000" for 100 USDC) |
destination_token_address | string | Yes | Token address the user wants to receive on chain |
payment_method | string | No | external_card (default), apple_pay, or google_pay. Any other value returns 400 ErrorInvalidField |
Trap:
payment_methodis the only place the instrument is named. Execute takes onlyestimation_id—
it reads the instrument off the estimation it references, so a wallet-method execute never repeats
payment_method.
Response
{
"estimation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"expires_at": 1704110400,
"source_amount": 100.50,
"source_amount_precise": "100500000000000000000",
"source_currency": "EUR",
"destination_amount": 100.00,
"destination_amount_precise": "100000000",
"destination_token_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"destination_token_symbol": "USDC",
"fee_amount": 0.50,
"fee_amount_precise": "500000000000000000",
"fee_currency": "EUR",
"rate": 1.0,
"provider": "checkout"
}| Field | Type | Description |
|---|---|---|
estimation_id | string (UUID) | Identifier for this estimation |
expires_at | integer | Unix timestamp (seconds) when the estimation expires |
source_amount | float | Fiat amount the card will be debited |
source_amount_precise | string | Source amount in base units of the source currency |
source_currency | string | Source (fiat) currency code |
destination_amount | float | Token amount the user receives |
destination_amount_precise | string | Destination amount in token base units |
destination_token_address | string | Destination token contract address |
destination_token_symbol | string | Destination token symbol |
fee_amount | float | Fee amount denominated in fee_currency |
fee_amount_precise | string | Fee amount in base units of the fee currency |
fee_currency | string | Fee currency code |
rate | float | Card-to-reference-currency conversion rate |
provider | string | Provider of the underlying top-up |
Execute Top-Up
Execute the top-up using a valid estimation.
Endpoint
POST /api/v1/cards/topup/execute
Request
{
"estimation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"device_id": "device-1234"
}For an external_card estimation, estimation_id and device_id are the whole body. A wallet-method
estimation (apple_pay / google_pay) additionally carries the payload the device payment sheet returned:
{
"estimation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"device_id": "device-1234",
"wallet_payment_token": "eyJzaWduYXR1cmUiOiJNRVU...",
"cardholder_name": "Alex Grey",
"billing_address": {
"country_code": "GB",
"city": "London",
"postal_code": "EC1A 1BB",
"address_line1": "123 Main St",
"address_line2": "Apt 4B"
},
"wallet_transaction_id": "1a2b3c4d5e6f"
}| Field | Type | Required | Description |
|---|---|---|---|
estimation_id | string (UUID) | Yes | Estimation identifier |
device_id | string | No | Client device identifier forwarded to the underlying provider |
wallet_payment_token | string | Conditional | The encrypted payload from the device payment sheet, relayed verbatim. Required for a wallet-method estimation, rejected otherwise. Max 16384 characters |
cardholder_name | string | Conditional | Name on the instrument behind the payment sheet. Required for a wallet-method estimation, rejected otherwise |
billing_address | object | Conditional | Address the payment sheet returned. Required for a wallet-method estimation, rejected otherwise. When present, every subfield below except state and address_line2 is required |
billing_address.country_code | string | Yes (within object) | ISO 3166-1 alpha-2 country |
billing_address.state | string | No | State or province, where the country has one |
billing_address.city | string | Yes (within object) | City |
billing_address.postal_code | string | Yes (within object) | Postal code |
billing_address.address_line1 | string | Yes (within object) | Street address |
billing_address.address_line2 | string | No | Additional address line |
wallet_transaction_id | string | No | Transaction identifier the payment sheet issued alongside the token. Rejected for an external_card estimation. Max 128 characters |
Trap: which fields execute accepts follows the estimation's payment method, not anything on this
request —wallet_payment_token,cardholder_name,billing_addressandwallet_transaction_idare all
rejected if the referenced estimation wasexternal_card.
Response
{
"id": "d4e5f6a7-b8c9-0123-def4-567890123456",
"status": "Pending",
"three_ds_state": {
"flow": "challenge",
"provider": "checkout",
"status": "pending",
"status_reason": "authentication_required",
"url": "https://3ds.example/challenge?session_token=abc123"
}
}| Field | Type | Description |
|---|---|---|
id | string (UUID) | Top-up transaction identifier |
status | string | Status of the top-up after initiation |
three_ds_state | object | 3DS state, present when a 3DS challenge is required |
three_ds_state.flow | string | 3DS flow: frictionless or challenge |
three_ds_state.provider | string | Provider that issued the 3DS challenge |
three_ds_state.status | string | Status of the 3DS challenge |
three_ds_state.status_reason | string | Reason for the current 3DS status |
three_ds_state.url | string | URL the client must open to complete the 3DS challenge |
3DS Handling
Card debits are subject to Strong Customer Authentication. When the execute response includes three_ds_state, complete 3DS based on the reported flow:
| Flow | Action |
|---|---|
frictionless | The issuer approved without a challenge. No client action is required; the top-up proceeds. |
challenge | Open three_ds_state.url so the cardholder can complete the issuer's 3DS challenge. |
Execute → three_ds_state?
├─ frictionless → top-up proceeds
└─ challenge → open url → cardholder authenticates → top-up proceeds
After 3DS is completed, the top-up settles and a CardTopup activity webhook is delivered. If three_ds_state is absent from the execute response, no 3DS step is required.
Transaction Webhook
After the top-up is processed, an activity webhook is delivered.
Endpoint: POST {base_url}/v2/webhooks/activities
{
"id": "d4e5f6a7-b8c9-0123-def4-567890123456",
"user_address": "0x7e1Cc1685D68D486b22D3880030C24434E3b90a9",
"type": "CardTopup",
"status": "Completed",
"direction": "Inbound",
"source": {
"type": "ExternalCard",
"card": {
"id": "c34908da-d980-4a9c-9b39-4dabd6f6144e",
"pan_last": "2701",
"payment_system": "VISA"
}
},
"destination": {
"type": "Wallet",
"wallet": {
"address": "0xAAFF0821A09A1Aac28B72dD3Ff410A7ea5FEb874"
}
},
"source_amount": {
"amount": 100.00,
"currency": "EUR"
},
"destination_amount": {
"amount": 100.00,
"token_symbol": "USDC",
"token_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
},
"fee_amount": {
"amount": 0.50,
"currency": "EUR"
},
"created_at": "2024-01-01T13:00:00.000Z",
"activity_steps": [
{
"type": "Initiated",
"status": "Completed",
"created_at": "2024-01-01T13:00:00.000Z",
"completed_at": "2024-01-01T13:00:00.000Z"
},
{
"type": "CardIn",
"status": "Completed",
"created_at": "2024-01-01T13:00:00.000Z",
"completed_at": "2024-01-01T13:00:15.000Z"
},
{
"type": "Review",
"status": "Completed",
"created_at": "2024-01-01T13:00:15.000Z",
"completed_at": "2024-01-01T13:00:20.000Z"
},
{
"type": "CryptoIn",
"status": "Completed",
"created_at": "2024-01-01T13:00:20.000Z",
"completed_at": "2024-01-01T13:00:30.000Z"
}
]
}Activity Steps
| Step | Description |
|---|---|
Initiated | Top-up request received |
CardIn | Fiat debited from the source card |
Review | Compliance review (automatic for most top-ups) |
CryptoIn | Tokens credited to the user's wallet |
Reversal | Present only when the top-up is reversed |
Webhook Fields
| Field | Type | Description |
|---|---|---|
type | string | CardTopup |
status | enum | Pending, Completed, Failed |
direction | string | Always Inbound for card top-up |
source.type | string | ExternalCard |
source.card.id | string | Registered card id (external_card_id) |
source.card.pan_last | string | Last 4 digits of the card |
source.card.payment_system | string | VISA or MASTERCARD |
destination.wallet.address | string | User's wallet address |
source_amount | object | Fiat amount debited from the card |
destination_amount | object | Token amount credited to the wallet |
Error Handling
Estimation Errors
| Error Reason | Field | Description |
|---|---|---|
ErrorMissingField | external_card_id | Card id is required |
ErrorInvalidField | external_card_id | Must be a valid UUID |
ErrorMissingField | destination_amount | Destination amount is required |
ErrorInvalidField | destination_amount | Must be an integer string greater than zero |
ErrorMissingField | destination_token_address | Destination token address is required |
ErrorInvalidField | destination_token_address | Must be a valid address |
ErrorNotFound | destination_token_address | Destination token was not found |
ErrorNotFound | user_id | User account was not found or does not belong to you |
ErrorInvalidField | payment_method | Must be one of external_card, apple_pay, or google_pay |
ErrorInvalidField | external_card_id | Sent alongside apple_pay or google_pay — not allowed for a wallet payment method |
Execution Errors
| Error Reason | Field | Description |
|---|---|---|
ErrorMissingField | estimation_id | Estimation id is required |
ErrorInvalidField | estimation_id | Must be a valid UUID |
ErrorNotFound | estimation_id | Estimation not found or expired |
ErrorNotFound | user_id | User account was not found or does not belong to you |
ErrorInvalidField | wallet_payment_token | Blank, or longer than 16384 characters |
ErrorInvalidField | cardholder_name | Does not match the expected name format |
ErrorInvalidField | wallet_transaction_id | Blank, or longer than 128 characters |
ErrorMissingField | billing_address.* | A required subfield (country_code, city, postal_code, address_line1) is missing whenever billing_address is present |
ErrorInvalidField | billing_address.* | A subfield fails its format check whenever billing_address is present |
An execute payload that contradicts its estimation's payment method (a card field on a wallet-method
estimation, or a wallet field on a card one) is rejected by the topup service.
Card top-up requires the CardTopup capability to be active for an external_card estimate and execute,
and WalletTopup for apple_pay / google_pay. Requests fail when the relevant capability is inactive.
Error Response Format
{
"error_reason": "ErrorInvalidField",
"error_description": "external card id must be a valid UUID",
"error_category": {
"category": "CategoryValidationFailure",
"http_status_code": 400
},
"error_details": [
{
"key": "field",
"details": "external_card_id"
},
{
"key": "issue",
"details": "invalid_uuid"
}
]
}Capability
Card top-up requires the CardTopup capability to be active for external_card top-ups, and the
WalletTopup capability for apple_pay / google_pay top-ups. Check capabilities via GET /api/v2/user
before initiating top-ups.
Updated about 1 month ago

