Skip to content

Run your own

Standing up a deployment from nothing: the service, a project, and a paired number. Roughly ten minutes, most of it waiting for a scan.

Most readers want the other page

If someone has already given you a project key, you do not need any of this — the deployment is running and you just need to talk to it. Go to Start here instead. This page is for operators.

1. Run it#

Requires Docker. The engine and Postgres come up together.

shell
git clone https://github.com/atrixdigital/atrix-switchboard
cd atrix-switchboard/engine/run

export GLOBAL_API_KEY=$(openssl rand -hex 32)
docker compose up -d

curl https://switchboard.atrix.dev/v1/ready   # {"status":"ready"}

Never run docker compose down -v

It drops the volume, and the volume is where the paired WhatsApp devices live. Every number unpairs and a human has to re-scan each one with the physical handset. Plain down/up is safe.

2. Create a project#

An operator does this once, with the root key. The webhook secret is shown once and never readable again.

shell
curl -X POST https://switchboard.atrix.dev/v1/projects \
  -H "apikey: $GLOBAL_API_KEY" \
  -H 'content-type: application/json' \
  -d '{"name":"my-project","webhookUrl":"https://my-project.example/api/webhooks/switchboard"}'

# -> { project: { id }, webhookSecret }   shown ONCE

Then mint it a key — also shown once.

shell
curl -X POST https://switchboard.atrix.dev/v1/projects/<id>/keys \
  -H "apikey: $GLOBAL_API_KEY" \
  -H 'content-type: application/json' -d '{"label":"production"}'

# -> { apiKey }

The webhook URL is checked when you set it

It must be a public http(s) address. Loopback and private ranges are refused unless the deployment sets WEBHOOK_ALLOW_PRIVATE; cloud metadata is refused either way. The URL is tenant-supplied and the service POSTs to it unattended, so an unguarded one is an SSRF into the host’s own network.

3. Stand up a number#

From here the project uses its own key. No root key is involved again.

shell
curl -X POST https://switchboard.atrix.dev/v1/instances -H "apikey: <projectKey>" \
  -H 'content-type: application/json' \
  -d '{"name":"sender-01","phone":"+971500000000","owner":"Acme Ltd","device":"Pixel 6a #3"}'

curl -X POST https://switchboard.atrix.dev/v1/instances/<id>/connect -H "apikey: <projectKey>"
curl https://switchboard.atrix.dev/v1/instances/<id>/qr -H "apikey: <projectKey>"   # scan it

Stop polling the QR the moment it reports paired

Asking for a QR on a paired number restarts the client and tears the pairing down about four seconds after it succeeded. Every scan appears to work and every pairing dies, and the logs look exactly like a user who never scanned. qr returns one of three outcomes — paired, pending, or a code — precisely so this is branchable rather than an error.

Always supply the phone, even though it is optional

It is recorded before pairing. The JID only appears after someone scans, so without it whoever holds the handsets cannot tell which SIM this instance is waiting for.

4. Install the client#

While the registry is unavailable the SDK ships as a vendored tarball. Copy it in and depend on the path.

package.json
"@atrixdigital/switchboard": "file:vendor/atrixdigital-switchboard-0.4.0.tgz"

Do not use a relative path to a sibling checkout. It 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.

typescript
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" },
})