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/v1Every endpoint lives under this prefix. One environment — no separate staging hosts.
Authentication
Authorization: Bearer YOUR_API_KEYKeys 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
curl https://app.piv.day/api/v1/account \
-H "Authorization: Bearer YOUR_API_KEY"{
"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
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
/api/v1/numbers/countriesReturns 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 https://app.piv.day/api/v1/numbers/countries \
-H "Authorization: Bearer YOUR_API_KEY"{
"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
/api/v1/numbersReturns 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 "https://app.piv.day/api/v1/numbers?country=SE&status=active&limit=10" \
-H "Authorization: Bearer YOUR_API_KEY"{
"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
/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 https://app.piv.day/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o \
-H "Authorization: Bearer YOUR_API_KEY"{
"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
/api/v1/numbers/purchaseBuys 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 -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"
}'{
"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
/api/v1/numbers/{piv_num_id}/renewExtends 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 -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 }'{
"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
/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 -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" }'{
"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
/api/v1/numbers/{piv_num_id}/auto-renewalTurns 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 -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 }'{
"success": true,
"data": {
"piv_num_id": "vzPA1-kHKSg-EAL7e-Jqd3o",
"phone_number": "+46764794425",
"auto_renew": true
}
}Restore expired numbers
/api/v1/numbers/restoreReclaims 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 -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"
]
}'{
"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
/api/v1/numbers/{piv_num_id}/smsReturns 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 "https://app.piv.day/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o/sms?limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"{
"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
/api/v1/numbers/{piv_num_id}/sms/sendAvailable 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 -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"
}'{
"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
/api/v1/numbers/{piv_num_id}/verifyStarts 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 -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/..." }'{
"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
/api/v1/proxy/serversAvailable proxy servers with prices.
curl https://app.piv.day/api/v1/proxy/servers \
-H "Authorization: Bearer YOUR_API_KEY"{
"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
/api/v1/proxy/ordersList 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 "https://app.piv.day/api/v1/proxy/orders?status=active&limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"{
"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
/api/v1/proxy/orders/{id}Get a single order's details, including the list of proxy credentials.
curl https://app.piv.day/api/v1/proxy/orders/ord-uuid-... \
-H "Authorization: Bearer YOUR_API_KEY"{
"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
/api/v1/proxy/purchaseBuy 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 -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
}'{
"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
/api/v1/proxy/orders/{id}/renewExtend an active proxy order by a number of months.
Request body
| Field | Type | Description |
|---|---|---|
months* | integer | Months to add. |
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 }'{
"success": true,
"data": {
"order_id": "ord-uuid-...",
"cost": 5.00,
"new_expires_at": "2026-03-01T00:00:00Z"
}
}Restore expired order
/api/v1/proxy/orders/{id}/restoreBring 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 -X POST https://app.piv.day/api/v1/proxy/orders/ord-uuid-.../restore \
-H "Authorization: Bearer YOUR_API_KEY"{
"success": true,
"data": {
"order_id": "ord-uuid-...",
"quantity": 10,
"total_cost": 5.00,
"expires_at": "2026-03-01T00:00:00Z"
}
}Export proxies
/api/v1/proxy/orders/{id}/exportExport 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 "https://app.piv.day/api/v1/proxy/orders/ord-uuid-.../export?format=socks5" \
-H "Authorization: Bearer YOUR_API_KEY"{
"success": true,
"data": {
"content": "user1:[email protected]:1080\nuser2:[email protected]: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
/api/v1/domains/searchCheck 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 "https://app.piv.day/api/v1/domains/search?q=mysite.com" \
-H "Authorization: Bearer YOUR_API_KEY"{
"success": true,
"data": {
"domain": "mysite.com",
"available": true,
"create_price": 12.50,
"renew_price": 12.50,
"currency": "USD"
}
}List domains
/api/v1/domainsList 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 "https://app.piv.day/api/v1/domains?status=active&limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"{
"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
/api/v1/domainsRegister 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. |
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 }
]
}'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"]
}'{
"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
/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 https://app.piv.day/api/v1/domains/queue/que-uuid-... \
-H "Authorization: Bearer YOUR_API_KEY"{
"success": true,
"data": {
"queue_id": "que-uuid-...",
"domain": "mysite.com",
"status": "pending"
}
}{
"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
/api/v1/domains/{id}Get full info on a registered domain. The nameservers field is present only when cloudflare_enabled: false.
curl https://app.piv.day/api/v1/domains/dom-uuid-... \
-H "Authorization: Bearer YOUR_API_KEY"{
"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
/api/v1/domains/{id}/renewRenew the domain registration. Returns queue_id for tracking.
Request body
| Field | Type | Description |
|---|---|---|
period* | integer | Years to add. |
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 }'{
"success": true,
"data": {
"queue_id": "que-uuid-...",
"domain_id": "dom-uuid-...",
"expires_at": "2027-01-01T12:00:00Z"
}
}Auto-renew toggle
/api/v1/domains/{id}/auto-renewTurn the domain's auto-renewal on or off.
Request body
| Field | Type | Description |
|---|---|---|
auto_renew* | boolean | New flag value. |
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 }'{
"success": true,
"data": {
"id": "dom-uuid-...",
"auto_renew": true
}
}DNS records
/api/v1/domains/{id}/dnsList the domain's DNS records (via Cloudflare).
curl https://app.piv.day/api/v1/domains/dom-uuid-.../dns \
-H "Authorization: Bearer YOUR_API_KEY"{
"success": true,
"data": {
"records": [
{
"id": "rec-uuid-...",
"type": "A",
"name": "@",
"content": "1.2.3.4",
"ttl": 1,
"priority": null,
"proxied": true
}
]
}
}Add DNS record
/api/v1/domains/{id}/dnsAdd 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 -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
}'{
"success": true,
"data": {
"record": {
"id": "rec-uuid-...",
"type": "A",
"name": "@",
"content": "1.2.3.4",
"ttl": 1,
"priority": null,
"proxied": true
}
}
}Update DNS record
/api/v1/domains/{id}/dns/{recordId}Update an existing DNS record by ID. Only the fields you pass are overwritten.
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
/api/v1/domains/{id}/dns/{recordId}Delete a DNS record by ID.
curl -X DELETE https://app.piv.day/api/v1/domains/dom-uuid-.../dns/rec-uuid-... \
-H "Authorization: Bearer YOUR_API_KEY"Enable Cloudflare
/api/v1/domains/{id}/cloudflareTurn 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 -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 }'{
"success": true,
"data": {
"domain_id": "dom-uuid-...",
"ssl_mode": "flexible"
}
}Set nameservers
/api/v1/domains/{id}/ns-serversSet custom nameservers for the domain.
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"] }'{
"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
/api/v1/whites/tiersAvailable 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 https://app.piv.day/api/v1/whites/tiers \
-H "Authorization: Bearer YOUR_API_KEY"{
"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
/api/v1/whitesList 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 "https://app.piv.day/api/v1/whites?status=done&limit=20" \
-H "Authorization: Bearer YOUR_API_KEY"{
"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
/api/v1/whitesQueue 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. [email protected]). 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 -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": "[email protected]",
"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"
}'{
"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
/api/v1/whites/{id}Get the current state of a generation job.
curl https://app.piv.day/api/v1/whites/job-uuid-... \
-H "Authorization: Bearer YOUR_API_KEY"{
"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
/api/v1/whites/{id}/downloadGet 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 "https://app.piv.day/api/v1/whites/job-uuid-.../download?format=php" \
-H "Authorization: Bearer YOUR_API_KEY"{
"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
/api/v1/whites/{id}/configUpdate the white-page's company contact data without re-generating.
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": "[email protected]",
"phone": "+49 30 123456",
"address": "Berliner Str. 1, Berlin",
"legal": "My Brand GmbH"
}'Webhooks
Subscribe to platform events: inbound SMS, finished sites, expiring orders. The notification URL is set in the API key profile.
How they arrive
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.
When delivery fails
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.
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": { ... }
}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.receivedMost 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.
{
"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_updatedUseful 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.
{
"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_completedCarries 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.
{
"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.completedIf an SMS code arrived during the flow, it's already here — no extra fetch needed. captured_phone and captured_message contain what Google sent.
{
"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.failederror_code tells you what specifically — timeout, rejection, invalid URL. Failed verifications are refunded to the balance automatically.
{
"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_soonDesigned 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.
{
"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.registeredIf 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.
{
"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.failedThe error field has a human-readable reason. The cost is refunded to the balance. Pick another name and try again.
{
"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_soonUseful 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.
{
"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.completedThe 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.
{
"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.failedThe error field explains why. Kick off a new job via POST /whites — no money lost.
{
"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.
| 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. |
{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Not enough balance: required $5.00, available $1.20"
}
}Spotted a gap or need an endpoint that isn't here? Support replies on Telegram within an hour during business hours.
Contact support