Top up a card
Move money onto a card from a wallet-backed account or a USDC wallet on the card's network, and confirm that it arrived.
This recipe moves money onto a card from a wallet-backed account or a USDC wallet on the card's network, and confirms that the money arrived.
Who it is for. Any card issuer that moves money onto a card. For example, a consumer app lets Ada move part of her USD balance to her card.
Status. Available.
Money example
Ada's USD account holds 400 USD. Her card holds 0.
- You send 150 USD from her account to her card. Her account now holds 400 USD minus 150 USD minus any fee, and her card holds 150 USD.
- Separately, a business tops up an employee's card from its USDC wallet. The wallet sends 100 USDC, and the card gains 100 USD.
Before you start
| Prerequisite | How to check |
|---|---|
| The card is not frozen or blocked | GET /v1/cards/{card_id} returns status active |
| The source is an account with a wallet, or a wallet holding USDC | GET /v2/accounts/{id} has wallet_address and blockchain_network; GET /v1/wallets/{wallet_id} has network and a USDC balance |
| The source and the card are on the same network | GET /v1/cards?card_id=… returns the card's blockchain_network |
| The source covers the amount and any fee | The account's balance, or the wallet's USDC balance |
A bank-held account, with no wallet, cannot fund cards.
Find out what is required
Use the reads in the table above. Gravv has no separate funding endpoint. To fund a card, send a transfer whose destination is the card.
Steps
1. Send the transfer
From an account:
curl --request POST \
--url https://api.gravv.xyz/v1/transfer \
--header 'Api-Key: <API_KEY>' \
--header 'Idempotency-Key: f802ce3c-4736-4ccb-9e51-965b36952c96' \
--header 'Content-Type: application/json' \
--data '
{
"source": {
"source_type": "internal_account",
"id": "6f1c2b8e-9a43-4f0e-8d35-2b7f0f6c1a90"
},
"destination": {
"destination_type": "card",
"id": "a14ec356-f33d-4384-ba68-4d9bfa19765b"
},
"amount": "150",
"customer_id": "5b0f6c1e-2a8f-4c39-9d0e-0c7e3b9a1f42",
"client_reference": "ada-card-topup-0001",
"description": "Card top-up"
}
'
const response = await fetch("https://api.gravv.xyz/v1/transfer", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Idempotency-Key": "f802ce3c-4736-4ccb-9e51-965b36952c96",
"Content-Type": "application/json",
},
body: JSON.stringify({
"source": {
"source_type": "internal_account",
"id": "6f1c2b8e-9a43-4f0e-8d35-2b7f0f6c1a90"
},
"destination": {
"destination_type": "card",
"id": "a14ec356-f33d-4384-ba68-4d9bfa19765b"
},
"amount": "150",
"customer_id": "5b0f6c1e-2a8f-4c39-9d0e-0c7e3b9a1f42",
"client_reference": "ada-card-topup-0001",
"description": "Card top-up"
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v1/transfer",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Idempotency-Key": "f802ce3c-4736-4ccb-9e51-965b36952c96",
"Content-Type": "application/json",
},
json={
"source": {
"source_type": "internal_account",
"id": "6f1c2b8e-9a43-4f0e-8d35-2b7f0f6c1a90",
},
"destination": {
"destination_type": "card",
"id": "a14ec356-f33d-4384-ba68-4d9bfa19765b",
},
"amount": "150",
"customer_id": "5b0f6c1e-2a8f-4c39-9d0e-0c7e3b9a1f42",
"client_reference": "ada-card-topup-0001",
"description": "Card top-up",
},
)
print(response.status_code, response.json())
From a USDC wallet:
curl --request POST \
--url https://api.gravv.xyz/v1/transfer \
--header 'Api-Key: <API_KEY>' \
--header 'Idempotency-Key: 7911f412-3ddf-4b08-bcbb-07f68ce313ad' \
--header 'Content-Type: application/json' \
--data '
{
"source": {
"source_type": "internal_crypto_wallet",
"id": "67e169c0-547f-410d-8e18-f616b6822d70",
"crypto_asset": "USDC"
},
"destination": {
"destination_type": "card",
"id": "e7b1c2d3-4f5a-4b6c-8d7e-9f0a1b2c3d4e"
},
"amount": "100",
"customer_id": "0d6c2f4e-6a1b-4f0e-9b8f-5c2a7d3e1f00",
"client_reference": "tunde-topup-0001"
}
'
const response = await fetch("https://api.gravv.xyz/v1/transfer", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Idempotency-Key": "7911f412-3ddf-4b08-bcbb-07f68ce313ad",
"Content-Type": "application/json",
},
body: JSON.stringify({
"source": {
"source_type": "internal_crypto_wallet",
"id": "67e169c0-547f-410d-8e18-f616b6822d70",
"crypto_asset": "USDC"
},
"destination": {
"destination_type": "card",
"id": "e7b1c2d3-4f5a-4b6c-8d7e-9f0a1b2c3d4e"
},
"amount": "100",
"customer_id": "0d6c2f4e-6a1b-4f0e-9b8f-5c2a7d3e1f00",
"client_reference": "tunde-topup-0001"
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v1/transfer",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Idempotency-Key": "7911f412-3ddf-4b08-bcbb-07f68ce313ad",
"Content-Type": "application/json",
},
json={
"source": {
"source_type": "internal_crypto_wallet",
"id": "67e169c0-547f-410d-8e18-f616b6822d70",
"crypto_asset": "USDC",
},
"destination": {
"destination_type": "card",
"id": "e7b1c2d3-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
},
"amount": "100",
"customer_id": "0d6c2f4e-6a1b-4f0e-9b8f-5c2a7d3e1f00",
"client_reference": "tunde-topup-0001",
},
)
print(response.status_code, response.json())
{
"data": {
"amount": "150",
"amount_currency": "USD",
"fee": "0",
"fee_currency": "USD",
"reference": "3f0c6a8e-6b1d-4c1e-9a55-0d2f7b9e4a11",
"requires_approval": false,
"transaction_id": "8d4e2b6c-1f3a-4e7b-b0c9-5a6d7e8f9a01",
"transfer_status": "pending"
},
"error": null
}
amountis a string in dollars, with at most 2 decimals ("150","12.50").transfer_statuspendingis normal.pausedmeans your limits held the transfer. See Stay within transfer limits.
2. Wait for the transfer to complete
Poll the transaction until status is completed or failed. In sandbox, the transfer takes about 75 seconds to complete.
curl --request GET \
--url https://api.gravv.xyz/v1/transactions/8d4e2b6c-1f3a-4e7b-b0c9-5a6d7e8f9a01 \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/transactions/8d4e2b6c-1f3a-4e7b-b0c9-5a6d7e8f9a01", {
method: "GET",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
},
});
console.log(response.status, await response.json());
import os
import requests
response = requests.get(
"https://api.gravv.xyz/v1/transactions/8d4e2b6c-1f3a-4e7b-b0c9-5a6d7e8f9a01",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
A completed funding has transaction_type card_funding, receiving_account_id set to the card id, and a tx_hash. Poll rather than wait for webhooks, because Gravv does not guarantee the completion events for a funding.
3. Confirm the balance
curl --request GET \
--url https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/balance \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/balance", {
method: "GET",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
},
});
console.log(response.status, await response.json());
import os
import requests
response = requests.get(
"https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/balance",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
{
"data": {
"balance": 150,
"card_group_id": "3f1d0c5e-2a8b-4b7e-9f3a-6c1e2d4b5a70",
"currency": "USD",
"group_balance": 150,
"shared": false
},
"error": null
}
What can go wrong
| Response (HTTP 400) | Why | Fix |
|---|---|---|
this card is freeze and cannot be funded / this card is blocked and cannot be funded |
The card is frozen or blocked | Unfreeze it. You cannot use a blocked card again |
no card account found for id <card> (customer <id>) |
The card id is wrong | Check it with GET /v1/cards?card_id=… |
the destination card has no wallet provisioned yet |
The card is not issued yet | Issue the card first |
the source account (ethereum network) and destination (polygon network) must be on the same blockchain network |
The source is on another network | Use a source on the card's network |
this account can only send to a bank account or to another account held at the same bank |
The source is a bank-held account | Use a wallet-backed account or a USDC wallet |
insufficient balance |
The amount plus the fee is above the source balance | Lower the amount or fund the source |
the source account could not be verified for this transfer — please try again shortly |
A temporary check failed | Retry once with a new key. If it repeats, contact support |
amount must have at most 2 decimal places |
The amount has too many decimals | Use at most 2 decimals |
source_type must be one of: internal_account, internal_crypto_wallet, card |
You sent crypto_wallet as a source |
Use internal_crypto_wallet |
200 with transfer_status paused |
A limit paused the transfer | Respond to the pause as in Stay within transfer limits |
Never retry a 400 with the same key. Use a new one.
Webhooks you will receive
Gravv does not guarantee these events, and they do not arrive in a fixed order.
transfer.status.pending, one or more, withworkflow_typecard_funding.transfer.status.completed, withamount,tx_hashanddestination_id.cards.transaction.funding, withtransaction.amount(a string) andtransaction.status. Itsclient_referencecan be empty, so match ontx_hash.- On failure,
transfer.status.failed.
Testing in sandbox
- Sandbox cards are on
polygon, so use a source onpolygon. - To put money in a source, simulate a deposit into a wallet-backed v2 account. Today only NGN accounts open in sandbox.
- In sandbox the card balance follows its wallet, so the balance shows the money once the transfer is
completed. - In sandbox, a card funding transfer sends only
transfer.status.pendingevents, so poll the transaction.