CallableAI

Documentation

Example — a signup flow

A complete phone-verified signup in Node/Express and Python/Flask, with the error handling a real funnel needs.

Two working backends, both doing the same thing: send a code, check it, and record the result. The comments mark the two places integrations most often go wrong — the key and the id both belong on your server.

Node / Express

// Your backend. Never the browser — an API key in browser code is
// an API key every visitor has.
const CALLABLE = "https://api.callable.com.au";
const KEY = process.env.CALLABLE_API_KEY;

async function callable(path, body) {
  const res = await fetch(CALLABLE + path, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) {
    throw Object.assign(new Error(json.error.message), { code: json.error.code });
  }
  return json;
}

app.post("/signup/phone", async (req, res) => {
  try {
    const { id } = await callable("/v1/otp/send", {
      phone: req.body.phone,
      country: "AU",
      channel: "sms",
      metadata: { user_id: req.user.id },
    });
    // Keep the id server-side. Never send it to the client.
    req.session.verificationId = id;
    res.json({ sent: true });
  } catch (err) {
    if (err.code === "resend_too_soon" || err.code === "phone_rate_limited") {
      return res.status(429).json({ error: err.message });
    }
    if (err.code === "invalid_phone") {
      return res.status(400).json({ error: "That doesn't look like a phone number." });
    }
    throw err;
  }
});

app.post("/signup/verify", async (req, res) => {
  const result = await callable("/v1/otp/check", {
    verification_id: req.session.verificationId,
    code: req.body.code,
  });

  if (!result.verified) {
    return res.status(400).json({
      error: "That code is not right.",
      attemptsRemaining: result.attempts_remaining,
    });
  }

  // Callable does not remember this. You do.
  await db.users.update(req.user.id, { phone_verified_at: new Date() });
  res.json({ verified: true });
});

Python / Flask

# Your backend. Never the browser.
import os
import requests
from flask import session, request, jsonify

CALLABLE = "https://api.callable.com.au"
KEY = os.environ["CALLABLE_API_KEY"]


class CallableError(Exception):
    def __init__(self, code, message):
        super().__init__(message)
        self.code = code


def callable_post(path, body):
    res = requests.post(
        f"{CALLABLE}{path}",
        json=body,
        headers={"Authorization": f"Bearer {KEY}"},
        timeout=30,
    )
    data = res.json()
    if not res.ok:
        raise CallableError(data["error"]["code"], data["error"]["message"])
    return data


@app.post("/signup/phone")
def send_code():
    try:
        result = callable_post("/v1/otp/send", {
            "phone": request.json["phone"],
            "country": "AU",
            "channel": "sms",
            "metadata": {"user_id": current_user.id},
        })
    except CallableError as err:
        if err.code == "invalid_phone":
            return jsonify(error="That doesn't look like a phone number."), 400
        if err.code in ("resend_too_soon", "phone_rate_limited"):
            return jsonify(error=str(err)), 429
        raise

    session["verification_id"] = result["id"]   # keep it server-side
    return jsonify(sent=True)


@app.post("/signup/verify")
def check_code():
    result = callable_post("/v1/otp/check", {
        "verification_id": session["verification_id"],
        "code": request.json["code"],
    })

    if not result["verified"]:
        return jsonify(
            error="That code is not right.",
            attempts_remaining=result.get("attempts_remaining"),
        ), 400

    # Callable does not remember this. You do.
    db.users.mark_phone_verified(current_user.id)
    return jsonify(verified=True)

Getting it right

  • Keep the key server-side. Environment variable or secret manager, never a repository, a browser bundle, or a mobile app.
  • Keep the id server-side too. Store it in the session. A client that can choose which verification is being checked is a client that can skip verification.
  • Store the verification result yourself. CallableAI does not track that a number is verified for your product.
  • Handle verified: false as a normal outcome, not an exception. Show attempts_remaining so your user knows where they stand.
  • Re-read the phone we return the first time you integrate. It confirms your country handling is right.
  • Expect the terminal statuses. expired and failed are ordinary in a real signup funnel; offer a resend rather than an error page.
  • Name your keys for where they run — "Signup form (production)". You will want to know which one to rotate.
  • Rotate by creating first. Create the new key, deploy it, then revoke the old one; revocation takes effect on the very next request.

Support

Something this reference does not cover? Email support@callable.com.au.