Raise a customer's limits with KYC
Read a customer's tier, caps and headroom, and lift a limits pause by completing their KYC and resuming the transfer.
This recipe reads a customer's tier, caps and headroom, and lifts a limits pause by completing the customer's KYC and resuming the transfer.
Who it is for. A remittance app whose user Kofi tries to send 800.00 USD. The app's support team must tell him why the transfer paused and what will lift the pause.
Status. Available when your business's limits policy is set to enforce. In off and shadow mode nothing pauses.
Money example
On the standard limits, with your policy set to enforce:
- Kofi has no completed KYC, so his tier is
none, with caps of 500.00 USD a day, 2,500.00 a week and 5,000.00 a month. - He already sent 200.00 USD today and now sends 800.00 USD. Together that is 1,000.00 USD, which is over the 500.00 daily cap. Gravv creates the transfer paused instead of refusing it. Gravv pays nothing out, and
remaining_usdshows300.00. - Kofi completes KYC. His tier becomes
basic, with a cap of 50,000.00 USD a day, a week and a month. - You resume the paused transfer, and Gravv sends the 800.00 USD.
Before you start
- The customer exists.
- Your business may have its own policy instead of the standard one. Read the tier, the caps and
next_tierfrom the API. Never hard-code them.
Find out what is required
curl --request GET \
--url https://api.gravv.xyz/v1/customers/9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13/limits \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/customers/9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13/limits", {
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/customers/9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13/limits",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
{
"data": {
"subject_type": "customer",
"subject_id": "9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13",
"tier_code": "none",
"tier_assignment": "kyc",
"caps_basis": "tier",
"mode": "enforce",
"blocked": false,
"usage_available": true,
"as_of": "2026-10-13T12:00:00Z",
"windows": {
"per_transfer": {"cap_usd": null, "used_usd": null, "remaining_usd": null},
"daily": {"cap_usd": "500.00", "used_usd": "200.00", "remaining_usd": "300.00"},
"weekly": {"cap_usd": "2500.00", "used_usd": "200.00", "remaining_usd": "2300.00"},
"monthly": {"cap_usd": "5000.00", "used_usd": "200.00", "remaining_usd": "4800.00"},
"annual": {"cap_usd": null, "used_usd": "200.00", "remaining_usd": null, "requires_approval": false}
},
"next_tier": {
"tier_code": "basic",
"caps": {"per_transfer": null, "daily": "50000", "weekly": "50000", "monthly": "50000", "annual": null, "annual_requires_approval": false}
},
"candidate_tiers": null
},
"error": null
}
tier_codeis the current tier.tier_assignmentkycmeans the tier comes from KYC.operatormeans Gravv set it by hand.windowsholds the cap, the amount used and the amount remaining for each window. Anullcap means no cap for that window.- Every amount is in USD, whatever the account currency. Days, weeks and months reset on UTC boundaries (weeks are ISO weeks). The year is a rolling 12 months.
next_tierholds the caps the customer gets one tier up.
The standard tiers:
| Tier | How a customer gets it | Daily | Weekly | Monthly |
|---|---|---|---|---|
none |
No completed KYC | 500.00 | 2,500.00 | 5,000.00 |
basic |
basic KYC completed, for people, associated persons and businesses |
50,000.00 | 50,000.00 | 50,000.00 |
intermediate |
Not yet available. No API starts this level |
Steps
1. Check before a large payout
Read the limits as above. If the amount is more than any remaining_usd, expect a pause.
2. Recognize the pause
The transfer request returns HTTP 200 with transfer_status paused, pause_reason daily, breached_window daily and remaining_usd 300.00. For a tier that comes from KYC, requirements lists only admin_review. You cannot send any field to prove more.
3. Lift the tier, then resume
Complete the customer's KYC (hosted page or your own documents). Then resume with an empty body and a new key. The resume runs the limit check again with the new tier.
curl --request POST \
--url https://api.gravv.xyz/v1/transfer/c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f/resume \
--header 'Api-Key: <API_KEY>' \
--header 'Idempotency-Key: f3fdb3bd-b779-4b8a-9d2e-84ce1c579cdb' \
--header 'Content-Type: application/json' \
--data '
{}
'
const response = await fetch("https://api.gravv.xyz/v1/transfer/c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f/resume", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Idempotency-Key": "f3fdb3bd-b779-4b8a-9d2e-84ce1c579cdb",
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v1/transfer/c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f/resume",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Idempotency-Key": "f3fdb3bd-b779-4b8a-9d2e-84ce1c579cdb",
"Content-Type": "application/json",
},
json={},
)
print(response.status_code, response.json())
resumed: true means the transfer continues. resumed: false lists what is still missing.
Other choices
- Wait for Gravv to release it (
transfer.resumedwithreasonreleased), or let it expire. On expiry, Gravv fails the transfer, refunds the fee and sendstransfer.status.failedwithreasonlimit_pause_expired. Pauses expire after 72 hours by default, andexpires_atgives the exact time. - Send less, within
remaining_usd.
For more on pauses, including businesses that verify their own senders, see Stay within transfer limits.
What can go wrong
| Response | Why | Fix |
|---|---|---|
404 not found: no limits policy applies |
Your business has no limits policy | Nothing is limited. Contact support if you expected limits |
404 not found: subject |
The customer is unknown, or belongs to another business | Check the id and environment |
400 subject_id "abc" is not a customer id |
The id is not a UUID | Send the customer id |
503 limits are temporarily unavailable, please retry (ERROR_CODE_LIMITS_UNAVAILABLE) |
The limits store is unavailable | Retry |
usage_available: false, with used_usd and remaining_usd null |
Gravv could not read usage, but the caps are still right | Retry for usage |
422 transfers are not permitted for this sender (ERROR_CODE_SUBJECT_BLOCKED) |
Compliance blocked the customer, and blocked is true |
Contact support |
410 this paused transfer has expired and can no longer be resumed; please initiate a new transfer |
The pause passed expires_at |
Start a new transfer |
The resume returns resumed: false |
KYC is not completed yet, or the amount is still over the cap | Check the KYC status and the limits again |
Webhooks you will receive
You receive transfer.requires_information, with event_data.reason set to the window and limit_details holding breached_window, cap_usd, used_usd, remaining_usd, amount_usd, tier and requirements. Then you receive transfer.resumed, or transfer.status.failed with reason limit_pause_expired or limit_pause_cancelled. See Transfer events.
Testing in sandbox
The limits read works in sandbox. A sandbox business can carry its own policy, so the figures, the tier name and tier_assignment (which can also be default) differ from the standard ones. Read them from the call. Whether transfers pause depends on your sandbox business's mode. Ask support to set shadow or enforce for testing.