Stay within transfer limits
Check a sender's headroom before you send, and answer a transfer that paused for a limit, for screening or for sender details.
Use this recipe to check a sender's headroom before you send, and to answer a transfer that paused for a limit, for screening or for sender details.
Who it is for. A remittance platform whose business runs an enforced limits policy, with daily, weekly and monthly caps in USD per sender.
Status. Available if your business has a limits policy.
Money example
- Sender Maria Lopez has a weekly cap of 2,500.00 USD and has sent 2,200.00 USD this week.
- You send 1,500.00 USD by ACH. That makes 3,700 USD this week, which is over the 2,500 USD cap. Gravv pauses the transfer instead of refusing it, with
breached_windowweeklyandremaining_usd300.00. - You can resume the transfer with stronger sender verification, if
requirementsoffers it. You can also wait for Gravv to review it, send 300.00 USD or less instead, or let the pause expire after 72 hours. If the pause expires, Gravv refunds the fee.
Before you start
Find out whether your business has a limits policy. GET /v1/customers/{customer_id}/limits returns mode off, shadow or enforce. In shadow mode, no transfer ever pauses. A 404 not found: no limits policy applies means there is no policy.
Find out what is required
Headroom before you send
curl --request GET \
--url https://api.gravv.xyz/v1/customers/7c9e6679-7425-40de-944b-e07fc1f90ae7/limits \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/customers/7c9e6679-7425-40de-944b-e07fc1f90ae7/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/7c9e6679-7425-40de-944b-e07fc1f90ae7/limits",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
The smallest remaining_usd across windows is the most this sender can send now. cap_usd null means that window has no cap. Weeks are ISO weeks, Monday to Sunday, in UTC. Transfer limits explains every field in the response. When you send on behalf of end senders, read GET /v1/limits/remitters/{remitter_id} instead.
The paused response
The paused response carries pause_reason, breached_window, remaining_usd, requirements[] and expires_at. pause_reason is one of per_transfer, daily, weekly, monthly, annual, annual_requires_approval, screening or sender_details_required.
Steps
1. Send, and read transfer_status
If the amount fits within the caps, send as usual. If the HTTP 200 response shows transfer_status paused, go to step 2.
{
"data": {
"amount": "1500",
"amount_currency": "USD",
"reference": "d2c4e6f8-0a1b-4c3d-9e5f-7a8b9c0d1e2f",
"date_created": "2026-10-13T12:00:00.000Z",
"transfer_status": "paused",
"transaction_id": "e3d5f7a9-1b2c-4d4e-8f60-8a9b0c1d2e3f",
"local_amount": null,
"local_currency": null,
"fee": "1.5",
"fee_currency": "USD",
"requires_approval": false,
"status": "awaiting_remitter_information",
"park_reason": "remitter_information",
"pause_reason": "weekly",
"requirements": [
{
"type": "remitter_verification",
"tier": "T2",
"levels": ["enhanced"],
"description": "resume with remitter_verification at one of these levels"
}
],
"breached_window": "weekly",
"remaining_usd": "300.00",
"expires_at": "2026-10-16T12:00:00Z"
},
"error": null
}
2. Answer what requirements asks for
remitter_verification. This applies to businesses that verify their own senders. Resume with a verification at one of the listed levels. Send only checks you ran.
curl --request POST \
--url https://api.gravv.xyz/v1/transfer/d2c4e6f8-0a1b-4c3d-9e5f-7a8b9c0d1e2f/resume \
--header 'Api-Key: <API_KEY>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: 7ee1ac93-8eec-4f46-95e0-fbc08703469f' \
--data '
{
"remitter_verification": {
"remitter_id": "rem-maria-lopez-01",
"level": "enhanced",
"provider": "acme-kyc",
"provider_reference": "chk_558120",
"verified_at": "2026-10-13T11:58:00Z",
"screening": {
"sanctions": "clear",
"pep": "clear",
"adverse_media": "clear"
}
}
}
'
const response = await fetch("https://api.gravv.xyz/v1/transfer/d2c4e6f8-0a1b-4c3d-9e5f-7a8b9c0d1e2f/resume", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "7ee1ac93-8eec-4f46-95e0-fbc08703469f",
},
body: JSON.stringify({
"remitter_verification": {
"remitter_id": "rem-maria-lopez-01",
"level": "enhanced",
"provider": "acme-kyc",
"provider_reference": "chk_558120",
"verified_at": "2026-10-13T11:58:00Z",
"screening": {
"sanctions": "clear",
"pep": "clear",
"adverse_media": "clear"
}
}
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v1/transfer/d2c4e6f8-0a1b-4c3d-9e5f-7a8b9c0d1e2f/resume",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Content-Type": "application/json",
"Idempotency-Key": "7ee1ac93-8eec-4f46-95e0-fbc08703469f",
},
json={
"remitter_verification": {
"remitter_id": "rem-maria-lopez-01",
"level": "enhanced",
"provider": "acme-kyc",
"provider_reference": "chk_558120",
"verified_at": "2026-10-13T11:58:00Z",
"screening": {
"sanctions": "clear",
"pep": "clear",
"adverse_media": "clear",
},
},
},
)
print(response.status_code, response.json())
{
"data": {
"transfer_id": "d2c4e6f8-0a1b-4c3d-9e5f-7a8b9c0d1e2f",
"status": "approved_awaiting_execution",
"transfer_status": "pending",
"resumed": true
},
"error": null
}
If the transfer is still over a cap, the response is resumed: false with new requirements.
screening_clear. Resume with a remitter_verification whose screening values are all clear.
admin_review. Only Gravv can release the transfer. Contact support with the reference, or wait for the next window to start.
Sender details. Some payout routes need the sender's identity in remitter_full_name, remitter_date_of_birth, remitter_address and remitter_country. Resume with additional_information:
curl --request POST \
--url https://api.gravv.xyz/v1/transfer/d2c4e6f8-0a1b-4c3d-9e5f-7a8b9c0d1e2f/resume \
--header 'Api-Key: <API_KEY>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: 17273857-9c44-4272-91e8-49bd4214e6f1' \
--data '
{
"additional_information": {
"remitter_full_name": "Maria Lopez",
"remitter_date_of_birth": "1988-04-17",
"remitter_country": "US",
"remitter_address": {
"address_line1": "55 Water Street",
"city": "Brooklyn",
"postal_code": "11201",
"country": "US"
}
}
}
'
const response = await fetch("https://api.gravv.xyz/v1/transfer/d2c4e6f8-0a1b-4c3d-9e5f-7a8b9c0d1e2f/resume", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "17273857-9c44-4272-91e8-49bd4214e6f1",
},
body: JSON.stringify({
"additional_information": {
"remitter_full_name": "Maria Lopez",
"remitter_date_of_birth": "1988-04-17",
"remitter_country": "US",
"remitter_address": {
"address_line1": "55 Water Street",
"city": "Brooklyn",
"postal_code": "11201",
"country": "US"
}
}
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v1/transfer/d2c4e6f8-0a1b-4c3d-9e5f-7a8b9c0d1e2f/resume",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Content-Type": "application/json",
"Idempotency-Key": "17273857-9c44-4272-91e8-49bd4214e6f1",
},
json={
"additional_information": {
"remitter_full_name": "Maria Lopez",
"remitter_date_of_birth": "1988-04-17",
"remitter_country": "US",
"remitter_address": {
"address_line1": "55 Water Street",
"city": "Brooklyn",
"postal_code": "11201",
"country": "US",
},
},
},
)
print(response.status_code, response.json())
customer_email. Add an email to the customer with PUT /v1/customers/{customer_id}, then resume with {}.
If you do nothing. At expires_at, the transfer fails with reason limit_pause_expired (or sender_details_expired), and Gravv refunds the fee.
Use a new Idempotency-Key for every resume attempt.
What can go wrong
| Response | Why | Fix |
|---|---|---|
200 transfer_status paused, pause_reason weekly |
The weekly cap is reached | Follow step 2, or send at most remaining_usd |
422 transfers are not permitted for this sender |
The sender is blocked | Contact support. Do not retry |
503 transfer limits are temporarily unavailable, please retry |
Gravv cannot check limits right now | Retry with backoff |
400 remitter_verification.remitter_id is required |
The verification has no sender id | Add remitter_id |
400 the remitter information does not match the sender this transfer was paused for; resume with the same remitter_id, or initiate a new transfer |
The resume has a different remitter_id |
Use the same remitter_id, or send a new transfer |
400 remitter_date_of_birth must be a date as YYYY-MM-DD |
The date has the wrong format, or is in the future | Send 1988-04-17 |
200 resumed: false with new requirements |
The transfer is still over a cap, or a detail is still missing | Send what is listed |
410 ERROR_CODE_TRANSFER_PAUSE_EXPIRED or ERROR_CODE_TRANSFER_PAUSE_CANCELLED |
The pause has ended | Send a new transfer |
409 ERROR_CODE_TRANSFER_IN_PROGRESS with Retry-After: 1 |
Gravv is still creating a transfer with the same client_reference |
Wait a second and retry |
404 not found: subject on the limits read |
The customer is not one of yours | Check the id |
Webhooks you will receive
transfer.requires_information, withevent_data.statuspaused,reason(the pause reason) andlimit_details(breached_window,cap_usd,used_usd,remaining_usd,amount_usd,requirements).- After a resume,
transfer.resumed, then the usual transfer events. - If the pause expires or is cancelled,
transfer.status.failedwithreasonlimit_pause_expired,limit_pause_cancelled,sender_details_expiredorsender_details_cancelled.
Testing in sandbox
A new sandbox customer's policy is in shadow mode. You cannot produce a limit pause there, and usage does not change. The limits read works, and a non-UUID id returns 400 subject_id "not-a-uuid" is not a customer id. Contact support for a test policy if you need to test a pause.