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:

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.

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:

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.

Related articles