# 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 en el plan Business. Tokens Bearer, JSON, endpoints idempotentes, códigos de error predecibles. Cada endpoint incluye un ejemplo de solicitud y respuesta.

### Base URL

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

Todos los endpoints se encuentran bajo este prefijo. Un solo entorno — no hay hosts de staging separados.

### Autenticación

```
Authorization: Bearer YOUR_API_KEY
```

Las claves se crean en el dashboard en `Configuración → API Keys`. Cada clave tiene permisos granulares (leer números, enviar SMS, comprar proxies, etc.). Un token comprometido se rota con un clic — sin necesidad de volver a registrar al equipo.

### Cuenta

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

**Un endpoint utilitario único — balance y email de la cuenta actual. Útil para confirmar que la clave funciona y la autenticación está configurada correctamente.**
```
{
  "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

### Formato de respuesta

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

Todas las respuestas son JSON. En caso de error, `success` es `false` y el cuerpo contiene `error.code` y `error.message`.

### Rate limits

**100 solicitudes por minuto por API key. El límite es compartido entre todos los endpoints. ¿Necesita más margen para un pico de tráfico? Contacte al soporte de Telegram y aumentaremos el límite.

Al superar el límite recibirá `429 RATE_LIMITED` y el encabezado `Retry-After` — cuántos segundos esperar antes de la próxima solicitud.**
```
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

## Números

Compra, renueva y restaura números. Recibe y envía SMS. Inicia verificaciones Google QR. Suscríbete a eventos mediante webhooks.

### Países y precios

`GET /api/v1/numbers/countries`

Devuelve los países disponibles para compra con el precio actual y capacidades SMS. Todos los precios incluyen el descuento de tu suscripción.

**Response fields**

| Field | Type | Description |
| --- | --- | --- |
| `country_code` | string | Código de país ISO de dos letras (p. ej. `SE`). |
| `price_per_month` | number | Precio final para tu plan — lo que se cobra realmente al comprar. Los descuentos de Premium / Business ya están aplicados. |
| `base_price` | number | Precio sin descuentos (plan Free). Se devuelve para comparación, para ver cuánto ahorra la suscripción. |
| `can_send_sms` | boolean | Si el envío de SMS salientes es compatible desde este número. |
| `can_receive_sms` | boolean | Si los SMS entrantes son compatibles. |
| `sms_send_price` | number\|null | Precio por SMS saliente, también con el descuento del plan aplicado. `null` cuando el envío no es compatible desde este país. |

**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
    }
  ]
}
```

### Listar números

`GET /api/v1/numbers`

Devuelve una página de números de cuenta con filtros por país, estado y búsqueda por número o nombre personalizado.

**Query parameters**

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | Tamaño de página (predeterminado 50, máximo 200). |
| `offset` | integer | Desplazamiento de paginación. |
| `country` | string | Filtro de código de país ISO. |
| `status` | string | Uno de `active`, `expired`, `pending_restore`. |
| `search` | string | Búsqueda de subcadena por número de teléfono o nombre personalizado. |

**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 }
  }
}
```

### Obtener un número por ID

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

Registro completo del número: estado, fechas, renovación automática, etiquetas, direcciones SMS permitidas.

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `NUMBER_NOT_FOUND` | 404 | El número no existe o no pertenece a la cuenta. |

**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
  }
}
```

### Comprar un número

`POST /api/v1/numbers/purchase`

Compra un número en el país seleccionado por el período indicado. El costo se debita del saldo de la cuenta. Devuelve `piv_num_id` que se utiliza en todas las operaciones posteriores del número.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `country_code` * | string | Código de país ISO. |
| `duration_months` * | integer | Período de alquiler en meses (mínimo 1). |
| `auto_renew` * | boolean | Activar la renovación automática justo después de la compra. |
| `custom_name` | string | Nombre opcional legible para el número. |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `COUNTRY_NOT_AVAILABLE` | 400 | El país no está disponible o no tiene precios. |
| `NO_NUMBERS_AVAILABLE` | 400 | No hay números disponibles en el país solicitado en este momento. |
| `INSUFFICIENT_BALANCE` | 402 | Fondos insuficientes en el saldo. |
| `VALIDATION_ERROR` | 400 | Campos inválidos en el cuerpo de la solicitud. |

**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"
    }
  ]
}
```

### Renovar un número

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

