OpenAPI spec, Postman collection, environment template, and runnable JavaScript and Python examples.
The API serves its own spec, so it is never out of date with the running service:
| Live spec | https://v2.ghala.io/api/v2/openapi.json |
| Browsable | https://v2.ghala.io/api/v2/docs |
| Downloadable copy | ghala-openapi-v2.json |
Prefer the live URL. The downloadable copy exists for offline work and for tools that will not fetch a remote spec; it differs only in carrying an absolute servers URL so imports work without extra configuration.
Generate a client with whichever generator you already use:
# TypeScript
npx openapi-typescript https://v2.ghala.io/api/v2/openapi.json -o ghala.d.ts
# Python
openapi-python-client generate --url https://v2.ghala.io/api/v2/openapi.json
A generated client is a convenience, not a requirement. Two endpoints and a bearer token do not need an SDK, and the examples below use nothing but fetch and requests.
| Collection | ghala-api.postman_collection.json |
| Environment template | ghala-api.postman_environment.json |
Import both, then set two values in the environment:
ACCESS_TOKEN — from Dashboard → Developer → CredentialsTO — your own number, full international format, no leading +Run "List sendable templates" first. It verifies your token without messaging anybody, and it populates TEMPLATE_NAME and TEMPLATE_LANGUAGE for the template requests that follow.
The collection covers every message type — text, template, template with variables, image, document, interactive buttons, interactive list, URL button — plus an idempotency request designed to be run twice so you can watch Idempotency-Replayed: true come back on the second call.
There is no sandbox. Every send in this collection reaches a real handset, so point
TOat a phone you own.
| File | What it is |
|---|---|
README.md |
Setup and run instructions |
env.example |
Environment template |
send-message.mjs |
Node 20+, no dependencies |
send_message.py |
Python, requests |
multi-tenant-webhook.mjs |
Express receiver, multi-tenant |
multi_tenant_webhook.py |
Flask receiver, multi-tenant |
The send examples are runnable as-is:
cp env.example .env # fill in ACCESS_TOKEN and TO
node --env-file=.env send-message.mjs templates # verify the token, send nothing
node --env-file=.env send-message.mjs text
node --env-file=.env send-message.mjs buttons
pip install requests python-dotenv
python send_message.py templates
python send_message.py text
python send_message.py buttons
They are written to be copied rather than admired. Each one demonstrates the three things that separate a working integration from one that pages you at 2am:
429, 5xx, and idempotency_in_progress, with exponential backoff and jitter. A 4xx will not change its mind.Idempotency-Key, so a retry after a crash replays rather than sending a second message to a real person.GET /templates?sendable=true says will be accepted.Both webhook examples are multi-tenant by default, with the tenant in the URL path:
POST /ghala/acme/webhook
POST /ghala/bakari-ltd/webhook
Every event subscription has its own signing secret, so the path is what tells you which secret to verify with. A single shared URL would force you to identify the tenant before you can verify the delivery, which is backwards. For a single number, use one tenant and ignore the rest.
They demonstrate signature verification against the raw bytes, constant-time comparison, timestamp-skew rejection, acknowledge-then-process, and deduplication on X-Ghala-Delivery scoped by tenant. Multiple Numbers and Multi-Tenant Platforms explains why each of those matters.
For local development, tunnel the port and register the tunnel URL — Ghala rejects http://, loopback, and private addresses:
ngrok http 3000
When reporting a send problem, include the Idempotency-Key you used, the status code, and the code from the error body. There is no per-request correlation id yet, so the idempotency key is the most useful thing you can quote.