Webhooks

Omnio posts signed events to your endpoint as things happen, and retries until you answer.

Endpoints

An owner or admin registers endpoints in Settings, Webhooks, picks the events and copies the signing secret, shown once. The URL must be HTTPS on port 443 and resolve to a public address. An organisation has up to 10 endpoints.

The envelope

Every event arrives as one JSON body. The payload of each type is in data.

{
  "id": "evt_8f14e45fceea167a5a36dedd4bea2543",
  "type": "message.received",
  "version": "2026-10-01",
  "org_id": "5f3e2b1c-7a9d-4e21-b8c6-0d4f9a2e7b13",
  "occurred_at": "2026-10-05T14:02:11Z",
  "data": {
    "message_id": "6f0b5a8e-1c52-4d5e-9d57-3f0f2a9d4c11",
    "seq": 42,
    "conversation_id": "b1c7e0a4-52a3-4a3c-8d0e-6c1b9e5f7a20",
    "channel_id": "0e8d3c1f-7a6b-4b52-9e34-2d1f5c8a9b70",
    "customer_id": "4829173650",
    "type": "text",
    "content": { "text": "Where is my transfer?" },
    "attachments": [],
    "received_at": "2026-10-05T14:02:11Z"
  }
}

Delivery is at least once and in no set order. Deduplicate on id and order a conversation's messages by seq.

Verify the signature

HeaderValue
X-SignatureHex HMAC-SHA256 of {X-Timestamp}.{raw body}, keyed with the endpoint's secret
X-TimestampUnix seconds when this attempt was sent
X-Event-IdThe envelope id
X-Event-TypeThe envelope type

Check the timestamp first and refuse one more than 5 minutes from your clock, which also refuses a replayed request. Then compute the HMAC over the raw bytes, before parsing them, and compare in constant time.

import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300


def is_authentic(secret: str, raw_body: bytes, signature: str, timestamp: str) -> bool:
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
        return False
    signed = timestamp.encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Retries

Answer 2xx within 5 seconds. Any other status, a timeout, a connection error or a redirect fails the attempt. Omnio tries again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours, each time with a fresh timestamp and signature. After the last one the delivery is dead.

An endpoint that fails for 24 hours in a row is disabled. Turn it back on in Settings once it answers again. Do the slow work after you reply, from a queue.

Events

TypeWhen
message.receivedA customer sent a message
message.sentMeta accepted an outbound message, from the inbox or the API
message.status_updatedAn outbound message moved to sent, delivered, read, failed or rejected
conversation.createdA customer's first message opened a conversation
conversation.assignedThe assigned member or team changed
conversation.status_changedThe status or snooze changed
note.createdSomeone added an internal note
contact.createdA contact was created
contact.updatedFields changed, listed in changed
contact.deletedA contact was deleted
contact.restoredA deleted contact came back
contact.blockedA contact was blocked
contact.unblockedA contact was unblocked
contact.lifecycle_changedA contact moved to another stage
contact.tag_addedA tag was put on a contact
contact.tag_removedA tag was taken off a contact
import.completedAn import finished or failed
pingA test sent from Settings

Events about a channel go out only while that channel's webhooks are switched on. Customers are named by customer_id, and every timestamp is UTC.

Message status

A status never goes back. When a customer reads a message, the earlier ones still unread each get their own event with inferred set to true.

{
  "message_id": "3c5e7a90-8d1b-4e26-a4f7-5b9d0c2e1a68",
  "seq": 43,
  "conversation_id": "b1c7e0a4-52a3-4a3c-8d0e-6c1b9e5f7a20",
  "channel_id": "0e8d3c1f-7a6b-4b52-9e34-2d1f5c8a9b70",
  "customer_id": "4829173650",
  "message_type": "text",
  "status": "read",
  "previous_status": "delivered",
  "inferred": false,
  "read_at": "2026-10-05T14:05:39Z",
  "error": null
}
NextContacts by Customer ID