Extiende un número activo por el período indicado. El costo se debita en el momento de la llamada.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `duration_months` * | integer | Número de meses a agregar. |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `NUMBER_NOT_FOUND` | 404 | El número no existe o no pertenece a la cuenta. |
| `COUNTRY_NOT_AVAILABLE` | 400 | No se encontraron precios para el país del número. |
| `INSUFFICIENT_BALANCE` | 402 | Fondos insuficientes para renovar. |

**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
  }
}
```

### Actualizar un número

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

Actualmente solo se puede actualizar el nombre personalizado.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `custom_name` * | string | Nuevo nombre para el número. |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `NUMBER_NOT_FOUND` | 404 | El número no existe o no pertenece a la cuenta. |
| `VALIDATION_ERROR` | 400 | Valor inválido en el cuerpo de la solicitud. |

**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": []
  }
}
```

### Gestión de renovación automática

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

Activa o desactiva la renovación automática del número. Cuando está habilitada, la renovación se debita automáticamente 24 horas antes del vencimiento. Si el saldo es insuficiente, el número expira; puedes reclamarlo mediante `POST /numbers/restore` dentro de 7 días.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `auto_renew` * | boolean | Nuevo valor del indicador de renovación automática. |

**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
  }
}
```

### Restaurar números vencidos

`POST /api/v1/numbers/restore`

Recupera números vencidos dentro de un período de 7 días. Pasa un array de `piv_num_id` — los números regresan con el historial SMS y la configuración conservados. La restauración cuesta más que comprar un número nuevo (incluye un multiplicador de restauración) — las cifras exactas se muestran en el dashboard antes de confirmar. El estado final de cada número llega mediante el webhook `number.restore_completed`; los reembolsos por números fallidos se acreditan automáticamente.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `piv_num_ids` * | string[] | Array de IDs de números a restaurar. |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `NO_RESTORABLE_NUMBERS` | 404 | Ninguno de los números proporcionados puede restaurarse (el período de 7 días venció o no son tuyos). |
| `INSUFFICIENT_BALANCE` | 402 | Fondos insuficientes para cubrir la restauración. |

**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"
    }
  ]
}
```

### Historial SMS del número

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

Devuelve los mensajes entrantes y salientes del número en orden cronológico inverso.

**Query parameters**

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | Cuántos mensajes devolver (predeterminado 50, máximo 200). |
| `offset` | integer | Desplazamiento de paginación. |

**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 }
  }
}
```

### Enviar SMS

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

Disponible para países donde el envío saliente está permitido (p. ej. CA, GB, SE). El costo del mensaje se debita en el momento del envío.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `to_number` * | string | Número del destinatario en formato internacional (E.164). |
| `message_body` * | string | Texto del mensaje. Se admiten GSM-7 y UCS-2. |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `NUMBER_NOT_FOUND` | 404 | El número no existe o no pertenece a la cuenta. |
| `NUMBER_EXPIRED` | 403 | El número ya ha vencido. |
| `NUMBER_NOT_ACTIVE` | 400 | El número está en un estado que no permite el envío. |
| `INSUFFICIENT_BALANCE` | 402 | Fondos insuficientes para enviar. |
| `INVALID_MESSAGE_FORMAT` | 400 | El cuerpo del mensaje supera la longitud permitida o contiene caracteres prohibidos. |

**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"
  }
}
```

### Iniciar verificación Google QR

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

Inicia una verificación Google QR usando la URL proporcionada. El resultado — éxito o fallo — llega mediante el webhook verify.completed o verify.failed. El costo se debita cuando la tarea se pone en cola.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `gv_url` * | string | URL completa de verificación de Google (obtenida de la página de Google). |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `NUMBER_NOT_FOUND` | 404 | El número no existe o no es tuyo. |
| `NUMBER_NOT_ACTIVE` | 400 | El número no está activo. |
| `SMS_DISABLED` | 400 | El envío de SMS está desactivado para este número. |
| `INSUFFICIENT_BALANCE` | 402 | Fondos insuficientes para la verificación. |
| `PROXY_DEAD` | 502 | Proxy no disponible — saldo reembolsado. |
| `QUEUE_FULL` | 503 | La cola está llena, inténtelo más tarde — saldo reembolsado. |
| `SERVER_UNAVAILABLE` | 503 | Servidor de automatización no disponible — saldo reembolsado. |

