> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kemycard.com/llms.txt
> Use this file to discover all available pages before exploring further.

# POA Flow

> Verify your customer's address: from verified to full

POA (Proof of Address) verifies a customer's residential address using a utility bill, bank statement, or official document. Once approved, the customer upgrades from `verified` to `full`.

The POA API has two endpoints: one to submit the document, one to read the result.

<Info>
  POA can only be submitted for customers whose KYC is approved (`verified` level).
</Info>

## Flow

```
Customer (VERIFIED)
    │
    ▼
POST /poa/{customerCode}/document   ← you send the document
    │
    ▼
Verification in progress (pending)
    │
    ├── approved  → Customer becomes FULL
    └── rejected  → Customer stays VERIFIED, submit a new document
```

## Step 1: Submit the document

You collect the document in your own application (file upload form, mobile camera, etc.), then your server forwards the file to KemyCard.

The request is a standard file upload:

* Method and URL: `POST /v1/poa/{customerCode}/document`
* Body: `multipart/form-data` (not JSON)
* One field named **`document`** containing the file itself
* Your usual `Authorization: Bearer` header

<Warning>
  Do not set the `Content-Type` header yourself: your HTTP client generates it with the multipart boundary. Do not send the file as base64 in a JSON body.
</Warning>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.kemycard.com/v1/poa/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document \
    -H "Authorization: Bearer sk_live_YOUR_KEY" \
    -F "document=@/path/to/electricity-bill.pdf"
  ```

  ```php PHP theme={null}
  $customerCode = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';

  $ch = curl_init("https://api.kemycard.com/v1/poa/{$customerCode}/document");
  curl_setopt_array($ch, [
      CURLOPT_POST => true,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => ['Authorization: Bearer sk_live_YOUR_KEY'],
      CURLOPT_POSTFIELDS => [
          'document' => new CURLFile('/path/to/electricity-bill.pdf', 'application/pdf', 'electricity-bill.pdf'),
      ],
  ]);

  $response = json_decode(curl_exec($ch), true);
  curl_close($ch);
  ```

  ```javascript Node.js theme={null}
  import { readFile } from 'node:fs/promises';

  const customerCode = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';

  const form = new FormData();
  form.append(
    'document',
    new Blob([await readFile('/path/to/electricity-bill.pdf')], { type: 'application/pdf' }),
    'electricity-bill.pdf'
  );

  const response = await fetch(`https://api.kemycard.com/v1/poa/${customerCode}/document`, {
    method: 'POST',
    headers: { Authorization: 'Bearer sk_live_YOUR_KEY' },
    body: form,
  });

  const result = await response.json();
  ```

  ```python Python theme={null}
  import requests

  customer_code = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'

  with open('/path/to/electricity-bill.pdf', 'rb') as f:
      response = requests.post(
          f'https://api.kemycard.com/v1/poa/{customer_code}/document',
          headers={'Authorization': 'Bearer sk_live_YOUR_KEY'},
          files={'document': ('electricity-bill.pdf', f, 'application/pdf')},
      )

  result = response.json()
  ```
</CodeGroup>

The call returns `201` as soon as the document is received. The verification then runs in the background:

```json theme={null}
{
  "success": true,
  "data": {
    "customer_code": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "poa_status": "pending",
    "verification_level": "verified",
    "document_name": "electricity-bill.pdf",
    "rejection_reason": null,
    "approved_at": null
  }
}
```

### Requirements

* Customer must be `active` and its KYC approved
* Accepted documents: utility bill, bank statement, government letter
* Accepted formats: PDF, JPG, PNG — 10 MB maximum
* Document must be less than 3 months old
* Name and address on the document must match the name and address you declared for the customer

The call is refused when a proof of address is already approved, or when one is already being verified.

## Step 2: Get the result

### Via webhook (recommended)

| Event | Meaning |
| - | - |
| `poa.approved` | Address verified — customer is now `full` |
| `poa.rejected` | Document rejected — customer stays `verified`, submit a new document |

### Via the status endpoint

```bash theme={null}
curl -X GET https://api.kemycard.com/v1/poa/a1b2c3d4-e5f6-7890-abcd-ef1234567890/status \
  -H "Authorization: Bearer sk_live_YOUR_KEY"
```

```json theme={null}
{
  "success": true,
  "data": {
    "customer_code": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "poa_status": "approved",
    "verification_level": "full",
    "document_name": "electricity-bill.pdf",
    "rejection_reason": null,
    "approved_at": "2026-10-06T10:12:00+00:00"
  }
}
```

## When a document is rejected

A rejection is not final: fix the cause and submit a new document. Nothing is billed for a rejected document.

| Cause | What to do |
| - | - |
| The address on the document differs from the address you declared | Correct the address with `PATCH /customers/{code}` if it was wrong, or submit a document showing the declared address |
| The document is too old, unreadable or cropped | Submit a recent, complete and readable document |
| The name on the document does not match the customer's verified name | See below |

<Note>
  **Name not fully shown on the document.** Proofs of address often carry only part of the customer's name (initials, a single first name, a maiden or married name, no middle name). Such a document can be rejected even though it genuinely belongs to your customer.

  If your customer has no document showing their full name:

  1. **Open a support ticket** from your secure KemyCard account, with the `customer_code`. This is the priority channel: we review the case and regularize it from the ticket.
  2. **To follow up only**, email [hi@kemycard.com](mailto:hi@kemycard.com) and quote your ticket number. An email without a ticket number is not handled as a request.
</Note>

## Pricing

A verification is billed **1.80 USD, only when it is approved**. The amount is debited from your `others` ops balance and triggers a `balance.debited` webhook with `reason: poa_verification`.

Submitting a document is free, but your `others` balance must cover the price of one verification, otherwise the call returns `INSUFFICIENT_BALANCE`.

## POA status values

| Status | Description |
| - | - |
| `not_started` | No document has been submitted yet |
| `pending` | A document has been submitted, verification in progress |
| `approved` | Address verified — customer upgrades to `full` |
| `rejected` | Document rejected — submit a new document to retry |

## What does `full` unlock?

| Product | Required level |
| - | - |
| Virtual bank accounts (IBAN) | `full` |
| Higher transaction limits | `full` |
| Physical card delivery | `full` |

## Automatic demotion

<Warning>
  If a customer's address fields are modified, its POA is reset: a `full` customer is automatically **demoted back to `verified`**, and a document being verified is discarded. Submit a new proof of address. Sending a field with its current value is not a modification.
</Warning>

| Fields modified | Before | After | Reason |
| - | - | - | - |
| `first_name`, `last_name`, `date_of_birth` | verified or full | **basic** | Identity changed — KYC + POA reset |
| `address`, `city`, `state`, `postal_code`, `country` | verified or full | **verified** | Address changed — POA reset (approved, pending or rejected), KYC stays valid |
| `email`, `phone_country_code`, `phone`, `metadata` | any | **unchanged** | No impact on verification |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.