OTPBox

Proporciona a los agentes de IA y suites de pruebas una bandeja de entrada de correo desechable: crea identidades de prueba, espera correos y extrae códigos OTP y enlaces de verificación automáticamente a través de REST o MCP.

Servidor MCP alojado

npx add-mcp 'https://otpbox.io/mcp'

Se instala en Claude Code, Codex, Cursor y más

Documentación

Documentación de la API para desarrolladores de OTPBox

Crea buzones desechables y lee códigos de un solo uso desde un conjunto de pruebas, un script o un trabajo de backend. Comienza con una clave gratuita — sin necesidad de cuenta — o crea una organización para compartir claves con un equipo y mejorar el plan.

Pruébalo ahora

En vivo contra la API real, directamente desde esta página — sin registro. Genera una clave gratuita real (con límite de velocidad), crea un buzón real y luego lo revisa en busca de un mensaje. Envía al buzón un correo electrónico real desde otra pestaña para verlo aparecer.

Click "Mint a free key" to start.

Obtén una clave

Gratuita, 200 solicitudes/mes, autenticación simple con token de portador. Genérala al instante, sin pago ni cuenta:

curl -X POST https://otpbox.io/api/v1/keys/free \
  -H "content-type: application/json" \
  -d '{}'
→ { "id": "lic_...", "key": "...", "plan": "free", "quotaLimit": 200 }

Guarda el key devuelto — como un token de buzón, se muestra una sola vez y nunca se almacena en ningún lugar desde donde puedas leerlo de nuevo. Limitado a 5 claves gratuitas/día por IP.

Claves sandbox para CI. POST /api/v1/keys/sandbox genera una clave de la misma manera (sin cuenta, mismas 200 solicitudes/mes, su propio límite de 5/día/IP) y devuelve { "id": "lic_...", "key": "...", "plan": "free", "quotaLimit": 200, "sandbox": true }. Con una clave sandbox, POST /api/v1/inboxes (y la herramienta MCP create_test_inbox) omite el correo real: el nuevo buzón ya contiene un mensaje sintetizado de noreply@sandbox.otpbox.io con un code aleatorio de 6 dígitos, por lo que una ejecución de prueba es totalmente determinista y no necesita entrega real. Los buzones sandbox nunca reciben correo real, por lo que nunca activan los webhooks message.received, otp.extracted o link.detected.

Planes

Las cuotas son por mes calendario. Los planes de organización agrupan la cuota entre todas las claves que la organización genera; una clave gratuita personal tiene la suya propia. Detalles completos en la página de precios.

PlanSolicitudes / mesPrecioCómo obtenerlo
Clave gratuita personal200 por clave$0POST /api/v1/keys/free, sin cuenta
Organización — Gratis200, agrupadas$0Regístrate, crea una organización, genera claves por proyecto
Organización — Pro5,000, agrupadas$9 / mesPanel → tu organización → Plan y uso → Mejorar a Pro
EmpresaPersonalizadoPersonalizadoCuéntanos tu caso de uso

Verifica a qué tiene derecho una clave en cualquier momento con GET /api/v1/usage — devuelve el plan, el límite aplicable y cuánto se ha utilizado este mes.

Autenticación

Envía tu clave como token de portador en cada solicitud:

Authorization: Bearer <your-key>

Endpoints

Todas las rutas están bajo https://otpbox.io. Cada ruta excepto las dos rutas de generación de claves necesita Authorization: Bearer <key>, y cada solicitud autenticada cuenta como una solicitud contra tu cuota mensual. Las marcas de tiempo son milisegundos Unix. Los cuerpos y las respuestas son JSON.

