# WhatIsUp.dev — Full LLM context > Developer-first WhatsApp gateway. REST + signed webhooks. One API key, full primitive surface. This file is the full, agent-readable description of the WhatIsUp API. It is auto-derived from the same Zod contracts that drive the OpenAPI 3.1 spec at https://api.whatisup.dev/openapi.json — that spec is the canonical, machine-checkable source. Use this file for grounding; use the spec for code generation. ## Identity - Base URL (production): `https://api.whatisup.dev` - API version: `v1` (path-prefix). Outbound webhook envelopes carry `api_version: 2026-04`. - Authentication: HTTP Bearer with an API key. Header: `Authorization: Bearer zpk_live_…` (or `zpk_test_…` in test mode). - Content type: `application/json` everywhere. UTF-8. - All timestamps: ISO-8601 with timezone, e.g. `2026-05-23T14:00:00Z`. ## Onboarding flow (zero-to-first-message) 1. Sign up at https://app.whatisup.dev/signup (Firebase OAuth or email/password). 2. Verify your email. 3. From the dashboard, click "API keys" → "New". The full secret is shown ONCE — store it in your secret manager. 4. Provision a channel: `POST /v1/channels` with `{ "name": "main" }`. You receive a channel UUID. 5. Boot the session: `POST /v1/channels/{id}/connect`. The channel transitions through `pending → qr → connecting → connected`. 6. Pair the WhatsApp account either by: - QR — poll `GET /v1/channels/{id}/qr` and render the returned PNG. The phone's WhatsApp app scans it. - Pair-code — `POST /v1/channels/{id}/pair-code { "phone_number": "..." }` returns an 8-character code shown to the user; they enter it inside WhatsApp. 7. Wait for the `channel.connected` webhook (or poll `GET /v1/channels/{id}/status`). 8. Send a message: `POST /v1/channels/{id}/messages { "type": "text", "to": "+155512345", "text": "hello" }`. ## Resources & primitives ### Channels (`/v1/channels`) A channel is one paired WhatsApp account. Lifecycle states: `pending`, `qr`, `connecting`, `connected`, `reconnecting`, `disconnected`, `logged_out`, `stopped_by_user`, `disabled_by_admin`, `error`, `failed`. Terminal states require a fresh pair to recover. - `GET /v1/channels` — list - `POST /v1/channels` — create - `GET /v1/channels/{id}` — one - `PATCH /v1/channels/{id}` — rename - `DELETE /v1/channels/{id}` — soft-delete - `GET /v1/channels/{id}/status` — lifecycle snapshot - `POST /v1/channels/{id}/connect` — boot session - `POST /v1/channels/{id}/disconnect` — pause - `POST /v1/channels/{id}/logout` — full unpair - `POST /v1/channels/{id}/pair-code` — pair via phone number - `GET /v1/channels/{id}/qr` — current QR (PNG base64) - `GET /v1/channels/{id}/contacts` — channel-scoped contact pull ### Messages (`/v1/channels/{id}/messages`) `POST` accepts a discriminated union on `type`: `text | image | video | audio | document | location | contact | sticker | reaction | template | interactive`. The route is async-by-default — you get `202 Accepted` with `{ message_id, client_ref, status: "sent" | "queued" }`. `client_ref` is echoed back for correlation. Typed alias: `POST /v1/channels/{id}/messages/{type}` lets you omit the `type` field in the body. History: - `GET /v1/messages` — all-channel cursor history - `GET /v1/channels/{id}/messages` — per-channel cursor history - `POST /v1/channels/{id}/messages/{messageId}/read` — mark single message read ### Chats (`/v1/channels/{id}/chats`) Chat metadata (pin / archive / mute / unread / ephemeral). Persisted gateway-side; deletions are local only. ### Contacts (`/v1/channels/{id}/contacts`) Contacts are backfilled from inbound traffic. `/lids` returns the phone↔LID map. `/{jid}/profile` refreshes push name + bio + picture. ### Groups (`/v1/channels/{id}/groups`) Full Whapi-shape surface — create, leave, update subject/description/settings, manage participants (add/remove/promote/demote), invite codes, pictures, applications. LIVE proxy to the paired WhatsApp socket — no local mirror. ### Communities, Newsletters, Stories, Calls, Presence, Profile, Settings, Labels, Blacklist Each is a thin REST surface mirroring the Whapi vocabulary. See the OpenAPI spec for shapes. ### Business / Catalog Catalogs and products. CRUD + bulk import (up to 500 products per call). Orders are read-only (mirror of inbound webhooks). ### Media `POST /v1/channels/{id}/media { "source_url", "mime_type" }` — pre-uploads once, returns a `media_id` reusable across many sends. `GET /v1/media/{token}` is a public signed proxy; the HMAC token IS the authorization, ~15min TTL. ## Webhooks Outbound HTTP delivered from a BullMQ-backed dispatcher (5 attempts, exponential backoff). Every request is signed. ### Headers - `X-WhatIsUp-Signature: t=,v1=` — HMAC-SHA256 of `.` with your endpoint's signing secret. - `X-WhatIsUp-Event: ` — duplicates `event` in the body for routing. - `X-WhatIsUp-Event-Id: ` — idempotency key for the consumer. - `X-WhatIsUp-Correlation-Id: ` — ties a delivery to its source request. ### Envelope ```json { "id": "wevt_…", "event": "message.received", "api_version": "2026-04", "sent_at": "2026-05-23T14:00:00Z", "attempt": 1, "channel_id": "…", "customer_id": "…", "payload": { … } } ``` ### Event catalog - `message.received`, `message.sent`, `message.delivered`, `message.failed`, `message.reaction` - `channel.connected`, `channel.disconnected` - `group.created`, `group.updated`, `group.participant_added`, `group.participant_removed`, `group.admin_promoted`, `group.admin_demoted` - `chat.updated`, `chat.archived`, `chat.pinned` - `presence.updated` - `call.offered`, `call.terminated` ## Errors Every 4xx/5xx response body: `{ "error": { "code", "message", "details"? }, "request_id" }`. Match on `code`, NOT `message`. Stable codes: `auth_missing`, `auth_invalid`, `auth_malformed`, `auth_expired`, `auth_no_email`, `channel_not_found`, `channel_access_denied`, `channel_unavailable`, `channel_scoped_key`, `invalid_lifecycle_transition`, `pair_code_unavailable`, `pair_code_failed`, `no_qr`, `not_disabled`, `concurrent_modification`, `plan_channel_limit`, `rate_limited`, `worker_at_capacity`, `webhook_endpoint_not_found`, `media_gone`, `invalid_request`, `not_found`, `bad_request`, `no_op`, `other`, `internal_error`. ## Rate limits Default per API key: 60 burst, 1 rps sustained. Response headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`. On 429, honor `Retry-After`. Peek without consuming: `GET /v1/limits`. ## Idempotency For outbound messages, supply `client_ref` (any unique string) in the request body. The response echoes it. Duplicate `client_ref` within a short window returns the original `message_id`. Best-effort, not transactional. ## SDKs Generate clients directly from the OpenAPI 3.1 spec at https://api.whatisup.dev/openapi.json — works with `openapi-typescript`, `oapi-codegen` (Go), `openapi-python-client`, etc. First-party TypeScript SDK is on the roadmap. Zod schemas (the source of truth) ship in the `@whatisup/contracts` package — internal today, will be published on npm once the surface stabilizes. ## Concepts - **Stateless on messages.** We persist the outbound queue and the inbound history, but the message send path is not a transaction. - **Stateful on chats / contacts / business catalogs.** Metadata flags persist gateway-side because there's no "fetch all chats" primitive on WhatsApp. - **LIVE proxy on groups / communities / newsletters / calls.** No mirror table; the paired socket is the source of truth. ## Support - Status page: https://whatisup.dev/status - Open a ticket: dashboard → Support → New ticket (`POST /v1/support/tickets`) - Email: hello@whatisup.dev