Skip to content

Quickstart

Send your first WhatsApp message in under ten minutes. You will use a test key, so you do not need a WhatsApp Business Account, a Meta app, or a connected phone number — the sandbox simulates the entire delivery lifecycle, including webhooks.

1. Get a test API key

Sign up at app.blueticked.com, then create a key from the quickstart preset (Developers → API keys): a test key with full access, so no step below is refused for a missing scope. Test keys start with blu_test_; live keys with blu_live_. Creating a key requires two-step verification on your account — enable it first if you skipped it during sign-up.

2. Install the SDK (optional)

Terminal
npm install @blueticked/sdk

The TypeScript SDK sends these same calls, typed, for TypeScript and JavaScript. For another language, point any OpenAPI generator at our OpenAPI 3.1 document, which is public and needs no key: https://app.blueticked.com/api/v1/openapi.json. For TypeScript types without the SDK: npx openapi-typescript https://app.blueticked.com/api/v1/openapi.json. The examples below use plain fetch so they run anywhere with no dependencies — every call is a normal HTTPS request with Authorization: Bearer blu_test_.... See the OpenAPI spec for the full surface.

3. Create a contact and send

On a test key this runs on a brand-new workspace — you do not need a connected channel, an approved template, or a business id.

send.ts
const API = "https://app.blueticked.com/api/v1";
const auth = {
  Authorization: `Bearer ${process.env.BLUETICKED_API_KEY}`,
  "Content-Type": "application/json",
};

// +27800000001 is a magic sandbox number: it always delivers.
await fetch(`${API}/contacts`, {
  method: "POST",
  headers: auth,
  body: JSON.stringify({ phone: "+27800000001", first_name: "Sam" }),
});

const res = await fetch(`${API}/messages`, {
  method: "POST",
  headers: { ...auth, "Idempotency-Key": crypto.randomUUID() },
  body: JSON.stringify({
    channel: "whatsapp",
    to: { phone: "+27800000001" },
    body: "Hello from the Blueticked sandbox.",
  }),
});

const message = await res.json();
console.log(message.status); // "queued"

Two more sandbox numbers let you build error handling and inbound before you go live: +27800000002 fails at the send step with recipient_unreachable, and +27800000003 delivers and then fires a simulated inbound reply — the same webhook a real customer reply produces.

4. Watch it deliver

The sandbox advances the message every few seconds: queued, then sent, then delivered, then read. (SMS and email stop at delivered — neither emits a read receipt live, so the sandbox does not invent one.) Poll it, or better, receive webhooks:

poll.ts
const status = await fetch(`${API}/messages/${message.message_id}`, {
  headers: auth,
}).then((r) => r.json());

console.log(status.status); // "delivered" a few seconds later

5. Receive webhooks

Add a webhook subscription under Developers → Webhooks in the dashboard, then verify and handle deliveries — test-mode events carry "test": true:

app/api/blueticked/route.ts (Next.js)
import { createHmac, timingSafeEqual } from "node:crypto";

// Refuse a timestamp further than this from now, so a captured delivery cannot be replayed.
const TOLERANCE_SECONDS = 300;

function verify(rawBody: string, headers: Headers, secret: string): boolean {
  const v2 = headers.get("x-blueticked-signature-v2"); // t=<unix seconds>,v1=<hex>
  if (!v2) return false;
  const parts = v2.split(",").map((part) => part.split("="));
  const stamp = parts.find(([key]) => key === "t")?.[1] ?? "";
  if (!/^\d{1,12}$/.test(stamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(stamp)) > TOLERANCE_SECONDS) return false;
  const expected = Buffer.from(
    createHmac("sha256", secret).update(`${stamp}.${rawBody}`).digest("hex"),
  );
  // During a secret rotation a second v1 carries the previous secret's signature: accept either.
  return parts
    .filter(([key]) => key === "v1")
    .some(([, v1]) => {
      const got = Buffer.from(v1 ?? "");
      return got.length === expected.length && timingSafeEqual(got, expected);
    });
}

export async function POST(req: Request) {
  const raw = await req.text(); // raw body — never re-serialize before verifying
  if (!verify(raw, req.headers, process.env.BLUETICKED_WEBHOOK_SECRET!)) {
    return new Response("bad signature", { status: 401 });
  }

  const event = JSON.parse(raw);
  if (event.event === "message.delivered") {
    // update your order, mark the statement sent, etc.
  }
  return new Response("ok"); // 2xx within 10s, or we retry
}

One thing worth knowing: webhooks you subscribe to here carry the timestamp inside x-blueticked-signature-v2, as above. Form post-submit actions and saved integrations sign the same HMAC(`${timestamp}.${rawBody}`) but send it as x-blueticked-signature with a separate x-blueticked-timestamp header, so verify against the scheme for the surface you subscribed to. The SDK's constructEvent does the subscription check for you.

Going live

  1. Connect your WhatsApp number in minutes via Embedded Signup (Onboarding → Connect WhatsApp). Blueticked is a verified Meta Tech Provider.
  2. Submit a template for approval — see Templates and approval.
  3. Swap blu_test_ for blu_live_. Nothing else changes.