API Reference

REST API for OTP and magic-link verification. Base URL: https://your-domain.com/api/v1

Overview

otpmagiclink issues one-time passwords and magic links on behalf of your project. You never store raw tokens — we handle delivery, expiry, rate limiting, and audit logging.

  1. Your backend calls Create verification.
  2. We deliver the OTP or magic link (or capture it in sandbox).
  3. The user enters the code or clicks the link.
  4. Your backend calls Check OTP, or the magic link verifies automatically.

Postman / OpenAPI

Download the API spec and import it into Postman, Insomnia, or any OpenAPI-compatible client. The Postman collection includes collection variables and test scripts that chain verificationId and otpToken across requests.

Import into Postman

Download a ready-made collection with variables. Set baseUrl and apiKey, then run requests — verificationId and otpToken are saved automatically from responses.

Postman: Import → choose file → open collection **Variables** tab → set apiKey to your sk_… key.

API Simulator

Pick a project and API key, edit the sample payload, and send live requests against /api/v1. Sandbox projects are recommended for testing — no real email or SMS is sent.

Authentication

Send your project API key on every request:

Authorization header
Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxx

Keys use the sk_ prefix. Sandbox project keys never send real email or SMS.

Create verification

POST/api/v1/verificationsAPI key requiredTry now

Creates a verification and delivers the OTP or magic link.

FieldTypeReqDescription
identifierstringrequiredEmail address or E.164 phone (+15551234567).
channel"EMAIL" | "SMS"requiredDelivery channel.
kind"OTP" | "MAGIC_LINK"requiredOTP code or clickable magic link.
redirectUrlstring (URL)optionalRedirect after magic link click. Must match allowlist if configured.
metadataobjectoptionalArbitrary key/value pairs stored with the verification.
Request body — OTP (email)
{
  "identifier": "user@example.com",
  "channel": "EMAIL",
  "kind": "OTP"
}
Request body — Magic link (email)
{
  "identifier": "user@example.com",
  "channel": "EMAIL",
  "kind": "MAGIC_LINK",
  "redirectUrl": "https://yourapp.com/dashboard"
}
Request body — OTP (SMS)
{
  "identifier": "+15551234567",
  "channel": "SMS",
  "kind": "OTP"
}
Response 201 — production
{
  "id": "cmqz5di97000qq48tkwksjltp",
  "status": "PENDING",
  "channel": "EMAIL",
  "kind": "MAGIC_LINK",
  "expiresAt": "2026-06-29T12:01:14.740Z"
}
Response 201 — sandbox (extra hint)
{
  "id": "cmqz5di97000qq48tkwksjltp",
  "status": "PENDING",
  "channel": "EMAIL",
  "kind": "MAGIC_LINK",
  "expiresAt": "2026-06-29T12:01:14.740Z",
  "sandbox": {
    "message": "No real email is sent in sandbox. Use the sandbox inbox or GET /api/v1/sandbox/inbox/{identifier}/latest-link to retrieve the magic link or OTP."
  }
}
Response 502 — delivery failed
{
  "error": "Message delivery failed. Check your delivery config.",
  "detail": "Resend delivery failed: Domain not verified"
}

Check OTP

POST/api/v1/verifications/:id/checkAPI key requiredTry now

Validates the OTP the user entered. Marks the verification VERIFIED on success.

FieldTypeReqDescription
tokenstringrequiredThe OTP code entered by the user.
Request body
{
  "token": "482910"
}
Response 200 — verified
{
  "id": "cmqz5di97000qq48tkwksjltp",
  "status": "VERIFIED",
  "verifiedAt": "2026-06-29T12:05:00.000Z",
  "redirectUrl": "https://yourapp.com/dashboard"
}
Response 422 — wrong code
{
  "error": "Invalid token",
  "attemptsRemaining": 2
}
Response 410 — expired
{
  "error": "Verification has expired"
}

Sandbox

Sandbox projects capture messages instead of sending real email or SMS. Read them in the dashboard inbox or via API.

GET/api/v1/sandbox/inbox/:identifierAPI key requiredTry now

All captured messages for an identifier, newest first.

Response 200
{
  "messages": [
    {
      "id": "msg_01jxxx",
      "identifier": "user@example.com",
      "subject": "Your verification code is 482910",
      "otp": "482910",
      "magicLink": null,
      "createdAt": "2026-06-29T12:00:00.000Z"
    },
    {
      "id": "msg_01jyyy",
      "identifier": "user@example.com",
      "subject": "Your sign-in link",
      "otp": null,
      "magicLink": "https://your-domain.com/api/v1/verify/magic?token=...&id=...",
      "createdAt": "2026-06-29T11:58:00.000Z"
    }
  ]
}
GET/api/v1/sandbox/inbox/:identifier/latest-linkAPI key requiredTry now

Most recent magic link for an identifier.

