---
title: "Test a card in sandbox"
description: "Issue, fund, freeze and change the limit of a sandbox card, and test the purchase, decline and withdrawal screens that sandbox cannot produce today."
---
Use this recipe to issue, fund, freeze and change the limit of a sandbox card, and to test the purchase, decline and withdrawal screens that sandbox cannot produce today.

**Who it is for.** An expense-card app that wants to show employees their card, its balance and its status changes before going live.

**Status.** Available for issuing, funding, reading and updating a virtual card. Not yet available in sandbox for purchases, declines, withdrawals from new cards and physical cards.

## Money example

1. Gravv issues Ada's sandbox card during activation, with a 100 USD daily limit.
2. You fund it with 100 USD from a sandbox source on `polygon`. When the transfer is `completed`, the card's `balance` reads 100.
3. You freeze it with `{"status": "freeze", "limit": 100}`. While the card is frozen, `balance` reads 0, and Gravv sends `cards.status.update`.
4. You cannot make it spend 60 USD in sandbox today, so you test the purchase screen by posting a recorded `cards.transaction.payment` payload to your own endpoint.

## Before you start

- You have a sandbox key and your sandbox card account. See [Set up your card account](/platform/cards/set-up-your-card-account).
- You have a sandbox cardholder who is already verified. Sandbox KYC does not complete for new customers today. See [Verify a customer in sandbox](/recipes/sandbox/verify-a-customer-in-sandbox).

## Find out what is required

| Step | In sandbox |
| :-- | :-- |
| Card application | Approved at once |
| First card | Issued during feature activation, with a daily limit equal to `estimated_monthly_limit` |
| Funding | A transfer to the card from a source on `polygon`. The balance follows the card's wallet |
| A daily limit of `0` | Means no limit in sandbox. In live, it declines every payment |
| Purchases and declines | Not yet available |
| Withdrawal from a card issued since 14 September 2026 | Not yet available |
| Physical cards | Issuance fails with `card creation failed!` |

## Steps

### 1. Issue the card

Follow [Issue a virtual card after a hosted check](/recipes/cards/issue-a-virtual-card-after-a-hosted-check), with your sandbox key and your verified cardholder.

### 2. Fund it

Follow [Top up a card](/recipes/cards/top-up-a-card). In sandbox, a funding transfer sends only `transfer.status.pending` events. Poll the transaction until it is `completed`, then read the balance.

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/balance \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
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());
```
```python title="Python"
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())
```
</CodeGroup>

### 3. Freeze it and change its limit

Always send `status` and `limit` together.

<CodeGroup>
```bash title="cURL"
curl --request PATCH \
     --url https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/update \
     --header 'Api-Key: <API_KEY>' \
     --header 'Content-Type: application/json' \
     --data '
{
  "status": "freeze",
  "limit": 100
}
'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/update", {
  method: "PATCH",
  headers: {
    "Api-Key": process.env.GRAVV_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "status": "freeze",
    "limit": 100
  }),
});
console.log(response.status, await response.json());
```
```python title="Python"
import os

import requests

response = requests.patch(
    "https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b/update",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
        "Content-Type": "application/json",
    },
    json={
        "status": "freeze",
        "limit": 100,
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

Unfreeze with `{"status": "active", "limit": 100}`. Gravv sends `cards.status.update` for each real status change.

### 4. Test the purchase and decline screens without a purchase

You cannot produce card purchases in sandbox today. To test your handler:

- Post a recorded payload, such as the one in [Card decline reasons](/platform/cards/card-decline-reasons#the-webhook), to your own endpoint. If your endpoint checks signatures, sign the payload yourself with your sandbox secret.
- Or [resend a stored event](/recipes/transactions/trace-a-transfer-and-resend-a-webhook#5-resend-it-once-your-server-is-fixed) that your endpoint already received. Gravv does not sign a resend.

## What can go wrong

| Response | Why | Fix |
| :-- | :-- | :-- |
| 400 `card creation failed!` | You requested a physical card in sandbox | Test virtual cards |
| 400 `this card withdraws by transfer: send a transfer from the card to its account` | You tried to withdraw from a new card | Not yet available. Test the withdraw screen with a recorded payload |
| 400 `the balance for this account could not be confirmed — please try again shortly` | You sent a transfer from a new card to its card account | Not yet available. Do not retry in a loop |
| The daily limit reads `0` after an update | You sent the update without `limit` | Send `status` and `limit` together |
| The card's `balance` does not move after a funding transfer | The transfer is not `completed` yet | Poll the transaction |
| 400 `card does not exist` | The id is a live card id, or belongs to another business's card | Use a sandbox card id from `GET /v1/cards` |

## Webhooks you will receive

You receive `cards.status.update` for each real status change. You do not need `cards.application.update` in sandbox, because sandbox approves the application at once. You receive no purchase events, because sandbox cannot produce purchases.

## Testing in sandbox

This page is the sandbox guide for cards. Sandbox also cannot produce refunds, reversals, settlement or foreign-currency purchases.

## Reference

- [Cards overview](/recipes/cards/overview)
- [Get card balance](/api-reference/cards/get-v1-cards-card-id-balance)
- [Update card status](/api-reference/cards/patch-v1-cards-card-id-update)
