# piv.day API Reference

Full reference for the piv.day REST API: every endpoint, request body, response shape, error code and webhook. Exported from https://piv.day/api/docs for use with AI assistants.

---

## Getting started

REST API on the Business plan. Bearer tokens, JSON, idempotent endpoints, predictable error codes. Every endpoint below ships with a request and response example.

### Base URL

```
https://app.piv.day/api/v1
```

Every endpoint lives under this prefix. One environment — no separate staging hosts.

### Authentication

```
Authorization: Bearer YOUR_API_KEY
```

Keys are issued in the dashboard under `Settings → API Keys`. Each key has fine-grained scopes (read numbers, send SMS, buy proxies, etc.). A compromised token rotates in one click — no need to re-onboard the team.

### Account

**200 OK**
```
curl https://app.piv.day/api/v1/account \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**A single utility endpoint — current account balance and email. Use it to confirm your key works and auth is wired correctly.**
```
{
  "success": true,
  "data": {
    "user_id": "usr-uuid-...",
    "balance": 150.50,
    "email": "u***@example.com",
    "team_id": null,
    "is_team_member": false
  }
}
```

GET /api/v1/account

### Response format

```
{
  "success": true,
  "data": { ... }
}
```

Every response is JSON. On error, `success` is `false` and the body contains `error.code` and `error.message`.

### Rate limits

**100 requests per minute per API key. The limit is shared across all endpoints. Need more headroom for a spike? Ping Telegram support and we'll raise the cap.

On overflow you get `429 RATE_LIMITED` and a `Retry-After` header — how many seconds to wait before the next call.**
```
HTTP/1.1 429 Too Many Requests
Retry-After: 12

{
  "success": false,
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded, retry in 12s"
  }
}
```

429 Too Many Requests

## Numbers

Buy, renew and restore numbers. Receive and send SMS. Kick off Google QR verifications. Subscribe to events via webhooks.

### Countries and prices

`GET /api/v1/numbers/countries`

Returns countries available for purchase with the current price and SMS capabilities. All prices include your subscription discount.

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `country_code` | string | Two-letter ISO country code (e.g. `SE`). |
| `price_per_month` | number | Final price for your plan — what actually gets charged on purchase. Premium / Business discounts are already applied. |
| `base_price` | number | Price without discounts (Free plan). Returned for comparison so you can see what the subscription saves. |
| `can_send_sms` | boolean | Whether outbound SMS is supported from this number. |
| `can_receive_sms` | boolean | Whether inbound SMS is supported. |
| `sms_send_price` | number\|null | Outbound SMS price, also with the plan discount applied. `null` when sending isn't supported from this country. |

**curl**
```bash
curl https://app.piv.day/api/v1/numbers/countries \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": [
    {
      "country_code": "SE",
      "price_per_month": 4.00,
      "base_price": 5.00,
      "can_send_sms": true,
      "can_receive_sms": true,
      "sms_send_price": 0.25
    },
    {
      "country_code": "GB",
      "price_per_month": 3.00,
      "base_price": 3.00,
      "can_send_sms": true,
      "can_receive_sms": true,
      "sms_send_price": 0.20
    }
  ]
}
```

### List numbers

`GET /api/v1/numbers`

Returns a page of account numbers with filters by country, status, and search across number or custom name.

**Query parameters**

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | Page size (default 50, max 200). |
| `offset` | integer | Pagination offset. |
| `country` | string | ISO country code filter. |
| `status` | string | One of `active`, `expired`, `pending_restore`. |
| `search` | string | Substring search across phone number or custom name. |

**curl**
```bash
curl "https://app.piv.day/api/v1/numbers?country=SE&status=active&limit=10" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "numbers": [
      {
        "piv_num_id": "vzPA1-kHKSg-EAL7e-Jqd3o",
        "phone_number": "+46764794425",
        "country_code": "SE",
        "status": "active",
        "created_at": "2026-05-01T10:00:00Z",
        "expires_at": "2026-05-31T10:00:00Z",
        "auto_renew": false,
        "custom_name": "Office line",
        "purchased_at": "2026-05-01T10:00:00Z",
        "next_renewal_date": "2026-05-31T10:00:00Z",
        "tags": ["support"],
        "can_send_sms": true,
        "can_receive_sms": true
      }
    ],
    "pagination": { "total": 1, "limit": 10, "offset": 0 }
  }
}
```

### Get one number

`GET /api/v1/numbers/{piv_num_id}`

Full number record: status, dates, auto-renew, tags, allowed SMS directions.

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `NUMBER_NOT_FOUND` | 404 | Number doesn't exist or doesn't belong to the account. |

**curl**
```bash
curl https://app.piv.day/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "piv_num_id": "vzPA1-kHKSg-EAL7e-Jqd3o",
    "phone_number": "+46764794425",
    "country_code": "SE",
    "status": "active",
    "created_at": "2026-05-01T10:00:00Z",
    "expires_at": "2026-05-31T10:00:00Z",
    "auto_renew": false,
    "custom_name": "Office line",
    "purchased_at": "2026-05-01T10:00:00Z",
    "next_renewal_date": "2026-05-31T10:00:00Z",
    "tags": ["support"],
    "can_send_sms": true,
    "can_receive_sms": true
  }
}
```

### Buy a number

`POST /api/v1/numbers/purchase`

Buys a single number in the selected country for the given period. Cost is debited from the account balance. Returns `piv_num_id` used in all later number operations.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `country_code` * | string | ISO country code. |
| `duration_months` * | integer | Rental period in months (minimum 1). |
| `auto_renew` * | boolean | Turn auto-renew on right after purchase. |
| `custom_name` | string | Optional human-friendly name for the number. |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `COUNTRY_NOT_AVAILABLE` | 400 | Country isn't available or has no pricing. |
| `NO_NUMBERS_AVAILABLE` | 400 | No free numbers in the requested country right now. |
| `INSUFFICIENT_BALANCE` | 402 | Not enough funds on the balance. |
| `VALIDATION_ERROR` | 400 | Invalid fields in the request body. |

**curl**
```bash
curl -X POST https://app.piv.day/api/v1/numbers/purchase \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "country_code": "SE",
    "duration_months": 1,
    "auto_renew": false,
    "custom_name": "My Sweden Number"
  }'
