Skip to content

Getting events

Have Amomic-AI POST a signed JSON event to your server when a customer writes, a call ends, or a ticket opens — how to add an endpoint, test it, and what every delivery looks like.

Open Developers

A webhook is an HTTPS endpoint you own. When something happens in your workspace — a customer writes on WhatsApp, a call finishes, a visitor leaves their phone number — Amomic-AI POSTs a signed JSON body to that URL. Your server does the rest.

Who can set one up: owners and admins, on every plan, from Developers → Webhooks in the console. Members see an Admins only message on the Developers page. There is no public API for managing endpoints; the console is the only place.

This page covers adding an endpoint and the delivery contract: what arrives, in what shape, with which headers, and the rules your endpoint has to follow. Verifying signatures covers the security half, Event reference lists every payload field, and Retries & failures covers what happens when your server has a bad day. The console side is also covered, screen by screen, in The Developers page.

Why not just poll #

Polling is the obvious alternative and it does not work well here, for two reasons.

Most of what you want has no list endpoint. There is no GET /api/v1/messages and no call list. An inbound WhatsApp message and a finished call are not things you can enumerate. To notice an inbound message by polling you would have to list conversations, spot a changed updated_at, then fetch that conversation's messages and diff them against what you already stored — three round trips to learn one fact you could have been told.

Your budget is 120 requests per minute per key (rate limits). Polling conversations every five seconds burns 12 of those every minute doing nothing, and still leaves you up to five seconds behind. A webhook costs you nothing while the workspace is quiet.

Polling is still the right tool for reconciliation — catching up after your endpoint was down. Retries & failures has that pattern.

Add an endpoint #

  1. 1

    Have an https URL ready

    The URL must start with https:// and be 8 to 500 characters long. Plain http:// is refused when you save: Amomic-AI does not send workspace events over an unencrypted connection, not even to localhost through a tunnel.

    For local development, use a tunnel that terminates TLS for you and gives you an https:// hostname.

  2. 2

    Open the dialog

    In the console, open Developers, then the Webhooks tab, and click Add endpoint (or Add your first endpoint if you have none yet).

  3. 3

    Enter the URL and pick events

    Paste the URL into Endpoint URL. Under Events to send, tick the events this endpoint should receive. Each one shows a plain description and its event name.

    Leaving every box unticked is meaningful: the endpoint then receives every event type, including types added later. Tick specific events when your receiver would be confused by anything else.

  4. 4

    Copy the signing secret

    Click Add endpoint. The Copy your signing secret dialog shows the endpoint's secret, which starts with whsec_. It is shown once and cannot be read back later — not in the console, not by support. Put it in your secret manager before you click Done.

A few things the dialog does not do:

  • No description field. Name your URL path after its purpose (/hooks/amomic/dispatch) if you run several.
  • No editing afterwards. An endpoint's URL and events can't be changed in the console. To change them, add a new endpoint with the new settings, move your receiver over to its secret, then delete the old one.
  • No secret rotation. If you lose the secret, add a new endpoint and delete the old one. Running both side by side for a while, with your receiver accepting either secret, avoids a gap.

ticket.replied appears in the list under its event name, with no description. It is a normal event; see Event reference.

Give it half a minute

A new endpoint, an Enable, or a deletion can take up to about 30 seconds to apply to every event. An event that happens in the first seconds after you add an endpoint may not reach it.

Send a test event #

Click Test on the endpoint. The console says Test webhook queued. Open deliveries to see the response. — the test goes through the same delivery path as a real event, so it is queued, not sent while you wait.

Click Deliveries to open Recent deliveries. The test shows as ping with queued until it has been sent; close and reopen the dialog to see the result — your server's status code, how long it took, and the error text if it failed.

The test body is an ordinary envelope with the type ping:

A test event
{
"id": "evt_6c1f9a2e7b4d08f3a5e1c9b2",
"type": "ping",
"created_at": "2026-10-14T05:49:12Z",
"tenant_id": "ten_01JA4M8Q2V7K9X3D5N6P1R0TZB",
"data": { "message": "Amomic-AI webhook test" }
}

