Start here
Someone has given you a project key and a webhook secret. This is everything you need to send your first message, and nothing you don’t.
If you have not been given a key
Quickstart for that side.1. Three environment variables#
These are the only three a consuming project needs.
SWITCHBOARD_URL=https://switchboard.atrix.dev SWITCHBOARD_KEY=<your project key> SWITCHBOARD_WEBHOOK_SECRET=<your webhook secret>
Both secrets are shown exactly once
POST /v1/projects/{id}/webhook/rotate, which also tells the operator which instances must reconnect.2. Install the SDK#
It is not on npm yet. The tarball lives in the Switchboard repository at sdk/releases/ — copy the current one into your project and depend on the path.
git clone https://github.com/atrixdigital/atrix-switchboard /tmp/switchboard mkdir -p vendor cp /tmp/switchboard/sdk/releases/atrixdigital-switchboard-0.4.0.tgz vendor/ npm install
"dependencies": { "@atrixdigital/switchboard": "file:vendor/atrixdigital-switchboard-0.4.0.tgz" }
Do not point at a sibling checkout
file:../../atrix-switchboard/sdk works on a laptop with both repos present and does not exist inside a Docker build or on a CI runner — which is where installs actually happen. Commit the tarball.3. Send a message#
import { Switchboard } from "@atrixdigital/switchboard" const wa = new Switchboard({ url: process.env.SWITCHBOARD_URL!, projectKey: process.env.SWITCHBOARD_KEY!, }) const { id } = await wa.outbound.send({ to: "+971500000001", message: { text: "hello" }, }) const { state } = await wa.outbound.status(id) // queued → sent → delivered → read
202, not 200
send() resolves when the message is accepted, not sent. Switchboard picks one of your numbers when the message is actually due, paces it, and retries it. Ask status(id) for what happened.4. Receive messages#
// app/api/webhooks/switchboard/[secret]/route.ts import { createWebhookHandler } from "@atrixdigital/switchboard" export const POST = createWebhookHandler({ secret: process.env.SWITCHBOARD_WEBHOOK_SECRET!, seen: (id) => db.deliveries.exists(id), onEvent: async ({ event, meta }) => { if (event.kind !== "inbound") return await db.deliveries.insert(meta.deliveryId) await queue.push(event) // do the WORK off the request }, })
The secret is the last path segment, not a header — the engine’s webhook producer cannot attach headers. The handler verifies it, parses, dedupes and acknowledges for you.
Delivery is at-least-once
meta.deliveryId — a unique index on it is the whole implementation. Without it, a handler that sends an auto-reply sends it twice.5. Check it worked#
# no key needed — is the service up at all curl https://switchboard.atrix.dev/v1/ready # your project's own view curl -s https://switchboard.atrix.dev/v1/usage -H "apikey: $SWITCHBOARD_KEY" curl -s https://switchboard.atrix.dev/v1/messages -H "apikey: $SWITCHBOARD_KEY" # why is nothing going out? curl -s https://switchboard.atrix.dev/v1/pool -H "apikey: $SWITCHBOARD_KEY"
| If you get | It means |
|---|---|
| 401 | Bad key — or your project has been suspended. The two answer identically on purpose. |
| 404 on someone else's id | Correct. Foreign resources are never 403, so ids cannot be enumerated. |
| 429 | Your rate limit, or a number's daily cap. retryAfter is set for the first. |
| 503 | A number is moving between replicas. The one error worth retrying. |
What to read next#
Sending for the pool and pacing, Receiving for the four things every integration gets wrong, and Numbers for why a number can accept every message and deliver none.