Bird

Build a verification form with Next.js or Express

Send a code, check it on your server and complete one intended application step. This local example stores the destination, action and successful result on the server. A browser cannot choose a different destination at check time or supply its own success flag.
The example uses a configured email address and completes an invented signup step. It does not create a real account, reset a password or prove a signed-in customer's identity. For recovery, load the destination and intended account from your application's recovery policy instead of accepting them from the check request.

Prepare the environment

Install @messagebird/sdk in an existing Next.js App Router app, or create an Express app with npm install express @messagebird/sdk and npm install --save-dev tsx @types/express @types/node. Use Node.js 20.9+.
Configure BIRD_API_KEY with the verify scope, DEMO_VERIFY_EMAIL with an address you control, APP_ORIGIN with http://127.0.0.1:3000, and APP_SECRET with a generated secret. Keep that secret stable across process restarts so pending request fingerprints remain comparable. Use server-only environment variables; do not prefix them with NEXT_PUBLIC_.
The configured address receives a real code. For a phone journey, replace the server-owned to value with { phone_number: "+your-international-number" } and follow country configuration and senders. Phone delivery can be billed. Channel order comes from the resolved plan, not from this demo.

Add the shared server handler

Save this as verify-server.ts in the application root. It uses a local file to retain demo sessions, request identities and completed results across process restarts. The code itself is not stored; a keyed fingerprint detects accidental key reuse with different input.
Ejemplo de código
import { BirdClient, BirdAPIError } from "@messagebird/sdk";
import { createHmac, randomUUID } from "node:crypto";
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";

