# Ghala Developer API examples

Runnable examples for the Ghala Developer API (v2).

- Base URL: `https://v2.ghala.io/api/v2`
- Reference: <https://ghala.tz/help-center/api-reference/ghala-api-reference>
- Live spec: <https://v2.ghala.io/api/v2/openapi.json>

> **There is no sandbox and no test mode.** Every send reaches a real WhatsApp
> handset. Send to your own number first.

## Setup

```bash
cp env.example .env
```

Fill in:

| Variable | Where from |
|---|---|
| `ACCESS_TOKEN` | Dashboard → Developer → Credentials, reveal the number's token |
| `TO` | Your own number, full international format, no leading `+` |
| `GHALA_WEBHOOK_SECRET` | Dashboard → Developer → Events, shown once at creation |

Then **message the Ghala number from your handset**. That opens the 24-hour
window; without it, free-form sends return `409 outside_messaging_window`.

## Sending

### Node

Node 20 or newer. No dependencies.

```bash
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 template
node --env-file=.env send-message.mjs buttons
node --env-file=.env send-message.mjs list
node --env-file=.env send-message.mjs image https://example.com/product.png
```

### Python

```bash
pip install requests python-dotenv

python send_message.py templates   # verify the token, send nothing
python send_message.py text
python send_message.py template
python send_message.py buttons
python send_message.py list
python send_message.py image https://example.com/product.png
```

Start with `templates`. It proves your token and plan are right without
messaging anybody: a `200` means you are good, `401` means the token is stale,
`402` means the plan does not include API access.

Both scripts show the same three things worth copying into real code:

1. **Retry only what is retryable** — `429`, `5xx`, and `idempotency_in_progress`.
   A `4xx` will not change its mind.
2. **Send an `Idempotency-Key`** derived from what you are messaging about, so a
   retry after a crash replays instead of sending twice.
3. **Fall back to a template** when the 24-hour window has shut, choosing one
   that `GET /templates?sendable=true` says will be accepted.

## Receiving events

Register your endpoint in **Dashboard → Developer → Events**, store the signing
secret (shown exactly once), and run one of the receivers.

### Node

```bash
npm install express
node --env-file=.env multi-tenant-webhook.mjs
```

### Python

```bash
pip install flask python-dotenv
python multi_tenant_webhook.py
```

Both are **multi-tenant by default**, with the tenant in the URL path:

```
POST /ghala/acme/webhook
POST /ghala/bakari-ltd/webhook
```

Each event subscription has its own signing secret, so the path is what tells
you which secret to verify with. A single shared URL forces 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.

### Local development

Ghala only delivers to public HTTPS: `http://`, loopback, link-local, and
private addresses are rejected with `400`. Tunnel the port and register the
tunnel URL:

```bash
ngrok http 3000
# or
cloudflared tunnel --url http://localhost:3000
```

### What the receivers demonstrate

- **Verify the raw bytes.** `express.raw` / `request.get_data()`, never a parsed
  and re-serialized body — re-encoding changes whitespace and key order and the
  signature will never match.
- **Constant-time comparison.** `timingSafeEqual` / `hmac.compare_digest`.
- **Reject stale timestamps.** More than 300 seconds of skew is a replay.
- **Acknowledge first, process after.** Slow responses count as failures, and
  **20 consecutive failures disable the endpoint**.
- **Deduplicate on `X-Ghala-Delivery`**, scoped by tenant. Delivery is
  at-least-once, so the same event will arrive twice eventually.
- **Never rethrow past the acknowledgement.** A handler exception must not
  become a non-2xx response.

Deliveries are **unordered**. Sequence with timestamps in the payload, never
with arrival order.

## Postman

Import both files from the parent directory:

- `ghala-api.postman_collection.json`
- `ghala-api.postman_environment.json`

Set `ACCESS_TOKEN` and `TO` in the environment, run **List sendable templates**
first — it populates `TEMPLATE_NAME` and `TEMPLATE_LANGUAGE` for the template
requests.

## Support

- Docs: <https://ghala.tz/help-center>
- Email: <info@ghala.io>
