Skip to content

One-time verification (OTP)

Fabric’s verify resource sends a one-time code over SMS and checks it for you, so you never store or compare codes yourself.

  1. Start a verification

    const started = await fabric.verify.start(
    { to: "+233201234567" },
    { idempotencyKey: verificationAttemptId },
    );
    // { id, status, to, channel: "sms", expiresIn, expiresAt, debugCode? }

    Persist started.data.id against the user’s session. Use absolute expiresAt for the deadline; unlike the convenience expiresIn duration, it stays exact after a replay. In sandbox, debugCode returns the code so you can test without a handset — it is never present on a live verification.

    Reuse the same stable key when retrying one logical attempt. Fabric replays the original verification reference instead of sending and charging for a second code.

  2. Check the submitted code

    const result = await fabric.verify.check({ id, code: userInput });
    if (result.data.verifiedAt !== null) {
    // verified — verifiedAt holds the timestamp; result.data.status reflects the outcome
    }

By default the code arrives in Fabric’s wording. To send your own, pass template — the stable key of a released SMS message definition — with any variables it declares:

await fabric.verify.start({
to: "+233201234567",
template: "merchant.otp",
variables: { merchant_name: "Jasper's Market" },
locale: "fr-FR",
});

The template is chosen per call, not configured per workspace, so one integration can send differently-branded codes for each merchant it serves. Requires SDK 0.1.0-beta.9 or later.

  • Use an application-scoped key. The key names the environment the definition was released in. A definition released into a different application’s environment answers verify_template_not_released — the single most common way this fails first time.
  • code, expires_minutes and expires_seconds are Fabric’s. Your template renders them, but passing one as a variable is a 400: the code is generated server-side and never accepted from a caller. Variable names reach the API exactly as your template declares them.
  • The definition must be transactional and contain {{code}}. Both are checked when the code is rendered, before the message is accepted or the wallet is touched, so an ineligible template costs nothing.

Authoring definitions is a dashboard task — an sk_* key cannot create them.

  • Cap attempts. Limit how many times a user may submit a code for one id, and how many codes a user or IP may request in a window. Verification is a common abuse target.
  • Do not reveal account existence. Show the same “we sent a code” response whether or not the number maps to an account. Branch on the verify result, not on whether the account exists.
  • Never log the code. Treat the OTP like a password. Log the verification id and outcome, not the code itself.
  • Respect expiry. After expiresIn, start a fresh verification rather than reusing the old id.

An OTP is transactional, so it is delivered around the clock — it is not subject to the promotional quiet-hours window described in compliance. Keep verification traffic on the transactional class; sending OTPs as promotional risks them being held or blocked.