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.

  1. 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.
  2. 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
}
  • amount is a string in dollars, with at most 2 decimals ("150", "12.50").
  • transfer_status pending is normal. paused means 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.

  1. transfer.status.pending, one or more, with workflow_type card_funding.
  2. transfer.status.completed, with amount, tx_hash and destination_id.
  3. cards.transaction.funding, with transaction.amount (a string) and transaction.status. Its client_reference can be empty, so match on tx_hash.
  4. On failure, transfer.status.failed.

Testing in sandbox

  • Sandbox cards are on polygon, so use a source on polygon.
  • 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.pending events, so poll the transaction.

Reference

Was this page helpful?