MétodoRutaPropósito
POST/api/v1/keys/freeGenera una clave gratuita (sin autenticación; 5/día/IP). Devuelve { id, key, plan, quotaLimit }.
POST/api/v1/keys/sandboxGenera una clave sandbox (sin autenticación; 5/día/IP).
POST/api/v1/inboxesCrea un buzón. Cuerpo: { domain?, local? }. Devuelve 201 { id, address, domain, createdAt, expiresAt, token }. Ámbito inbox:create. Acepta una Idempotency-Key.
GET/api/v1/inboxes/:idMetadatos del buzón: { id, address, domain, createdAt, expiresAt }. Solo lo puede leer la clave que lo creó. Ámbito inbox:read.
DELETE/api/v1/inboxes/:idElimina el buzón y todos sus mensajes inmediatamente. Devuelve { ok: true }. Ámbito inbox:delete.
GET/api/v1/inboxes/:id/messagesLista mensajes, del más reciente al más antiguo (hasta 200), como { messages: [{ id, from, fromName, subject, code, linkHost, linkType, size, receivedAt, read }] }. Ámbito message:read.
GET/api/v1/messages/:idMensaje completo: { id, from, fromName, subject, text, html, code, link, size, receivedAt, attachments, preview } donde link es { url, host, type } o null. El HTML está saneado. Ámbito message:read.
GET/api/v1/usagePlan, límite y uso de este mes — consulta Límites de velocidad y cuota.
POST/api/v1/batchesCrea hasta 50 buzones en una sola llamada — consulta Lotes. Ámbito bulk:create.
GET/api/v1/batches/:idEstado de un lote y resultados por elemento.
POST/api/v1/identitiesCrea una identidad de prueba: una persona sintética respaldada por un buzón real.
POST/api/v1/identities/bulkCrea hasta 50 identidades de prueba en una sola llamada.
GET/api/v1/identities/:idObtiene una identidad de prueba (el token del buzón nunca se vuelve a mostrar).
DELETE/api/v1/identities/:idElimina una identidad y su buzón de respaldo.
GET/api/v1/webhooksLista los webhooks de esta clave. Ámbito webhook:manage.
POST/api/v1/webhooksRegistra un webhook. Ámbito webhook:manage.
DELETE/api/v1/webhooks/:idElimina un webhook. Ámbito webhook:manage.
GET/api/v1/metricsVolumen de llamadas, tasa de error y latencia de las llamadas a herramientas MCP de esta clave en las últimas 24 horas y 7 días: { last24h, last7d }, cada una con totalCalls, errorRatePct, avgLatencyMs, p95LatencyMs y un desglose por herramienta byTool.

Los buzones se eliminan cuando expiran — 1 hora después de su creación por defecto — o tan pronto como los elimines. La lista de mensajes solo informa lo que se extrajo (code, linkHost, linkType); obtén el mensaje completo para la URL del enlace. linkType (y link.type) es uno de verification, password_reset, magic_login, unsubscribe, tracking o general.

Ejemplo de respuesta de mensaje:

{
  "id": "msg_a1b2c3d4e5f6",
  "from": "noreply@example.com",
  "fromName": "Example",
  "subject": "Your verification code",
  "text": "Your code is 482913",
  "html": null,
  "code": "482913",
  "link": { "url": "https://example.com/verify?t=...", "host": "example.com", "type": "verification" },
  "size": 2048,
  "receivedAt": 1790000000000,
  "attachments": [],
  "preview": "Your code is 482913"
}

Ejemplo: obtener un código en una prueba de Playwright

const key = process.env.OTPBOX_KEY;
const res = await fetch('https://otpbox.io/api/v1/inboxes', {
  method: 'POST',
  headers: { authorization: 'Bearer ' + key, 'content-type': 'application/json' },
  body: '{}',
});
const inbox = await res.json();
// use inbox.address to sign up, then poll:
const msgs = await fetch('https://otpbox.io/api/v1/inboxes/' + inbox.id + '/messages', {
  headers: { authorization: 'Bearer ' + key },
}).then((r) => r.json());
const code = msgs.messages[0]?.code;

Lotes

Crea hasta 50 buzones en una sola llamada — útil para aprovisionar una matriz de pruebas completa de antemano. El lote se ejecuta de forma síncrona y devuelve cada dirección y token en la respuesta.

POST /api/v1/batches
{ "count": 3, "ttlHours": 2 }

→ 201
{
  "id": "batch_...",
  "status": "partial",
  "requestedCount": 3,
  "items": [
    { "id": "a1b2c3d4e5f6", "status": "completed", "address": "bold.quartz844@otpbox.io", "token": "...", "expiresAt": 1790007200000 },
    { "id": "bi_...", "status": "failed", "error": "address_unavailable" }
  ]
}
  • count es obligatorio, 1–50; cualquier otro valor devuelve 400 { "error": "bad_count", "max": 50 }. ttlHours es opcional y está limitado al máximo del servidor (actualmente 3 horas); sin él, se aplica la vida útil predeterminada del buzón.
  • El status del lote es completed (todos los elementos tuvieron éxito), partial (algunos fallaron) o failed (ninguno tuvo éxito). El id de un elemento completado es el id del buzón, por lo que puedes leerlo con GET /api/v1/inboxes/:id/messages. (El ejemplo anterior está abreviado a dos elementos).
  • El token de cada elemento es el token de portador propio del buzón. Solo se devuelve en esta llamada de creación.
  • GET /api/v1/batches/:id devuelve { id, status, requestedCount, createdAt, completedAt, items: [{ id, status, address, expiresAt, error }] } (sin tokens); id es null para un elemento fallido. Lote desconocido o de otra persona: 404 not_found.
  • Requiere el ámbito bulk:create y acepta una Idempotency-Key. 503 no_domains si no hay ningún dominio de buzón disponible actualmente.