```

**200 OK**
```json
{
  "success": true,
  "cost": 5.00,
  "numbers": [
    {
      "piv_num_id": "vzPA1-kHKSg-EAL7e-Jqd3o",
      "country_code": "SE",
      "phone_number": "+46764794425",
      "created_at": "2026-05-01T10:00:00Z",
      "expires_at": "2026-05-31T10:00:00Z",
      "auto_renew": false,
      "custom_name": "My Sweden Number"
    }
  ]
}
```

### Renew a number

`POST /api/v1/numbers/{piv_num_id}/renew`

Extends an active number for the given period. Cost is debited at call time.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `duration_months` * | integer | Number of months to add. |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `NUMBER_NOT_FOUND` | 404 | Number doesn't exist or doesn't belong to the account. |
| `COUNTRY_NOT_AVAILABLE` | 400 | No pricing found for the number's country. |
| `INSUFFICIENT_BALANCE` | 402 | Not enough funds to renew. |

**curl**
```bash
curl -X POST https://app.piv.day/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o/renew \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "duration_months": 1 }'
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "piv_num_id": "vzPA1-kHKSg-EAL7e-Jqd3o",
    "phone_number": "+46764794425",
    "old_expires_at": "2026-05-31T10:00:00Z",
    "new_expires_at": "2026-06-30T10:00:00Z",
    "cost": 5.00
  }
}
```

### Update a number

`PATCH /api/v1/numbers/{piv_num_id}`

Currently only the custom name can be updated.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `custom_name` * | string | New name for the number. |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `NUMBER_NOT_FOUND` | 404 | Number doesn't exist or doesn't belong to the account. |
| `VALIDATION_ERROR` | 400 | Invalid value in the request body. |

**curl**
```bash
curl -X PATCH https://app.piv.day/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "custom_name": "Support line" }'
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "piv_num_id": "vzPA1-kHKSg-EAL7e-Jqd3o",
    "phone_number": "+46764794425",
    "country_code": "SE",
    "status": "active",
    "custom_name": "Support line",
    "auto_renew": false,
    "expires_at": "2026-05-31T10:00:00Z",
    "tags": []
  }
}
```

### Auto-renewal toggle

`PATCH /api/v1/numbers/{piv_num_id}/auto-renewal`

Turns auto-renew on or off. When enabled, renewal is debited automatically 24 hours before expiry. If the balance is short, the number expires; you can reclaim it via `POST /numbers/restore` within 7 days.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `auto_renew` * | boolean | New auto-renew flag value. |

**curl**
```bash
curl -X PATCH https://app.piv.day/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o/auto-renewal \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "auto_renew": true }'
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "piv_num_id": "vzPA1-kHKSg-EAL7e-Jqd3o",
    "phone_number": "+46764794425",
    "auto_renew": true
  }
}
```

### Restore expired numbers

`POST /api/v1/numbers/restore`

Reclaims expired numbers within a 7-day window. Pass an array of `piv_num_id` — numbers come back with SMS history and settings preserved. Restore costs more than buying a fresh number (includes a restore multiplier) — exact figures show up in the dashboard before confirmation. The final status per number arrives via the `number.restore_completed` webhook; refunds for unsuccessful numbers are credited automatically.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `piv_num_ids` * | string[] | Array of number IDs to restore. |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `NO_RESTORABLE_NUMBERS` | 404 | None of the supplied numbers can be restored (7-day window passed or they aren't yours). |
| `INSUFFICIENT_BALANCE` | 402 | Not enough funds to cover the restore. |

**curl**
```bash
curl -X POST https://app.piv.day/api/v1/numbers/restore \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "piv_num_ids": [
      "vzPA1-kHKSg-EAL7e-Jqd3o",
      "abc12-defgh-ijklm-nopqr"
    ]
  }'
