<!-- ZenPays documentation · https://docs.zenpayz.com/docs/api-reference/ramp-intents -->

# Ramp Intents

Create and manage ramp intents — merchant-initiated buy/sell requests with embeddable widget URLs. Access via `zenpays.rampIntents`.

## Methods

### create

Create a new ramp intent.

#### Parameters

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `userId` | string | Yes | Unique user identifier from your system — used as the KYC record key |
| `kycVerified` | boolean | Yes | When `true`, ZenPays KYC verification is skipped. When `false`, the widget may prompt the user for identity verification. |
| `type` | string | Yes | `buy` (fiat → crypto) or `sell` (crypto → fiat) |
| `wallets` | array | Yes | Array of `{ network, address }` objects (min 1) |
| `fiatCurrency` | string | Yes | Fiat currency ISO code (e.g. `USD`, `EUR`, `INR`) |
| `cryptoCurrency` | string | No | Pre-selected crypto (e.g. `btc_bitcoin`, `usdt_tron`) |
| `amount` | number | No | Pre-filled amount in the widget |
| `successUrl` | string | No | URL to redirect on success |
| `cancelUrl` | string | No | URL to redirect on cancel |
| `customerEmail` | string | No | Customer email for pre-filling |
| `metadata` | object | No | Custom key-value pairs (returned in webhooks) |

<Tabs groupId="language">
  <TabItem value="javascript" label="JavaScript" default>

```typescript
const intent = await zenpays.rampIntents.create({
  userId: 'usr_abc123',
  kycVerified: false,
  type: 'buy',
  wallets: [
    { network: 'ethereum', address: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28' },
  ],
  fiatCurrency: 'USD',
  cryptoCurrency: 'eth_ethereum',
  amount: 100,
  successUrl: 'https://yoursite.com/success',
  cancelUrl: 'https://yoursite.com/cancel',
})
```

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

```python
intent = zenpays.ramp_intents.create({
    "userId": "usr_abc123",
    "kycVerified": False,
    "type": "buy",
    "wallets": [
        {"network": "ethereum", "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28"}
    ],
    "fiatCurrency": "USD",
    "cryptoCurrency": "eth_ethereum",
    "amount": 100,
    "successUrl": "https://yoursite.com/success",
    "cancelUrl": "https://yoursite.com/cancel",
})
```

  </TabItem>
</Tabs>

#### Response

```json
{
  "intentId": "ri_1710345678000_a1b2c3d4e5f6g7h8",
  "redirectUrl": "https://checkout.zenpayz.com/payments/ramping/widget/ri_1710345678000_a1b2c3d4e5f6g7h8",
  "expiresAt": "2026-03-15T10:00:00.000Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `intentId` | string | Unique intent identifier (`ri_{timestamp}_{hex}`) |
| `redirectUrl` | string | URL to redirect the customer to the ramp widget |
| `expiresAt` | string | ISO 8601 expiry timestamp (1 hour from creation) |

---

### list

<Tabs groupId="language">
  <TabItem value="javascript" label="JavaScript" default>

```typescript
const { data, meta } = await zenpays.rampIntents.list({
  type: 'buy',
  status: 'active',
  page: 1,
  limit: 20,
})
```

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

```python
result = zenpays.ramp_intents.list({"type": "buy", "status": "active", "limit": 20})
```

  </TabItem>
</Tabs>

#### Filters

| Field | Type | Description |
|-------|------|-------------|
| `page` | number | Page number (default: 1) |
| `limit` | number | Items per page (default: 20, max: 100) |
| `status` | string | Filter by status: `created`, `active`, `completed`, `expired`, `cancelled` |
| `type` | string | Filter by direction: `buy` or `sell` |
| `search` | string | Search by intent ID or customer email |

---

### get

Retrieve intent details including widget URL and KYC status.

```typescript
const result = await zenpays.rampIntents.get('ri_xxx')
```

#### Response

```json
{
  "intent": {
    "intentId": "ri_xxx",
    "type": "buy",
    "fiatCurrency": "USD",
    "cryptoCurrency": "eth_ethereum",
    "amount": 100,
    "status": "active",
    "kycStatus": "approved",
    "cancelReason": null,
    "wallets": [{ "network": "ethereum", "address": "0x742d..." }],
    "userId": "usr_abc123",
    "kycVerified": true,
    "successUrl": "https://yoursite.com/success",
    "cancelUrl": "https://yoursite.com/cancel",
    "expiresAt": "2026-03-15T10:00:00.000Z"
  },
  "widgetUrl": "https://widget.onramper.com/...signed_url...",
  "kycRequired": false,
  "kycVerificationUrl": null
}
```

| Field | Type | Description |
|-------|------|-------------|
| `intent` | object | Full intent details |
| `intent.status` | string | Lifecycle state: `created`, `active`, `completed`, `expired`, or `cancelled` |
| `intent.kycStatus` | string | KYC sub-state: `pending`, `in_review`, `approved`, `declined`, or `expired` |
| `intent.cancelReason` | string \| null | Set when `status='cancelled'`. `kyc_rejected` when KYC was declined; `kyc_expired` for expiry; otherwise a free-form string. |
| `widgetUrl` | string \| null | Signed widget URL for iframe embedding. `null` if KYC required or expired. |
| `kycRequired` | boolean | Whether ZenPays KYC verification is needed |
| `kycVerificationUrl` | string \| null | Verification URL (call `initiateKyc` if null and KYC is required) |

:::tip Distinguishing decline reasons
When a customer's KYC is rejected, `intent.status` flips to `'cancelled'` and `intent.cancelReason` becomes `'kyc_rejected'`. You can also drive your UI off `intent.kycStatus === 'declined'` directly — both are set together.
:::

---

### updateStatus

```typescript
const updated = await zenpays.rampIntents.updateStatus('ri_xxx', 'cancelled')
```

---

### initiateKyc

Start ZenPays KYC verification for a ramp intent. Call this when `kycRequired` is `true` and `kycVerificationUrl` is `null`.

<Tabs groupId="language">
  <TabItem value="javascript" label="JavaScript" default>

```typescript
const kyc = await zenpays.rampIntents.initiateKyc('ri_xxx', {
  email: 'user@example.com',
  firstName: 'John',
  lastName: 'Doe',
})

