API reference

Every action in the Chirp UI is also a plain HTTP endpoint. Log in once, reuse the session cookie (or plug in your own bearer auth), and drive Chirp from any stack.

Quickstart

From a fresh sign-up to your first send in under five minutes. The web app does steps 1–3 for you click-by-click; the deep API equivalents are in the reference below.

  1. Sign up

    Open register, enter your phone number, get an OTP on WhatsApp, and set a password. You're now logged into the web app at /app.

  2. Pair your WhatsApp

    In the web app, the Session tab will show a QR code. Scan it with WhatsApp on your phone (Settings → Linked Devices). Status flips to connected when ready — usually a few seconds.

  3. Create an API key

    Open the Developers tab → Create key → name it and pick scope messages. Copy the secret — it's shown once.

  4. Send your first message

    Replace the phone, body, and bearer token. The phone is digits-only with country code (no + or spaces).

    curl -X POST https://chirp.deviastro.in/chats/919xxxxxxxxx/send \
      -H 'authorization: Bearer chirp_live_xxxxxxxxxxxxxxxxxxxxxxxx' \
      -H 'content-type: application/json' \
      -d '{"body":"Hello from Chirp!"}'
    
    # → { "id": "3EB0xxxxx", "quota": { ... } }
Base URL — replace https://chirp.deviastro.in in every example with wherever your Chirp server lives. All requests and responses are application/json unless noted.

Full reference

Every endpoint, click a section to expand. Cookie auth (-b cookies.txt) and API-key bearer auth are interchangeable except where noted.

Jump to

1. Authentication

Log in with phone + password. Chirp sets an HttpOnly session cookie; pass it back on every subsequent call with -b cookies.txt.

POST/auth/login

curl -s -c cookies.txt -X POST https://chirp.deviastro.in/auth/login \
  -H 'content-type: application/json' \
  -d '{"phone":"919xxxxxxxxx","password":"your-password"}'

Response

{ "ok": true, "user": { "id": 1, "phone": "919xxxxxxxxx", "role": "admin" } }

GET/auth/me

Returns the current user and daily quota status. Useful for a UI badge.

curl -s -b cookies.txt https://chirp.deviastro.in/auth/me
# { "user": {...}, "quota": { "unlimited": false, "limit": 100, "used": 37, "remaining": 63, "date": "2026-05-09" } }

POST/auth/logout

curl -s -b cookies.txt -X POST https://chirp.deviastro.in/auth/logout

POST/auth/register/request   POST/auth/register/verify

Self-serve signup — phone → OTP on WhatsApp → password. If the phone is already registered, the same flow becomes a password-reset (response includes reset: true).

curl -s -X POST https://chirp.deviastro.in/auth/register/request \
  -H 'content-type: application/json' \
  -d '{"phone":"14155551234"}'
# → { "ok": true, "phone": "14155551234", "expiresInSec": 900, "reset": false }

curl -s -c cookies.txt -X POST https://chirp.deviastro.in/auth/register/verify \
  -H 'content-type: application/json' \
  -d '{"phone":"14155551234","otp":"123456","password":"min8chars"}'
# → { "ok": true, "user": {...}, "reset": false }

2. API keys

For scripts, servers, and automations. Keys share your user account but can be scope-limited and revoked at any time. Cookie auth still works everywhere and bypasses scope checks — API keys are about locking down non-interactive clients.

The full secret is returned once at creation. Chirp stores only a SHA-256 hash, so there's no way to recover it later — save it to your secret manager immediately.

Using a key

Send it on every request as a bearer token (or via X-API-Key if you prefer):

curl -s https://chirp.deviastro.in/chats \
  -H 'authorization: Bearer chirp_live_xxxxxxxxxxxxxxxxxxxxxxxx'

# or
curl -s https://chirp.deviastro.in/chats -H 'x-api-key: chirp_live_xxxx...'