Identidades de prueba

Una identidad de prueba es una persona sintética — nombre, país y un pequeño perfil — respaldada por un buzón desechable real, de modo que un flujo de registro bajo prueba pueda usar una persona realista y aun así recibir su correo de verificación. Elige un template: us_customer, indian_customer, european_customer, business_customer, student o employee.

POST /api/v1/identities
{ "template": "business_customer", "ttlHours": 2 }

→ 201
{
  "id": "ident_...",
  "status": "active",
  "template": "business_customer",
  "name": "Mary Johnson",
  "email": "bold.quartz844@otpbox.io",
  "country": "US",
  "profile": { "company": "Acme Corp", "role": "Product Manager" },
  "token": "...",
  "createdAt": 1790000000000,
  "expiresAt": 1790007200000
}
  • Usa email en el formulario bajo prueba. token es el token de portador del buzón de respaldo, que se muestra solo en esta respuesta; úsalo con la API pública de buzones (GET /api/inboxes/me/messages con Authorization: Bearer <token>) para leer lo que llegue.
  • El profile de la persona depende de la plantilla: business_customer agrega company y role, student agrega university y major, employee agrega company, department y jobTitle; las plantillas de cliente devuelven un perfil vacío.
  • ttlHours es opcional y está limitado al máximo del servidor (actualmente 3 horas). Errores: 400 bad_template (con validTemplates), 409 address_unavailable, 503 no_domains.
  • POST /api/v1/identities/bulk con { "template", "count", "ttlHours"? } (count 1–50, de lo contrario 400 bad_count) devuelve 201 { batchId, template, requestedCount, items: [...] }; batchId se ve como idbatch_.... Cada elemento es { id, status: "active", name, email, country, profile, token, expiresAt }, o { id, status: "failed", error }.
  • GET /api/v1/identities/:id devuelve la identidad sin su token, además de batchId y error. DELETE /api/v1/identities/:id elimina la identidad y su buzón de respaldo y devuelve { ok: true, id, inboxDeleted }. Los ids desconocidos devuelven 404 not_found.

Claves de idempotencia

Si un trabajo de CI reintenta una llamada de creación después de un tiempo de espera de red, puede terminar con dos buzones. Envía un encabezado Idempotency-Key en POST /api/v1/inboxes o POST /api/v1/batches y un reintento con la misma clave devuelve la respuesta original en lugar de crear otro recurso.

curl -X POST https://otpbox.io/api/v1/inboxes \
  -H "authorization: Bearer $OTPBOX_KEY" \
  -H "idempotency-key: signup-test-run-8841" \
  -H "content-type: application/json" -d '{}'
  • La clave tiene de 1 a 255 caracteres de A-Z a-z 0-9 _ . : - (los UUID funcionan bien); cualquier otro valor devuelve 400 invalid_idempotency_key. El encabezado es opcional.
  • Las claves están limitadas a tu clave de API y al endpoint, por lo que el mismo valor se puede reutilizar contra /inboxes y /batches sin colisión.
  • Tanto las respuestas exitosas como las de error se almacenan y se reproducen con el código de estado original. Los registros se conservan durante 24 horas.
  • Si una solicitud con la misma clave aún está en curso, la segunda recibe 409 idempotency_key_in_progress; reintenta después de un momento.
  • Una solicitud reproducida aún cuenta como una solicitud contra tu cuota. Las rutas de identidades no leen este encabezado.

Webhooks

Recibe un HTTPS POST cuando algo sucede en lugar de sondear. Los webhooks pertenecen a la clave de API que los registró y se activan para los buzones, identidades y uso de esa clave. Registrar, listar y eliminarlos requiere el ámbito webhook:manage (o una clave sin restricciones de ámbito).

Registrar

POST /api/v1/webhooks
{ "url": "https://your-app.example.com/webhooks/otpbox", "events": ["otp.extracted", "usage.approaching"] }

