Sending One-Time Passcodes With the 2FA API
If you want to add OTP-based verification to your own product, Blocx provides a purpose-built API across SMS, email, and Telegram channels. You write code; we handle code generation, secure storage, expiry, delivery, and rate limits.
Manage templates at 2FA → Templates. View performance at 2FA → Reports. Inspect specific attempts at 2FA → Debug.
Why use the 2FA API instead of /messaging/send
The 2FA API:
- Generates the code server-side so it never appears in your application logs.
- Verifies with built-in rate limiting per tenant, per recipient, and per verification — making brute-force attacks impractical.
- Expires codes automatically.
- Reports delivery and verification separately from your other messaging traffic.
Templates
Define a 2FA template under 2FA → Templates. A template is scoped to a single channel (SMS or EMAIL) and controls the wording that wraps the generated code.
- Channel —
SMSorEMAIL. - Name — internal label.
- Body — the message to send. Must include the
{{code}}placeholder, which we substitute with the generated code at send time. SMS bodies are limited to 480 characters; email bodies to 20,000. - Subject (EMAIL only) — the email subject line.
- Body type (EMAIL only) —
HTMLorTEXT.
New templates are approved on create and usable immediately — no review step. Editing an existing template keeps it approved; you don't have to resubmit. (If you have older templates still sitting in PENDING_REVIEW from before this change, those continue through the legacy review flow.)
SMS templates require your own number
Templated SMS can only be sent from a phone number provisioned to your tenant. The shared system longcode does not accept templated content — sends without fromPhoneNumberId will fail with a 422 and code: template_requires_own_number.
Buy a number at Numbers → Buy, attach it to a messaging profile, and include its ID as fromPhoneNumberId on every templated SMS request. Verifications that use the default code-only message (templateId omitted) can still go through the system number — the restriction is specific to templates.
Email templates have no equivalent restriction.
Sending a code
curl -X POST https://api.otterblocx.com/twofa/verifications \
-H "x-access-key-id: $BLOCX_ACCESS_KEY_ID" \
-H "x-secret-access-key: $BLOCX_SECRET_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "SMS",
"to": "+15557654321",
"templateId": "tmpl_xyz",
"fromPhoneNumberId": 42
}'
Response (201 Created):
{
"verificationId": "ver_...",
"status": "pending",
"expiresAt": "2026-05-11T17:19:02Z"
}
For email, send "channel": "EMAIL" and an email address as to. For Telegram (where supported), send "channel": "TELEGRAM" with an E.164 phone number; Telegram does not support custom templates.
Keep the returned verificationId — you'll use it to verify the code the user enters.
Verifying
curl -X POST https://api.otterblocx.com/twofa/verifications/ver_.../check \
-H "x-access-key-id: $BLOCX_ACCESS_KEY_ID" \
-H "x-secret-access-key: $BLOCX_SECRET_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{ "code": "123456" }'
Response (200 OK):
{
"verificationId": "ver_...",
"status": "approved"
}
status is one of pending, approved, expired, or failed. A 404 means the verificationId doesn't belong to your tenant or never existed.
SDK
import { createBlocxClient, FaVerifications } from '@otterlabs/blocx'
const { client } = createBlocxClient({
accessKeyId: process.env.BLOCX_ACCESS_KEY_ID!,
secretAccessKey: process.env.BLOCX_SECRET_ACCESS_KEY!,
})
const created = await FaVerifications.createVerification({
client,
body: {
channel: 'SMS',
to: '+15557654321',
templateId: 'tmpl_xyz',
fromPhoneNumberId: 42, // required for templated SMS
},
})
const checked = await FaVerifications.checkVerification({
client,
path: { id: created.data!.verificationId },
body: { code },
})
Reports & Debug
2FA → Reports breaks down sends, deliveries, and verification outcomes by template and channel. 2FA → Debug shows individual verifications, including each check attempt and its outcome.
Compliance
Carriers privilege the 2FA use case on 10DLC — to keep that privilege:
- Send only transactional verification codes via this API. Don't route promotional SMS through it.
- Don't include marketing in the template body.
- Use a separate template per app/product so reports stay meaningful.
Because new templates are auto-approved, the content you publish goes live without us reviewing it. That's also why templated SMS has to ride on a tenant-owned number — the message is sent under your campaign registration, not ours. If we see abuse on a shared system identity (templated content slipping through email, for instance) we may reintroduce review without notice.