Anywhere the rest of this reference shows -b cookies.txt, you can swap in -H 'authorization: Bearer ...' (subject to the key's scopes).

Scopes

ScopeCovers
*Everything this user can do, except the admin panel and key management.
messages/messages, /chats/*, /send/*, /schedule/*, /calls/*
media/media/* — upload, metadata, download, delete
webhooks/webhooks/* — CRUD and test-dispatch
/admin/* and /api-keys always require a cookie session. Even a *-scoped key can't reach them, so a leaked key can't create more keys or take over the tenant. /session/reset also requires *.

POST/api-keys

Create a new key. Cookie auth only.

curl -s -b cookies.txt -X POST https://chirp.deviastro.in/api-keys \
  -H 'content-type: application/json' \
  -d '{"name":"my-bot","scopes":["messages","media"]}'
# → {
#   "id": 3,
#   "name": "my-bot",
#   "prefix": "chirp_live_AbC12d",
#   "scopes": ["messages","media"],
#   "key": "chirp_live_AbC12d...xyz",       ← shown once, store it now
#   "note": "Store this key securely — it will not be shown again."
# }

GET/api-keys

curl -s -b cookies.txt https://chirp.deviastro.in/api-keys
# → {
#   "keys": [
#     { "id": 3, "name": "my-bot", "prefix": "chirp_live_AbC12d",
#       "scopes": ["messages","media"], "created_at": 1736020800,
#       "last_used_at": 1736107200, "revoked_at": null }
#   ],
#   "available_scopes": ["*","messages","media","webhooks"]
# }

Lists are prefix-only — the full secret is never returned again.

DELETE/api-keys/:id

Revoke a key. Idempotent — revoking an already-revoked key returns { "ok": true, "already_revoked": true }.

curl -s -b cookies.txt -X DELETE https://chirp.deviastro.in/api-keys/3

3. WhatsApp session

Each logged-in user has their own Baileys socket. Start it, check status, and fetch the pairing QR when needed.

POST/session/start

Idempotent. Kicks off the Baileys socket for your user. You'll need to call this once after login on a fresh device.

curl -s -b cookies.txt -X POST https://chirp.deviastro.in/session/start
# → { "status": "pairing" }

GET/session/status

curl -s -b cookies.txt https://chirp.deviastro.in/session/status
# → { "status": "connected", "hasQR": false }

States: disconnected, pairing (a QR is available), connected.

GET/qr

Returns the current pairing QR as a PNG image, or 204 if no QR is staged. Add ?format=text to get the raw QR string instead (handy for rendering in a terminal).

curl -s -b cookies.txt https://chirp.deviastro.in/qr -o qr.png
curl -s -b cookies.txt 'https://chirp.deviastro.in/qr?format=text'

POST/session/reset

Logs out from WhatsApp on the server side and clears auth state — next /session/start returns a fresh QR.

4. Reading messages & chats

Chirp persists every text message it sees. Read the full stream, filter by chat, by read-state, or since a timestamp.

GET/messages

Query paramTypeDescription
limitint (≤500)Page size, default 50.
offsetintPagination offset.
chatstringFull JID to filter to one chat (e.g. 14155551234@s.whatsapp.net).
unread1Only rows where is_read = 0.
sinceepoch msOnly rows newer than this timestamp.
# latest 50 messages across all chats
curl -s -b cookies.txt 'https://chirp.deviastro.in/messages?limit=50'

# unread only, since a given point in time
curl -s -b cookies.txt 'https://chirp.deviastro.in/messages?unread=1&since=1730000000000'

# one chat
curl -s -b cookies.txt 'https://chirp.deviastro.in/messages?chat=14155551234@s.whatsapp.net'

Response shape

{
  "total": 1284,
  "limit": 50,
  "offset": 0,
  "messages": [
    {
      "id": "3EB0...",
      "chat_jid": "14155551234@s.whatsapp.net",
      "from_jid": "14155551234@s.whatsapp.net",
      "from_me": 0,
      "body": "hello",
      "timestamp": 1736020800000,
      "push_name": "Alice",
      "quoted_id": null,
      "quoted_body": null,
      "is_read": 0,
      "is_deleted": 0
    }
  ]
}

GET/chats

Chat list with last-message preview, unread count, and group subject (for groups).

curl -s -b cookies.txt https://chirp.deviastro.in/chats

5. Sending a message

Pass a phone number (digits + country code, no +) or a full JID. Chirp pre-warms the Signal session on first contact so the recipient's device decrypts immediately instead of showing "Waiting for this message".

POST/chats/:jid/send

# to a phone number
curl -s -b cookies.txt -X POST https://chirp.deviastro.in/chats/14155551234/send \
  -H 'content-type: application/json' \
  -d '{"body":"Hello from Chirp"}'

# to a group JID
curl -s -b cookies.txt -X POST \
  'https://chirp.deviastro.in/chats/120363025012345678@g.us/send' \
  -H 'content-type: application/json' \
  -d '{"body":"Hello team"}'

Response

{ "id": "3EB0A8F2...", "quota": { "unlimited": false, "limit": 100, "used": 38, "remaining": 62 } }
Counts as 1 against your daily quota. Admin users are unlimited.

6. Replies & reactions

POST/messages/:id/reply

Sends a WhatsApp-native quoted reply. The recipient sees the original message quoted above yours.

curl -s -b cookies.txt -X POST \
  https://chirp.deviastro.in/messages/3EB0A8F2.../reply \
  -H 'content-type: application/json' \
  -d '{"body":"sure, on it"}'

POST/messages/:id/react

React with a single emoji. Pass an empty string to remove the reaction.

curl -s -b cookies.txt -X POST \
  https://chirp.deviastro.in/messages/3EB0A8F2.../react \
  -H 'content-type: application/json' \
  -d '{"emoji":"👍"}'
Replies count as 1 against quota. Reactions don't count — they're tiny protocol messages, not outbound content.

7. Mark, hide, delete

Mark as read, soft-hide locally, or revoke on WhatsApp ("deleted for everyone").

POST/messages/:id/read

curl -s -b cookies.txt -X POST https://chirp.deviastro.in/messages/3EB0.../read

POST/messages/bulk-action

Apply an action to many messages at once. action must be read, hide, or delete-remote.

curl -s -b cookies.txt -X POST \
  https://chirp.deviastro.in/messages/bulk-action \
  -H 'content-type: application/json' \
  -d '{"ids":["id1","id2","id3"],"action":"read"}'

DELETE/messages/:id

Without query string, soft-deletes locally. Add ?remote=1 to also revoke on WhatsApp for the recipient.

curl -s -b cookies.txt -X DELETE \
  'https://chirp.deviastro.in/messages/3EB0.../?remote=1'
None of these count against quota.

8. Media

Send images, voice notes, videos, documents, and stickers. Chirp uploads media as base64 in JSON so there's no multipart plumbing. Incoming attachments are auto-downloaded and linked to the message row.

Supported types: image, video, audio, document, sticker. Max upload size: 16 MB. Files are stored under data/media/<user>/ and hashed with SHA-256.

POST/media

Upload a file. Returns a media_id to reference when sending.

B64=$(base64 -w0 photo.jpg)
curl -s -b cookies.txt -X POST https://chirp.deviastro.in/media \
  -H 'content-type: application/json' \
  -d "{\"type\":\"image\",\"filename\":\"photo.jpg\",\"mime\":\"image/jpeg\",\"data_base64\":\"$B64\"}"
# → { "id": "m_abc123...", "size": 184320, "sha256": "9f...", "type": "image" }

POST/chats/:jid/send-media

Send an uploaded file. Optional caption for images/videos/documents.

curl -s -b cookies.txt -X POST \
  https://chirp.deviastro.in/chats/14155551234/send-media \
  -H 'content-type: application/json' \
  -d '{"media_id":"m_abc123...","caption":"see attached"}'
# → { "id": "3EB0...", "media_id": "m_abc123...", "quota": {...} }

Counts as 1 against your daily quota.

GET/media/:id   GET/media/:id/content

The first returns metadata (type, mime, filename, size_bytes, sha256, origin). The second streams the raw bytes back so you can save or forward the file.

curl -s -b cookies.txt https://chirp.deviastro.in/media/m_abc123
curl -s -b cookies.txt https://chirp.deviastro.in/media/m_abc123/content -o photo.jpg

DELETE/media/:id

Removes the file from disk and the row from the DB. Safe even if the file is missing.

Inbound media

When someone sends you an image/video/audio/document/sticker, Chirp auto-downloads it, stores it under your user, and sets media_id + media_type on the resulting message row. Fetch with /media/:id/content.

9. Bulk send

Enqueue a broadcast. Chirp paces sends with jittered delays (2–6 seconds by default) so your number doesn't trip anti-spam heuristics. The queue survives restarts and auto-retries failures up to 3x.

POST/send/bulk

curl -s -b cookies.txt -X POST https://chirp.deviastro.in/send/bulk \
  -H 'content-type: application/json' \
  -d '{
    "recipients": ["919xxxxxxxxx", "14155551234"],
    "body": "Maintenance at 10pm tonight.",
    "delayMs": [2000, 6000]
  }'
# → { "jobId": "a1b2c3...", "count": 2, "quota": {...} }

GET/send/jobs/:id

curl -s -b cookies.txt https://chirp.deviastro.in/send/jobs/a1b2c3...
# → {
#   "jobId": "a1b2c3...",
#   "counts": { "pending": 0, "sent": 2 },
#   "items": [
#     { "id": 1, "recipient": "919...", "status": "sent", "attempts": 1,
#       "last_error": null, "sent_at": 1736020900, "wa_message_id": "3EB0..." }
#   ]
# }
A bulk submit is rejected whole-or-nothing if it would exceed your daily quota. Trim your recipient list to quota.remaining or wait for tomorrow.

10. Scheduled messages

Queue a send for a future time. Reuses the bulk-send pipeline, so delivery paces with the same jittered delays, retries, and status tracking. Quota is reserved at schedule time.

POST/schedule

Schedule text or media to one or more recipients. scheduled_for is a Unix timestamp in seconds and must be in the future.

# text, five minutes from now
curl -s -b cookies.txt -X POST https://chirp.deviastro.in/schedule \
  -H 'content-type: application/json' \
  -d "{
    \"recipients\": [\"14155551234\", \"919xxxxxxxxx\"],
    \"body\": \"Reminder: standup in 5 minutes.\",
    \"scheduled_for\": $(date -d '+5 minutes' +%s)
  }"
# → { "jobId": "a1b2c3...", "count": 2, "scheduled_for": 1736021100, "quota": {...} }

# media, with caption
curl -s -b cookies.txt -X POST https://chirp.deviastro.in/schedule \
  -H 'content-type: application/json' \
  -d '{
    "recipients": ["14155551234"],
    "media_id": "m_abc123...",
    "caption": "Poster for tonight",
    "scheduled_for": 1736100000
  }'

GET/schedule

Everything you've scheduled — pending, sent, or cancelled — ordered by send time.

curl -s -b cookies.txt https://chirp.deviastro.in/schedule
# → { "scheduled": [
#   { "id": 1, "job_id": "a1b2c3...", "recipient": "14155551234@s.whatsapp.net",
#     "body": "Reminder...", "media_id": null, "caption": null,
#     "status": "pending", "scheduled_for": 1736021100, "attempts": 0, ... }
# ]}

DELETE/schedule/:jobId

Cancel any still-pending sends for a scheduled job. Rows already sent, sending, or failed are untouched.

curl -s -b cookies.txt -X DELETE https://chirp.deviastro.in/schedule/a1b2c3...
# → { "ok": true, "cancelled": 2 }
Quota is reserved at schedule time, not send time. Cancelling a scheduled batch does not refund quota for the current day.

11. Webhooks

Register an HTTPS endpoint and Chirp will POST to it whenever something happens — an inbound message, an outgoing message ack, a phone call, or a session state change.

Events

EventFires when
message.inSomeone sends you a message.
message.outYour paired WhatsApp sends a message (via Chirp or any linked device).
message.statusAn ack on a prior message changes (server-ack, delivered, read).
call.offeredSomeone is calling (voice or video).
session.statusYour Baileys session changes state (pairing, connected, disconnected).

Signatures

Every delivery is signed with HMAC-SHA256 of the raw request body, using the secret returned when you registered the hook. Verify before trusting a payload:

// node
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, body, sigHex) {
  const expected = createHmac('sha256', secret).update(body).digest('hex');
  const a = Buffer.from(expected, 'hex'), b = Buffer.from(sigHex, 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}
// in your handler:
if (!verify(process.env.CHIRP_SECRET, rawBody, req.headers['x-chirp-signature'])) {
  return res.status(401).end();
}

Each delivery carries these headers:

X-Chirp-EventEvent name, e.g. message.in.
X-Chirp-DeliveryUnique delivery id (integer).
X-Chirp-SignatureHex HMAC-SHA256 of the body.
X-Chirp-Attempt1-indexed attempt number (useful for deduping retries).

Retries

Non-2xx or a 10-second timeout triggers retry with backoff 2s → 10s → 60s → 5m → 30m (max 5 attempts). After 20 consecutive failures across any deliveries, the hook is auto-disabled — re-enable it with a PATCH once your endpoint is fixed.

POST/webhooks

curl -s -b cookies.txt -X POST https://chirp.deviastro.in/webhooks \
  -H 'content-type: application/json' \
  -d '{
    "url": "https://yourapp.example.com/hooks/chirp",
    "events": ["message.in", "call.offered"]
  }'
# → { "id": 1, "url": "...", "events": [...], "is_active": 1,
#     "secret": "whsec_..." }   ← save this; it will never be shown again

GET/webhooks   GET/webhooks/:id/deliveries

List your hooks, or the last 50 deliveries for one hook (with retry history and the response status your endpoint returned).

PATCH /webhooks/:id   DELETE/webhooks/:id

Update url, events, or is_active (re-activating also resets consecutive_failures to zero). DELETE is permanent.

POST/webhooks/:id/test

Send a synthetic message.in event to the hook's URL so you can verify your endpoint is wired up without waiting for real traffic.

Payload shape

{
  "event": "message.in",
  "user_id": 1,
  "timestamp": 1736020800,
  "data": {
    "id": "3EB0...",
    "chat_jid": "14155551234@s.whatsapp.net",
    "from_jid": "14155551234@s.whatsapp.net",
    "from_me": false,
    "body": "hello",
    "timestamp": 1736020800000,
    "push_name": "Alice",
    "quoted_id": null,
    "media": null
  }
}

12. Quota

Non-admin users get 100 outbound messages per day. Reset happens at server-local midnight.

Counted against quota: /chats/:jid/send, /messages/:id/reply, /send/bulk (N = recipients.length).

Not counted: reactions, read-marks, local hides, remote deletes, OTP dispatches, /contact-admin.

Query quota at any time via /auth/me. When a send is rejected for quota, you'll get an HTTP 429 with a quota object in the body.

POST/contact-admin

Need more than your daily cap? Send a short note to the Chirp admin asking for an increase. The message is delivered from your paired WhatsApp number to the admin's WhatsApp, so they can reply directly and raise your limit from the admin panel.

curl -s -b cookies.txt -X POST https://chirp.deviastro.in/contact-admin \
  -H 'content-type: application/json' \
  -d '{"message":"Hi — could you bump my daily limit to 300? Running a drip campaign next week."}'
# → { "ok": true, "id": "3EB0..." }
Your WhatsApp must be connected — that's how the message gets delivered. Doesn't count against quota. Max 2000 characters.

13. Data retention

Chirp sweeps old records once every 24 hours so the database stays lean. The first sweep runs ~60 seconds after the server boots; subsequent sweeps run on a daily interval.

DataKept forNotes
Messages (inbound & outbound)7 days (default)Per-user override available — an admin can raise or lower this for any account. Messages older than the cutoff are deleted permanently on the next sweep.
Bulk send jobs30 daysOnly sent and failed rows in send_queue are aged out. In-flight jobs are never deleted.
OTP requests24 hoursConsumed OTPs are removed immediately; unused OTPs are swept after a day.
Expired sessionsuntil expirySession cookies past their expires_at are purged on the next sweep.
Quota usage counters30 daysDaily per-user tallies used for enforcement and history.
Call log30 daysIncoming voice/video call records.
Webhook deliveries14 daysOnly sent and failed rows are swept. Pending retries stick around until they resolve.
Media filestied to messagesInbound media is unlinked from disk and removed from the media table when its parent message is pruned. Uploaded media (origin: "upload") is kept until you DELETE /media/:id.
Avatar filesorphans onlyProfile pictures are kept while the contact still references them; if the file becomes unreferenced (e.g., contact removed or path rotated) the next sweep deletes it.
Your WhatsApp pairing (Baileys auth state) lives on disk under data/ and is not touched by retention — it stays until you log out of the device or clear the session.

14. Errors

Errors are returned with an appropriate HTTP status and a body of { "error": "human-readable message" }. Common codes:

StatusMeaning
400Validation failure. Also used when a recipient isn't on WhatsApp.
401Not authenticated, bad credentials, invalid/expired OTP, or an invalid/revoked API key.
403Authenticated but not permitted — admin-only endpoint, suspended account, or API key missing the required scope.
404Message/resource not found or not owned by you.
429Daily quota exceeded, or OTP resend cooldown (60s). Body includes useful state.
502Upstream (Baileys/WhatsApp) error during send.
503Your WhatsApp session is not connected yet.