<!-- ZenPays documentation · https://docs.zenpayz.com/docs/rest-api/endpoints/ramp/get-kyc-status -->

# Get KYC Status

Poll the current KYC verification status for a ramp intent. The checkout widget
calls this endpoint every 5 seconds while a customer is verifying — use the
same endpoint if you embed verification yourself.

<EndpointHeader verb="GET" path="/payment/api/v1/ramp-intents/:intentId/kyc-status" />

## Request

### Headers

<ParamTable
  label="Header"
  rows={[
    { name: "x-request-id", desc: "Custom request ID for tracing" },
  ]}
/>

:::note Public Endpoint
This endpoint does not require authentication headers. It is designed to be
called from your frontend / checkout page.
:::

### Path Parameters

<ParamTable
  rows={[
    { name: "intentId", type: "string", required: true, desc: "The ramp intent ID (e.g. `ri_1710345678000_a1b2c3d4e5f6g7h8`)" },
  ]}
/>

## Response

### Success (200 OK)

```json
{
  "success": true,
  "data": {
    "verified": false,
    "kycStatus": "in_review",
    "intentStatus": "active",
    "cancelReason": null
  },
  "message": "KYC status retrieved"
}
```

### Response Fields

<ParamTable
  rows={[
    { name: "verified", type: "boolean", desc: "Convenience flag — `true` when `kycStatus === 'approved'`." },
    { name: "kycStatus", type: "string", desc: "`pending`, `in_review`, `approved`, `declined`, or `expired`." },
    { name: "intentStatus", type: "string", desc: "Current ramp intent lifecycle state (`created`, `active`, `completed`, `expired`, `cancelled`)." },
    { name: "cancelReason", type: "string | null", desc: "Set when the intent moved to `cancelled` because of KYC. `kyc_rejected` for declines, `kyc_expired` for expiry." },
  ]}
/>

## Understanding KYC states

| `kycStatus` | What it means | What to do |
|-------------|---------------|------------|
| `pending` | Customer hasn't completed the Didit flow yet | Keep polling. Optionally show a "verification in progress" UI. |
| `in_review` | Identity passed but AML or face-match was flagged for manual review | Keep polling. Surface a friendly "under review" message — this can take a few minutes. |
| `approved` | KYC and AML both cleared | Proceed with the checkout. Stop polling. |
| `declined` | Hard rejection (identity mismatch, document issue, AML rejected) | Stop polling. `intentStatus` is now `cancelled` with `cancelReason='kyc_rejected'`. Show a terminal "verification could not be completed" message. |
| `expired` | Verification session expired before the customer finished | Stop polling. Same handling as `declined`. Optionally offer to start a new intent. |

## Polling cadence

The checkout widget polls every 5 seconds with a hard ceiling of ~5 minutes
(60 attempts) before showing a "still confirming your verification" message.
If you implement your own polling, mirror that pattern so you don't leave
customers on a spinner indefinitely.

Recommended pattern:

1. Poll every 5 seconds.
2. Stop on any of: `kycStatus === 'approved'`, `'declined'`, or `'expired'`.
3. After ~60 attempts with no terminal state, surface a non-blocking
   "we're still confirming your verification" message with a manual refresh.

## Error Responses

<ErrorTable
  rows={[
    { code: "NOT_FOUND", status: "404", message: "Intent with the given ID does not exist" },
  ]}
/>

<CodeRail>

## Examples

<Tabs groupId="language">
  <TabItem value="curl" label="cURL" default>

```bash
curl https://api.zenpayz.com/payment/api/v1/ramp-intents/ri_1710345678000_a1b2c3d4e5f6g7h8/kyc-status
```

  </TabItem>
  <TabItem value="javascript" label="JavaScript">

```javascript
const intentId = 'ri_1710345678000_a1b2c3d4e5f6g7h8';

async function pollKycStatus() {
  const res = await fetch(
    `https://api.zenpayz.com/payment/api/v1/ramp-intents/${intentId}/kyc-status`
  );
  const { data } = await res.json();

  switch (data.kycStatus) {
    case 'approved':
      console.log('KYC verified — proceed with checkout');
      return 'done';
    case 'in_review':
      console.log('Identity OK, under review — keep waiting');
      return 'continue';
    case 'declined':
    case 'expired':
      console.log(`Terminal: ${data.cancelReason}`);
      return 'stop';
    default:
      return 'continue';
  }
}

const intervalId = setInterval(async () => {
  const next = await pollKycStatus();
  if (next !== 'continue') clearInterval(intervalId);
}, 5000);
```

  </TabItem>
  <TabItem value="python" label="Python">

```python
import time, requests

intent_id = "ri_1710345678000_a1b2c3d4e5f6g7h8"

for _ in range(60):  # 5 minutes worth of polls
    res = requests.get(
        f"https://api.zenpayz.com/payment/api/v1/ramp-intents/{intent_id}/kyc-status"
    )
    data = res.json()["data"]

    if data["kycStatus"] == "approved":
        print("KYC verified — proceed")
        break
    if data["kycStatus"] in ("declined", "expired"):
        print(f"Terminal: {data['cancelReason']}")
        break
    time.sleep(5)
else:
    print("Still confirming — show a manual refresh UI")
```

  </TabItem>
</Tabs>

</CodeRail>

## Next Steps

- [Get Ramp Intent](/docs/rest-api/endpoints/ramp/get-ramp-intent) — Fetch the full intent including the widget URL
- [KYC Webhooks](/docs/guides/webhooks/kyc-webhooks) — Get notified when KYC state changes without polling
- [On-Ramp Flow](/docs/examples/on-ramp-flow) — End-to-end buy integration