```

**200 OK**
```json
{
  "success": true,
  "queued": 2,
  "skipped": 0,
  "total_charged": 9.00,
  "numbers": [
    {
      "piv_num_id": "vzPA1-kHKSg-EAL7e-Jqd3o",
      "phone_number": "+46764794425",
      "status": "pending"
    }
  ]
}
```

### SMS history

`GET /api/v1/numbers/{piv_num_id}/sms`

Returns inbound and outbound messages for the number in reverse chronological order.

**Query parameters**

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | How many messages to return (default 50, max 200). |
| `offset` | integer | Pagination offset. |

**curl**
```bash
curl "https://app.piv.day/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o/sms?limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "messages": [
      {
        "id": "42",
        "from_number": "+46764794425",
        "to_number": "+14155551234",
        "message_body": "Hello from piv.day",
        "direction": "outbound",
        "status": "delivered",
        "received_at": "2026-05-22T11:30:00Z"
      },
      {
        "id": "41",
        "from_number": "+14155551234",
        "to_number": "+46764794425",
        "message_body": "Hey, got your message!",
        "direction": "inbound",
        "status": "received",
        "received_at": "2026-05-22T11:35:00Z"
      }
    ],
    "pagination": { "total": 2, "limit": 20, "offset": 0 }
  }
}
```

### Send SMS

`POST /api/v1/numbers/{piv_num_id}/sms/send`

Available for countries where outbound is allowed (e.g. CA, GB, SE). Message cost is debited at send time.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `to_number` * | string | Recipient number in international format (E.164). |
| `message_body` * | string | Message text. GSM-7 and UCS-2 are supported. |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `NUMBER_NOT_FOUND` | 404 | Number doesn't exist or doesn't belong to the account. |
| `NUMBER_EXPIRED` | 403 | Number has already expired. |
| `NUMBER_NOT_ACTIVE` | 400 | Number is in a state that doesn't allow sending. |
| `INSUFFICIENT_BALANCE` | 402 | Not enough funds to send. |
| `INVALID_MESSAGE_FORMAT` | 400 | Message body exceeds the allowed length or contains forbidden characters. |

**curl**
```bash
curl -X POST https://app.piv.day/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o/sms/send \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to_number": "+14155551234",
    "message_body": "Hello from piv.day"
  }'
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "message_id": "42",
    "from_number": "+46764794425",
    "to_number": "+14155551234",
    "message_body": "Hello from piv.day",
    "status": "queued",
    "created_at": "2026-05-22T11:30:00Z"
  }
}
```

### Run Google QR verification

`POST /api/v1/numbers/{piv_num_id}/verify`

Starts a Google QR verification using the provided URL. The result — success or failure — arrives via the verify.completed or verify.failed webhook. Cost is debited when the task is queued.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `gv_url` * | string | Full Google verification URL (grabbed from Google's page). |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `NUMBER_NOT_FOUND` | 404 | Number doesn't exist or isn't yours. |
| `NUMBER_NOT_ACTIVE` | 400 | Number isn't active. |
| `SMS_DISABLED` | 400 | SMS sending is disabled for this number. |
| `INSUFFICIENT_BALANCE` | 402 | Not enough funds for the verification. |
| `PROXY_DEAD` | 502 | Proxy unavailable — balance refunded. |
| `QUEUE_FULL` | 503 | Queue is full, try later — balance refunded. |
| `SERVER_UNAVAILABLE` | 503 | Automation server unavailable — balance refunded. |

**curl**
```bash
curl -X POST https://app.piv.day/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o/verify \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "gv_url": "https://gv.google.com/..." }'
```

**202 Accepted**
```json
{
  "success": true,
  "message": "Verification initiated"
}
```

## Proxy

Residential IPv6 across dozens of countries. Bulk purchase, renewal, Restore that keeps the same login and password, export in the format you need.

### List servers

`GET /api/v1/proxy/servers`

Available proxy servers with prices.

**curl**
```bash
curl https://app.piv.day/api/v1/proxy/servers \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "servers": [
      {
        "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "name": "EU-DE-01",
        "country": "DE",
        "socks5_port": 1080,
        "http_port": 3128,
        "price_per_proxy": 0.50
      },
      {
        "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
        "name": "EU-NL-01",
        "country": "NL",
        "socks5_port": 1080,
        "http_port": 3128,
        "price_per_proxy": 0.45
      }
    ]
  }
}
```

### List orders

`GET /api/v1/proxy/orders`

List of your proxy orders with filters by status or search.

**Query parameters**

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | Page size. Default 100, max 500. |
| `offset` | integer | Pagination offset. |
| `status` | string | Status filter: `active`, `expired`. |
| `search` | string | Search across orders. |

**curl**
```bash
curl "https://app.piv.day/api/v1/proxy/orders?status=active&limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "orders": [
      {
        "id": "ord-uuid-...",
        "country": "DE",
        "quantity": 10,
        "price_per_proxy": 0.50,
        "price_total": 5.00,
        "status": "active",
        "purchased_at": "2026-01-01T00:00:00Z",
        "expires_at": "2026-02-01T00:00:00Z"
      }
    ],
    "pagination": { "total": 1, "limit": 20, "offset": 0 }
  }
}
```

### Get one order

`GET /api/v1/proxy/orders/{id}`

Get a single order's details, including the list of proxy credentials.

**curl**
```bash
curl https://app.piv.day/api/v1/proxy/orders/ord-uuid-... \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "order": {
      "id": "ord-uuid-...",
      "country": "DE",
      "quantity": 10,
      "price_per_proxy": 0.50,
      "price_total": 5.00,
      "status": "active",
      "purchased_at": "2026-01-01T00:00:00Z",
      "expires_at": "2026-02-01T00:00:00Z",
      "created_at": "2026-01-01T00:00:00Z",
      "items": [
        {
          "login": "user1",
          "password": "pass1",
          "host": "185.1.2.3",
          "socks5_port": 1080,
          "http_port": 3128,
          "ipv6": "2a00:1:2:3::1"
        }
      ]
    }
  }
}
```

### Buy proxies

`POST /api/v1/proxy/purchase`

Buy proxies on the chosen server. One request creates one order with N proxies (up to 500). There are no bulk orders — call the method again if you need more.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `server_id` * | string | Server ID from `GET /proxy/servers`. |
| `quantity` * | integer | How many proxies to buy (1–500). |
| `months` * | integer | Rental period in months (1–12). |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `INSUFFICIENT_BALANCE` | 402 | Not enough funds. |
| `NOT_FOUND` | 404 | Proxy server not found or inactive. |
| `VALIDATION_ERROR` | 400 | Invalid request body. |

**curl**
```bash
curl -X POST https://app.piv.day/api/v1/proxy/purchase \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "server_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "quantity": 5,
    "months": 1
  }'
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "order": {
      "id": "ord-uuid-...",
      "country": "DE",
      "quantity": 5,
      "price_total": 2.50,
      "status": "active",
      "expires_at": "2026-02-01T00:00:00Z",
      "created_at": "2026-01-01T00:00:00Z",
      "items": [
        {
          "login": "user1",
          "password": "pass1",
          "host": "185.1.2.3",
          "socks5_port": 1080,
          "http_port": 3128,
          "ipv6": "2a00:1:2:3::1"
        },
        {
          "login": "user2",
          "password": "pass2",
          "host": "185.1.2.3",
          "socks5_port": 1080,
          "http_port": 3128,
          "ipv6": "2a00:1:2:3::2"
        }
      ]
    }
  }
}
```

### Renew an order

`POST /api/v1/proxy/orders/{id}/renew`

Extend an active proxy order by a number of months.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `months` * | integer | Months to add. |

**curl**
```bash
curl -X POST https://app.piv.day/api/v1/proxy/orders/ord-uuid-.../renew \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "months": 1 }'
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "order_id": "ord-uuid-...",
    "cost": 5.00,
    "new_expires_at": "2026-03-01T00:00:00Z"
  }
}
```

### Restore expired order

`POST /api/v1/proxy/orders/{id}/restore`

Bring an expired proxy order back to life without re-purchasing. Login, password, IPv6 addresses, country and bindings — all the same. Term extends by 1 month. No antidetect reconfiguration.

**curl**
```bash
curl -X POST https://app.piv.day/api/v1/proxy/orders/ord-uuid-.../restore \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "order_id": "ord-uuid-...",
    "quantity": 10,
    "total_cost": 5.00,
    "expires_at": "2026-03-01T00:00:00Z"
  }
}
```

### Export proxies

`GET /api/v1/proxy/orders/{id}/export`

Export the order's proxy credentials as a text list (login:password@host:port).

**Query parameters**

| Field | Type | Description |
| --- | --- | --- |
| `format` | string | Export format: `socks5`, `http` or `both` (default `socks5`). |

**curl**
```bash
curl "https://app.piv.day/api/v1/proxy/orders/ord-uuid-.../export?format=socks5" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "content": "user1:pass1@1.2.3.4:1080\nuser2:pass2@1.2.3.4:1080",
    "filename": "piv-day-proxy-ord-uuid--socks5.txt",
    "format": "socks5",
    "count": 2
  }
}
```

## Domains

Search and register without KYC, automatic Cloudflare and SSL, DNS records and nameserver management — all through the API.

### Availability check

`GET /api/v1/domains/search`

Check whether a domain is available and how much it costs.

**Query parameters**

| Field | Type | Description |
| --- | --- | --- |
| `q` * | string | Domain name to look up, e.g. `mysite.com`. |

**curl**
```bash
curl "https://app.piv.day/api/v1/domains/search?q=mysite.com" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "domain": "mysite.com",
    "available": true,
    "create_price": 12.50,
    "renew_price": 12.50,
    "currency": "USD"
  }
}
```

### List domains

`GET /api/v1/domains`

List of your registered domains with filtering.

**Query parameters**

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | Page size. Default 100, max 500. |
| `offset` | integer | Pagination offset. |
| `status` | string | Domain status: `active`, `pending`, `expired`. |
| `cloudflare` | string | Filter by Cloudflare status: `true` or `false`. |
| `auto_renew` | string | Filter by auto-renew: `true` or `false`. |
| `search` | string | Search by domain name. |

**curl**
```bash
curl "https://app.piv.day/api/v1/domains?status=active&limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "domains": [
      {
        "id": "dom-uuid-...",
        "domain_name": "mysite.com",
        "status": "active",
        "registered_at": "2026-01-01T00:00:00Z",
        "expires_at": "2027-01-01T00:00:00Z",
        "auto_renew": true,
        "cloudflare_enabled": true,
        "cloudflare_status": "active",
        "ssl_type": "lets_encrypt",
        "ssl_status": "active",
        "ssl_mode": "full",
        "ssl_expires_at": "2026-04-01T00:00:00Z",
        "created_at": "2026-01-01T00:00:00Z",
        "updated_at": "2026-01-01T00:00:00Z"
      }
    ],
    "pagination": { "total": 1, "limit": 20, "offset": 0 }
  }
}
```

### Register a domain

`POST /api/v1/domains`

Register a new domain. The request is async — returns `queue_id` for tracking. Cloudflare ON: don't pass `nameservers`; you may optionally pass `dns_records` and `ssl_mode`. Cloudflare OFF: don't pass `dns_records` or `ssl_mode`; `nameservers` is required (at least 2). We never expose Cloudflare nameservers.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `domain` * | string | Domain name to register, e.g. `mysite.com`. |
| `period` * | integer | Registration period in years (1–10). |
| `cloudflare` * | boolean | Wire the domain up to Cloudflare automatically. |
| `auto_renew` * | boolean | Enable auto-renew. |
| `ssl_mode` | string | `flexible` \| `full` \| `strict`. Only with `cloudflare: true`. |
| `nameservers` | string[] | Required with `cloudflare: false` (≥2 NS). Forbidden with `cloudflare: true`. |
| `dns_records` | object[] | Initial DNS records. Only with `cloudflare: true`. |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `DOMAIN_NOT_AVAILABLE` | 400 | Domain isn't available for registration. |
| `INSUFFICIENT_BALANCE` | 402 | Not enough funds. |
| `VALIDATION_ERROR` | 400 | Invalid request body. |

**Cloudflare ON**
```bash
curl -X POST https://app.piv.day/api/v1/domains \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "mysite.com",
    "period": 1,
    "cloudflare": true,
    "auto_renew": true,
    "ssl_mode": "full",
    "dns_records": [
      { "type": "A", "name": "@", "content": "1.2.3.4", "proxied": true }
    ]
  }'
