Sending messages, statuses and groups
Send/list WhatsApp messages, poll delivery state, post a status, list groups, and manage consented contacts.
Cet article n'a pas été traduit, il est donc affiché en anglais.
Sending messages, statuses and groups
Messages
| Method | Path | Permission | Purpose ||---|---|---|---|| POST | /shops/{idOrName}/messages | messages.send | queue a WhatsApp message → 202 || GET | /shops/{idOrName}/messages?status=&limit=&cursor= | messages.read | list, newest first || GET | /messages/{id} | messages.read | one message's delivery state |curl -s -X POST -H "Authorization: Bearer omk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"channel":"whatsapp","to":"+24177000000","text":"Hello!",
"mediaUrl":"https://example.com/photo.jpg","mediaType":"image",
"idempotencyKey":"order-1234-confirmation"}' \
https://app.omni-manager.com/api/v1/shops/SHOP_ID/messages
Every field but channel and to is optional; either text or mediaUrl is required (422 EMPTY_MESSAGE otherwise). to must be E.164 (+CCxxxxxxxxx). At most one mediaUrl per call — this endpoint's contract is one attachment, always. Repeating a call with the same idempotencyKey returns the original message (200, header X-Idempotent-Replay: true) and never sends twice.
A successful call is 202 with the queued message and the policy verdict that let it through:
{ "id": "…", "status": "queued", "statusUrl": "/api/v1/messages/…",
"policy": {
"recipientClass": "known", // "known" | "cold" | "declared" | "group"
"warmupDay": 12,
"caps": { "perDay": 100, "perHour": 16, "coldPerDay": 10, "declaredPerDay": 50 },
"usage": { "today": 3, "lastHour": 1, "coldToday": 0, "declaredToday": 0 }
} }
Poll GET /messages/{id} for the terminal state: sent, failed (delivery error — BOT_RESTARTED, WHATSAPP_SESSION_LOST, BOT_SERVICE_UNREACHABLE, or WHATSAPP_NOT_CONNECTED once a message has waited too long for a session that never came back), or rejected (RECIPIENT_NOT_ON_WHATSAPP) — a message never stays queued forever.
Statuses
| Method | Path | Permission | Purpose ||---|---|---|---|| POST | /shops/{idOrName}/statuses | statuses.post | post a WhatsApp status through the shop's number → 202 |Body: { caption?, mediaUrl?, mediaType? } ("image" or "video"; either caption or mediaUrl required, caption capped at ~700 characters, one attachment). Subject to its own daily cap, statusesPerDay, distinct from the message caps above — 429 STATUSES_DAILY_CAP_REACHED once spent.
Groups
| Method | Path | Permission | Purpose ||---|---|---|---|| GET | /shops/{idOrName}/groups?canSend=&minParticipants=&q= | groups.read | the shop's WhatsApp groups |Each item is { id, name, participantCount, isAdmin, announce, canSend }. canSend is exactly isAdmin — not !announce || isAdmin — because opt-out cannot work per-member inside a group and mere membership is not evidence of consent to broadcast; administering the group is the only signal available, so it is required to send to ANY group, not only "announce" ones. Filter with ?canSend=true, ?minParticipants=50, ?q=name substring.
Sending to a group is the same POST …/messages above with a group JID as to; recipientClass in the policy verdict is then "group", and the group-specific caps in the anti-ban policy article apply instead of the 1:1 ones.
Consented contacts
A tenant can declare that a number is not a stranger, moving it from the cold bucket to declared (its own, usually larger, daily budget — see Messaging & the anti-ban policy).
| Method | Path | Permission | Purpose ||---|---|---|---|| GET | /shops/{idOrName}/contacts/consented?activeOnly=&limit=&cursor= | contacts.read | list declared contacts (not shop-scoped in storage — every tenant shop sees the same list) || POST | /shops/{idOrName}/contacts/consented | contacts.write | declare one or more numbers || DELETE | /shops/{idOrName}/contacts/consented | contacts.write | revoke one number |POST is always a batch, even of one: { "contacts": [{ "phone": "+241…", "note"?, "expiresAt"? }] }. Capped at a batch size and at a genuinely-new-declarations-per-day ceiling (both server-side, not tenant-governed); exceeding either returns 429 DECLARE_BATCH_TOO_LARGE / 429 DECLARE_DAILY_CAP_REACHED without touching the ones that would have fit. A declaration made through this route always records source: "api". DELETE takes { "phone": "+241…" } and revokes (revokedAt set), never hard-deletes — the audit trail stays.
A number that replies STOP / ARRÊT / DÉSABONNER / UNSUBSCRIBE opts out regardless of whether it was ever declared — the next send to it fails 422 RECIPIENT_OPTED_OUT until it writes in again.