Adds the machine-to-machine auth layer the public REST API needs so external scripts and automations can drive wacrm (issue #245). This is the groundwork release; data endpoints land one at a time on top of it. - Migration 026: account-scoped `api_keys` table. SHA-256 hash at rest (plaintext shown once), display `key_prefix`, `scopes[]`, RLS via `is_account_member` (members read, admin+ manage). - `src/lib/api-keys/`: key gen/hash (wacrm_live_ prefix, timing-safe), scope vocabulary, service-role auth-path store. - `requireApiKey(request, scope?)` (src/lib/auth/api-context.ts): bearer parse -> hash lookup -> liveness -> per-key rate limit -> scopes-only authz. Returns a service-role-scoped account context. - `/api/v1` envelope ({data} / {error:{code,message}}) + GET /api/v1/me reference endpoint that exercises the whole stack. - Dashboard: admin+ key management routes + Settings -> API keys tab. - Docs (docs/public-api.md), CHANGELOG, README, .env note. Checks: typecheck clean, lint 0 errors, full suite green (+27 tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4.5 KiB
Public API (/api/v1)
The public API lets you drive your wacrm instance from your own scripts and automations — send messages, manage contacts, launch broadcasts — without going through the dashboard UI.
Status: groundwork release. Authentication, scopes, rate limiting, and the
GET /api/v1/meprobe ship now. The data endpoints (messages,contacts, …) land one at a time in follow-up releases — see Roadmap.
Authentication
Every request authenticates with an API key, sent as a bearer token:
Authorization: Bearer wacrm_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Keys are account-scoped: a key acts on exactly one account, the one it was created in. There is no cross-account access.
Creating a key
In the dashboard: Settings → API keys → New API key. Only admins and owners can create keys.
- Give the key a name (after the integration that will use it).
- Grant the scopes it needs — nothing more (see below).
- Copy the key. The full key is shown exactly once. wacrm stores only a SHA-256 hash, so it can never be shown again. If you lose it, revoke it and create a new one.
Revoking a key
Settings → API keys → Revoke. Revocation is effective on the key's next request. Revoked keys stay in the list as an audit trail.
Scopes
A key can do only what its scopes allow — independent of who created it. Grant the minimum.
| Scope | Allows |
|---|---|
messages:send |
Send WhatsApp messages |
messages:read |
Read messages and delivery status |
contacts:read |
List and read contacts |
contacts:write |
Create and update contacts |
conversations:read |
List and read conversations |
broadcasts:send |
Launch broadcast campaigns |
A key with no scopes still authenticates and can call
GET /api/v1/me — useful for verifying a key works.
Response envelope
Every response uses one of two shapes:
// success
{ "data": { /* ... */ } }
// failure
{ "error": { "code": "forbidden", "message": "This API key is missing the 'messages:send' scope" } }
Branch on error.code (stable); error.message is for humans and
may be reworded.
| Status | code |
Meaning |
|---|---|---|
| 401 | unauthorized |
Missing / malformed / unknown / revoked / expired key |
| 403 | forbidden |
Valid key, but missing the required scope |
| 429 | rate_limited |
Per-key rate limit exceeded |
| 400 | bad_request |
Malformed input |
| 404 | not_found |
No such resource |
| 500 | internal |
Server error |
Rate limits
Requests are limited per key: 120 requests per minute. On a
429, these headers tell you when to retry:
Retry-After— seconds until the window resetsX-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset
The limiter is in-memory and per process. A single-instance deploy (the common case for a self-hosted fork) is fine as-is. If you scale to multiple instances, swap the limiter for a shared store (Redis/Upstash) — see the note at the top of
src/lib/rate-limit.ts. The limit is otherwise unenforced across instances.
Endpoints
GET /api/v1/me
Returns the account a key is bound to and the scopes it carries. Requires only a valid key (no scope). Use it to verify a key works and to discover its scopes.
curl https://your-crm.example.com/api/v1/me \
-H "Authorization: Bearer wacrm_live_xxx"
{
"data": {
"account": { "id": "…", "name": "Acme Inc" },
"key": { "id": "…", "scopes": ["messages:send"] }
}
}
Roadmap
Planned endpoints, shipping one per release (tracked in #245):
POST /api/v1/messages— send a message to a phone number (messages:send)GET/POST /api/v1/contacts,GET/PATCH /api/v1/contacts/{id}(contacts:read/contacts:write)GET /api/v1/conversations(conversations:read)POST /api/v1/broadcasts(broadcasts:send)- Outbound event webhooks (so automations can react to inbound messages)