```

**Cloudflare OFF — own NS**
```bash
curl -X POST https://app.piv.day/api/v1/domains \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "mysite.com",
    "period": 1,
    "cloudflare": false,
    "auto_renew": false,
    "nameservers": ["ns1.myhost.com", "ns2.myhost.com"]
  }'
```

**202 Accepted**
```json
{
  "success": true,
  "data": {
    "queue_id": "que-uuid-...",
    "domain": {
      "id": "dom-uuid-...",
      "domain_name": "mysite.com",
      "status": "purchasing",
      "period": 1,
      "cloudflare_enabled": true,
      "auto_renew": true,
      "ssl_mode": "full",
      "created_at": "2026-01-01T00:00:00Z"
    }
  }
}
```

### Registration queue status

`GET /api/v1/domains/queue/{id}`

Check the status of a registration or renewal queue entry. On `completed`, the response already carries the full domain card — no separate `/domains/{id}` call needed.

**curl**
```bash
curl https://app.piv.day/api/v1/domains/queue/que-uuid-... \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**pending / processing**
```json
{
  "success": true,
  "data": {
    "queue_id": "que-uuid-...",
    "domain": "mysite.com",
    "status": "pending"
  }
}
```

**completed — full card**
```json
{
  "success": true,
  "data": {
    "queue_id": "que-uuid-...",
    "status": "completed",
    "domain": {
      "id": "dom-uuid-...",
      "domain_name": "mysite.com",
      "status": "active",
      "registered_at": "2026-01-01T12:00:00Z",
      "expires_at": "2027-01-01T12:00:00Z",
      "auto_renew": true,
      "cloudflare_enabled": true,
      "cloudflare_status": "active",
      "ssl_type": "lets_encrypt",
      "ssl_status": "active",
      "ssl_mode": "full",
      "ssl_expires_at": "2026-04-01T00:00:00Z",
      "created_at": "2026-01-01T00:00:00Z",
      "updated_at": "2026-01-01T12:00:00Z",
      "dns_records": [
        { "name": "@", "type": "A", "content": "1.2.3.4", "ttl": 1, "priority": null, "proxied": true }
      ]
    }
  }
}
```