→ 201
{
  "id": "wh_...",
  "url": "https://your-app.example.com/webhooks/otpbox",
  "events": ["otp.extracted", "usage.approaching"],
  "secret": "...",
  "createdAt": 1790000000000
}
  • url debe comenzar con https://, de lo contrario 400 { "error": "bad_url" }.
  • events es una lista del catálogo a continuación. Los nombres desconocidos se ignoran; si ninguno es válido, obtienes 400 { "error": "bad_events", "validEvents": [...] }.
  • El secret se devuelve una vez, solo en esta respuesta. Guárdalo ahora — lo necesitas para verificar firmas y no se puede recuperar más tarde.
  • GET /api/v1/webhooks devuelve { webhooks: [{ id, url, events, status, createdAt }] } (nunca el secreto). status es active o disabled.
  • DELETE /api/v1/webhooks/:id devuelve { ok: true }, o 404 not_found.

Catálogo de eventos

Cada entrega es un POST con content-type: application/json. El cuerpo es un objeto JSON cuyo campo event nombra el evento, seguido de los campos que se muestran a continuación. El nombre del evento también se envía en el encabezado x-otpbox-event.

EventoSe activa cuando
inbox.createdSe crea un buzón con POST /api/v1/inboxes o la herramienta MCP create_test_inbox. No se activa para buzones de lotes o identidades.
inbox.deletedSe elimina un buzón con DELETE /api/v1/inboxes/:id o la herramienta MCP delete_inbox.
inbox.expiredUno de tus buzones alcanza su expiración y es eliminado por el trabajo de limpieza (que se ejecuta cada 15 minutos).
message.receivedLlega un correo electrónico a uno de tus buzones.
otp.extractedUn correo entrante contenía un código de un solo uso. Mismo payload que message.received.
link.detectedUn correo entrante contenía una confirmación u otro enlace.
identity.createdSe crea una identidad de prueba. Una creación masiva activa un evento para todo el lote, con un payload con forma de lote.
identity.deletedSe elimina una identidad de prueba.
usage.approachingEl uso alcanza por primera vez el 80% del límite mensual. Una vez por período de facturación.
usage.exceededEl uso alcanza el 100% del límite mensual. Una vez por período de facturación.

Ejemplos de payloads (las marcas de tiempo son milisegundos Unix):

inbox.created

{ "event": "inbox.created", "inboxId": "a1b2c3d4e5f6", "address": "bold.quartz844@otpbox.io", "domain": "otpbox.io", "createdAt": 1790000000000, "expiresAt": 1790003600000 }

inbox.deleted

{ "event": "inbox.deleted", "inboxId": "a1b2c3d4e5f6", "address": "bold.quartz844@otpbox.io", "deletedAt": 1790000900000 }

inbox.expired

{ "event": "inbox.expired", "inboxId": "a1b2c3d4e5f6", "address": "bold.quartz844@otpbox.io", "expiresAt": 1790003600000, "expiredAt": 1790004200000 }

message.received

{ "event": "message.received", "inboxId": "a1b2c3d4e5f6", "messageId": "msg_...", "from": "noreply@example.com", "fromName": "Example", "subject": "Your verification code", "code": "482913", "link": null, "receivedAt": 1790000300000 }

otp.extracted

{ "event": "otp.extracted", "inboxId": "a1b2c3d4e5f6", "messageId": "msg_...", "from": "noreply@example.com", "fromName": "Example", "subject": "Your verification code", "code": "482913", "link": null, "receivedAt": 1790000300000 }

link.detected

{ "event": "link.detected", "inboxId": "a1b2c3d4e5f6", "messageId": "msg_...", "linkUrl": "https://example.com/verify?t=...", "linkHost": "example.com", "linkType": "verification", "receivedAt": 1790000300000 }

identity.created (identidad única)

{ "event": "identity.created", "identityId": "ident_...", "template": "us_customer", "email": "bold.quartz844@otpbox.io", "country": "US", "createdAt": 1790000000000, "expiresAt": 1790003600000 }

identity.created (de /identities/bulk)

{ "event": "identity.created", "batchId": "idbatch_...", "template": "student", "requestedCount": 10, "createdCount": 10, "failedCount": 0, "createdAt": 1790000000000 }

identity.deleted

{ "event": "identity.deleted", "identityId": "ident_...", "inboxId": "a1b2c3d4e5f6", "email": "bold.quartz844@otpbox.io", "deletedAt": 1790000900000 }

usage.approaching y usage.exceeded

{ "event": "usage.approaching", "orgId": "org_...", "licenseId": "lic_...", "scope": "organization", "period": "2026-09", "used": 160, "limit": 200, "percent": 80 }

