Documentation

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

GET/api/v1/account
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

Documentation

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

FieldTypeDescription
country_codestringTwo-letter ISO country code (e.g. SE).
price_per_monthnumberFinal price for your plan — what actually gets charged on purchase. Premium / Business discounts are already applied.
base_pricenumberPrice without discounts (Free plan). Returned for comparison so you can see what the subscription saves.
can_send_smsbooleanWhether outbound SMS is supported from this number.
can_receive_smsbooleanWhether inbound SMS is supported.
sms_send_pricenumber|nullOutbound SMS price, also with the plan discount applied. null when sending isn't supported from this country.
GET/api/v1/numbers/countries
curl https://app.piv.day/api/v1/numbers/countries \
  -H "Authorization: Bearer YOUR_API_KEY"
200OK
{
  "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

FieldTypeDescription
limitintegerPage size (default 50, max 200).
offsetintegerPagination offset.
countrystringISO country code filter.
statusstringOne of active, expired, pending_restore.
searchstringSubstring search across phone number or custom name.
GET/api/v1/numbers
curl "https://app.piv.day/api/v1/numbers?country=SE&status=active&limit=10" \
  -H "Authorization: Bearer YOUR_API_KEY"
200OK
{
  "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

CodeHTTPWhen it fires
NUMBER_NOT_FOUND404Number doesn't exist or doesn't belong to the account.
GET/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o
curl https://app.piv.day/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o \
  -H "Authorization: Bearer YOUR_API_KEY"
200OK
{
  "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

FieldTypeDescription
country_code*stringISO country code.
duration_months*integerRental period in months (minimum 1).
auto_renew*booleanTurn auto-renew on right after purchase.
custom_namestringOptional human-friendly name for the number.

Errors

CodeHTTPWhen it fires
COUNTRY_NOT_AVAILABLE400Country isn't available or has no pricing.
NO_NUMBERS_AVAILABLE400No free numbers in the requested country right now.
INSUFFICIENT_BALANCE402Not enough funds on the balance.
VALIDATION_ERROR400Invalid fields in the request body.
POST/api/v1/numbers/purchase
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"
  }'
200OK
{
  "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

FieldTypeDescription
duration_months*integerNumber of months to add.

Errors

CodeHTTPWhen it fires
NUMBER_NOT_FOUND404Number doesn't exist or doesn't belong to the account.
COUNTRY_NOT_AVAILABLE400No pricing found for the number's country.
INSUFFICIENT_BALANCE402Not enough funds to renew.
POST/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o/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 }'
200OK
{
  "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

FieldTypeDescription
custom_name*stringNew name for the number.

Errors

CodeHTTPWhen it fires
NUMBER_NOT_FOUND404Number doesn't exist or doesn't belong to the account.
VALIDATION_ERROR400Invalid value in the request body.
PATCH/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o
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" }'
200OK
{
  "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

FieldTypeDescription
auto_renew*booleanNew auto-renew flag value.
PATCH/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o/auto-renewal
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 }'
200OK
{
  "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

FieldTypeDescription
piv_num_ids*string[]Array of number IDs to restore.

Errors

CodeHTTPWhen it fires
NO_RESTORABLE_NUMBERS404None of the supplied numbers can be restored (7-day window passed or they aren't yours).
INSUFFICIENT_BALANCE402Not enough funds to cover the restore.
POST/api/v1/numbers/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"
    ]
  }'
200OK
{
  "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

FieldTypeDescription
limitintegerHow many messages to return (default 50, max 200).
offsetintegerPagination offset.
GET/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o/sms
curl "https://app.piv.day/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o/sms?limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
200OK
{
  "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

FieldTypeDescription
to_number*stringRecipient number in international format (E.164).
message_body*stringMessage text. GSM-7 and UCS-2 are supported.

Errors

CodeHTTPWhen it fires
NUMBER_NOT_FOUND404Number doesn't exist or doesn't belong to the account.
NUMBER_EXPIRED403Number has already expired.
NUMBER_NOT_ACTIVE400Number is in a state that doesn't allow sending.
INSUFFICIENT_BALANCE402Not enough funds to send.
INVALID_MESSAGE_FORMAT400Message body exceeds the allowed length or contains forbidden characters.
POST/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o/sms/send
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"
  }'
200OK
{
  "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

FieldTypeDescription
gv_url*stringFull Google verification URL (grabbed from Google's page).

Errors

CodeHTTPWhen it fires
NUMBER_NOT_FOUND404Number doesn't exist or isn't yours.
NUMBER_NOT_ACTIVE400Number isn't active.
SMS_DISABLED400SMS sending is disabled for this number.
INSUFFICIENT_BALANCE402Not enough funds for the verification.
PROXY_DEAD502Proxy unavailable — balance refunded.
QUEUE_FULL503Queue is full, try later — balance refunded.
SERVER_UNAVAILABLE503Automation server unavailable — balance refunded.
POST/api/v1/numbers/vzPA1-kHKSg-EAL7e-Jqd3o/verify
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/..." }'
202Accepted
{
  "success": true,
  "message": "Verification initiated"
}
Documentation

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.

GET/api/v1/proxy/servers
curl https://app.piv.day/api/v1/proxy/servers \
  -H "Authorization: Bearer YOUR_API_KEY"
200OK
{
  "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

FieldTypeDescription
limitintegerPage size. Default 100, max 500.
offsetintegerPagination offset.
statusstringStatus filter: active, expired.
searchstringSearch across orders.
GET/api/v1/proxy/orders
curl "https://app.piv.day/api/v1/proxy/orders?status=active&limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
200OK
{
  "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.

GET/api/v1/proxy/orders/ord-uuid-...
curl https://app.piv.day/api/v1/proxy/orders/ord-uuid-... \
  -H "Authorization: Bearer YOUR_API_KEY"
200OK
{
  "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

FieldTypeDescription
server_id*stringServer ID from GET /proxy/servers.
quantity*integerHow many proxies to buy (1–500).
months*integerRental period in months (1–12).

Errors

CodeHTTPWhen it fires
INSUFFICIENT_BALANCE402Not enough funds.
NOT_FOUND404Proxy server not found or inactive.
VALIDATION_ERROR400Invalid request body.
POST/api/v1/proxy/purchase
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
  }'
200OK
{
  "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

FieldTypeDescription
months*integerMonths to add.
POST/api/v1/proxy/orders/ord-uuid-.../renew
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 }'
200OK
{
  "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.

POST/api/v1/proxy/orders/ord-uuid-.../restore
curl -X POST https://app.piv.day/api/v1/proxy/orders/ord-uuid-.../restore \
  -H "Authorization: Bearer YOUR_API_KEY"
200OK
{
  "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

FieldTypeDescription
formatstringExport format: socks5, http or both (default socks5).
GET/api/v1/proxy/orders/ord-uuid-.../export
curl "https://app.piv.day/api/v1/proxy/orders/ord-uuid-.../export?format=socks5" \
  -H "Authorization: Bearer YOUR_API_KEY"
200OK
{
  "success": true,
  "data": {
    "content": "user1:[email protected]:1080\nuser2:[email protected]:1080",
    "filename": "piv-day-proxy-ord-uuid--socks5.txt",
    "format": "socks5",
    "count": 2
  }
}
Documentation

Domains

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

List domains

GET/api/v1/domains

List of your registered domains with filtering.

Query parameters

FieldTypeDescription
limitintegerPage size. Default 100, max 500.
offsetintegerPagination offset.
statusstringDomain status: active, pending, expired.
cloudflarestringFilter by Cloudflare status: true or false.
auto_renewstringFilter by auto-renew: true or false.
searchstringSearch by domain name.
GET/api/v1/domains
curl "https://app.piv.day/api/v1/domains?status=active&limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
200OK
{
  "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

FieldTypeDescription
domain*stringDomain name to register, e.g. mysite.com.
period*integerRegistration period in years (1–10).
cloudflare*booleanWire the domain up to Cloudflare automatically.
auto_renew*booleanEnable auto-renew.
ssl_modestringflexible | full | strict. Only with cloudflare: true.
nameserversstring[]Required with cloudflare: false (≥2 NS). Forbidden with cloudflare: true.
dns_recordsobject[]Initial DNS records. Only with cloudflare: true.

Errors

CodeHTTPWhen it fires
DOMAIN_NOT_AVAILABLE400Domain isn't available for registration.
INSUFFICIENT_BALANCE402Not enough funds.
VALIDATION_ERROR400Invalid request body.
POST/api/v1/domains
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 }
    ]
  }'
POST/api/v1/domains
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"]
  }'
202Accepted
{
  "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.

GET/api/v1/domains/queue/que-uuid-...
curl https://app.piv.day/api/v1/domains/queue/que-uuid-... \
  -H "Authorization: Bearer YOUR_API_KEY"
pending / processing
{
  "success": true,
  "data": {
    "queue_id": "que-uuid-...",
    "domain": "mysite.com",
    "status": "pending"
  }
}
completed — full card
{
  "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.

GET/api/v1/domains/dom-uuid-...
curl https://app.piv.day/api/v1/domains/dom-uuid-... \
  -H "Authorization: Bearer YOUR_API_KEY"
200OK
{
  "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

FieldTypeDescription
period*integerYears to add.
POST/api/v1/domains/dom-uuid-.../renew
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 }'
202Accepted
{
  "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

FieldTypeDescription
auto_renew*booleanNew flag value.
POST/api/v1/domains/dom-uuid-.../auto-renew
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 }'
200OK
{
  "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).

GET/api/v1/domains/dom-uuid-.../dns
curl https://app.piv.day/api/v1/domains/dom-uuid-.../dns \
  -H "Authorization: Bearer YOUR_API_KEY"
200OK
{
  "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

FieldTypeDescription
type*stringRecord type: A, AAAA, CNAME, MX, TXT, etc.
name*stringRecord name, e.g. @ or subdomain.
content*stringRecord value.
ttlintegerTTL in seconds (1 = auto).
proxiedbooleanProxy through Cloudflare.
POST/api/v1/domains/dom-uuid-.../dns
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
  }'
201Created
{
  "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.

PUT/api/v1/domains/dom-uuid-.../dns/rec-uuid-...
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.

DELETE/api/v1/domains/dom-uuid-.../dns/rec-uuid-...
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.

POST/api/v1/domains/dom-uuid-.../cloudflare
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 }'
200OK
{
  "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.

POST/api/v1/domains/dom-uuid-.../ns-servers
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"] }'
200OK
{
  "success": true,
  "data": {
    "nameservers": [
      { "nameserver": "ns1.example.com", "order_index": 1 },
      { "nameserver": "ns2.example.com", "order_index": 2 }
    ]
  }
}
Documentation

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.

GET/api/v1/whites/tiers
curl https://app.piv.day/api/v1/whites/tiers \
  -H "Authorization: Bearer YOUR_API_KEY"
200OK
{
  "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

FieldTypeDescription
limitintegerPage size. Default 100, max 500.
offsetintegerPagination offset.
statusstringqueued | processing | done | failed.
tier_keystringv2_tier1 | v2_tier2 | v2_tier3.
GET/api/v1/whites
curl "https://app.piv.day/api/v1/whites?status=done&limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
200OK
{
  "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

FieldTypeDescription
name*stringJob name (2–20 chars).
tier_key*stringv2_tier1 | v2_tier2 | v2_tier3.
niche*stringSite niche (up to 25 chars).
country*stringCountry code (ISO 3166-1 alpha-2).
language*stringPrimary site language (ISO 639-1).
qualitystringstandard | premium (default standard).
languagesstring[]List of languages. First is included; each extra one costs more.
domainstringFull domain (e.g. example.com). No auto-completion.
emailstringFull email (e.g. [email protected]). No auto-completion.
phonestringContact phone.
addressstringCompany address.
legal_namestringLegal name.
style_hintstringDesign style (Random, Minimal, Corporate, etc.).
keywordsstring[]SEO keywords.
banned_wordsstring[]Words banned from the content.
blog_countintegerBlog article count 3–20. v2_tier3 only. Articles beyond 3 cost extra.
contacts_modestringsame | template.
contacts_templatestringContact page template (when contacts_mode=template).
facebookstringFull Facebook page URL.
instagramstringFull Instagram profile URL.
linkedinstringFull LinkedIn profile URL.
youtubestringFull YouTube channel URL.
tiktokstringFull TikTok profile URL.

Errors

CodeHTTPWhen it fires
INSUFFICIENT_BALANCE402Not enough funds.
INVALID_TIER400No tier with that key.
VALIDATION_ERROR400Invalid request body.
POST/api/v1/whites
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"
  }'
200OK
{
  "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.

GET/api/v1/whites/job-uuid-...
curl https://app.piv.day/api/v1/whites/job-uuid-... \
  -H "Authorization: Bearer YOUR_API_KEY"
200OK
{
  "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

FieldTypeDescription
formatstringArchive format. Defaults to php.

Errors

CodeHTTPWhen it fires
NOT_READY409White page is still generating — retry after white.completed.
GET/api/v1/whites/job-uuid-.../download
curl "https://app.piv.day/api/v1/whites/job-uuid-.../download?format=php" \
  -H "Authorization: Bearer YOUR_API_KEY"
200OK
{
  "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.

POST/api/v1/whites/job-uuid-.../config
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"
  }'
Documentation

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.

request format
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

EventWhen it fires
sms.receivedA trusted sender just texted one of your numbers.
sms.status_updatedAn outbound SMS changed state — delivered, failed, etc.
number.restore_completedA batch Restore finished — final breakdown lands here.
verify.completedGoogle account verification finished successfully.
verify.failedSomething went wrong — Google didn't accept it.
proxy.expires_soonAbout three days left on the proxy order.
domain.registeredRegistration finished — the domain is live.
domain.failedOrder didn't go through — common cause: name was just taken.
domain.expires_soonAbout a week left before the domain expires.
white.completedWhite generation finished successfully.
white.failedJob 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.

payload
{
  "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.

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_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.

payload
{
  "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.

payload
{
  "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.

payload
{
  "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.

payload
{
  "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.

payload
{
  "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.

payload
{
  "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.

payload
{
  "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.

payload
{
  "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.

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

Error codes

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

CodeHTTPWhen it fires
UNAUTHORIZED401Missing, expired or revoked key.
INSUFFICIENT_PERMISSIONS403The key doesn't have the required scope for this action.
PERMISSION_DENIED403Account-level access doesn't allow this action (e.g. feature disabled).
VALIDATION_ERROR400Invalid request body. The message explains which field and why.
NUMBER_NOT_FOUND404Number doesn't exist or doesn't belong to the account.
NUMBER_EXPIRED403Number has expired — use Restore (within 7 days) or buy a new one.
NUMBER_NOT_ACTIVE400Number is in a state that doesn't allow this operation.
INSUFFICIENT_BALANCE402Not enough funds for this operation.
INVALID_MESSAGE_FORMAT400SMS body is too long or contains disallowed characters.
COUNTRY_NOT_AVAILABLE400Country isn't available for purchase or renewal.
NO_NUMBERS_AVAILABLE400No free numbers in the chosen country right now.
NO_RESTORABLE_NUMBERS404None of the supplied numbers can be restored anymore.
RATE_LIMITED429Rate limit exceeded. Wait for the time given in Retry-After.
INTERNAL_ERROR500Something broke on our side. If it persists — ping support.
Error example
{
  "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
piv.day API reference — numbers, domains, proxies, whites