### Get one domain

`GET /api/v1/domains/{id}`

Get full info on a registered domain. The `nameservers` field is present only when `cloudflare_enabled: false`.

**curl**
```bash
curl https://app.piv.day/api/v1/domains/dom-uuid-... \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "domain": {
      "id": "dom-uuid-...",
      "domain_name": "mysite.com",
      "status": "active",
      "registered_at": "2026-01-01T12:00:00Z",
      "expires_at": "2027-01-01T12:00:00Z",
      "auto_renew": true,
      "cloudflare_enabled": true,
      "cloudflare_status": "active",
      "ssl_type": "lets_encrypt",
      "ssl_status": "active",
      "ssl_mode": "full",
      "ssl_expires_at": "2026-04-01T00:00:00Z",
      "created_at": "2026-01-01T00:00:00Z",
      "updated_at": "2026-01-01T12:00:00Z",
      "dns_records": [
        { "name": "@", "type": "A", "content": "1.2.3.4", "ttl": 1, "priority": null, "proxied": true },
        { "name": "www", "type": "CNAME", "content": "mysite.com", "ttl": 1, "priority": null, "proxied": true }
      ]
    }
  }
}
```

### Renew domain

`POST /api/v1/domains/{id}/renew`

Renew the domain registration. Returns `queue_id` for tracking.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `period` * | integer | Years to add. |

**curl**
```bash
curl -X POST https://app.piv.day/api/v1/domains/dom-uuid-.../renew \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "period": 1 }'
```

**202 Accepted**
```json
{
  "success": true,
  "data": {
    "queue_id": "que-uuid-...",
    "domain_id": "dom-uuid-...",
    "expires_at": "2027-01-01T12:00:00Z"
  }
}
```

### Auto-renew toggle

`POST /api/v1/domains/{id}/auto-renew`

Turn the domain's auto-renewal on or off.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `auto_renew` * | boolean | New flag value. |

**curl**
```bash
curl -X POST https://app.piv.day/api/v1/domains/dom-uuid-.../auto-renew \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "auto_renew": true }'
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "id": "dom-uuid-...",
    "auto_renew": true
  }
}
```

### DNS records

`GET /api/v1/domains/{id}/dns`

List the domain's DNS records (via Cloudflare).

**curl**
```bash
curl https://app.piv.day/api/v1/domains/dom-uuid-.../dns \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "records": [
      {
        "id": "rec-uuid-...",
        "type": "A",
        "name": "@",
        "content": "1.2.3.4",
        "ttl": 1,
        "priority": null,
        "proxied": true
      }
    ]
  }
}
```

### Add DNS record

`POST /api/v1/domains/{id}/dns`

Add a new DNS record.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `type` * | string | Record type: `A`, `AAAA`, `CNAME`, `MX`, `TXT`, etc. |
| `name` * | string | Record name, e.g. `@` or `subdomain`. |
| `content` * | string | Record value. |
| `ttl` | integer | TTL in seconds (`1` = auto). |
| `proxied` | boolean | Proxy through Cloudflare. |

**curl**
```bash
curl -X POST https://app.piv.day/api/v1/domains/dom-uuid-.../dns \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "A",
    "name": "@",
    "content": "1.2.3.4",
    "ttl": 1,
    "proxied": true
  }'
```

**201 Created**
```json
{
  "success": true,
  "data": {
    "record": {
      "id": "rec-uuid-...",
      "type": "A",
      "name": "@",
      "content": "1.2.3.4",
      "ttl": 1,
      "priority": null,
      "proxied": true
    }
  }
}
```

### Update DNS record

`PUT /api/v1/domains/{id}/dns/{recordId}`

Update an existing DNS record by ID. Only the fields you pass are overwritten.

**curl**
```bash
curl -X PUT https://app.piv.day/api/v1/domains/dom-uuid-.../dns/rec-uuid-... \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "content": "5.6.7.8" }'
```

### Delete DNS record

`DELETE /api/v1/domains/{id}/dns/{recordId}`

Delete a DNS record by ID.