**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

IPv6 residencial en docenas de países. Compra masiva, renovación, Restore que mantiene el mismo login y contraseña, exportación en el formato que necesitas.

### Lista de servidores

`GET /api/v1/proxy/servers`

Servidores proxy disponibles con precios.

**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
      }
    ]
  }
}
```

### Lista de pedidos

`GET /api/v1/proxy/orders`

Lista de tus pedidos de proxy con filtros por estado o búsqueda.

**Query parameters**

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | Tamaño de página. Por defecto 100, máximo 500. |
| `offset` | integer | Desplazamiento de paginación. |
| `status` | string | Filtro por estado: `active`, `expired`. |
| `search` | string | Búsqueda en pedidos. |

**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 }
  }
}
```

### Obtener un pedido

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

Obtener los detalles de un pedido específico, incluida la lista de credenciales de proxy.

**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"
        }
      ]
    }
  }
}
```

### Comprar proxies

`POST /api/v1/proxy/purchase`

Comprar proxies en el servidor elegido. Una solicitud crea un pedido con N proxies (hasta 500). No existen pedidos masivos: llama al método nuevamente si necesitas más.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `server_id` * | string | ID del servidor de `GET /proxy/servers`. |
| `quantity` * | integer | Cuántos proxies comprar (1–500). |
| `months` * | integer | Período de arrendamiento en meses (1–12). |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `INSUFFICIENT_BALANCE` | 402 | Fondos insuficientes. |
| `NOT_FOUND` | 404 | Servidor proxy no encontrado o inactivo. |
| `VALIDATION_ERROR` | 400 | Cuerpo de solicitud inválido. |

**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"
        }
      ]
    }
  }
}
```

### Renovar un pedido

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

Extender un pedido de proxy activo por un número de meses.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `months` * | integer | Meses a añadir. |

**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 de orden vencida

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

Recupera una orden de proxy vencida sin recomprarla. Login, contraseña, direcciones IPv6, país y vínculos — todo igual. El plazo se extiende 1 mes. No es necesario reconfigurar el antidetect.

**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"
  }
}
```

### Exportar proxies

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

Exportar las credenciales de proxy del pedido como una lista de texto (login:password@host:port).

**Query parameters**

| Field | Type | Description |
| --- | --- | --- |
| `format` | string | Formato de exportación: `socks5`, `http` o `both` (por defecto `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
  }
}
```

## Dominios

Búsqueda y registro sin KYC, Cloudflare y SSL automáticos, gestión de registros DNS y servidores NS a través de la API.

### Verificación de disponibilidad

`GET /api/v1/domains/search`

Verificar si un dominio está disponible y cuánto cuesta registrarlo.

**Query parameters**

| Field | Type | Description |
| --- | --- | --- |
| `q` * | string | Nombre de dominio a buscar, p. ej. `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"
  }
}
```

### Listar dominios

`GET /api/v1/domains`

Lista de sus dominios registrados con posibilidad de filtrado.

**Query parameters**

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | Tamaño de página. Por defecto 100, máximo 500. |
| `offset` | integer | Desplazamiento para paginación. |
| `status` | string | Estado del dominio: `active`, `pending`, `expired`. |
| `cloudflare` | string | Filtrar por estado de Cloudflare: `true` o `false`. |
| `auto_renew` | string | Filtrar por renovación automática: `true` o `false`. |
| `search` | string | Buscar por nombre de dominio. |

**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 }
  }
}
```

### Registrar un dominio

`POST /api/v1/domains`

Registrar un nuevo dominio. La solicitud es asíncrona — devuelve `queue_id` para seguimiento. Cloudflare ACTIVADO: no pasar `nameservers`; opcionalmente puede pasar `dns_records` y `ssl_mode`. Cloudflare DESACTIVADO: no pasar `dns_records` ni `ssl_mode`; `nameservers` es obligatorio (mínimo 2). Nunca exponemos los nameservers de Cloudflare.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `domain` * | string | Nombre de dominio a registrar, p. ej. `mysite.com`. |
| `period` * | integer | Período de registro en años (1–10). |
| `cloudflare` * | boolean | Conectar el dominio a Cloudflare automáticamente. |
| `auto_renew` * | boolean | Habilitar renovación automática. |
| `ssl_mode` | string | `flexible` \| `full` \| `strict`. Solo con `cloudflare: true`. |
| `nameservers` | string[] | Obligatorio con `cloudflare: false` (≥2 NS). Prohibido con `cloudflare: true`. |
| `dns_records` | object[] | Registros DNS iniciales. Solo con `cloudflare: true`. |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `DOMAIN_NOT_AVAILABLE` | 400 | El dominio no está disponible para registro. |
| `INSUFFICIENT_BALANCE` | 402 | Fondos insuficientes. |
| `VALIDATION_ERROR` | 400 | Cuerpo de la solicitud no válido. |

