# KYC for regulated countries

Submit the identity details a regulated country requires, hand the form to your customer with a branded link, and fix a declined number.

Many countries regulate who can hold a phone number. Before a number in those countries is ordered, the registrant (you, or your customer) proves who they are and where they are. Pozy collects the details, files them with the carrier and tracks the review. Numbers usually activate 1 to 3 business days after you submit.

You hit this flow when `GET /v1/phone-numbers/countries` shows `needsKyc: true`, or when a purchase answers `202` with `status: "kyc_required"`.

## Two ways to collect the details

| | Hosted link | API |
| --- | --- | --- |
| Who fills it in | Your customer, on a page with your logo and colour | Your own UI |
| Build effort | One call | A form per country, document upload, address checks |
| Best for | Platforms whose customers hold the legal details | Teams that already collect business documents |

## Hand the form to your customer

> **Not available yet:**
> 
> Hosted KYC links are not available yet: `POST /v1/phone-numbers/kyc/share` answers `403` with `code: "feature_not_available"`. Collect the details in your own UI and [submit them through the API](#submit-through-the-api) until it opens.

`POST /v1/phone-numbers/kyc/share` returns a single-use page, valid for 7 days, that walks your customer through the country's requirements. It needs no Pozy login.

```bash cURL
curl -X POST https://api.pozy.ai/v1/phone-numbers/kyc/share \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "6650c1f0a2b3c4d5e6f70812",
    "country": "GB",
    "language": "en",
    "branding": { "companyName": "Acme", "logoUrl": "https://acme.example.com/logo.png", "brandColor": "#1a73e8" },
    "redirect_url": "https://acme.example.com/numbers/done"
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/phone-numbers/kyc/share", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    profileId: "6650c1f0a2b3c4d5e6f70812",
    country: "GB",
    language: "en",
    branding: { companyName: "Acme", logoUrl: "https://acme.example.com/logo.png", brandColor: "#1a73e8" },
    redirect_url: "https://acme.example.com/numbers/done",
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.url, data.expiresAt);
```
```python Python
import os, requests

resp = requests.post(
    "https://api.pozy.ai/v1/phone-numbers/kyc/share",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "profileId": "6650c1f0a2b3c4d5e6f70812",
        "country": "GB",
        "branding": {"companyName": "Acme", "logoUrl": "https://acme.example.com/logo.png"},
        "redirect_url": "https://acme.example.com/numbers/done",
    },
    timeout=30,
)
print(resp.json()["url"])
```

- `branding` puts your company name, logo and colour on the page.
- `language` sets the page language: `en`, `es` or `pt-BR`. Without it, the page follows the browser and falls back to English.
- `redirect_url` sends the customer back to your app afterwards, with `kyc=submitted` and `country=<code>` added to the query string.
- `areaCode` holds the order to one area.

When your customer submits, the number enters review under your workspace and `whatsapp.number.kyc_submitted` fires.

## Submit through the API

### Check stock and the address rule
`GET /v1/phone-numbers/availability?country=GB` says whether numbers are in stock and which address the registrant needs (`addressConstraint`: `geo`, `country` or `none`). Checking first avoids a filing that passes review but can never get a number because the address is in the wrong area.

### Get the form for the country
`GET /v1/phone-numbers/kyc?country=GB` returns the fields the country requires. Add `numberType` if you want a type other than the default, and send the same value when you submit.

```json
{
  "country": "GB",
  "numberType": "local",
  "fields": [
    { "requirementId": "d7e8f9a0-b1c2-4d3e-9f4a-5b6c7d8e9f0a", "label": "Business name", "kind": "text", "example": "Acme Ltd" },
    { "requirementId": "e8f9a0b1-c2d3-4e4f-8a5b-6c7d8e9f0a1b", "label": "Proof of identity", "kind": "file" },
    { "requirementId": "f9a0b1c2-d3e4-4f5a-9b6c-7d8e9f0a1b2c", "label": "Business address", "kind": "address", "localTo": "GB" }
  ],
  "reusable": null,
  "pendingReview": false
}
```

| `kind` | What to send |
| --- | --- |
| `text` | A string in `values`, keyed by `requirementId` |
| `date` | An ISO date in `values` |
| `address` | The `address` object |
| `file` | An entry in `documents`, either inline base64 or a `documentId` from `POST /v1/phone-numbers/kyc/upload-document` (about 4 MB per document). Base64 makes a file about a third larger and the whole request can carry about 4 MB, so keep inline files to about 3 MB in total per submission |
| `action` | An identity check the end user completes after the order, through a link. Send `endUserFirstName` and `endUserLastName` with the submit. |

### Check the packet (optional)
`POST /v1/phone-numbers/kyc/validate-address` checks an address before you upload anything. `POST /v1/phone-numbers/kyc/review-packet` reviews the whole packet for likely decline reasons.

### Submit
```bash
curl -X POST https://api.pozy.ai/v1/phone-numbers/kyc \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "6650c1f0a2b3c4d5e6f70812",
    "country": "GB",
    "submissionId": "acme-gb-0001",
    "values": { "d7e8f9a0-b1c2-4d3e-9f4a-5b6c7d8e9f0a": "Acme Ltd" },
    "documents": [
      { "requirementId": "e8f9a0b1-c2d3-4e4f-8a5b-6c7d8e9f0a1b", "filename": "passport-both-sides.pdf", "base64": "JVBERi0xLjQK..." }
    ],
    "address": {
      "requirementId": "f9a0b1c2-d3e4-4f5a-9b6c-7d8e9f0a1b2c",
      "country_code": "GB",
      "street_address": "221 Baker Street",
      "locality": "London",
      "administrative_area": "London",
      "postal_code": "NW1 6XE"
    }
  }'
```

The response has `status: "kyc_submitted"` and the new number in `pending_regulatory`.

> **ID cards need both sides:**
> 
> Carriers reject one-sided ID cards often. Combine front and back into one file before you upload it.

| Submit field | What it does |
| --- | --- |
| `submissionId` | Your key for this attempt. A retry with the same value returns the same number instead of ordering another. |
| `quantity` | 1 to 5 numbers in the same country from one verification (a pre-order is always 1) |
| `wantsSms` | Order from SMS-capable stock and turn SMS on at activation |
| `reuse`, `reuseOptionId` | Reuse an approved verification you already hold for this country. The form response lists it in `reusable`. No documents needed, and the order is placed at once. |
| `areaCode`, `preOrder` | Order in one area, and accept a sourced number when it is out of stock |

Document bytes go straight to the carrier with the request. Pozy does not keep a copy.

## Out of stock: pre-order

When `available` is `false` but `preOrderable` is `true`, submit the same form with `preOrder: true`. Pozy takes the next number that comes back in stock, or one the carrier sources for this registration.

- Sourcing usually takes 2 to 4 weeks and is not guaranteed.
- The number stays `pending_regulatory` until sourced. Nothing is billed until it is active.
- If the carrier cannot source it, the number is declined and nothing is charged.
- To cancel, release the pending number with `DELETE /v1/phone-numbers/{id}`.

In `geo` countries the area comes from the address. If that area has no stock, the first submit returns `409` with `code: "area_pre_order_available"` and `details` (`areaCode`, `areaName`, `estimatedWeeks`, `billedWhenActive`). The `error` text is ready to show your customer. Once they agree to wait, resend the same body with `preOrder: true`.

## Forward the identity-check link

Some countries (Australian mobiles, for example) need the end user to pass a live identity check after the order is placed. When the form had an `action` field:

1. Poll `GET /v1/phone-numbers/{id}` after submitting. The check link appears on the number in `onfidoVerificationUrl` once the order exists.
2. Or listen for `whatsapp.number.verification_required`, which carries the same link in `verificationUrl`.
3. Send the link to the person whose ID is on file. The review waits until they finish.

## Fix a declined number

A declined number stays in your list with `status: "regulatory_declined"` and a `regulatoryDeclineReason`. You do not start over.

1. `GET /v1/phone-numbers/{id}/remediate` returns only the requirements the reviewer flagged.
2. `POST /v1/phone-numbers/{id}/remediate` with corrected `values`, `documents` or `address`.

The number goes back to `pending_regulatory` on the same record and is not billed again.

### Answer the reviewer

Sometimes the reviewer asks a question rather than flagging a field ("is this a personal or a business line?"). `POST /v1/phone-numbers/{id}/remediate/respond` sends a `message`, plus any corrected `documents` or `address` and up to 5 `attachments` (PDF, JPG, PNG or WebP, sent as base64, about 3 MB in total per request). The response `status` is `replied` for a message alone and `resubmitted` when corrections went with it.

```bash
curl -X POST https://api.pozy.ai/v1/phone-numbers/6650d2e1b3c4d5e6f7081923/remediate/respond \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "message": "This is a business line for Acme Ltd. The utility bill is in the company name." }'
```

## Statuses and events

| What happened | Number status | Event |
| --- | --- | --- |
| Customer submitted a hosted link | `pending_regulatory` | `whatsapp.number.kyc_submitted` |
| Reviewer needs more information | stays `pending_regulatory` | `whatsapp.number.action_required` (with `reason` and per-requirement `requirements`) |
| End user must pass an ID check | stays `pending_regulatory` | `whatsapp.number.verification_required` |
| Approved and live | `active` | `whatsapp.number.activated` |
| Declined | `regulatory_declined` | `whatsapp.number.declined` |

These events carry a `whatsapp.` prefix for historical reasons and fire for every number. No event fires when you submit through the API, because you already know.

> **Note:**
> 
> `verification.approved` and `verification.failed` are not KYC events. They belong to the [one-time code API](https://pozy.ai/docs/telephony/sms#send-one-time-codes).

## Limits

| Limit | Value |
| --- | --- |
| Review time | Usually 1 to 3 business days |
| Numbers per verification | 1 to 5, same country |
| Hosted link | Single use, valid 7 days |
| Reviewer attachments | Up to 5 files, about 3 MB in total per request (they travel as base64) |

## Errors

| Status | Code | Cause | Fix |
| --- | --- | --- | --- |
| `400` | | A value failed validation, often an address outside the country or an oversized file | Run `validate-address` first |
| `409` | `area_pre_order_available` | The address area is out of stock but can be pre-ordered | Resend with `preOrder: true` once the customer agrees |
| `409` | `area_code_unavailable` | The area has no stock and cannot be pre-ordered | Pick another area, or create a stock watch |
| `409` | `country_out_of_stock` | Nothing deliverable in the country and type | Create a [stock watch](https://pozy.ai/docs/telephony/buy-a-number#watch-a-sold-out-country) |
| `409` | | `reuse: true` with no approved verification on file | Submit the full form |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/phone-numbers/kyc`](https://pozy.ai/docs/api/getPhoneNumberKycForm) | Get KYC form spec |
| [`POST /v1/phone-numbers/kyc`](https://pozy.ai/docs/api/submitPhoneNumberKyc) | Submit KYC |
| [`POST /v1/phone-numbers/kyc/upload-document`](https://pozy.ai/docs/api/uploadPhoneNumberKycDocument) | Upload a KYC document |
| [`GET /v1/phone-numbers/kyc/document/{documentId}`](https://pozy.ai/docs/api/viewPhoneNumberKycDocument) | View a KYC document on file |
| [`POST /v1/phone-numbers/kyc/validate-address`](https://pozy.ai/docs/api/validatePhoneNumberKycAddress) | Pre-validate KYC address |
| [`POST /v1/phone-numbers/kyc/review-packet`](https://pozy.ai/docs/api/reviewPhoneNumberKycPacket) | Pre-review a KYC packet |
| [`POST /v1/phone-numbers/kyc/share`](https://pozy.ai/docs/api/createPhoneNumberKycLink) | Create a hosted KYC link (not available yet) |
| [`GET /v1/phone-numbers/{id}/remediate`](https://pozy.ai/docs/api/getPhoneNumberRemediation) | Get declined requirements |
| [`POST /v1/phone-numbers/{id}/remediate`](https://pozy.ai/docs/api/remediatePhoneNumber) | Resubmit a declined number |
| [`POST /v1/phone-numbers/{id}/remediate/reply`](https://pozy.ai/docs/api/replyToPhoneNumberReviewer) | Reply to the regulatory reviewer |
| [`POST /v1/phone-numbers/{id}/remediate/respond`](https://pozy.ai/docs/api/respondToPhoneNumberReviewer) | Respond to the regulatory reviewer (message + corrections) |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`whatsapp.number.activated`](https://pozy.ai/docs/webhooks/events#whatsapp-number-activated) | WhatsApp number activated. |
| [`whatsapp.number.declined`](https://pozy.ai/docs/webhooks/events#whatsapp-number-declined) | WhatsApp number declined. |
| [`whatsapp.number.action_required`](https://pozy.ai/docs/webhooks/events#whatsapp-number-action-required) | WhatsApp number action required. |
| [`whatsapp.number.verification_required`](https://pozy.ai/docs/webhooks/events#whatsapp-number-verification-required) | WhatsApp number verification-required. |
| [`whatsapp.number.kyc_submitted`](https://pozy.ai/docs/webhooks/events#whatsapp-number-kyc-submitted) | WhatsApp number KYC submitted. |