**curl**
```bash
curl -X DELETE https://app.piv.day/api/v1/domains/dom-uuid-.../dns/rec-uuid-... \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Enable Cloudflare

`POST /api/v1/domains/{id}/cloudflare`

Turn on Cloudflare for the domain. We don't return Cloudflare nameservers — delegation is set up on our side. Disabling Cloudflare via this endpoint isn't supported.

**curl**
```bash
curl -X POST https://app.piv.day/api/v1/domains/dom-uuid-.../cloudflare \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true }'
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "domain_id": "dom-uuid-...",
    "ssl_mode": "flexible"
  }
}
```

### Set nameservers

`POST /api/v1/domains/{id}/ns-servers`

Set custom nameservers for the domain.

**curl**
```bash
curl -X POST https://app.piv.day/api/v1/domains/dom-uuid-.../ns-servers \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "nameservers": ["ns1.example.com", "ns2.example.com"] }'
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "nameservers": [
      { "nameserver": "ns1.example.com", "order_index": 1 },
      { "nameserver": "ns2.example.com", "order_index": 2 }
    ]
  }
}
```

## Whites

White-page generation by niche and tier. Each site is unique. Swap domain and contacts without re-generating.

### List tiers

`GET /api/v1/whites/tiers`

Available generation tiers with prices. `price` is the per-generation price already scoped to your account (subscription applied), a single number. `quality: "premium"` costs more (computed at create time). Blog only on `v2_tier3`: first 3 articles included, each extra one (up to 20) charged at `extra_article_price`.

**curl**
```bash
curl https://app.piv.day/api/v1/whites/tiers \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "tiers": [
      {
        "tier_key": "v2_tier1",
        "name": "Landing",
        "generator_version": "v2",
        "min_articles": 0,
        "max_articles": 0,
        "price": 3.00,
        "extra_article_price": null
      },
      {
        "tier_key": "v2_tier2",
        "name": "Multi-page",
        "generator_version": "v2",
        "min_articles": 0,
        "max_articles": 0,
        "price": 6.00,
        "extra_article_price": null
      },
      {
        "tier_key": "v2_tier3",
        "name": "Full + Blog",
        "generator_version": "v2",
        "min_articles": 3,
        "max_articles": 20,
        "price": 10.00,
        "extra_article_price": 1.50
      }
    ]
  }
}
```

### List whites

`GET /api/v1/whites`

List of your generation jobs with filtering.

**Query parameters**

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | Page size. Default 100, max 500. |
| `offset` | integer | Pagination offset. |
| `status` | string | `queued` \| `processing` \| `done` \| `failed`. |
| `tier_key` | string | `v2_tier1` \| `v2_tier2` \| `v2_tier3`. |

**curl**
```bash
curl "https://app.piv.day/api/v1/whites?status=done&limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "whites": [
      {
        "id": "job-uuid-...",
        "name": "My White",
        "status": 10,
        "tier_key": "v2_tier3",
        "quality": "standard",
        "niche": "IT Consulting",
        "country": "DE",
        "language": "de",
        "domain": "mysite.com",
        "result_url": null,
        "created_at": "2026-01-01T00:00:00Z"
      }
    ],
    "pagination": { "total": 1, "limit": 20, "offset": 0 }
  }
}
```

### Start generation

`POST /api/v1/whites`

Queue a white-page generation job. Payment is taken immediately. The final status arrives via the `white.completed` / `white.failed` webhook or you can poll `GET /whites/{id}`.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `name` * | string | Job name (2–20 chars). |
| `tier_key` * | string | `v2_tier1` \| `v2_tier2` \| `v2_tier3`. |
| `niche` * | string | Site niche (up to 25 chars). |
| `country` * | string | Country code (ISO 3166-1 alpha-2). |
| `language` * | string | Primary site language (ISO 639-1). |
| `quality` | string | `standard` \| `premium` (default `standard`). |
| `languages` | string[] | List of languages. First is included; each extra one costs more. |
| `domain` | string | Full domain (e.g. `example.com`). No auto-completion. |
| `email` | string | Full email (e.g. `info@example.com`). No auto-completion. |
| `phone` | string | Contact phone. |
| `address` | string | Company address. |
| `legal_name` | string | Legal name. |
| `style_hint` | string | Design style (Random, Minimal, Corporate, etc.). |
| `keywords` | string[] | SEO keywords. |
| `banned_words` | string[] | Words banned from the content. |
| `blog_count` | integer | Blog article count 3–20. `v2_tier3` only. Articles beyond 3 cost extra. |
| `contacts_mode` | string | `same` \| `template`. |
| `contacts_template` | string | Contact page template (when `contacts_mode=template`). |
| `facebook` | string | Full Facebook page URL. |
| `instagram` | string | Full Instagram profile URL. |
| `linkedin` | string | Full LinkedIn profile URL. |
| `youtube` | string | Full YouTube channel URL. |
| `tiktok` | string | Full TikTok profile URL. |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `INSUFFICIENT_BALANCE` | 402 | Not enough funds. |
| `INVALID_TIER` | 400 | No tier with that key. |
| `VALIDATION_ERROR` | 400 | Invalid request body. |

**curl**
```bash
curl -X POST https://app.piv.day/api/v1/whites \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Tech Blog DE",
    "tier_key": "v2_tier3",
    "niche": "IT Consulting",
    "country": "DE",
    "language": "de",
    "quality": "premium",
    "languages": ["de", "en"],
    "domain": "techblog-de.com",
    "email": "info@techblog-de.com",
    "phone": "+49 30 123456",
    "address": "Berliner Str. 1, 10115 Berlin",
    "legal_name": "TechBlog GmbH",
    "style_hint": "Корпоративный",
    "keywords": ["IT", "consulting", "cloud"],
    "banned_words": ["cheap", "free"],
    "blog_count": 8,
    "contacts_mode": "same",
    "facebook": "https://facebook.com/techblogde",
    "instagram": "https://instagram.com/techblogde",
    "linkedin": "https://linkedin.com/company/techblogde"
  }'
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "status": "queued",
    "white": {
      "id": "job-uuid-...",
      "name": "Tech Blog DE",
      "status": 0,
      "tier_key": "v2_tier3",
      "quality": "premium",
      "niche": "IT Consulting",
      "country": "DE",
      "language": "de",
      "domain": "techblog-de.com",
      "result_url": null,
      "created_at": "2026-01-01T00:00:00Z"
    }
  }
}
```

### Get one white

`GET /api/v1/whites/{id}`

Get the current state of a generation job.

**curl**
```bash
curl https://app.piv.day/api/v1/whites/job-uuid-... \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "white": {
      "id": "job-uuid-...",
      "name": "Tech Blog DE",
      "status": 10,
      "tier_key": "v2_tier3",
      "quality": "premium",
      "niche": "IT Consulting",
      "country": "DE",
      "language": "de",
      "domain": "techblog-de.com",
      "result_url": null,
      "created_at": "2026-01-01T00:00:00Z"
    }
  }
}
```

### Download archive

`GET /api/v1/whites/{id}/download`

Get a short-lived signed link to the finished white-page archive. The link is valid for ~5 minutes (see `expires_at`). If it has expired, just call `/download` again — we'll issue a new one. If the white page is not generated yet, returns `409 NOT_READY`.

**Query parameters**

| Field | Type | Description |
| --- | --- | --- |
| `format` | string | Archive format. Defaults to `php`. |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `NOT_READY` | 409 | White page is still generating — retry after `white.completed`. |

**curl**
```bash
curl "https://app.piv.day/api/v1/whites/job-uuid-.../download?format=php" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**200 OK**
```json
{
  "success": true,
  "data": {
    "url": "https://strg.piv.day/download/<user_id>/<job_id>?format=php&token=<hmac>&filename=<name>.zip",
    "filename": "example.com_2026-05-28_DE_en.zip",
    "format": "php",
    "expires_at": "2026-05-28T14:46:00Z"
  }
}
```

