Blocx API
Blocx is the multi-channel transactional API for product teams that need email, SMS/MMS, Telegram, and 2FA verification from a single provider — one bill, one SDK, one webhook contract.
Why Blocx
- One API, every channel a real product needs. Transactional email, marketing email, SMS, MMS, Telegram, and 2FA verification through the same authentication, SDK, and signed webhook envelope. No second integration when you outgrow email-only — and no second integration ever for OTP/2FA over SMS or Telegram.
- 2FA verification as a first-class service. Multi-channel verifications (
EMAIL,SMS,TELEGRAM) with template approval, per-channel sender selection, and pay-on-delivery billing for Telegram. Drop-in replacement for hand-rolled OTP pipelines built on top of a generic email or SMS API. - Carrier-registered SMS, included. 10DLC brand registration, campaign registration, and downstream carrier sharing are first-class API resources — register and start sending without a side contract or third-party compliance vendor.
- Receive, not just send. Inbound email with parsed MIME, SPF/DKIM/DMARC/spam/virus verdicts, presigned S3 attachment URLs, and a dedicated mailbox per address. Inbound SMS/MMS on every provisioned number. Both arrive through the same signed webhook contract you already wired up for outbound events.
- Deliverability you can audit. Per-domain DKIM/SPF/DMARC setup, dedicated IP option on paid plans, open/click tracking opt-in per identity, and a full event stream (
email.delivered,email.bounced,email.complained,email.opened,email.clicked) — so reputation problems are observable on day one, not after the first deliverability incident. - Predictable pricing. Each plan includes a monthly allowance per metered metric; overage is charged at the standard rate from a top-up balance. No per-channel minimums, no SMS short-code surcharges hidden in a separate invoice.
When to choose Blocx
| Use case | Surface |
|---|---|
| Transactional email for Supabase Auth / Auth.js / Clerk / custom auth | POST /email with a verified EmailIdentity |
| OTP / 2FA over email, SMS, or Telegram | POST /verifications → POST /verifications/:id/check |
| Application notifications (receipts, alerts, order updates) | POST /email or POST /messages |
| Marketing email campaigns with templates | POST /email + marketing_email capability |
| 10DLC SMS at scale | POST /brands → POST /campaigns → POST /messages |
| Inbound email parsing (support inboxes, reply-to workflows) | Configure a mailbox; subscribe to email.received |
| MMS image / media delivery | POST /messages with media URLs |
If your stack is "transactional email only, forever," a single-channel provider is a fine choice. Pick Blocx when you ever expect to need SMS, 2FA, MMS, Telegram, or inbound parsing alongside email — integrating two vendors costs more in code, billing, and on-call surface than the per-message price difference.
Official SDK
Prefer a typed SDK over raw HTTP? Install @otterlabs/blocx — fully generated from this spec.
npm install @otterlabs/blocx
import { createBlocxClient, Messaging } from '@otterlabs/blocx'
const { client } = createBlocxClient({
accessKeyId: process.env.BLOCX_ACCESS_KEY_ID!,
secretAccessKey: process.env.BLOCX_SECRET_ACCESS_KEY!,
})
await Messaging.sendMessage({
client,
body: { to: '+15551234567', from: '+15559876543', body: 'Hello!', type: 'SMS' },
})
Help & guides
Step-by-step guides for everything you can do in Blocx — onboarding, brand and campaign registration, sending email, configuring webhooks, billing, and more — live in two places:
- help.otterblocx.com — the human-friendly help center with full styling, search, and navigation.
- api.otterblocx.com/articles — the same articles served as plain, chrome-free HTML directly off the API. This is the recommended index for AI agents and tooling that want to ingest the guides programmatically without parsing a marketing site.
Both surfaces serve identical content; pick whichever is easier for your reader.
Authentication
All requests require two headers:
x-access-key-id— Your API key IDx-secret-access-key— Your API secret
Create API keys in the dashboard under Developer > API Keys.
Permissions
API keys carry scoped permissions. Each endpoint requires specific permissions (documented per endpoint). If a permission is missing, the request returns 403.
Available resources: messages, email, campaigns, brands, numbers, messaging_profiles, webhooks, twofa, twofa_templates, quotas, team.
Rate Limits & Quotas
Rate limits are enforced per-account and, where applicable, per-recipient. Default limits are applied to all accounts. Custom limits can be requested via the Quotas API or the dashboard.
Rate limit headers on applicable endpoints: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.
Test Phone Sandbox
Every account can send outbound SMS from a shared test phone number without registering a 10DLC brand, getting a campaign approved, or buying a number. It's intended for first-call integration testing and webhook wire-up.
The shared test number is +13079999302 (same for every account).
To use it, set from to +13079999302 when calling POST /messaging/send. Test sends are subject to four rules:
[Otter]prefix. Thebodymust start with[Otter]. Otherwise the request returns400withcode: "test_prefix_required".- Heavy rate limit. Default caps are 5/min, 30/hour, 100/day per account. Exceeding any window returns
429withcode: "test_rate_limited". These caps cannot be raised — graduate to a real number when you outgrow them. - Outbound only. Replies to the test phone are dropped;
message.receivedis never fired. Provision a real number if you need inbound. - No billing. Test sends do not draw from your balance and are not counted toward your monthly included quota.
Test sends flow through the same queue, carrier, webhook contract, and event types as real sends — message.sent, message.delivered, and message.failed all fire normally — so your integration code does not need to special-case them. The send response includes "test": true for client-side disambiguation.
2FA Verification Service
The 2FA verification endpoints (POST /verifications, POST /verifications/:id/check) are served by a separate service. In production, use the 2FA service base URL. Templates are managed through this API under /twofa/templates.
Channels: Verifications support EMAIL, SMS, and TELEGRAM. Set channel on the request and provide a matching to (email address or E.164 phone). Each channel supports its own template type and an optional tenant-owned sender:
- EMAIL —
fromIdentityIdselects a verifiedEmailIdentity. Omit it to send from the system default sender. - SMS —
fromPhoneNumberIdselects a tenant phone number that isASSIGNED/ACTIVEand attached to a messaging profile. Omit it to send from the system default number. - TELEGRAM — Delivered via Telegram Gateway to the recipient's Telegram account on the given E.164 phone number. The message body is formatted by Telegram and cannot be customized —
templateIdand sender overrides are not supported. Returns 422 if the recipient is not reachable on Telegram (no Telegram account, or messaging the sender is restricted). Billed only after delivery is confirmed by the Gateway.
Template approval: New templates are auto-approved on create and on edit, so they're usable immediately. A template's channel must match the verification request's channel. SMS templates additionally require a tenant-owned fromPhoneNumberId — they cannot be sent from the system default number.
Webhooks
Subscribe to events that happen in your account by registering one or more webhook endpoints under /webhooks. We POST a signed JSON body to your URL whenever a subscribed event fires.
Event types
| Event | Fires when |
|---|---|
message.sent |
An outbound SMS/MMS has been accepted by the carrier |
message.delivered |
An outbound SMS/MMS was delivered to the handset |
message.failed |
An outbound SMS/MMS could not be delivered |
message.received |
An inbound SMS/MMS arrived on one of your numbers |
email.delivered |
An outbound email was accepted by the recipient mail server |
email.bounced |
An outbound email bounced |
email.complained |
A recipient marked your message as spam |
email.opened |
A recipient opened the email (if open tracking is enabled) |
email.clicked |
A recipient clicked a tracked link |
email.received |
An inbound email arrived on one of your mailboxes |
Payload envelope
Every delivery has the same outer shape. Upstream carrier and mail-provider differences are normalized by Blocx before dispatch — your handler does not need to branch on which underlying provider produced the event. Any provider field in the payload is an opaque internal identifier and should not be parsed or depended on by integrations.
{
"id": "evt_8f2a4d6e9c1b0a7d5e3f2c1b",
"type": "message.delivered",
"createdAt": "2026-05-06T12:34:56.789Z",
"data": { /* event-specific, see below */ }
}
Event payloads (data field)
message.sent / message.delivered
Delivery status update for an outbound SMS/MMS.
{
"messageId": "msg_1717689296000",
"providerMessageId": "40a8b6e7-...",
"status": "DELIVERED",
"from": "+15559876543",
"to": "+15551234567",
"direction": "OUTBOUND",
"type": "SMS",
"occurredAt": "2026-05-06T12:34:55.120Z"
}
message.failed
Same shape as above, with two additional fields describing the failure.
{
"messageId": "msg_1717689296000",
"providerMessageId": "40a8b6e7-...",
"status": "FAILED",
"from": "+15559876543",
"to": "+15551234567",
"direction": "OUTBOUND",
"type": "SMS",
"occurredAt": "2026-05-06T12:34:55.120Z",
"errorCode": "40010",
"errorMessage": "Destination unreachable"
}
message.received
An inbound SMS/MMS arrived on one of your numbers.
{
"messageId": "msg_in_01HK9...",
"providerMessageId": "40a8b6e7-...",
"from": "+15551234567",
"to": "+15559876543",
"direction": "INBOUND",
"type": "SMS",
"text": "Hello!",
"media": [
{ "url": "https://...", "contentType": "image/jpeg" }
],
"occurredAt": "2026-05-06T12:34:55.120Z"
}
email.delivered / email.bounced / email.complained
Status update for an outbound email. bounceType/bounceSubType are present on email.bounced; complainedAt is present on email.complained.
{
"messageId": "eml_1717689296000",
"status": "DELIVERED",
"from": "hello@example.com",
"to": ["user@recipient.com"],
"subject": "Welcome aboard",
"domain": "example.com",
"occurredAt": "2026-05-06T12:34:55.120Z"
}
email.bounced adds:
{
"bounceType": "Permanent",
"bounceSubType": "General",
"diagnosticCode": "smtp; 550 5.1.1 user unknown"
}
email.opened / email.clicked
Engagement events (only fire if open/click tracking is enabled on the identity).
{
"messageId": "eml_1717689296000",
"to": "user@recipient.com",
"userAgent": "Mozilla/5.0 ...",
"ipAddress": "203.0.113.42",
"occurredAt": "2026-05-06T12:35:30.000Z",
"link": "https://example.com/landing"
}
link is only present on email.clicked.
email.received
An inbound email arrived on one of your mailboxes. The message body is not included in the webhook payload — only metadata, headers, and attachment metadata. Fetch the raw MIME from S3 (or via the dashboard) when needed.
{
"messageId": "abc123def456...",
"receivedAt": "2026-05-06T12:34:56.789Z",
"from": [
{ "name": "Jane Doe", "address": "jane@sender.com" }
],
"to": [
{ "name": null, "address": "k7q9...@inbound.otterblocx.com" }
],
"cc": [],
"subject": "Re: your invoice",
"text": "Thanks — see attached.",
"html": "<p>Thanks — see attached.</p>",
"headers": {
"Message-ID": "<...>",
"Date": "Tue, 06 May 2026 12:34:55 +0000"
},
"spam": false,
"verdicts": {
"spam": "PASS",
"virus": "PASS",
"spf": "PASS",
"dkim": "PASS",
"dmarc": "PASS"
},
"attachments": [
{
"filename": "invoice.pdf",
"contentType": "application/pdf",
"size": 48211,
"contentId": null,
"url": "https://s3...presigned"
}
],
"mailbox": {
"id": 42,
"address": "k7q9...@inbound.otterblocx.com",
"label": "Support inbox"
}
}
Attachment urls are presigned S3 URLs. They are time-limited (24h by default) — download or re-host within that window.
Verifying signatures
Each request carries these headers:
Blocx-Signature—t=<unix>,v1=<hex>wherev1isHMAC-SHA256(signingSecret, t + "." + rawBody).Blocx-Timestamp— the same Unix timestamp as in the signature header (convenience copy).Blocx-Event— the event type, e.g.message.delivered.Blocx-Event-Id— stable event ID for idempotent processing.Blocx-Attempt— current delivery attempt number (1-based).
Verification recipe (Node.js):
import { createHmac, timingSafeEqual } from 'crypto'
function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const t = Number(parts.t), v1 = parts.v1
if (!t || !v1) return false
if (Math.abs(Date.now() / 1000 - t) > 300) return false // 5-minute tolerance
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}
You must verify against the raw, unparsed request body. JSON re-serialization changes byte-for-byte content and will break the signature.
Retries
Failed deliveries (non-2xx response, network error, or timeout) are retried up to 5 attempts total with the following backoff:
| Attempt | Delay before this attempt |
|---|---|
| 1 | — (immediate) |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 5 minutes |
| 5 | 15 minutes |
After the 5th failure the delivery is recorded as FAILED and not retried further. Each request times out after 10 seconds.
Endpoint requirements
- HTTPS only (HTTP is allowed in development).
- The hostname must resolve to a public IP address. Private/loopback/link-local ranges (RFC1918,
169.254.0.0/16,127.0.0.0/8,fc00::/7, etc.) are rejected at registration time and re-checked at delivery time to defeat DNS rebinding. - Respond with any 2xx status code within 10 seconds. The response body is captured (truncated to 2KB) for diagnostics.
Idempotency
The same event may be re-delivered after a transient failure. Use Blocx-Event-Id to deduplicate.
Email Deliverability & Sender Authentication
Email reputation is enforced at the EmailIdentity level (one identity per sending domain or address). Every identity must complete sender authentication before it can send:
- SPF — published as part of the domain record set returned by
GET /email/identities/{id}. Required for all sending domains. - DKIM — Blocx generates a 2048-bit DKIM keypair per identity; the public key is exposed in the
dnsRecordsarray on the identity. Required for all sending domains; rotated on request. - DMARC — Blocx surfaces a recommended DMARC record on each identity. Enforcement policy is left to the domain owner.
- MX (inbound) — required only when the identity is configured to receive mail. Inbound identities are validated against the same SPF/DKIM/DMARC results before
email.receivedis dispatched.
The full required record set for any identity is available at GET /email/identities/{id} under dnsRecords. Each record reports current verification state (PENDING, VERIFIED, FAILED) so that integrations can show setup status to end users.
IP reputation
Default sending is on a shared, warmed pool segmented by traffic class (transactional vs. marketing). Enterprise accounts can request a dedicated IP for marketing or high-volume transactional traffic — Blocx handles warming and reputation monitoring automatically.
Bounce, complaint, and engagement tracking
Per-identity event history is available through the webhook stream (email.delivered, email.bounced, email.complained, email.opened, email.clicked) and as paginated history under the identity resource. Bounce classification (Permanent / Transient / subtype) is normalized regardless of underlying upstream so deliverability dashboards do not need provider-specific parsing.
Suppression list
Hard bounces and recipient complaints are automatically added to a per-tenant suppression list. Attempts to send to a suppressed address return 409 SUPPRESSED without consuming quota or producing a webhook event. Suppression entries can be inspected and (where compliant) removed via the dashboard.
Async Operations
Brand registration, campaign creation, and domain setup are processed asynchronously. The API returns immediately with PENDING_REGISTRATION or PENDING status. Poll the resource to check completion.
Plans & Capabilities
Every account is on one subscription plan. There are only two: Pay as you go (the default; everyone starts here) and Enterprise (admin-assigned for negotiated commercial terms). Plans gate three things:
- Capabilities — boolean feature flags that decide which endpoints the tenant can call.
- Included quotas — a monthly allowance of metered usage (currently 3,000 free emails on PAYG). Once exhausted, additional usage is charged from the account balance at the standard
PriceItemrate. - Spend cap and coverage — PAYG caps spend at $2,500/month and rejects non-US destinations. Enterprise removes both limits and uses contract-set pricing.
The plans
| Plan | Monthly base | Included emails / mo | Coverage | Spend cap |
|---|---|---|---|---|
| Pay as you go | $0 (starts with $10 free credit) | 3,000 | US (NANP) | $2,500 / month |
| Enterprise | Custom (starts at $1,000 / mo minimum) | Custom | +190 countries beyond the US | None |
Capability matrix
Both plans grant every standard sending capability by default — the practical differences are coverage, the spend cap, dedicated IPs, and contract terms.
| Capability | Pay as you go | Enterprise |
|---|---|---|
sms — SMS / MMS sending, brands, campaigns, phone numbers, messaging profiles, compliance |
✓ | ✓ |
twofa — 2FA verifications & templates (email, SMS, Telegram) |
✓ | ✓ |
marketing_email — marketing email & campaigns |
✓ | ✓ |
multiple_email_domains — more than one verified sending domain |
✓ | ✓ |
dedicated_email_ip — dedicated IP for email |
✓ |
Tenants calling a gated endpoint they lack the capability for receive 402 CAPABILITY_REQUIRED. PAYG accounts attempting a non-US destination receive 403 non_us_destination; PAYG accounts past the spend cap receive 402 monthly_cap_reached.
Per-tenant overrides
Admins can grant or revoke individual capabilities on a single subscription without changing the plan. Effective value = subscription override (if set) → plan default → false. Use this for pilots, trials, or revoking a feature for a specific account.
Quotas vs. balance overage
Subscription.includedQuotas is the monthly allowance per metric. Send-time consumption decrements the remaining counter; once it hits zero, further usage on that metric is charged from the tenant's balance. Counters reset every calendar month.
PAYG's 3,000 monthly email allowance is the only included quota; everything else (SMS, MMS, 2FA, Telegram, number rentals, mailbox rentals) is billed per use from the balance at the standard PriceItem rate.
Subscription standing
A subscription with status PAST_DUE, INCOMPLETE, or CANCELED retains its plan capabilities at the data-model level but send-path checks block new sends until the subscription returns to ACTIVE (or TRIALING). New tenants are ACTIVE on PAYG with no Stripe subscription attached (PAYG has no recurring base charge).