Choosing a 2FA SMS provider: sender registration sets your date

For US and EU two-factor login, the registration queue decides your launch, not the API. A country-by-country rubric with runnable OTP and signature calls.

Pick your 2FA provider on how it handles sender identity in each country you’ll log users in from, because that’s the queue that sets your ship date. Writing the code takes an afternoon: Infrai’s managed OTP is two calls, POST /v1/sms/otp to issue and POST /v1/sms/verify to check, with code generation, expiry and attempt limits handled server-side. Getting a US carrier to accept your traffic takes considerably longer, and no API design shortens it.

Two teams with identical code can be four weeks apart on launch. The difference is whether someone started the registration paperwork on day one — so treat “how does this vendor get me registered” as the selection criterion, and treat the SDK ergonomics as a tiebreaker.

Managed OTP changes what you need to register

If you generate the six digits yourself and send them through a normal message, you own code storage, TTL, attempt counting and the replay window. If the gateway does it, you own none of that:

export INFRAI_API_KEY="your_infrai_api_key"

curl -sS -X POST "https://api.infrai.cc/v1/sms/otp" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+447700900123",
    "template": "Your Acme code is {code}. It expires in 5 minutes."
  }'
{
  "ok": true,
  "data": { "request_id": "smsotp_01JTQ4W7M2B9XK5C0RZ8VNHDEP", "sent": true }
}

Never put the digits in that body yourself — the gateway generates and stores them, and a code field on the issue call is a design smell that means the codes are living somewhere in your logs.

Checking is the other half:

curl -sS -X POST "https://api.infrai.cc/v1/sms/verify" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"to": "+15005550001", "code": "000000"}'
{
  "ok": true,
  "data": { "verified": false, "reason": "no_code_issued", "to": "+15005550001" },
  "metadata": { "cost_usd": 0.005, "latency_ms": 83 }
}

Note what that response is: HTTP 200 with verified: false. A wrong code is a successful API call, so branch on data.verified rather than on the status code — and the drawback worth budgeting for is that the check bills whether or not the code was right, so a brute-force attempt against your login form costs you money as well as the attacker’s time. Rate-limit verification attempts per account in your own controller.

The country matrix that actually drives the schedule

RegionSender identityRegistration realityAlphanumeric allowed
United StatesLong code or short codeA2P 10DLC brand plus campaign vetting; unregistered traffic gets filtered silentlyNo
United Kingdom, Germany, NetherlandsAlphanumeric sender IDUsually usable immediatelyYes
France, Italy, SpainAlphanumeric sender IDPer-country pre-registration schemesYes, once registered
India, ChinaSignature plus approved templateMandatory carrier review before any sendTemplate-bound

The US row is the one that surprises European teams. Unregistered A2P traffic on a long code isn’t rejected with an error you can act on — it’s filtered, so your API call reports success and the handset stays quiet. The sender-ID rules for the rest of the list are maintained publicly by several providers, including this country-by-country reference.

Registering identity as an API call

curl -sS -X POST "https://api.infrai.cc/v1/sms/signature/create" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "AcmeID",
    "type": "company",
    "proof_url": "https://acme.example.com/legal/registration.pdf"
  }'

The response carries signature_id and a review_state that starts at pending. Poll the list rather than guessing:

curl -sS "https://api.infrai.cc/v1/sms/signature/list" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"

Each entry comes back with signature_id, name, type, created_at and review_state — and a signature sitting at pending is exactly the state that produces SMS_SENDER_NOT_REGISTERED when someone tries to ship. Wire that read into your deployment checklist and the failure becomes a blocked deploy instead of a silent outage in one country.

One limitation to know before designing around templates: submitting a custom template through POST /v1/sms/template/create answers HTTP 402 PRO_REQUIRED on a standard account. Carrier-approved templates are a paid-plan capability. For US and European 2FA you don’t need them — the OTP template string above is sufficient — but if your first market is China or India, budget for the plan and the review time together.

Routing by region in code

// otp-region.mjs — Node 22, ESM. Picks sender config from the dialling code.
const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("set INFRAI_API_KEY");

const PROFILES = [
  { match: /^\+1\d{10}$/, sender: null, note: "US/CA: long code, 10DLC registered" },
  { match: /^\+(44|49|31)\d{6,}$/, sender: "AcmeID", note: "UK/DE/NL: alphanumeric" },
  { match: /^\+(33|39|34)\d{6,}$/, sender: "AcmeID", note: "FR/IT/ES: pre-registered alphanumeric" },
];

export function profileFor(phone) {
  const hit = PROFILES.find((p) => p.match.test(phone));
  if (!hit) throw new Error(`no sender profile for ${phone}`);
  return hit;
}

export async function issueCode(phone) {
  const profile = profileFor(phone);
  const res = await fetch(`${API}/v1/sms/otp`, {
    method: "POST",
    headers: { authorization: `Bearer ${KEY}`, "content-type": "application/json" },
    body: JSON.stringify({
      to: phone,
      template: "Your Acme code is {code}. It expires in 5 minutes.",
    }),
  });
  const payload = await res.json();
  if (!res.ok) {
    const err = new Error(payload?.error?.message ?? `HTTP ${res.status}`);
    err.code = payload?.error?.code;
    throw err;
  }
  return { requestId: payload.data.request_id, profile: profile.note };
}

export async function checkCode(phone, code) {
  const res = await fetch(`${API}/v1/sms/verify`, {
    method: "POST",
    headers: { authorization: `Bearer ${KEY}`, "content-type": "application/json" },
    body: JSON.stringify({ to: phone, code }),
  });
  const payload = await res.json();
  if (!res.ok) throw new Error(payload?.error?.message ?? `HTTP ${res.status}`);
  return { verified: payload.data.verified === true, reason: payload.data.reason ?? null };
}

Keeping the region table as data rather than branching logic means adding a country is a one-line change and a registration ticket, not a refactor.

What a verified login costs

Two billable events per successful login: the message and the check. Issuing is $0.007475 per message and verification is $0.005 per call, both verified 2026-07-26, so a clean login lands near $0.0125 and a user who fat-fingers the code once costs $0.0175. New accounts get $2 of free credit to test against. Signature reads and suppression checks are free. Confirm the live figures before you model a funnel on them, because rates here move down over time:

curl -sS "https://api.infrai.cc/v1/discovery" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  | jq '[.capabilities[] | select(.id == "sms.otp" or .id == "sms.verify")
        | {id, unit: .billing.unit, price: .billing.price_usd, trial: .billing.new_account_trial_uses}]'

Who to pick

Twilio Verify and Vonage Verify are the established choices, and both are the better pick if you want the provider to also run WhatsApp or voice fallback when SMS doesn’t land — that’s real functionality, not packaging, and rebuilding it is a project. Sinch is worth a quote if your volume is large enough to negotiate.

Infrai’s argument for a 2FA flow is narrower and honest: the OTP lifecycle is managed for you, sender registration is a REST call instead of a portal, and the same credential already covers the session store, the audit log, the email fallback and the error tracking around the login — one bill, one usage view, no second vendor to onboard for the thing your login does next. If two-way messaging or channel fallback is in your requirements, that’s the boundary, and Twilio is the safer answer.

References

Browse more sms developer guides