### Swap domain and contacts

`POST /api/v1/whites/{id}/config`

Update the white-page's company contact data without re-generating.

**curl**
```bash
curl -X POST https://app.piv.day/api/v1/whites/job-uuid-.../config \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "company": "My Brand",
    "domain": "newdomain.com",
    "email": "info@newdomain.com",
    "phone": "+49 30 123456",
    "address": "Berliner Str. 1, Berlin",
    "legal": "My Brand GmbH"
  }'
```

## Webhooks

We send a plain POST with a JSON body to your URL. The shape is the same for every event: `event_type`, `timestamp` and a `data` block. Your server should answer with any 2xx within 5 seconds.

```
POST <your webhook URL>
Content-Type: application/json
User-Agent: piv.day-webhook/1.0

{
  "event_type": "<event identifier>",
  "timestamp": "2026-05-22T12:34:56Z",
  "data": { ... }
}
```

**Retries.** On non-2xx or timeout — up to 3 retries with 1s / 2s / 4s backoff. Full delivery history lives on the API keys page in the dashboard. If all three fail the event is marked failed and can be replayed manually.

**Security.** Use an HTTPS endpoint. Every request arrives with the `User-Agent: piv.day-webhook/1.0` header. The webhook URL is configured in the API key profile.

### All events

| Event | When it fires |
| --- | --- |
| `sms.received` | A trusted sender just texted one of your numbers. |
| `sms.status_updated` | An outbound SMS changed state — delivered, failed, etc. |
| `number.restore_completed` | A batch Restore finished — final breakdown lands here. |
| `verify.completed` | Google account verification finished successfully. |
| `verify.failed` | Something went wrong — Google didn't accept it. |
| `proxy.expires_soon` | About three days left on the proxy order. |
| `domain.registered` | Registration finished — the domain is live. |
| `domain.failed` | Order didn't go through — common cause: name was just taken. |
| `domain.expires_soon` | About a week left before the domain expires. |
| `white.completed` | White generation finished successfully. |
| `white.failed` | Job failed — funds are refunded to the balance. |

### Inbound SMS

**`sms.received`**

Most often this is verification codes from ad networks. The payload includes the raw text and an auto-detected code (when present) so you can pass it straight to your workflow without parsing. Messages from known spam senders aren't forwarded.

```json
{
  "event_type": "sms.received",
  "timestamp": "2026-05-22T12:34:56Z",
  "data": {
    "piv_num_id": "vzPA1-kHKSg-EAL7e-Jqd3o",
    "text": "Your verification code is 123456",
    "code": "123456",
    "country": "SE",
    "received_at": "2026-05-22T12:34:56Z"
  }
}
```

### Outbound SMS status changed

**`sms.status_updated`**

Useful when you send confirmations from your own number and need to know they actually landed. If status is failed, the carrier's error code and message are in the payload.

```json
{
  "event_type": "sms.status_updated",
  "timestamp": "2026-05-22T12:34:56Z",
  "data": {
    "piv_num_id": "vzPA1-kHKSg-EAL7e-Jqd3o",
    "message_id": "42",
    "old_status": "sent",
    "new_status": "delivered",
    "error_code": null,
    "error_message": null
  }
}
```

### Number restore completed

**`number.restore_completed`**

Carries two lists — which numbers came back and which didn't. The refunded amount for failed ones is in the payload too. Handy for scripts to retry intelligently.

```json
{
  "event_type": "number.restore_completed",
  "timestamp": "2026-05-22T12:34:56Z",
  "data": {
    "restored": [
      {
        "piv_num_id": "vzPA1-kHKSg-EAL7e-Jqd3o",
        "phone_number": "+46764794425",
        "country": "SE"
      }
    ],
    "failed": [
      {
        "piv_num_id": "abc12-defgh-ijklm-nopqr",
        "phone_number": "+46123456789",
        "country": "SE",
        "refunded": 7.50
      }
    ],
    "total_restored": 1,
    "total_failed": 1,
    "total_refunded": 7.50
  }
}
```