**Cloudflare ACTIVADO**
```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 DESACTIVADO — NS propios**
```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"
    }
  }
}
```

### Estado de la tarea de registro

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

Verificar el estado de una entrada en la cola de registro o renovación de dominio. Con el estado `completed`, la respuesta ya contiene la ficha completa del dominio — no es necesaria una llamada separada a `/domains/{id}`.

**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 — ficha completa**
```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 }
      ]
    }
  }
}
```

### Obtener un dominio

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

Obtener información completa sobre un dominio registrado. El campo `nameservers` solo está presente cuando `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 }
      ]
    }
  }
}
```

### Renovar dominio

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

Renovar el registro del dominio. Devuelve `queue_id` para seguimiento.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `period` * | integer | Años a agregar. |

**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"
  }
}
```

### Toggle de renovación automática

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

Activar o desactivar la renovación automática del dominio.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `auto_renew` * | boolean | Nuevo valor del indicador. |

**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
  }
}
```

### Registros DNS del dominio

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

Listar los registros DNS del dominio (a través de 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
      }
    ]
  }
}
```

### Agregar registro DNS

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

Crear un nuevo registro DNS.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `type` * | string | Tipo de registro: `A`, `AAAA`, `CNAME`, `MX`, `TXT`, etc. |
| `name` * | string | Nombre del registro, p. ej. `@` o `subdomain`. |
| `content` * | string | Valor del registro. |
| `ttl` | integer | TTL en segundos (`1` = automático). |
| `proxied` | boolean | Redirigir a través de 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
    }
  }
}
```

### Actualizar registro DNS

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

Actualizar un registro DNS existente por ID. Solo se sobreescriben los campos enviados.

**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" }'
```

### Eliminar registro DNS

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

Eliminar un registro DNS por ID.

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

### Habilitar Cloudflare

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

Activar Cloudflare para el dominio. No devolvemos los nameservers de Cloudflare — la delegación se configura de nuestra parte. No se admite la desactivación de Cloudflare mediante este endpoint.

**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"
  }
}
```

### Configurar nameservers

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

Configurar nameservers personalizados para el dominio.

**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

Generación de white-pages por nicho y tier. Cada sitio es único. Cambia dominio y contactos sin regenerar.

### Listar tiers

`GET /api/v1/whites/tiers`

Tiers de generación disponibles con precios. `price` es el precio por generación ya ajustado a tu cuenta (suscripción aplicada), un único número. `quality: "premium"` cuesta más (se calcula al crear). Blog solo en `v2_tier3`: los primeros 3 artículos están incluidos; cada artículo adicional (hasta 20) se cobra a `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
      }
    ]
  }
}
```

### Listar whites

`GET /api/v1/whites`

Lista de tus trabajos de generación con filtrado.

**Query parameters**

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | Tamaño de página. Por defecto 100, máximo 500. |
| `offset` | integer | Desplazamiento de paginación. |
| `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 }
  }
}
```

### Iniciar generación

`POST /api/v1/whites`

Encola un job de generación de white-page. El pago se cobra de inmediato. El estado final llega vía el webhook `white.completed` / `white.failed` o puede consultarse mediante `GET /whites/{id}`.

**Request body**

| Field | Type | Description |
| --- | --- | --- |
| `name` * | string | Nombre del trabajo (2–20 caracteres). |
| `tier_key` * | string | `v2_tier1` \| `v2_tier2` \| `v2_tier3`. |
| `niche` * | string | Nicho del sitio (hasta 25 caracteres). |
| `country` * | string | Código de país (ISO 3166-1 alpha-2). |
| `language` * | string | Idioma principal del sitio (ISO 639-1). |
| `quality` | string | `standard` \| `premium` (por defecto `standard`). |
| `languages` | string[] | Lista de idiomas. El primero está incluido en el precio; cada idioma adicional tiene un costo extra. |
| `domain` | string | Dominio completo (p. ej. `example.com`). Sin autocompletado. |
| `email` | string | Email completo (p. ej. `info@example.com`). Sin autocompletado. |
| `phone` | string | Teléfono de contacto. |
| `address` | string | Dirección de la empresa. |
| `legal_name` | string | Nombre legal. |
| `style_hint` | string | Estilo de diseño (Random, Minimal, Corporativo, etc.). |
| `keywords` | string[] | Palabras clave SEO. |
| `banned_words` | string[] | Palabras prohibidas en el contenido. |
| `blog_count` | integer | Cantidad de artículos del blog 3–20. Solo para `v2_tier3`. Los artículos que superen 3 tienen costo adicional. |
| `contacts_mode` | string | `same` \| `template`. |
| `contacts_template` | string | Plantilla de la página de contacto (cuando `contacts_mode=template`). |
| `facebook` | string | URL completa de la página de Facebook. |
| `instagram` | string | URL completa del perfil de Instagram. |
| `linkedin` | string | URL completa del perfil de LinkedIn. |
| `youtube` | string | URL completa del canal de YouTube. |
| `tiktok` | string | URL completa del perfil de TikTok. |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `INSUFFICIENT_BALANCE` | 402 | Fondos insuficientes. |
| `INVALID_TIER` | 400 | No se encontró ningún tier con esa clave. |
| `VALIDATION_ERROR` | 400 | Cuerpo de solicitud inválido. |

**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"
    }
  }
}
```

