---
title: "Raise a customer's limits with KYC"
description: "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`:

1. 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.
2. 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_usd` shows `300.00`.
3. Kofi completes KYC. His tier becomes `basic`, with a cap of 50,000.00 USD a day, a week and a month.
4. 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_tier` from the API. Never hard-code them.

## Find out what is required

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url https://api.gravv.xyz/v1/customers/9d2f4b6a-1c3e-4a5b-8d7f-2e4a6c8e0b13/limits \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
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());
```
```python title="Python"
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())
```
</CodeGroup>

```json title="Response"
{
  "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_code` is the current tier. `tier_assignment` `kyc` means the tier comes from KYC. `operator` means Gravv set it by hand.
- `windows` holds the cap, the amount used and the amount remaining for each window. A `null` cap 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_tier` holds 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](/recipes/customers-and-kyc/verify-a-person-on-a-hosted-page) or [your own documents](/recipes/customers-and-kyc/verify-a-person-with-documents-you-upload)). Then resume with an empty body and a **new** key. The resume runs the limit check again with the new tier.

<CodeGroup>
```bash title="cURL"
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 '
{}
'
```
```javascript title="Node"
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());
```
```python title="Python"
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())
```
</CodeGroup>

`resumed: true` means the transfer continues. `resumed: false` lists what is still missing.

### Other choices

- **Wait** for Gravv to release it (`transfer.resumed` with `reason` `released`), or let it expire. On expiry, Gravv fails the transfer, refunds the fee and sends `transfer.status.failed` with `reason` `limit_pause_expired`. Pauses expire after 72 hours by default, and `expires_at` gives the exact time.
- **Send less**, within `remaining_usd`.

For more on pauses, including businesses that verify their own senders, see [Stay within transfer limits](/recipes/bank-payouts/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](/platform/webhooks/event-catalogue#transfers).

## 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.

## Reference

- [Get customer limits](/api-reference/limits/get-v1-customers-customer-id-limits)
- [Resume a transfer](/api-reference/transfer/post-v1-transfer-id-resume)
- [Transfer limits](/platform/transfers/transfer-limits)