function required(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Set ${name}.`);
  return value;
}
const bird = new BirdClient({ apiKey: required("BIRD_API_KEY") });
const origin = required("APP_ORIGIN");
const secret = required("APP_SECRET");
const destination = required("DEMO_VERIFY_EMAIL");
const intendedAction = "demo-signup";
const file = "verify-state.json";
const html = readFileSync("verify-form.html", "utf8");
type Operation = {
  operation: string;
  digest: string;
  response?: { status: number; body: Record<string, unknown> };
};
type Attempt = {
  to: { email: string };
  action: string;
  expires: number;
  verificationId?: string;
  completed?: boolean;
  pending?: string;
  operations: Record<string, Operation>;
};
const state: Record<string, Attempt> = existsSync(file)
  ? JSON.parse(readFileSync(file, "utf8"))
  : {};
function save() {
  writeFileSync(file + ".tmp", JSON.stringify(state));
  renameSync(file + ".tmp", file);
}
const busy = new Set<string>();
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
function json(body: Record<string, unknown>, status = 200) {
  return Response.json(body, { status, headers: { "Cache-Control": "no-store" } });
}

export async function handle(request: Request): Promise<Response> {
  let token = (request.headers.get("Cookie") ?? "")
    .split(";")
    .map((x) => x.trim())
    .find((x) => x.startsWith("verify_session="))
    ?.slice(15);
  if (token && !uuid.test(token)) token = undefined;
  if (request.method === "GET") {
    if (!token || !state[token] || state[token].expires < Date.now()) {
      for (const [key, attempt] of Object.entries(state))
        if (attempt.expires < Date.now()) delete state[key];
      if (Object.keys(state).length >= 100)
        return json({ error: "Local session limit reached." }, 503);
      token = randomUUID();
      state[token] = {
        to: { email: destination },
        action: intendedAction,
        expires: Date.now() + 3600000,
        operations: {},
      };
      save();
    }
    const attempt = state[token];
    const pending = attempt.pending
      ? { key: attempt.pending, operation: attempt.operations[attempt.pending].operation }
      : null;
    const page = html.replace(
      /<script type="application\/json" id="pending-request">\s*null\s*<\/script>/,
      `<script type="application/json" id="pending-request">${JSON.stringify(pending)}</script>`,
    );
    return new Response(page, {
      headers: {
        "Content-Type": "text/html; charset=utf-8",
        "Cache-Control": "no-store",
        "Set-Cookie": `verify_session=${token}; HttpOnly; SameSite=Strict; Path=/; Max-Age=3600`,
      },
    });
  }
  if (request.method !== "POST") return json({ error: "Method not allowed." }, 405);
  if (request.headers.get("Origin") !== origin) return json({ error: "Origin refused." }, 403);
  if (!token || !state[token] || state[token].expires < Date.now())
    return json({ error: "Open a fresh demo session." }, 401);
  const attempt = state[token];
  if (attempt.completed) return json({ resolved: true, completed: true, action: attempt.action });
  const key = request.headers.get("Idempotency-Key");
  if (!key || !uuid.test(key))
    return json({ resolved: true, error: "Supply an Idempotency-Key UUID." }, 400);
  let input: { operation?: unknown; code?: unknown };
  try {
    input = await request.json();
  } catch {
    return json({ resolved: !attempt.pending, error: "Invalid JSON." }, 400);
  }
  if (!input || !["start", "check", "resend", "next"].includes(String(input.operation)))
    return json({ resolved: !attempt.pending, error: "Choose a supported operation." }, 400);
  const operation = String(input.operation);
  const code = operation === "check" && typeof input.code === "string" ? input.code : "";
  if (operation === "check" && !/^\d{4,12}$/.test(code))
    return json({ resolved: !attempt.pending, error: "Enter the 4–12 digit code." }, 400);
  if (operation !== "start" && !attempt.verificationId)
    return json({ resolved: !attempt.pending, error: "Start this verification first." }, 409);
  const digest = createHmac("sha256", secret)
    .update(JSON.stringify([operation, code]))
    .digest("hex");
  const prior = attempt.operations[key];
  if (prior && prior.digest !== digest)
    return json({ error: "This key belongs to a different request." }, 409);
  if (prior?.response) return json(prior.response.body, prior.response.status);
  if (attempt.pending && attempt.pending !== key)
    return json({ error: "Resolve the earlier request before changing the step or code." }, 409);
  if (busy.has(token)) return json({ error: "A request is still running; retry it shortly." }, 409);
  if (!prior && Object.keys(attempt.operations).length >= 50)
    return json({ resolved: true, error: "Local attempt limit reached." }, 429);
  busy.add(token);
  attempt.operations[key] = { operation, digest };
  attempt.pending = key;
  save();
  let body: Record<string, unknown>;
  let status = 200;
  try {
    if (operation === "check") {
      const result = await bird.verify.verifications.check(
        { to: attempt.to, code },
        { idempotencyKey: key },
      );
      if (result.verification.id !== attempt.verificationId) {
        body = {
          resolved: true,
          error: "The result belongs to a different verification. Start a new flow.",
        };
        status = 409;
      } else {
        attempt.completed = result.success && result.verification.status === "verified";
        body = {
          resolved: true,
          completed: attempt.completed,
          action: attempt.action,
          reason: result.reason ?? null,
          attempts_remaining: result.attempts_remaining ?? null,
          verification_status: result.verification.status,
        };
      }
    } else {
      const result =
        operation === "next"
          ? await bird.verify.verifications.nextChannel({ to: attempt.to }, { idempotencyKey: key })
          : await bird.verify.verifications.create({ to: attempt.to }, { idempotencyKey: key });
      attempt.verificationId = result.id;
      body = {
        resolved: true,
        completed: false,
        verification_status: result.status,
        expires_at: result.expires_at,
        last_channel: result.last_channel ?? null,
      };
    }
  } catch (error) {
    if (error instanceof BirdAPIError && [404, 422, 429].includes(error.statusCode ?? 0)) {
      status = error.statusCode!;
      body = {
        resolved: true,
        error: error.code,
        message:
          status === 404
            ? "No active verification was found. This does not prove a failed code; review the earlier outcome."
            : status === 429
              ? "Wait before requesting another code or check."
              : "The request or next channel was refused; review your configuration.",
      };
    } else {
      console.error(error);
      busy.delete(token);
      return json({ error: "Outcome unknown. Retry with the same key, step and code." }, 503);
    }
  }
  attempt.operations[key].response = { status, body };
  delete attempt.pending;
  save();
  busy.delete(token);
  return json(body, status);
}
This file store is for one local process. In a deployed application, put the attempt, pending request and completed application action in the owning transactional database, with concurrency control, expiry and request limits. Do not run this file-backed example across serverless instances.

Choose a framework

For Next.js, save this as app/api/verify/route.ts and start the local server on 127.0.0.1:3000. Adjust the relative import if your application uses a src directory. Open /api/verify.
Ejemplo de código
import { handle } from "../../../verify-server";
export const runtime = "nodejs";
export const GET = handle;
export const POST = handle;
For Express, save this as server.ts, run npx tsx server.ts, and open /verify:
Ejemplo de código
import express from "express";
import { handle } from "./verify-server";
const app = express();
app.use(express.text({ type: "application/json", limit: "8kb" }));
app.all("/verify", async (req, res) => {
  const headers = new Headers();
  for (const [key, value] of Object.entries(req.headers))
    if (typeof value === "string") headers.set(key, value);
  const request = new Request("http://127.0.0.1:3000/verify", {
    method: req.method,
    headers,
    ...(req.method === "POST" ? { body: req.body } : {}),
  });
  const response = await handle(request);
  response.headers.forEach((value, key) => res.setHeader(key, value));
  res.status(response.status).send(await response.text());
});
app.listen(3000, "127.0.0.1");

Add the form

Save verify-form.html in the application working directory. Reloading recovers the pending request key and step from the server session. For a pending check, enter the original code again; the server stores only its keyed fingerprint. Retry an uncertain request with the same step and code. Changing the input while a request is unresolved is refused rather than silently starting another charge or check.
Ejemplo de código
<!doctype html>
<html lang="en">
  <meta charset="utf-8" /><meta
    name="viewport"
    content="width=device-width, initial-scale=1"
  /><title>Verify a destination · Bird example</title>
  <style>
    body {
      font: 16px/1.6 system-ui;
      max-width: 42rem;
      margin: 8vh auto;
      padding: 24px;
      color: #173c30;
      background: #fafaf7;
    }
    button,
    input,
    select {
      font: inherit;
      padding: 12px;
      border: 1px solid #bbcbbf;
      border-radius: 8px;
      margin: 6px;
    }
    pre {
      white-space: pre-wrap;
      padding: 20px;
      background: #eaf0e9;
      border-radius: 12px;
    }
  </style>
  <main>
    <h1>Verify your destination.</h1>
    <p>
      This local example sends a real verification to the destination configured on the server. It
      only completes a demo application action.
    </p>
    <form>
      <label
        >Step
        <select name="operation">
          <option value="start">Send a code</option>
          <option value="check">Check code</option>
          <option value="resend">Resend</option>
          <option value="next">Next channel</option>
        </select></label
      ><label>Code <input name="code" inputmode="numeric" autocomplete="one-time-code" /></label
      ><button>Continue</button>
    </form>
    <pre role="status">Ready</pre>
  </main>
  <script type="application/json" id="pending-request">
    null
  </script>
  <script>
    const form = document.querySelector("form"),
      output = document.querySelector("pre");
    let attempt = JSON.parse(document.querySelector("#pending-request").textContent);
    if (attempt) {
      form.elements.operation.value = attempt.operation;
      output.textContent = "A request is pending. Retry this step with the same code, if required.";
    }
    form.addEventListener("submit", async (e) => {
      e.preventDefault();
      const body = {
        operation: form.elements.operation.value,
        code: form.elements.operation.value === "check" ? form.elements.code.value : "",
      };
      if (attempt && attempt.operation !== body.operation) {
        output.textContent = "Resolve the previous request with the same step and code first.";
        return;
      }
      attempt ??= { operation: body.operation, key: crypto.randomUUID() };
      form.querySelector("button").disabled = true;
      try {
        const r = await fetch(location.pathname, {
          method: "POST",
          headers: { "Content-Type": "application/json", "Idempotency-Key": attempt.key },
          body: JSON.stringify(body),
        });
        const data = await r.json();
        output.textContent = JSON.stringify(data, null, 2);
        if (data.resolved) attempt = undefined;
      } catch {
        output.textContent =
          "Outcome unknown. Retry the same step and code; reload to recover a pending request.";
      } finally {
        form.querySelector("button").disabled = false;
      }
    });
  </script>
</html>

Test the customer journey

  1. Choose Send a code. Read verification_status, expires_at and last_channel from the actual response.
  2. Choose Check code, enter a wrong code, and inspect reason and attempts_remaining. HTTP 200 alone is not success.
  3. Enter the correct code. Completion requires success: true, the expected verification ID and the verified state. The local record retains completion so a duplicate request does not repeat the action or check a terminal verification again.
  4. Interrupt a request after it reaches the server, then reload the form or restart the server without removing its state file. The form recovers the pending key and step. Re-enter the same code for a pending check and retry. A completed session retains its application result.
  5. Test expiry, send limits and an exhausted channel plan. A 404 is not proof of an incorrect code; a verification may already be final.
Resend uses create again for the same recipient, subject to the configured cooldown and hourly limit. Resending does not extend the verification's expiry. Next channel advances the existing plan and can return 422 when exhausted. Earlier codes remain valid after advancing. For the email-only example, there may be no next channel.
The browser uses a new key for a deliberate new operation and retains a key for an uncertain retry. The API's idempotency retention is finite; follow idempotency before retrying old requests. Reopening the page with the same still-valid session recovers a pending identity from the server record. Keep the original code available until its check resolves; the recovery data contains no passcode.

Apply it to your product

Replace demo-signup with the exact application action you own. Commit the successful verification and that action's consumed marker together. An email code confirms control of that destination; it does not independently authorize an arbitrary account recovery or payment.
Use your application's existing session authentication, HTTPS, secure cookies, abuse limits and retention policy. Keep sender and country setup linked from the code screen's support path.
For the Python implementation, build the FastAPI verification form.