Response 200
{
  "id": "msg_01jyyy",
  "identifier": "user@example.com",
  "magicLink": "https://your-domain.com/api/v1/verify/magic?token=...&id=...",
  "otp": null,
  "createdAt": "2026-06-29T11:58:00.000Z"
}
Response 404
{
  "error": "No magic link found for this identifier"
}

Webhooks

Configure webhook URLs in the dashboard. Each POST is signed with X-Webhook-Signature (HMAC-SHA256(secret, "{timestamp}.{body}")).

Events

  • verification.created — right after POST /verifications succeeds
  • verification.verified — user completed verification (OTP check or magic link click)
Webhook POST body — verification.created
{
  "id": "wh_evt_abc123",
  "event": "verification.created",
  "timestamp": "2026-06-29T12:00:00.000Z",
  "environment": "production",
  "project": {
    "id": "cmqz44lie0009q48t1mtk2wv7",
    "name": "My App",
    "slug": "my-app"
  },
  "apiKey": {
    "id": "key_01jxxx",
    "name": "Production server"
  },
  "source": "api",
  "data": {
    "id": "cmqz5di97000qq48tkwksjltp",
    "projectId": "cmqz44lie0009q48t1mtk2wv7",
    "identifier": "user@example.com",
    "channel": "EMAIL",
    "kind": "MAGIC_LINK",
    "status": "PENDING",
    "expiresAt": "2026-06-29T12:10:00.000Z"
  }
}
Webhook POST body — verification.verified
{
  "id": "wh_evt_def456",
  "event": "verification.verified",
  "timestamp": "2026-06-29T12:05:00.000Z",
  "environment": "sandbox",
  "project": {
    "id": "cmqz25yvw0000x48txt367rpc",
    "name": "Sandbox",
    "slug": "sandbox"
  },
  "apiKey": {
    "id": "key_01jyyy",
    "name": "Dev key"
  },
  "source": "otp_check",
  "data": {
    "id": "cmqz5di97000qq48tkwksjltp",
    "projectId": "cmqz25yvw0000x48txt367rpc",
    "identifier": "user@example.com",
    "kind": "OTP",
    "channel": "EMAIL",
    "verifiedAt": "2026-06-29T12:05:00.000Z"
  }
}
Webhook POST body — magic link (no API key)
{
  "id": "wh_evt_ghi789",
  "event": "verification.verified",
  "timestamp": "2026-06-29T12:05:00.000Z",
  "environment": "production",
  "project": {
    "id": "cmqz44lie0009q48t1mtk2wv7",
    "name": "My App",
    "slug": "my-app"
  },
  "apiKey": null,
  "source": "magic_link",
  "data": {
    "id": "cmqz5di97000qq48tkwksjltp",
    "projectId": "cmqz44lie0009q48t1mtk2wv7",
    "identifier": "user@example.com",
    "kind": "MAGIC_LINK",
    "channel": "EMAIL",
    "verifiedAt": "2026-06-29T12:05:00.000Z"
  }
}

Monitoring & smoke tests

Use layered checks to catch API issues early — from lightweight uptime probes to full end-to-end verification flows.

Liveness — GET /api/health

Returns 200 when the process is running. Use for load balancer pings (no DB/Redis check).

Readiness — GET /api/health/ready

Returns 200 only when PostgreSQL and Redis are reachable. Returns 503 with dependency status when degraded — ideal for uptime monitors (Better Stack, Pingdom, etc.).

CLI smoke test — yarn smoke

Runs health, readiness, and a full sandbox OTP flow (create → inbox → check). Exit code 0 = pass. Use locally or in CI.

Run against staging or local
BASE_URL=https://your-app.run.app API_KEY=sk_sandbox_key yarn smoke

Scheduled cron smoke — POST /api/cron/smoke

Server-side smoke test triggered by Cloud Scheduler or GitHub Actions. Requires CRON_SECRET and a sandbox SMOKE_API_KEY in env.

Trigger
curl -X POST https://your-app.run.app/api/cron/smoke \
  -H "Authorization: Bearer $CRON_SECRET"

CI runs Playwright API tests before deploy, then yarn smoke after deploy. If smoke fails, the GitHub Actions workflow fails (staging on main, dev on develop). Required secrets: K6_STAGING_API_KEY / K6_DEV_API_KEY (sandbox sk_… keys).

Errors

Errors return JSON with an error string and optional detail or details.

StatusMeaning
401Missing or invalid API key.
403Sandbox endpoint called on a production project.
404Verification or resource not found.
409Already verified.
410Expired or max attempts reached.
422Invalid request body.
429Rate limit exceeded.
502Delivery provider failed.
Response 422 — validation error
{
  "error": "Invalid request body",
  "details": {
    "fieldErrors": {
      "channel": ["Invalid enum value. Expected 'EMAIL' | 'SMS'"]
    }
  }
}