# 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//?format=php&token=&filename=.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 Content-Type: application/json User-Agent: piv.day-webhook/1.0 { "event_type": "", "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. |