Para una clave de organización, el umbral se mide contra la cuota agrupada de la organización, y se incluyen orgId y scope: "organization"; cada clave en la organización que tenga un webhook suscrito recibe el evento. Para una clave personal, esos dos campos se omiten y used / limit son los propios de esa clave. usage.exceeded tiene la misma forma con un percent de 100 o más. Verificar la firma

Cada entrega incluye un encabezado x-otpbox-signature: el HMAC-SHA256 en minúsculas hexadecimal del cuerpo de la solicitud sin procesar, codificado con el secret del webhook. Calcúlalo sobre los bytes exactos que recibiste (antes de cualquier análisis JSON) y compáralo en tiempo constante. Rechaza cualquier cosa que no coincida.

// Node (Express)
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const app = express();

app.post('/webhooks/otpbox', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = createHmac('sha256', process.env.OTPBOX_WEBHOOK_SECRET)
    .update(req.body) // Buffer: the raw body
    .digest('hex');
  const given = String(req.get('x-otpbox-signature') ?? '');

  const a = Buffer.from(expected);
  const b = Buffer.from(given);
  if (a.length !== b.length || !timingSafeEqual(a, b)) return res.status(401).end();

  const event = JSON.parse(req.body.toString('utf8'));
  if (event.event === 'otp.extracted') console.log('code:', event.code);
  res.status(200).end();
});
# Python (Flask)
import hashlib, hmac, os
from flask import Flask, abort, request

app = Flask(__name__)

@app.post('/webhooks/otpbox')
def otpbox_webhook():
    raw = request.get_data()  # bytes: the raw body
    expected = hmac.new(os.environ['OTPBOX_WEBHOOK_SECRET'].encode(), raw, hashlib.sha256).hexdigest()
    given = request.headers.get('x-otpbox-signature', '')

    if not hmac.compare_digest(expected, given):
        abort(401)

    event = request.get_json(force=True)
    if event['event'] == 'otp.extracted':
        print('code:', event['code'])
    return '', 200

El paquete otpbox-sdk incluye la misma verificación que verifyWebhookSignature(secret, rawBody, signatureHeader).

Entrega, reintentos y desactivación automática

  • Una entrega tiene éxito cuando tu endpoint responde con un estado 2xx dentro de 8 segundos. Cualquier otra cosa — otro estado, un tiempo de espera, un error de conexión — es un fallo.
  • El primer intento se envía inmediatamente. Una entrega fallida se reintenta hasta 5 intentos en total, esperando 5 minutos, 15 minutos, 1 hora y 4 horas después de los intentos 1 a 4. Los reintentos son recogidos por un trabajo que se ejecuta cada 15 minutos, por lo que un reintento puede llegar hasta 15 minutos más tarde de estos retrasos nominales. Cada reintento reenvía el cuerpo idéntico, así que deduplica según el payload (por ejemplo messageId) si tu manejador no es idempotente.
  • Después de 10 intentos fallidos consecutivos, el webhook se marca como disabled (visible en GET /api/v1/webhooks) y deja de recibir eventos; cualquier entrega exitosa restablece el contador. Una vez que tu endpoint vuelva a estar sano, vuelve a habilitarlo desde el panel (Proyectos y claves → la clave → Webhooks → Habilitar, luego Enviar prueba para confirmar) o registra uno nuevo con POST /api/v1/webhooks. El panel también envía un evento webhook.test bajo demanda y te permite reentregar cualquier entrega pasada.
  • Las entregas no están ordenadas. Responde rápidamente con un 2xx y haz el trabajo pesado después.

CI/CD: OTPs en GitHub Actions

La configuración más común: una suite E2E de Playwright o Cypress que registra una cuenta real y necesita un código real para pasar la pantalla de OTP. El paquete otpbox-sdk (npm install otpbox-sdk) envuelve los endpoints anteriores en createInbox() / waitForOtp() / deleteInbox() para que tu flujo de trabajo y tu código de prueba no tengan que hacer llamadas fetch a mano.

