API keys
WhatIsUp.dev uses bearer tokens for API auth. Sign up, grab a key from the dashboard, drop it in an Authorization: Bearer … header, you're done.
Lifecycle
Format
Keys look like:
zpk_2tQfTk9bJtq1Vj8x4nKxqL2g
- One prefix:
zpk_. There is nozpk_live_/zpk_test_distinction and no sandbox environment — earlier revisions of this page described both; neither was ever implemented. Every key is a live key. - The tail is 24 URL-safe base64 characters (18 random bytes, 144 bits of entropy), so a full key is 28 characters.
- The first 12 characters — e.g.
zpk_2tQfTk9b— are the key'sprefix. That is what the dashboard andGET /v1/api-keysdisplay, and it is safe to log. - Keys are stored as an HMAC-SHA256 digest under a server-side pepper. The plaintext is shown exactly once at creation. We never log it and cannot show it again.
Scope
Every key belongs to a customer. By default, a key can act on every channel under that customer. You can also issue a channel-scoped key by passing channel_id at create time — that key can only send messages from / read deliveries for that one channel. Channel keys are useful for least-privilege apps (e.g. a marketing tool that should only ever post from one number).
Shortcut: POST /v1/channels with "issue_scoped_key": true creates the channel
and its bound key in one call, returning scoped_key.secret once.
Scopes
scopes narrows what a key may do; channel_id narrows where. Both are
enforced. The vocabulary is <resource>.<read|write> — read covers GET,
write covers every other method — plus * (everything) and <resource>.*.
Resources: channels, messages, media, chats, contacts, groups,
communities, newsletters, labels, blacklist, stories, calls,
presences, profile, settings, business, webhooks, keys, events,
limits, metrics.
A send-only key for one channel — the shape most integrations want:
{
"name": "notifier",
"channel_id": "8d653c66-e4ff-43ee-97da-3de5ad5680d4",
"scopes": ["messages.write", "messages.read", "channels.read"]
}A call outside the grant gets 403 insufficient_scope, with
details.required and details.granted so the gap is visible.
An empty scopes array means UNRESTRICTED, not "no access". Keys issued
before scopes were enforced carry [], so empty is treated as full account
scope — narrowing it would have revoked every existing key. Your key is
restricted only when you explicitly pass a non-empty scopes array at issue
time.
Authentication header
Authorization: Bearer zpk_2tQfTk9bJtq1Vj8x4nKxqL2gDon't put the key in URL query strings — they leak to logs, referrer headers, and CI screenshots.
Rotation
Issue a new key, swap your env, then revoke the old one. There's no separate "rotate without downtime" endpoint because issue-then-revoke covers it: both keys are valid in the overlap window.
Issue a new key (the existing key authenticates this call):
curl -sX POST "$WHATISUP_API/v1/api-keys" \
-H "Authorization: Bearer $WHATISUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"app-2026-q2"}'Then your app reads the new key from env and redeploys. After the new key is in use, revoke the old one:
curl -sX DELETE "$WHATISUP_API/v1/api-keys/0f2c5ad1-8b47-4e93-a5d2-6c81e04fb7a9" \
-H "Authorization: Bearer $WHATISUP_API_KEY"Rate limits
Every authenticated request charges 1 token from a per-customer bucket. The defaults give you a 60-request burst and ~1 request / second sustained — plenty for most workloads.
The response includes:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
…and on rejection:
HTTP/1.1 429 Too Many Requests
Retry-After: 4
{"error": {"code": "rate_limited", "message": "…"}}
If you bump up against the limit a lot, talk to us before sharding API keys — we'd rather raise your bucket.
Audit log
Every issue, every revoke, every authentication failure leaves an entry in the audit log. View it from the dashboard's Activity tab. Audit entries survive the resource they reference, so the trail outlives what it points at.