### Obtener un white

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

Obtener el estado actual de un trabajo de generación.

**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"
    }
  }
}
```

### Descargar archivo

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

Obtén un enlace firmado de corta duración al archivo terminado de la white-page. El enlace es válido ~5 minutos (ver `expires_at`). Si expiró, llama a `/download` de nuevo — emitiremos uno nuevo. Si la white-page aún no está generada, devuelve `409 NOT_READY`.

**Query parameters**

| Field | Type | Description |
| --- | --- | --- |
| `format` | string | Formato del archivo. Por defecto `php`. |

**Errors**

| Code | HTTP | When it fires |
| --- | --- | --- |
| `NOT_READY` | 409 | La white-page aún se está generando — reintenta después de `white.completed`. |

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

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

### Cambiar dominio y contactos

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

Actualiza los datos de contacto de la empresa en la white-page sin regenerarla.

**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

Enviamos un POST simple con cuerpo JSON a tu URL. La estructura es la misma para todos los eventos: `event_type`, `timestamp` y un bloque `data`. Tu servidor debe responder con cualquier 2xx en 5 segundos.

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

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

**Retries.** En caso de no-2xx o timeout — hasta 3 reintentos con espera de 1s / 2s / 4s. El historial completo de entregas está en la página de claves API en el dashboard. Si los tres fallan, el evento se marca como failed y puede reenviarse manualmente.

**Security.** Usa un endpoint HTTPS. Cada solicitud llega con el encabezado `User-Agent: piv.day-webhook/1.0`. La URL del webhook se configura en el perfil de la clave API.

### All events

| Event | When it fires |
| --- | --- |
| `sms.received` | Un remitente de confianza acaba de enviar un SMS a uno de tus números. |
| `sms.status_updated` | Un SMS saliente cambió de estado — entregado, fallido, etc. |
| `number.restore_completed` | Un Restore por lotes finalizó — el resumen final llega aquí. |
| `verify.completed` | La verificación de la cuenta de Google finalizó correctamente. |
| `verify.failed` | Algo salió mal — Google no lo aceptó. |
| `proxy.expires_soon` | Quedan aproximadamente tres días en el pedido de proxy. |
| `domain.registered` | El pedido de registro finalizó correctamente — el dominio está activo. |
| `domain.failed` | El pedido no se completó — causa común: el nombre acaba de ser tomado. |
| `domain.expires_soon` | Queda aproximadamente una semana antes de que venza el dominio. |
| `white.completed` | La tarea de generación del White finalizó correctamente. |
| `white.failed` | La tarea falló — los fondos se reembolsan al balance. |

### SMS entrante a tu número

**`sms.received`**

La mayoría de las veces son códigos de verificación de redes publicitarias. El payload incluye el texto sin procesar y un código detectado automáticamente (cuando está presente) para que puedas pasarlo directamente a tu flujo de trabajo sin parsear. Los mensajes de remitentes conocidos de spam no se reenvían.

```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"
  }
}
```

### Estado del SMS enviado cambió

**`sms.status_updated`**

Útil cuando envías confirmaciones desde tu propio número y necesitas saber si realmente llegaron. Si el estado es failed, el código de error y el mensaje del operador están en el 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
  }
}
```