It is signed like any other delivery and has a fresh id every time, so a receiver that deduplicates still processes each test. ping is not in the event list: you can't subscribe to it or opt out of it, and an endpoint subscribed to only call.ended still receives it. Your router needs a default branch that answers 200 for a type it doesn't know.

Tests only reach enabled endpoints

A test on a Disabled endpoint is queued but never sent, and stays queued in Deliveries. Click Enable first, then Test. A test that fails counts toward the automatic switch-off, just like a real event.

The delivery #

Every event is a single POST with a JSON body.

Envelope #

JSON
{
"id": "evt_1b7d4f9a2c6e80b3d5f7a9c1",
"type": "message.received",
"created_at": "2026-10-14T05:44:00Z",
"tenant_id": "ten_01JA4M8Q2V7K9X3D5N6P1R0TZB",
"data": {
"channel": "whatsapp",
"message_id": "wamid.HBgMOTE5NzY3NDUyOTA4FQIAEhgUM0E4RjI1QjcxQzE5RDA0RTZBNTMA",
"from": "+919767452908",
"type": "text",
"event_id": "evt_01JF9X2K6N0Q4T8W2Y6A0C4E8G"
}
}

That is a complete message.received. It does not carry the message text or a conversation id — see what message.received carries and how to fetch the rest.

idstringrequired

evt_ followed by 24 hex characters. The same for every endpoint the event goes to, every retry, and every Replay. The value you deduplicate on. Also sent as the Amomic-Event-Id header.

typestringrequired

One of the twenty event types, or ping. See Event reference.

created_atstringrequired

When the event was sent out, in UTC, second precision, literal Z suffix — 2026-10-14T05:44:00Z. REST resources use a different shape, with microseconds and a +00:00 offset.

tenant_idstringrequired

Your workspace id, ten_-prefixed. Useful if one receiver serves several workspaces.

dataobjectrequired

The event payload. The fields depend on type; each event sends a fixed list of fields, and a field with no value is left out rather than sent as null.

data.event_idstringoptional

An internal id, on every event except ping. It is not the top-level id. Deduplicate on the top-level id.

`data.event_id` is not the dedupe key

Two ids ship in every event and they are not interchangeable. The top-level id matches Amomic-Event-Id and is the correct dedupe key. data.event_id is an internal identifier that rides along inside the payload. Keying your dedupe table on the wrong one is a bug you will not notice until the first retry.

Headers #

Text
Content-Type: application/json
User-Agent: Amomic-AI-Webhooks/<version>
Amomic-Signature: t=1792042140,v1=8f3ac91b0d7e4f2a6c5b8e1d0a9f3c72e4b6d8a1c3e5f7091b2d4a6c8e0f2a4b6
Amomic-Event-Id: evt_1b7d4f9a2c6e80b3d5f7a9c1
Amomic-Event-Type: message.received
Amomic-Delivery-Attempt: 1
Header Meaning
Amomic-Signature Timestamped HMAC-SHA256 over the raw body, t=<unix seconds>,v1=<lowercase hex>. Verify it on every request — see Verifying signatures.
Amomic-Event-Id The envelope's id. Lets you deduplicate before you parse the body.
Amomic-Event-Type The envelope's type. Lets you route before you parse the body.
Amomic-Delivery-Attempt 1 to 4. Above 1 means an earlier attempt failed and this is a retry. A Replay from the console starts again at 1. See Retries & failures.
User-Agent Always begins Amomic-AI-Webhooks/. Handy for filtering your access logs; useless as proof of anything, since anyone can send it.

Headers are not proof

The signature is the only thing that establishes an event came from Amomic-AI. Every other header can be forged by anyone who guesses your URL. Do not act on Amomic-Event-Type before you have verified Amomic-Signature.

The body is plain ASCII #

The body is compact JSON — no spaces after : or , — and any character outside ASCII is escaped. A customer named प्रिया arrives as "प्रिया". Every JSON parser turns that back into the right text; the point is only that you must verify the bytes you received, not a re-serialised copy. See Verify against the raw bytes.

Rules your endpoint has to follow #