if (kyc.kycVerificationUrl) {
  // Redirect user to verification
  window.location.href = kyc.kycVerificationUrl
}
```

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

```python
kyc = zenpays.ramp_intents.initiate_kyc("ri_xxx", {
    "email": "user@example.com",
    "firstName": "John",
    "lastName": "Doe",
})

if kyc["kycVerificationUrl"]:
    print(f"Redirect to: {kyc['kycVerificationUrl']}")
```

  </TabItem>
</Tabs>

#### Response

```json
{
  "kycRequired": true,
  "kycVerificationUrl": "https://verify.zenpayz.com/session/abc123",
  "sessionId": "abc123"
}
```

---

### getKycStatus

Poll the current KYC verification status for a ramp intent. This is the same
endpoint the checkout widget polls every 5 seconds while a customer is verifying
— call it the same way from your own UI if you embed the widget yourself.

<Tabs groupId="language">
  <TabItem value="javascript" label="JavaScript" default>

```typescript
const status = await zenpays.rampIntents.getKycStatus('ri_xxx')

switch (status.kycStatus) {
  case 'approved':
    // KYC passed — proceed with the checkout
    break
  case 'in_review':
    // Identity OK but flagged (often AML) — keep waiting, surface "under review"
    break
  case 'declined':
  case 'expired':
    // Terminal failure — `status.intentStatus` is now 'cancelled' and
    // `status.cancelReason` will tell you why ('kyc_rejected' or 'kyc_expired')
    break
  case 'pending':
  default:
    // Verification not yet completed — keep polling (or call initiateKyc)
    break
}
```

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

```python
status = zenpays.ramp_intents.get_kyc_status("ri_xxx")

if status["kycStatus"] == "approved":
    print("KYC verified — proceed")
elif status["kycStatus"] == "in_review":
    print("Identity OK, flagged for review — keep waiting")
elif status["kycStatus"] in ("declined", "expired"):
    print(f"Terminal: {status['cancelReason']}")
else:
    print("Still pending — keep polling")
```

  </TabItem>
</Tabs>

#### Response

```json
{
  "verified": false,
  "kycStatus": "in_review",
  "intentStatus": "active",
  "cancelReason": null
}
```

| Field | Type | Description |
|-------|------|-------------|
| `verified` | boolean | Convenience flag — `true` when `kycStatus === 'approved'` |
| `kycStatus` | string | `pending`, `in_review`, `approved`, `declined`, or `expired` |
| `intentStatus` | string | Current ramp intent lifecycle state (`created`, `active`, `completed`, `expired`, `cancelled`) |
| `cancelReason` | string \| null | 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 / surface `widgetUrl`. |
| `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. |

:::tip 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.
:::
