Find out why a payment was declined
The API does not return a decline reason today. Work out the most likely cause from the card's status, limit and balance.
Use this recipe to work out the most likely reason a card payment was declined. The API does not return a decline reason today, so you check the card's status, limit and balance.
Who it is for. A support agent at any card issuer answering "my card was declined".
Status. Decline reasons through the API are Not yet available. The checks below find the most likely cause.
Money example
Ada tries to pay 45 USD and it is declined.
- Her card holds 30 USD. You top it up with 20 USD, and she pays.
- Or her card holds 200 USD, but its daily limit reads 0 because you sent an update without
limit. You set the limit back to 300, and she pays.
Before you start
You need the card id. Read it from event_data.card.id in the cards.transaction.payment webhook, or from GET /v1/cards?customer_id=….
Find out what is required
A declined payment arrives as cards.transaction.payment with transaction.status failed, and the event has no reason field. For each payment, Gravv sends pending, then completed or failed.
Steps
1. Read the card
curl --request GET \
--url https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/cards/a14ec356-f33d-4384-ba68-4d9bfa19765b", {
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",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
Check that status is active, card_type is virtual, and spending_controls.daily_transaction_limit is above the amount.
2. Read 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())
balance must cover the amount.
3. Match what you see
| What you see | Likely cause | Fix |
|---|---|---|
status freeze |
The card is frozen | Unfreeze it |
status blocked |
The card is closed | Issue a new card |
status inactive, or card_type physical |
A physical card cannot spend today | Use a virtual card |
balance below the amount |
The card does not hold enough money | Top up the card |
daily_transaction_limit 0 |
You sent an update without limit |
Set the limit |
| Today's payments add up past the limit | The daily limit is reached | Wait 24 hours, or raise the limit |
| Everything above is fine | A merchant rule, a card network rule or a fraud rule | Contact support with the card id, amount and time |
Tell the cardholder that this is the most likely cause. None of these checks proves it.
What can go wrong
| Response | Why | Fix |
|---|---|---|
400 card does not exist |
The card id is wrong | Check the id |
Several failed payments in a row, and all checks pass |
The cause is not visible through the API | Contact support |
Webhooks you will receive
You receive cards.transaction.payment with transaction.status pending, completed or failed, transaction.amount (a string in dollars) and transaction.transaction_type card_payment.
Testing in sandbox
Spend and decline simulation through the API is Not yet available. To test your handler, post a recorded cards.transaction.payment payload to your own endpoint, or resend a stored event. Gravv does not sign a resend.