### Restauración de números completada

**`number.restore_completed`**

Contiene dos listas: qué números volvieron y cuáles no. El monto reembolsado por los fallidos también está en el payload. Práctico para scripts que reintentan de forma inteligente.

```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
  }
}
```

### Verificación Google QR exitosa

**`verify.completed`**

Si llegó un SMS con código durante el proceso, ya está aquí — no hace falta una solicitud adicional. captured_phone y captured_message contienen lo que envió Google.

```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"
  }
}
```

### Verificación Google QR fallida

**`verify.failed`**

error_code indica qué ocurrió específicamente — timeout, rechazo, URL inválida. Las verificaciones fallidas se reembolsan al balance automáticamente.

```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 próximo a vencer

**`proxy.expires_soon`**

Diseñado para quienes no renuevan automáticamente: lo ves con anticipación y decides — extender o cerrar la campaña. Vincúlalo a POST /proxy/orders/{id}/renew para renovación totalmente automática.

```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"
  }
}
```

### Dominio registrado

**`domain.registered`**

Si solicitaste Cloudflare y SSL al comprar, ambos ya están activos — cloudflare_enabled refleja el estado. Puedes agregar registros DNS o desplegar el sitio de inmediato.

```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
  }
}
```

### Registro de dominio fallido

**`domain.failed`**

El campo error tiene una razón legible por humanos. El costo se reembolsa al balance. Elige otro nombre e inténtalo de nuevo.

```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"
  }
}
```

### Dominio próximo a vencer

**`domain.expires_soon`**

Útil cuando la renovación automática está desactivada: tienes tiempo de renovar manualmente antes de que el dominio entre en redención. El indicador auto_renew indica si es necesaria la intervención.

```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 generado

**`white.completed`**

El archivo está disponible vía GET /whites/{id}/download. Si el White tenía un dominio configurado, el sitemap y los contactos ya están vinculados — listo para desplegar.

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

### Generación de White fallida

**`white.failed`**

El campo error explica por qué. Inicia un nuevo trabajo vía POST /whites — sin pérdida de dinero.

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

## Error codes

Todos los errores siguen el mismo formato: estado HTTP + campo `error.code` + un mensaje legible en `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 | Clave ausente, vencida o revocada. |
| `INSUFFICIENT_PERMISSIONS` | 403 | La clave no tiene el scope requerido para esta acción. |
| `PERMISSION_DENIED` | 403 | El acceso a nivel de cuenta no permite esta acción (p. ej., función deshabilitada). |
| `VALIDATION_ERROR` | 400 | Cuerpo de la solicitud inválido. El mensaje explica qué campo falló y por qué. |
| `NUMBER_NOT_FOUND` | 404 | El número no existe o no pertenece a la cuenta. |
| `NUMBER_EXPIRED` | 403 | El número ha vencido — use Restore (dentro de los 7 días) o adquiera uno nuevo. |
| `NUMBER_NOT_ACTIVE` | 400 | El número se encuentra en un estado que no permite esta operación. |
| `INSUFFICIENT_BALANCE` | 402 | Fondos insuficientes para realizar esta operación. |
| `INVALID_MESSAGE_FORMAT` | 400 | El cuerpo del SMS es demasiado largo o contiene caracteres no permitidos. |
| `COUNTRY_NOT_AVAILABLE` | 400 | El país no está disponible para compra o renovación. |
| `NO_NUMBERS_AVAILABLE` | 400 | No hay números disponibles en el país seleccionado en este momento. |
| `NO_RESTORABLE_NUMBERS` | 404 | Ninguno de los números proporcionados puede ser restaurado. |
| `RATE_LIMITED` | 429 | Se superó el rate limit. Espere el tiempo indicado en el encabezado `Retry-After`. |
| `INTERNAL_ERROR` | 500 | Algo falló de nuestro lado. Si persiste, contacte al soporte. |
