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.
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
Have an https URL ready
The URL must start with
https://and be 8 to 500 characters long. Plainhttp://is refused when you save: Amomic-AI does not send workspace events over an unencrypted connection, not even tolocalhostthrough a tunnel.For local development, use a tunnel that terminates TLS for you and gives you an
https://hostname. - 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
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
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:
{"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.
{"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.
idstringrequiredevt_ 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.
typestringrequiredOne of the twenty event types, or ping. See Event reference.
created_atstringrequiredWhen 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_idstringrequiredYour workspace id, ten_-prefixed. Useful if one receiver serves several workspaces.
dataobjectrequiredThe 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_idstringoptionalAn 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 #
Content-Type: application/jsonUser-Agent: Amomic-AI-Webhooks/<version>Amomic-Signature: t=1792042140,v1=8f3ac91b0d7e4f2a6c5b8e1d0a9f3c72e4b6d8a1c3e5f7091b2d4a6c8e0f2a4b6Amomic-Event-Id: evt_1b7d4f9a2c6e80b3d5f7a9c1Amomic-Event-Type: message.receivedAmomic-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:
- Verify the signature.
- Write the event somewhere durable — a queue, a table, a log.
- Return
200. - 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:
-- the whole mechanismINSERT 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.
import crypto from "node:crypto";import express from "express";const app = express();const SECRET = process.env.AMOMIC_WEBHOOK_SECRET; // the whsec_… stringfunction 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.