Freeze or block a lost card
Freeze a card while it is missing, unfreeze it when the cardholder finds it, or block a stolen card for good and issue a replacement on the same balance.
Use this recipe to freeze a card while it is missing and unfreeze it when the cardholder finds it. If the card is stolen, block it for good and issue a replacement that spends the same balance.
Who it is for. Any issuer whose cardholder taps "freeze card" or reports the card stolen.
Status. Available.
Money example
Ada's card holds 131.25 USD with a 300 USD daily limit.
- Ada cannot find her phone, so you freeze the card.
balanceshows 0, andgroup_balancestill shows 131.25. - Ada finds her phone, so you unfreeze the card.
balanceis 131.25 again. - Later the card is stolen. You block it and issue a new card, which spends the same 131.25 USD.
Before you start
Read the card. GET /v1/cards/{card_id} returns the current status and spending_controls.daily_transaction_limit. Every update must send that limit back.
The table lists the status changes the API allows.
| From | Allowed moves |
|---|---|
active |
freeze, blocked |
freeze |
active, blocked |
inactive |
active, blocked |
blocked |
None. blocked is final |
If your API key is scoped, it needs card:freeze or card:terminate.
Find out what is required
A freeze is reversible, and a block is permanent. If the card has money of its own and you will not replace it, withdraw the money first, because you cannot withdraw from a blocked card. See Close a card and get the money back.
Steps
1. Freeze
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": 300
}
'
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": 300
}),
});
console.log(response.status, await response.json());
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": 300,
},
)
print(response.status_code, response.json())
2. Unfreeze
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": "active",
"limit": 300
}
'
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": "active",
"limit": 300
}),
});
console.log(response.status, await response.json());
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": "active",
"limit": 300,
},
)
print(response.status_code, response.json())
3. Block a stolen card
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": "blocked",
"limit": 300
}
'
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": "blocked",
"limit": 300
}),
});
console.log(response.status, await response.json());
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": "blocked",
"limit": 300,
},
)
print(response.status_code, response.json())
4. Issue a replacement
Send a new Idempotency-Key and no funding, so the new card joins the same balance.
curl --request POST \
--url https://api.gravv.xyz/v1/cards \
--header 'Api-Key: <API_KEY>' \
--header 'Idempotency-Key: 5c653268-8d8a-44e9-a6d7-a03a52a2b518' \
--header 'Content-Type: application/json' \
--data '
{
"customer_id": "5b0f6c1e-2a8f-4c39-9d0e-0c7e3b9a1f42",
"card_type": "virtual",
"card_limit": 300,
"name_on_card": "Ada Obi"
}
'
const response = await fetch("https://api.gravv.xyz/v1/cards", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Idempotency-Key": "5c653268-8d8a-44e9-a6d7-a03a52a2b518",
"Content-Type": "application/json",
},
body: JSON.stringify({
"customer_id": "5b0f6c1e-2a8f-4c39-9d0e-0c7e3b9a1f42",
"card_type": "virtual",
"card_limit": 300,
"name_on_card": "Ada Obi"
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v1/cards",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Idempotency-Key": "5c653268-8d8a-44e9-a6d7-a03a52a2b518",
"Content-Type": "application/json",
},
json={
"customer_id": "5b0f6c1e-2a8f-4c39-9d0e-0c7e3b9a1f42",
"card_type": "virtual",
"card_limit": 300,
"name_on_card": "Ada Obi",
},
)
print(response.status_code, response.json())
{
"data": {
"message": "Card creation request is processing",
"status": true,
"id": "f4c3b2a1-0e9d-4c8b-a7f6-e5d4c3b2a190",
"fee_charged": false
},
"error": null
}
The new card has a new id, number and PIN.
5. Check the balance
curl --request GET \
--url https://api.gravv.xyz/v1/cards/f4c3b2a1-0e9d-4c8b-a7f6-e5d4c3b2a190/balance \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/cards/f4c3b2a1-0e9d-4c8b-a7f6-e5d4c3b2a190/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/f4c3b2a1-0e9d-4c8b-a7f6-e5d4c3b2a190/balance",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
balance is 131.25. shared reads true, because the blocked card still counts in the group.
What can go wrong
| Response | Why | Fix |
|---|---|---|
400 card is blocked and cannot be updated |
The card is already blocked | Issue a new card |
400 internal server error on a freeze |
The status change is not allowed, such as inactive to freeze |
Use an allowed change. Read the card back, and contact support if it looks wrong |
400 an Idempotency-Key is required to issue an additional card |
You sent the replacement request without a key | Send a new key |
400 Idempotency-Key was already used for a different request |
You reused a key with another body | Use a new key |
403 forbidden, required_any card:freeze, card:terminate |
The scoped key has no card permissions | Grant card:freeze or card:terminate |
| The limit is lost after a freeze | You sent the update without limit |
Set it again |
Webhooks you will receive
You receive cards.status.update for every real change, with event_data.card.status (freeze, active, blocked), card.last4 and card.id. Gravv sends no webhook for a call that keeps the same status.
{
"event_category": "cards",
"event_type": "cards.status.update",
"event_group_id": "a14ec356-f33d-4384-ba68-4d9bfa19765b",
"event_data": {
"card": {
"customer_id": "5b0f6c1e-2a8f-4c39-9d0e-0c7e3b9a1f42",
"id": "a14ec356-f33d-4384-ba68-4d9bfa19765b",
"last4": "9351",
"name_on_card": "Ada Obi",
"network": "visa",
"status": "freeze"
}
}
}
Testing in sandbox
Every status change works in sandbox. Gravv sends one cards.status.update for each real change, 20 to 60 seconds later. A frozen card shows balance 0 and the full group_balance.