# .github/workflows/e2e.yml
name: E2E
on: [push]
jobs:
  e2e:
    runs-on: ubuntu-latest
    env:
      OTPBOX_KEY: ${{ secrets.OTPBOX_KEY }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npx playwright test

Añade OTPBOX_KEY en Configuración → Secretos y variables → Actions del repositorio para que se inyecte como variable de entorno y nunca se incluya en el archivo de flujo de trabajo.

Dentro de la prueba, crea una bandeja de entrada real, rellena el formulario de registro con su dirección y luego bloquea esperando el código:

import { test, expect } from '@playwright/test';
import { OTPBox } from 'otpbox-sdk';

test('sign up with a real OTP', async ({ page }) => {
  const client = new OTPBox({ apiKey: process.env.OTPBOX_KEY! });
  const inbox = await client.createInbox();

  await page.goto('https://your-app.example.com/signup');
  await page.fill('[name="email"]', inbox.address);
  await page.click('button[type="submit"]');

  const code = await client.waitForOtp(inbox.id, { timeoutMs: 20_000 });
  await page.fill('[name="otp"]', code);
  await page.click('button[type="submit"]');
  await expect(page.locator('text=Welcome')).toBeVisible();

  await client.deleteInbox(inbox.id);
});

Algunos problemas específicos de CI: nunca codifiques la clave en el YAML del flujo de trabajo ni la confirmes en el repositorio — siempre léela de un secreto como arriba. Vigila la cuota gratuita de 200 solicitudes/mes cuota si la suite se ejecuta en cada push o en una matriz — cada creación/consulta/eliminación de bandeja de entrada cuenta contra ella, así que una suite con mucho tráfico en un repositorio ocupado puede agotarla rápido; actualiza o dedica una clave a CI si eso ocurre. Llama a deleteInbox() en un bloque finally /after-hook para que las ejecuciones fallidas no dejen bandejas de entrada huérfanas — aunque si lo olvidas, el cron de expiración de 15 minutos las limpia automáticamente de todos modos.

CI/CD: OTPs en GitLab CI

Misma idea, ejecutada como un trabajo de GitLab CI: usa la imagen Docker de Playwright para que playwright install no sea necesaria, inyecta la clave como variable de CI/CD enmascarada y ejecuta la suite.

# .gitlab-ci.yml
e2e:
  stage: test
  image: mcr.microsoft.com/playwright:v1.48.0-jammy
  variables:
    OTPBOX_KEY: $OTPBOX_KEY
  script:
    - npm ci
    - npx playwright test

Añade OTPBOX_KEY en Configuración → CI/CD → Variables de tu proyecto, marcada como Enmascarada y Protegida para que nunca aparezca en los registros de trabajos y solo se ejecute en ramas protegidas.

El código de prueba es idéntico al ejemplo de Playwright anterior: crea una bandeja de entrada con createInbox(), rellena el formulario de registro con inbox.address, bloquea esperando waitForOtp(), luego deleteInbox() en un bloque finally. Se aplican los mismos problemas — nunca imprimas la clave, vigila la cuota mensual en un pipeline ocupado y deja que el cron de expiración de 15 minutos limpie cualquier cosa que un trabajo fallido deje atrás.

CI/CD: OTPs en Jenkins

Una etapa declarativa Jenkinsfile que instala los navegadores de Playwright y ejecuta la suite, con la clave extraída del propio almacén de credenciales de Jenkins en lugar de una variable de entorno establecida en la configuración del trabajo:

// Jenkinsfile
pipeline {
  agent any
  environment {
    OTPBOX_KEY = credentials('otpbox-key')
  }
  stages {
    stage('E2E') {
      steps {
        sh 'npm ci'
        sh 'npx playwright install --with-deps chromium'
        sh 'npx playwright test'
      }
    }
  }
}

Añade otpbox-key en Administrar Jenkins → Credenciales como credencial de Texto secreto — eso es lo que credentials('otpbox-key') anterior resuelve y enmascara en el registro de la consola.

Mismo código de prueba que el ejemplo de GitHub Actions: crea una bandeja de entrada, maneja el formulario de registro con su dirección, espera el código, elimina la bandeja de entrada al terminar. Mismos problemas también — nada específico de Jenkins cambia la historia de cuota o limpieza.

CI/CD: OTPs en CircleCI

Un solo trabajo que usa la imagen Docker de Playwright, con la clave establecida como variable de entorno del proyecto (CircleCI las inyecta automáticamente en cada trabajo, sin necesidad de configuración explícita):

# .circleci/config.yml
version: 2.1
jobs:
  e2e:
    docker:
      - image: mcr.microsoft.com/playwright:v1.48.0-jammy
    steps:
      - checkout
      - run: npm ci
      - run: npx playwright test
workflows:
  test:
    jobs:
      - e2e

Añade OTPBOX_KEY en Configuración del proyecto → Variables de entorno — estará disponible como process.env.OTPBOX_KEY dentro de la prueba de la misma manera que localmente, sin necesidad de bloque environment: en la configuración.

Código de prueba, cuota y limpieza son exactamente como se describió anteriormente: crea la bandeja de entrada, ejecuta el flujo de registro de la aplicación contra su dirección, espera el código, elimina la bandeja de entrada después.

MCP (para agentes de IA)

La misma clave también funciona como servidor MCP para clientes de agente/asistente de codificación (Claude, Cursor y otros) que hablan MCP. Es un servidor remoto (HTTP transmisible) en https://otpbox.io/mcp; autentica con el mismo encabezado Authorization: Bearer que la API REST:

{
  "mcpServers": {
    "otpbox": {
      "url": "https://otpbox.io/mcp",
      "headers": { "Authorization": "Bearer <your-key>" }
    }
  }
}

Expone 13 herramientas — los mismos datos subyacentes que /api/v1, invocables directamente por un agente en medio de una tarea. Las herramientas de bandeja de entrada y mensajes toman el id de bandeja de entrada devuelto por create_test_inbox.

HerramientaArgumentosQué hace
create_test_inboxdomain?, local?Crea una bandeja de entrada desechable; devuelve su id, dirección y expiración.
wait_for_emailinboxId, timeoutSeconds? (1–25, predeterminado 20)Bloquea hasta que llegue un nuevo mensaje a la bandeja de entrada o el tiempo de espera expire ({ timedOut, message }). Prefiere esto sobre el sondeo.
get_otpinboxIdEl código de un solo uso extraído más recientemente en la bandeja de entrada, si lo hay.
get_verification_linkinboxIdEl enlace de confirmación/verificación extraído más recientemente (url, host, type), si lo hay.
get_latest_emailinboxIdEl mensaje más reciente completo: remitente, asunto, texto/HTML, código, enlace y adjuntos.
search_emailsinboxId, query?Lista mensajes del más nuevo al más antiguo (hasta 50), opcionalmente filtrados por coincidencia de subcadena en remitente o asunto.
delete_inboxinboxIdElimina permanentemente una bandeja de entrada y todos sus mensajes.
create_batchcount (1–50), ttlHours?Crea varias bandejas de entrada en una sola llamada; cada elemento tiene su propia dirección, token y expiración.
create_test_identitytemplate, ttlHours?Crea una persona sintética (nombre, correo, país, perfil) respaldada por una bandeja de entrada real. Plantillas: us_customer, indian_customer, european_customer, business_customer, student, employee.
create_bulk_test_identitiestemplate, count (1–50), ttlHours?Crea varias identidades de prueba en una sola llamada.
delete_test_identityidentityIdElimina una identidad de prueba y su bandeja de entrada subyacente.
register_webhookurl (https), eventsRegistra un webhook. Devuelve el secreto de firma, mostrado una vez. Acepta los eventos de bandeja de entrada, mensaje, enlace e identidad (no los dos eventos usage.* — regístralos por REST).
get_usageningunoIgual que GET /api/v1/usage: plan, quotaLimit, used este mes, period, status y pooled (verdadero cuando la cuota se comparte en una organización).

Cada llamada a herramienta cuenta como una solicitud contra tu cuota mensual, y las herramientas aplican los mismos ámbitos de clave que REST. Los fallos vuelven como errores de herramienta MCP (por ejemplo, una llamada por encima de la cuota o una llamada limitada por velocidad) en lugar de códigos de estado HTTP, y una clave que falta, es inválida, ha expirado o está inactiva se rechaza con 401 / 402 antes de que se ejecute cualquier herramienta. Aparte de la cuota mensual, un agente está limitado a 60 llamadas de herramienta por minuto por clave.

Ámbitos de clave

Una clave puede restringirse a un conjunto de ámbitos cuando se crea en el panel de la organización (junto con un tipo de clave opcional, etiqueta y fecha de expiración). Una clave creada sin ámbitos — incluyendo cada clave gratuita personal — puede usar todo. Una clave con ámbito que llama a algo fuera de sus ámbitos recibe 403 { "error": "insufficient_scope", "required": "<scope>" }.

ÁmbitoPermite
inbox:createPOST /api/v1/inboxes; MCP create_test_inbox
inbox:readGET /api/v1/inboxes/:id
inbox:deleteDELETE /api/v1/inboxes/:id; MCP delete_inbox, delete_test_identity
message:readGET /api/v1/inboxes/:id/messages, GET /api/v1/messages/:id; MCP wait_for_email, get_latest_email, search_emails
otp:readMCP get_otp, get_verification_link
identity:createMCP create_test_identity, create_bulk_test_identities
bulk:createPOST /api/v1/batches; MCP create_batch
webhook:manageGET / POST / DELETE /api/v1/webhooks; MCP register_webhook

GET /api/v1/usage, GET /api/v1/metrics, GET /api/v1/batches/:id y las rutas /api/v1/identities no están limitadas por un ámbito. Las claves también pueden llevar una expiración: una clave expirada recibe 401 key_expired.

Límites de velocidad y cuota

Medido por mes calendario (UTC), no una ventana móvil. Cada solicitud /api/v1 autenticada y cada llamada de herramienta MCP cuenta como una solicitud — incluyendo lecturas, GET /api/v1/usage en sí, y llamadas que terminan en un error como 404. Las claves de organización comparten la cuota agrupada de su organización (ver Planes); una clave gratuita personal tiene la suya propia. Exceder tu cuota devuelve 429 con { "error": "quota_exceeded", "used": …, "limit": … } — nunca se cobra nada por excedente; actualiza o espera a que el mes cambie. No hay CAPTCHA en /api/v1 o /mcp — la cuota en sí es el control de abuso.

Consulta dónde estás en cualquier momento:

GET /api/v1/usage
→ { "plan": "api_5k_monthly", "quotaLimit": 5000, "used": 812, "period": "2026-09", "status": "active", "pooled": true }
CampoSignificado
planfree, api_5k_monthly (Pro) o enterprise. Para una clave de organización, este es el plan de la organización.
quotaLimitSolicitudes permitidas en este período, o null cuando no hay límite fijo.
usedSolicitudes usadas hasta ahora en este período. Para una clave de organización, este es el conteo agrupado de toda la organización.
periodEl mes calendario, como YYYY-MM (UTC). Los contadores se restablecen cuando cambia.
statusEl estado de la clave (active para una clave que funciona).
pooledtrue cuando la cuota se comparte entre las claves de una organización.

Para que te avisen antes de llegar al límite, suscríbete a un webhook a usage.approaching (80%) y usage.exceeded (100%). Otros dos límites son separados de la cuota mensual: la creación de claves está limitada a 5 por día por IP (429 rate_limited), y MCP está limitado a 60 llamadas de herramienta por minuto por clave.

Errores

Los errores son JSON con un código error y un estado HTTP apropiado; algunos llevan campos adicionales. Los errores de autenticación (401/402) se devuelven antes de que se cuente nada contra tu cuota.

EstadoCódigoSignificado
400bad_countcount debe ser 1–50 (se devuelve max).
400bad_templatePlantilla de identidad desconocida (se devuelve validTemplates).
400bad_domainEl domain solicitado no es un dominio activo.
400bad_url / bad_eventsEl url del webhook no es https://, o no se proporcionaron nombres de eventos válidos (se devuelve validEvents).
400invalid_idempotency_keyEl encabezado Idempotency-Key está mal formado — consulte Claves de idempotencia.
401missing_keyNo hay encabezado Authorization: Bearer.
401invalid_keyLa clave no es reconocida.
401key_expiredLa clave tenía una fecha de expiración y ya ha pasado.
402license_inactiveLa clave ha sido revocada o no está activa.
403insufficient_scopeLos alcances de la clave no incluyen lo que esta llamada necesita (required nombra el alcance).
404not_foundNo existe tal bandeja de entrada, mensaje, lote, identidad o webhook — o pertenece a una clave diferente.
409address_unavailableLa parte de local solicitada está ocupada, o no se pudo asignar una dirección para una identidad o elemento del lote.
409try_againNo se pudo asignar una dirección aleatoria en este momento; reintente.
409idempotency_key_in_progressUna solicitud con el mismo Idempotency-Key aún se está ejecutando.
429quota_exceededCuota mensual agotada (se devuelven used y limit) — consulte Límites de tasa y cuota.
429rate_limitedDemasiadas solicitudes de creación de claves desde su IP hoy (se devuelve retryAfter, en segundos).
503no_domainsNo hay ningún dominio de buzón disponible actualmente; reintente en breve.

¿Necesita mayor volumen o un SLA?

¿Ejecuta una gran matriz de pruebas o desea un plan dedicado con soporte prioritario? Cuéntenos su caso de uso y le responderemos. Estado actual: página de estado.

Preguntas: abuse@otpbox.io.

Consulte también: guías (Playwright, Cypress, agentes de IA), precios y el registro de cambios.

← Volver a OTPBox