### Google QR verification succeeded

**`verify.completed`**

If an SMS code arrived during the flow, it's already here — no extra fetch needed. captured_phone and captured_message contain what Google sent.

```json
{
  "event_type": "verify.completed",
  "timestamp": "2026-05-22T12:34:56Z",
  "data": {
    "task_id": 12,
    "piv_num_id": "vzPA1-kHKSg-EAL7e-Jqd3o",
    "status": "sms_sent",
    "captured_phone": "+14155551234",
    "captured_message": "Your Google verification code is 123456",
    "sms_message_id": 42,
    "sms_status": "queued"
  }
}
```

### Google QR verification failed

**`verify.failed`**

error_code tells you what specifically — timeout, rejection, invalid URL. Failed verifications are refunded to the balance automatically.

```json
{
  "event_type": "verify.failed",
  "timestamp": "2026-05-22T12:34:56Z",
  "data": {
    "task_id": 12,
    "piv_num_id": "vzPA1-kHKSg-EAL7e-Jqd3o",
    "status": "failed",
    "error_code": "VERIFY_TIMEOUT",
    "error_message": "Verification timed out"
  }
}
```

### Proxy expiring soon

**`proxy.expires_soon`**

Designed for non-auto-renewers: you see it ahead of time and decide — extend or wind down the campaign. Hook it to POST /proxy/orders/{id}/renew for fully automatic renewal.

```json
{
  "event_type": "proxy.expires_soon",
  "timestamp": "2026-05-22T12:34:56Z",
  "data": {
    "order_id": "ord-uuid-...",
    "country": "DE",
    "quantity": 10,
    "expires_at": "2026-05-25T00:00:00Z"
  }
}
```

### Domain registered

**`domain.registered`**

If you asked for Cloudflare and SSL at purchase, both are already up — cloudflare_enabled reflects the status. You can add DNS records or deploy the site right away.

```json
{
  "event_type": "domain.registered",
  "timestamp": "2026-05-22T12:34:56Z",
  "data": {
    "domain_id": "dom-uuid-...",
    "domain_name": "mysite.com",
    "registered_at": "2026-05-22T12:34:56Z",
    "expires_at": "2027-05-22T12:34:56Z",
    "cloudflare_enabled": true
  }
}
```

### Domain registration failed

**`domain.failed`**

The error field has a human-readable reason. The cost is refunded to the balance. Pick another name and try again.

```json
{
  "event_type": "domain.failed",
  "timestamp": "2026-05-22T12:34:56Z",
  "data": {
    "domain_id": "dom-uuid-...",
    "domain_name": "mysite.com",
    "error": "Domain is no longer available"
  }
}
```

### Domain expiring soon

**`domain.expires_soon`**

Useful when auto-renew is off: you have time to renew manually before the domain hits redemption. The auto_renew flag tells you whether intervention is needed.

```json
{
  "event_type": "domain.expires_soon",
  "timestamp": "2026-05-22T12:34:56Z",
  "data": {
    "domain_id": "dom-uuid-...",
    "domain_name": "mysite.com",
    "expires_at": "2026-05-29T00:00:00Z",
    "auto_renew": false
  }
}
```

### White generated

**`white.completed`**

The archive is available via GET /whites/{id}/download. If the white had a domain set, sitemap and contacts are already wired to it — ready to deploy.

```json
{
  "event_type": "white.completed",
  "timestamp": "2026-05-22T12:34:56Z",
  "data": {
    "job_id": "job-uuid-...",
    "name": "My White",
    "tier_key": "t1",
    "country": "DE"
  }
}
```

### White generation failed

**`white.failed`**

The error field explains why. Kick off a new job via POST /whites — no money lost.

```json
{
  "event_type": "white.failed",
  "timestamp": "2026-05-22T12:34:56Z",
  "data": {
    "job_id": "job-uuid-...",
    "name": "My White",
    "error": "Generation failed"
  }
}
```

## Error codes

Every error follows the same shape: HTTP status + an `error.code` field + a human-readable `error.message`.

```json
{
  "success": false,
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Not enough balance: required $5.00, available $1.20"
  }
}
```

| Code | HTTP | When it fires |
| --- | --- | --- |
| `UNAUTHORIZED` | 401 | Missing, expired or revoked key. |
| `INSUFFICIENT_PERMISSIONS` | 403 | The key doesn't have the required scope for this action. |
| `PERMISSION_DENIED` | 403 | Account-level access doesn't allow this action (e.g. feature disabled). |
| `VALIDATION_ERROR` | 400 | Invalid request body. The message explains which field and why. |
| `NUMBER_NOT_FOUND` | 404 | Number doesn't exist or doesn't belong to the account. |
| `NUMBER_EXPIRED` | 403 | Number has expired — use Restore (within 7 days) or buy a new one. |
| `NUMBER_NOT_ACTIVE` | 400 | Number is in a state that doesn't allow this operation. |
| `INSUFFICIENT_BALANCE` | 402 | Not enough funds for this operation. |
| `INVALID_MESSAGE_FORMAT` | 400 | SMS body is too long or contains disallowed characters. |
| `COUNTRY_NOT_AVAILABLE` | 400 | Country isn't available for purchase or renewal. |
| `NO_NUMBERS_AVAILABLE` | 400 | No free numbers in the chosen country right now. |
| `NO_RESTORABLE_NUMBERS` | 404 | None of the supplied numbers can be restored anymore. |
| `RATE_LIMITED` | 429 | Rate limit exceeded. Wait for the time given in `Retry-After`. |
| `INTERNAL_ERROR` | 500 | Something broke on our side. If it persists — ping support. |
