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.
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.
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.
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.
Create an API key
Open the Developers tab → Create key → name it and pick scope messages. Copy the secret — it's shown once.
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": { ... } }
https://chirp.deviastro.in in every example with wherever your Chirp server lives. All requests and responses are application/json unless noted.
Every endpoint, click a section to expand. Cookie auth (-b cookies.txt) and API-key bearer auth are interchangeable except where noted.
Log in with phone + password. Chirp sets an HttpOnly session cookie; pass it back on every subsequent call with -b cookies.txt.
curl -s -c cookies.txt -X POST https://chirp.deviastro.in/auth/login \
-H 'content-type: application/json' \
-d '{"phone":"919xxxxxxxxx","password":"your-password"}'
{ "ok": true, "user": { "id": 1, "phone": "919xxxxxxxxx", "role": "admin" } }
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" } }
curl -s -b cookies.txt -X POST https://chirp.deviastro.in/auth/logout
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 }
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.
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).
| Scope | Covers |
|---|---|
* | 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 *.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."
# }
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.
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
Each logged-in user has their own Baileys socket. Start it, check status, and fetch the pairing QR when needed.
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" }
curl -s -b cookies.txt https://chirp.deviastro.in/session/status
# → { "status": "connected", "hasQR": false }
States: disconnected, pairing (a QR is available), connected.
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'
Logs out from WhatsApp on the server side and clears auth state — next /session/start returns a fresh QR.
Chirp persists every text message it sees. Read the full stream, filter by chat, by read-state, or since a timestamp.
| Query param | Type | Description |
|---|---|---|
limit | int (≤500) | Page size, default 50. |
offset | int | Pagination offset. |
chat | string | Full JID to filter to one chat (e.g. 14155551234@s.whatsapp.net). |
unread | 1 | Only rows where is_read = 0. |
since | epoch ms | Only 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'
{
"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
}
]
}
Chat list with last-message preview, unread count, and group subject (for groups).
curl -s -b cookies.txt https://chirp.deviastro.in/chats
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".
# 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"}'
{ "id": "3EB0A8F2...", "quota": { "unlimited": false, "limit": 100, "used": 38, "remaining": 62 } }
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"}'
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":"👍"}'
Mark as read, soft-hide locally, or revoke on WhatsApp ("deleted for everyone").
curl -s -b cookies.txt -X POST https://chirp.deviastro.in/messages/3EB0.../read
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"}'
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'
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.
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" }
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.
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
Removes the file from disk and the row from the DB. Safe even if the file is missing.
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.
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.
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": {...} }
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..." }
# ]
# }
quota.remaining or wait for tomorrow.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.
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
}'
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, ... }
# ]}
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 }
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.
| Event | Fires when |
|---|---|
message.in | Someone sends you a message. |
message.out | Your paired WhatsApp sends a message (via Chirp or any linked device). |
message.status | An ack on a prior message changes (server-ack, delivered, read). |
call.offered | Someone is calling (voice or video). |
session.status | Your Baileys session changes state (pairing, connected, disconnected). |
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-Event | Event name, e.g. message.in. |
X-Chirp-Delivery | Unique delivery id (integer). |
X-Chirp-Signature | Hex HMAC-SHA256 of the body. |
X-Chirp-Attempt | 1-indexed attempt number (useful for deduping 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.
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
List your hooks, or the last 50 deliveries for one hook (with retry history and the response status your endpoint returned).
Update url, events, or is_active (re-activating also resets consecutive_failures to zero). DELETE is permanent.
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.
{
"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
}
}
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.
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..." }
connected — that's how the message gets delivered. Doesn't count against quota. Max 2000 characters.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.
| Data | Kept for | Notes |
|---|---|---|
| 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 jobs | 30 days | Only sent and failed rows in send_queue are aged out. In-flight jobs are never deleted. |
| OTP requests | 24 hours | Consumed OTPs are removed immediately; unused OTPs are swept after a day. |
| Expired sessions | until expiry | Session cookies past their expires_at are purged on the next sweep. |
| Quota usage counters | 30 days | Daily per-user tallies used for enforcement and history. |
| Call log | 30 days | Incoming voice/video call records. |
| Webhook deliveries | 14 days | Only sent and failed rows are swept. Pending retries stick around until they resolve. |
| Media files | tied to messages | Inbound 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 files | orphans only | Profile 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. |
data/ and is not touched by retention — it stays until you log out of the device or clear the session.Errors are returned with an appropriate HTTP status and a body of { "error": "human-readable message" }. Common codes:
| Status | Meaning |
|---|---|
400 | Validation failure. Also used when a recipient isn't on WhatsApp. |
401 | Not authenticated, bad credentials, invalid/expired OTP, or an invalid/revoked API key. |
403 | Authenticated but not permitted — admin-only endpoint, suspended account, or API key missing the required scope. |
404 | Message/resource not found or not owned by you. |
429 | Daily quota exceeded, or OTP resend cooldown (60s). Body includes useful state. |
502 | Upstream (Baileys/WhatsApp) error during send. |
503 | Your WhatsApp session is not connected yet. |