REST API for OTP and magic-link verification. Base URL: https://your-domain.com/api/v1
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.
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.
Recommended — includes {{baseUrl}}, {{apiKey}}, {{verificationId}}, and auto-chaining scripts.
Import into Postman, Insomnia, or Swagger. Bearer auth and all public endpoints.
Postman: Import → choose file → open collection **Variables** tab → set apiKey to your sk_… key.
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.
Send your project API key on every request:
Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxKeys use the sk_ prefix. Sandbox project keys never send real email or SMS.
Creates a verification and delivers the OTP or magic link.
| Field | Type | Req | Description |
|---|---|---|---|
| identifier | string | required | Email address or E.164 phone (+15551234567). |
| channel | "EMAIL" | "SMS" | required | Delivery channel. |
| kind | "OTP" | "MAGIC_LINK" | required | OTP code or clickable magic link. |
| redirectUrl | string (URL) | optional | Redirect after magic link click. Must match allowlist if configured. |
| metadata | object | optional | Arbitrary key/value pairs stored with the verification. |
{
"identifier": "user@example.com",
"channel": "EMAIL",
"kind": "OTP"
}{
"identifier": "user@example.com",
"channel": "EMAIL",
"kind": "MAGIC_LINK",
"redirectUrl": "https://yourapp.com/dashboard"
}{
"identifier": "+15551234567",
"channel": "SMS",
"kind": "OTP"
}{
"id": "cmqz5di97000qq48tkwksjltp",
"status": "PENDING",
"channel": "EMAIL",
"kind": "MAGIC_LINK",
"expiresAt": "2026-06-29T12:01:14.740Z"
}{
"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."
}
}{
"error": "Message delivery failed. Check your delivery config.",
"detail": "Resend delivery failed: Domain not verified"
}Validates the OTP the user entered. Marks the verification VERIFIED on success.
| Field | Type | Req | Description |
|---|---|---|---|
| token | string | required | The OTP code entered by the user. |
{
"token": "482910"
}{
"id": "cmqz5di97000qq48tkwksjltp",
"status": "VERIFIED",
"verifiedAt": "2026-06-29T12:05:00.000Z",
"redirectUrl": "https://yourapp.com/dashboard"
}{
"error": "Invalid token",
"attemptsRemaining": 2
}{
"error": "Verification has expired"
}Magic links verify automatically when the user clicks the link in their email. You do not call this endpoint yourself.
/api/v1/verify/magic?token={token}&id={verificationId}Called by the link in the email. Marks VERIFIED and redirects to redirectUrl.
On failure the user is redirected to /verify/error?reason=expired|invalid|not_found.
Sandbox projects capture messages instead of sending real email or SMS. Read them in the dashboard inbox or via API.
All captured messages for an identifier, newest first.
{
"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"
}
]
}Most recent magic link for an identifier.
{
"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"
}{
"error": "No magic link found for this identifier"
}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 succeedsverification.verified — user completed verification (OTP check or magic link click){
"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"
}
}{
"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"
}
}{
"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"
}
}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.
BASE_URL=https://your-app.run.app API_KEY=sk_sandbox_key yarn smokeScheduled 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.
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 return JSON with an error string and optional detail or details.
| Status | Meaning |
|---|---|
| 401 | Missing or invalid API key. |
| 403 | Sandbox endpoint called on a production project. |
| 404 | Verification or resource not found. |
| 409 | Already verified. |
| 410 | Expired or max attempts reached. |
| 422 | Invalid request body. |
| 429 | Rate limit exceeded. |
| 502 | Delivery provider failed. |
{
"error": "Invalid request body",
"details": {
"fieldErrors": {
"channel": ["Invalid enum value. Expected 'EMAIL' | 'SMS'"]
}
}
}Masked preview only. Requests are authenticated server-side using the selected project and key — the full secret is never sent to the browser.
/api/v1/verificationsRequest preview
POST /api/v1/verifications Authorization: Bearer sk_xxxxxxxxxxxx
Response
Send a request to see the live response here.
Create an OTP or magic-link verification. · Authenticated calls are proxied through /api/dashboard/simulator/proxy to /api/v1