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.

  1. Ada cannot find her phone, so you freeze the card. balance shows 0, and group_balance still shows 131.25.
  2. Ada finds her phone, so you unfreeze the card. balance is 131.25 again.
  3. 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.

Reference

Was this page helpful?