Return 2xx immediately, then work #

Each attempt has a 10-second total timeout (5 seconds to connect). If your handler is still busy at 10 seconds, the connection is cut and the attempt counts as failed — even if your code goes on to finish. You then get a retry of the same event and process it twice.

The pattern that survives:

  1. Verify the signature.
  2. Write the event somewhere durable — a queue, a table, a log.
  3. Return 200.
  4. Do the real work in a worker.

Anything that calls a third party, sends an email or writes to a slow database belongs in step 4.

Deliveries are at-least-once #

The same id can arrive more than once — a retry after a timeout, or a Replay from the console. Keep a table of processed event ids and check it first:

sql
-- the whole mechanism
INSERT INTO amomic_events (event_id, received_at) VALUES ($1, now())
ON CONFLICT (event_id) DO NOTHING;
-- zero rows affected means you have seen this event; acknowledge and stop.

Events are not ordered #

Events are sent independently and retried independently. A read status can arrive before its delivered, and call.outcome can arrive before call.ended. Order by the envelope's created_at, never by arrival.

Redirects are not followed #

A 301, 302 or 307 is a failed delivery and is not retried. Amomic-AI does not follow redirects — following one would mean sending a signed payload to a host you never registered.

If your platform redirects http to https, or /hooks/amomic to /hooks/amomic/, register the final URL.

One event, one POST #

There is no batching. Ten inbound messages are ten separate requests, each with its own signature and its own id.

A minimal receiver #

Both examples do the same four things: read the raw bytes, verify, enqueue, acknowledge. Verifying signatures has the fully commented version for four languages.

Node.js
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.AMOMIC_WEBHOOK_SECRET; // the whsec_… string
function verify(rawBody, header, secret, toleranceSeconds = 300) {
if (!header) return false;
const fields = {};
for (const piece of header.split(",")) {
const i = piece.indexOf("=");
if (i > 0) fields[piece.slice(0, i).trim()] = piece.slice(i + 1).trim();
}
if (!fields.t || !fields.v1) return false;
const timestamp = Number.parseInt(fields.t, 10);
if (!Number.isFinite(timestamp)) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSeconds) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(fields.t + ".")
.update(rawBody) // Buffer — the bytes exactly as received
.digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(fields.v1, "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// express.raw gives you a Buffer. Do NOT register express.json() ahead of this
// route — if it runs first, the stream is consumed and the bytes are gone.
app.post("/hooks/amomic", express.raw({ type: "application/json" }), async (req, res) => {
if (!verify(req.body, req.get("Amomic-Signature"), SECRET)) {
return res.status(400).send("bad signature");
}
const event = JSON.parse(req.body.toString("utf8"));
try {
await queue.enqueue(event.id, event); // durable and fast — no business logic here
} catch (err) {
return res.status(500).send("could not enqueue"); // a 500 gets retried
}
return res.status(200).send("ok");
});
app.listen(8080);

The single most common integration bug

Verify against the raw request bytes. Parsing the body and serialising it again produces different bytes and a signature that never matches, however correct your HMAC code is. Verifying signatures explains how to avoid it in each framework.

Troubleshooting #

What you see What it means
The test stays queued The endpoint is Disabled, or the dialog has not been reopened since the test was sent. Enable it, test again, and reopen Deliveries.
no response with ReadTimeout Your server took longer than 10 seconds. Acknowledge first, work afterwards.
no response with a connection or TLS error Nothing reached your application: DNS, firewall, or an expired certificate.
A 3xx status A redirect. Register the final URL.
400 from your own verifier Usually a re-serialised body or the wrong secret. See Troubleshooting.
Real events never arrive, the test works The endpoint isn't subscribed to that event, or the event isn't sent for what you did — check Event reference.

Next #

Verify a delivery

Verifying signatures — the header format, the tolerance window, and verifiers in four languages.

What each event contains

Event reference — all twenty event types with the fields each one really sends.

When your endpoint fails

Retries & failures — the retry ladder, auto-disable, Replay, and catching up after an outage.

The console screens

The Developers page — keys, webhooks, logs and quickstart, as the console shows them.